- 보드 : 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 참고해 설정)
[코드]
<듀티 사이클 조절 함수 (버튼 인터럽트 콜백 함수)>
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 함수>
/* @ 버튼 디바운스 함수
* 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 측정 함수>
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 계산
<타이머 콜백 함수>
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
rpm_calculation(&htim2,TIM_CHANNEL_1);
}
- 타이머 인터럽트가 발생됐을 때 실행되는 콜백 함수
- rpm_calculation() 함수를 사용해 설정한 시간 간격(1초)마다 현재 팬의 RPM 측정
<main 함수>
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 변화 과정>
- (위에서부터 각각 27.65ms / 26.75ms / 25.27ms 주기)
- 듀티 사이클 변경 직후, RPM이 해당 듀티 사이클에 해당하는 RPM으로 변화하기 전까지는 조금 시간이 걸린다
- (RPM 변경 완료까지 대략 18주기 정도 걸리는 것으로 측정)
<듀티 사이클 99%, 100%>
- 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은 완벽히 고정되지 않고 일정 간격 내에서 계속 변화한다
'STM32' 카테고리의 다른 글
STM32 - SPI 이용 ST7735 LCD 드라이버 제어 (0) | 2020.06.21 |
---|---|
STM32 - SPI통신 MAX7219 7-segment 모듈 (2) | 2019.12.25 |
STM32 - I2C 통신 MLX90614 적외선 비접촉 온도 측정 센서 (2) | 2019.12.10 |
STM32 - UART 기반 PMS7003 먼지 센서 제어 (17) | 2019.12.04 |
P-NUCLEO-WB55 보드 무선 통신 사용 설정 (1) | 2019.10.16 |