출처: https://rs29.tistory.com/27
– 보드 : P-NUCLEO-WB55
– 개발 툴 : STM32CubeMX, True Studio
– 팬 : 녹투아 NF-A8 5V PWM
– 팬 작동 전압 : 5V
– 팬 소비 전력 : 0.75W
– 팬 Logic Level : PWM (Input) – Logic Low 최대 전압 0.8V / Logic High 최대 전압 5.25V
– 팬 PWM 주파수 : 25kHz (허용범위 21kHz ~ 28kHz)
– 팬 듀티 사이클 : 0 ~ 100%
– 팬 전원 : 18650 배터리
– 부스트 컨버터 : TI TPS61322 (18650 (2.7V~4.2V) -> TPS61322 (5V) -> NF-A8 5V PWM)
– 팬 제어 참고 자료
https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf
https://www.youtube.com/watch?v=gKHww3qJbs8
[연결]
[작동 방식]
<PWM>
– PWM을 이용해 팬 속도 조절
– 팬 속도는 듀티 사이클에 비례 (0~100%)
– 팬의 PWM 핀에 입력되는 PWM 신호가 0 V일 때 (듀티 사이클 0 %), 팬이 정지하고
– PWM 신호가 5 V일 때 (듀티 사이클 100%), 팬은 최고 속도로 회전한다
– 한 번의 PWM 신호 입력으로 팬 RPM이 고정되는 것이 아니라 지속적으로 PWM 신호를 입력해줘야 한다
<RPM>
– 팬의 Tachometer 출력 신호는 팬의 현재 RPM을 알아내는데 사용
– 출력 신호는 Hz 이고 팬 속도는 일반적으로 RPM (Revolutions Per Minute)으로 표기된다
– RPM으로 변환하기 위해 출력 신호를 60배 증가 시키고 팬이 회전당 두 개의 임펄스를 출력하므로 2로 나누어 준다
– fan speed [RPM] = frequency [Hz] * 60 / 2
– 86 [Hz] * 60 / 2 = 2580 [rpm]
– 녹투아 팬에서 Tachometer 신호를 읽어들이기 위해선 위의 그림과 같은 연결이 필요하다
– 5V 팬을 사용하므로 Link에 5mA 미만의 전류가 흐르게 해야한다
– 그림과 다르게 Vcc 5V를 사용하고 1.2k 저항을 사용한 결과 정상 작동 확인
[CubeMX 설정]
<PWM 설정>
– PWM 출력에 사용되는 클럭은 해당 타이머가 속하는 APB의 타이머 클럭을 기반으로 한다
– (위 설정의 경우, TIM2는 APB1에 속하고 APB1의 타이머 클럭은 32MHz 이다)
– PWM 주파수는 Timer Clock / (Prescaler+1)(Counter Period+1) 로 결정된다
– (Prescaler, Counter Period 는 실제 주파수 계산 때 +1이 되므로 원하는 값에 -1을 함)
– 따라서 위 설정에서 PWM 주파수는 32 Mhz / ((10-1)+1)((128-1)+1) = 25kHz 가 된다
– 듀티 사이클 (%) = (사용자 입력값 / Counter Period) * 100
– Pulse : Counter Compare Register (CCR) 초기화 값
– Counter Mode : 카운트 증가 방향
– PWM mode 1 : 업카운팅 모드에서 period (CNT) < pulse (CCR)일 때, 타이머 채널 활성화 이외엔 비활성화
– 카운터 모드가 업이므로 timer counter register (CNT)는 1씩 증가하고 pulse (CCR) 값보다 작을 때는 타이머 채널이 활성화(CH Polarity : High 이므로 High 출력) pulse 값 이상일 땐 비활성화된다
– period (CNT)는 1씩 증가하다 Counter Period(AutoReload Register(ARR))+1 을 초과할 때 0이 된다
– (위의 설정에선 128을 넘어서면 다시 0부터 1씩 증가)
<Tachometer Input 핀 설정>
– Tachometer 신호를 입력받는데 사용할 GPIO 핀을 GPIO_Input 으로 설정한다 (사용하지 않는 핀들 중에 선택)
<타이머 설정 (PWM 측정용)>
– PWM 실시간 측정용 타이머 설정
– 타이머 주기 = 32 MHz / {((32000-1)+1)*((1000-1)+1)} = 1 sec
– (사용 타이머가 속하는 APBx 타이머 클럭 (MHz) / {(Prescaler+1)(Counter Period+1)})
<버튼 설정 (필수 X, 듀티 사이클 조절하는데 사용)>
– 보드에 탑재되어있는 버튼들을 사용해 PWM 듀티 사이클 조절
– 사용할 버튼을 GPIO_EXTIx 로 변경해 인터럽트 모드 사용
– 풀업 설정 및 폴링 엣지에서 인터럽트가 발생하게 설정
– (내부 풀업으로 인해 버튼에 연결된 핀은 High 상태를 유지하다 버튼이 눌리는 순간 Low로 바뀐다)
<인터럽트 우선 순위 설정>
– SysTick 핸들러 내에서 증가하는 카운터에 의존하는 모든 딜레이와 타임아웃 HAL 함수들을 다른 인터럽트 내에서 사용할 경우, SysTick 인터럽트 우선 순위를 다른 인터럽트 함수보다 높게 설정해야 한다 (낮은 순자)
– (우선 순위 안 바꿀 경우, 버튼 및 타이머 인터럽트 콜백에서 HAL_GetTick() 정상 작동 X)
– 기본적으로 우선 순위가 0으로 설정되므로 다른 인터럽트들의 우선 순위를 낮춰서 사용 (숫자 ↑ 우선 순위 ↓)
– (https://stackoverflow.com/questions/53899882/hal-delay-stuck-in-a-infinite-loop 참고해 설정)
[코드]
<듀티 사이클 조절 함수 (버튼 인터럽트 콜백 함수)>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { //count : 버튼이 눌려있던 시간을 저장하는 변수 //lower_limit : 버튼이 눌러졌다고 인식되기 위해 필요한 최소 시간(ms) int count=0, lower_limit=80; //타이머-채널에 설정된 듀티 사이클을 저장하는 변수 선언 및 현재 듀티 사이클 읽어와서 저장 uint8_t current_dutyCycle=HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1); //사용자가 누른 버튼이 1번 버튼일 때 if(GPIO_Pin==B1_Pin) { //버튼 디바운스 함수를 통해 사용자가 버튼을 누르고 있던 시간을 측정 count=button_debounce(B1_GPIO_Port,B1_Pin); //하한을 넘어가는 시간동안 버튼을 누르고 있었다면 듀티 사이클 조절 실행 if(lower_limit < count) { if(10<=current_dutyCycle) { //현재 설정된 듀티 사이클을 저장한 변수에 -10값을 새로운 듀티 사이클로 설정 __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,dutyCycle-10); //위의 HAL 드라이버 대신 직접 타이머-채널 레지스터에 접근해 듀티 사이클 설정도 가능 // TIM2->CCR1-=10; } else { __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,0); // TIM2->CCR1=0; } } } //사용자가 누른 버튼이 2번 버튼일 때 else if(GPIO_Pin==B2_Pin) { count=button_debounce(B2_GPIO_Port,B2_Pin); if(lower_limit < count) { if(120<=current_dutyCycle) { __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,128); // TIM2->CCR1=128; } else { __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,dutyCycle+10); // TIM2->CCR1+=10; } } } //사용자가 누른 버튼이 3번 버튼일 때 else if(GPIO_Pin==B3_Pin) { count=button_debounce(B3_GPIO_Port,B3_Pin); if(lower_limit < count) { //RPM 측정 함수 호출 rpm_calculation(&htim2,TIM_CHANNEL_1); } } else { } printf("PWM : %ld\r\n",HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1)); // printf("PWM : %ld\r\n",TIM2->CCR1); } |
– 버튼 입력을 통해 PWM 듀티 사이클을 조절하므로 HAL_GPIO_EXTI_Callback() (GPIO 인터럽트 콜백 함수) 사용
– HAL_TIM_ReadCapturedValue() 함수를 통해 변경하려는 PWM 채널의 현재 듀티 사이클 읽어옴
– ((&htim2, TIM_CHANNEL_1) : Timer 2의 채널 1)
– B1 (버튼1) : 듀티 사이클 감소, B2 (버튼2) : 듀티 사이클 증가, B2 (버튼3) : RPM 측정
– 콜백 함수가 호출되고 전달된 매개변수 GPIO_Pin을 통해 인터럽트가 발생한 GPIO 핀이 어떤 핀인지 확인
– if~else 문을 통해 핀에 따라 듀티 사이클 감소, 증가 or RPM 측정 실행
– button_debounce() 함수를 통해 버튼이 몇 ms동안 눌려졌는지 측정하고 그 값을 count 변수에 저장
– 설정한 하한(lower_limit 변수)을 넘어가는 시간만큼(ms, count변수) 버튼이 눌렸을 경우에만 듀티 사이클 조절 실행
– __HAL_TIM_SET_COMPARE() 함수를 통해 듀티 사이클을 변경하려는 타이머, 채널 및 듀티 사이클 값을 입력
– (HAL_TIM_ReadCapturedValue() 함수를 통해 읽어 온 현재 듀티 사이클 값을 기반으로 ±10 가감)
– 듀티 사이클 최대값은 CubeMX에서 설정한 Counter Period 값 +1 이다
– HAL_TIM_ReadCapturedValue(), __HAL_TIM_SET_COMPARE() 같은 HAL 드라이버를 사용하지 않고 TIMx->CCRn 로 직접 레지스터에 접근해 현재 설정된 듀티 사이클을 읽어오거나 설정할 수도 있다
<버튼 Debounce 함수>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* @ 버튼 디바운스 함수 * gpio_port : 버튼과 연결된 GPIO 핀이 속한 GPIO Port * gpio_pin : 버튼과 연결된 GPIO 핀 */ int button_debounce(GPIO_TypeDef* gpio_port, uint16_t gpio_pin) { //count : 버튼을 누르고 있던 시간을 저장할 변수 //start_time, end_time : 버튼을 누르고 있던 시간을 측정하기 위해 사용할 변수 int count, start_time, end_time; //버튼과 연결된 GPIO 핀 상태를 저장할 변수 선언 및 현재 핀 상태 저장 GPIO_PinState pin_stat=HAL_GPIO_ReadPin(gpio_port,gpio_pin); //HAL_GetTick() 함수를 통해 측정 시작하는 시간을 start_time 변수에 저장 start_time=HAL_GetTick(); while(pin_stat==GPIO_PIN_RESET) { //버튼 GPIO 핀 상태 읽어와 저장 pin_stat=HAL_GPIO_ReadPin(gpio_port,gpio_pin); } //버튼에서 손을 떼 측정이 종료된 시간을 end_time 변수에 저장 end_time=HAL_GetTick(); //종료 시간-시작 시간으로 버튼이 눌려있던 시간을 계산해 저장 후 반환 count=end_time-start_time; return count; } |
– 버튼 디바운스 함수
– Nordic사의 App button 에서 따옴
– HAL_GetTick() 함수를 사용해 버튼이 눌린 시각과 버튼에서 손을 뗀 시각을 저장한 후 이를 이용해 버튼이 눌려져있던 시간을 측정
– 버튼 인터럽트 발생 직후에 호출되어 버튼에 연결된 GPIO 핀이 RESET(Low) 상태일 동안 while()문이 반복
– GPIO 핀이 SET(High) 상태가 되면 while()문 종료 및 눌린 상태로 존재하던 시간 계산 후 반환
<RPM 측정 함수>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
void rpm_calculation(TIM_HandleTypeDef *tim_pwm, uint32_t tim_channel) { //start_time : 측정 시작 시간 저장 //end_time : 측정 종료 시간 저장 int start_time=0, end_time=0, rpm=0; //측정된 주기를 바탕으로 계산한 주파수를 저장할 변수 long double freq=0; //현재 듀티 사이클을 읽어와 저장 uint8_t current_dutyCycle=HAL_TIM_ReadCapturedValue(tim_pwm, tim_channel); if(current_dutyCycle==0) { printf("RPM : 0\r\n"); return; } GPIO_PinState pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); //현재 Tachometer 핀이 Low 상태일 때 if(HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin)==GPIO_PIN_RESET) { while(pin_stat==GPIO_PIN_RESET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } //측정 시작 시간 저장 start_time=HAL_GetTick(); while(pin_stat==GPIO_PIN_SET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } while(pin_stat==GPIO_PIN_RESET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } //측정 종료 시간 저장 end_time=HAL_GetTick(); } //현재 Tachometer 핀이 High 상태일 때 else if(HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin)==GPIO_PIN_SET) { while(pin_stat==GPIO_PIN_SET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } //측정 시작 시간 저장 start_time=HAL_GetTick(); while(pin_stat==GPIO_PIN_RESET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } while(pin_stat==GPIO_PIN_SET) { pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin); } //측정 종료 시간 저장 end_time=HAL_GetTick(); } else { } //측정된 주기를 바탕으로 주파수 계산 (측정된 시간은 ms 단위이므로 *1000) freq=(1.0/(end_time-start_time))*1000; printf("Freq : %Lf\r\n",freq); //변환된 주파수를 주어진 RPM 계산 공식에 대입해 현재 RPM 계산 rpm=(int)((freq*60.0/2.0)); printf("RPM : %d\r\n",rpm); } |
– 팬의 Tachometer 핀에 연결된 GPIO 핀 (Input)의 상태를 측정해 RPM 계산
– 먼저 현재 듀티 사이클을 읽어와 0일 경우, 함수 종료 (RPM이 0일 때, Tachometer 핀은 Low 고정이므로)
– 팬 Tachometer 핀에 연결된 GPIO 핀 (Input)의 현재 상태를 측정한 뒤 두 가지 상황으로 나누어 진행
– 1. Tachometer 핀이 Reset (Low) 상태일 때
– Tachometer 핀이 Low 상태 중간부터 측정이 시작됐다고 가정해 일단 Low 상태가 끝날 때까지 while()문으로 대기
– 핀이 High로 전환되어 첫번째 while()문이 종료된 직후, HAL_GetTick() 함수를 사용해 Low 상태가 끝나고
High 상태가 시작되는 시간을 start_time 변수에 저장 (ms 단위)
– while()문을 통해 Tachometer 핀 High -> Low 한 주기가 지난 뒤, HAL_GetTick() 를 사용해 end_time 변수에
한 주기가 끝나는 시간을 저장
– 2. Tachometer 핀이 Set (High) 상태일 때
– Tachometer 핀이 High 상태 중간부터 측정이 시작됐다고 가정해 일단 High 상태가 끝날 때까지 while()문으로 대기
– 핀이 Low로 전환되어 첫번째 while()문이 종료된 직후, HAL_GetTick() 함수를 사용해 High 상태가 끝나고
Low 상태가 시작되는 시간을 start_time 변수에 저장 (ms 단위)
– while()문을 통해 Tachometer 핀 Low -> High 한 주기가 지난 뒤, HAL_GetTick() 를 사용해 end_time 변수에
한 주기가 끝나는 시간을 저장
– end_time-start_time을 통해 한 주기를 계산하고 이 값을 주파수로 변환한 뒤(f=1/T) 주어진 공식을 사용해 RPM 계산
<타이머 콜백 함수>
1 2 3 4 5 6 |
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { rpm_calculation(&htim2,TIM_CHANNEL_1); } |
– 타이머 인터럽트가 발생됐을 때 실행되는 콜백 함수
– rpm_calculation() 함수를 사용해 설정한 시간 간격(1초)마다 현재 팬의 RPM 측정
<main 함수>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
int main(void) { ... /* USER CODE BEGIN 2 */ printf("Start\r\n"); HAL_TIM_Base_Start_IT(&htim16); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); /* USER CODE END 2 */ ... while(1) { ... } } |
– 타이머 인터럽트(PWM 측정용으로 사용할) 시작 함수와 PWM 시작 함수 실행
<작동 결과>
– (세 번째 스크린샷의 dutycycle 표기는 오기입. 듀티사이클이 아닌 Counter Compare Register (CCR) 값)
[작동 테스트]
– 사용한 녹투아 NF-A8 5V PWM 의 경우, 듀티 사이클 0에서 정지, 듀티 사이클 6.25% (8/128)에서 팬 작동 시작
– 이때 측정되는 RPM은 192~200 RPM 사이, 듀티 사이클 10.15% (13/128)까지 동일 범위의 RPM 유지
<듀티 사이클 변화에 따른 RPM 변화 과정>
듀티 사이클 증가 직후, Tachometer 신호 변화 추이
– (위에서부터 각각 27.65ms / 26.75ms / 25.27ms 주기)
– 듀티 사이클 변경 직후, RPM이 해당 듀티 사이클에 해당하는 RPM으로 변화하기 전까지는 조금 시간이 걸린다
– (RPM 변경 완료까지 대략 18주기 정도 걸리는 것으로 측정)
<듀티 사이클 99%, 100%>
듀티 사이클 99.22% 신호 (127/128)
– Counter Compare Register (CCR) 최대값(또한, Timer Counter Register(CNT)의 최대값)은 CubeMX 타이머 PWM에서 설정한 Counter Period (AutoReload Register) 값+1이다
– (Counter Period (AutoReload Register) = 128-1 로 설정된 상태)
– (위 두 그래프 CCR1=127, 마지막 그래프 CCR1=128인 상태)
<고정 듀티 사이클에서 RPM 변화>
– 듀티 사이클이 고정된 상태라도 RPM은 완벽히 고정되지 않고 일정 간격 내에서 계속 변화한다