我在 2022 年 9 月重新寫了與本文內容相近的文章,建議可以觀看新文章:
前言
LibOpenCM3 是一個 Open-Source 的 ARM Cortex-M3 微控制器底層硬體函式庫,支援包含 STM32 在內的多種微控制器。
本文將以 STM32F103RB(Nucleo F103RB)作為示範,介紹如何使用 LibOpenCM3 寫出 STM32 的 PWM(Pulse-Width Modulation) 功能,並且可以控制頻率與 Duty Cycle(佔空比)。
正文
在 STM32 中,PWM 的功能是由 Timer 所實現的,並藉由設定 CCR(捕獲/比較暫存器) 的值來調整 Duty Cycle。
完整程式
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 74 75
|
#include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/timer.h>
#define PWM_FREQUENCY (1000) #define PWM_DUTY_CYCLE (72.5)
#define PWM_TIMER_PRESCALER (48 - 1) #define PWM_TIMER_PERIOD (((rcc_apb1_frequency * 2) / ((PWM_TIMER_PRESCALER + 1) * PWM_FREQUENCY)) - 1)
void gpio_setup(void) { rcc_periph_clock_enable(RCC_GPIOA); gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO7); }
void pwm_setup(void) { rcc_periph_clock_enable(RCC_TIM3);
timer_set_mode(TIM3, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); timer_disable_preload(TIM3); timer_continuous_mode(TIM3);
timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER); timer_set_period(TIM3, PWM_TIMER_PERIOD);
timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1); timer_set_oc_value(TIM3, TIM_OC2, (PWM_TIMER_PERIOD + 1) * (PWM_DUTY_CYCLE / 100.0));
timer_enable_oc_output(TIM3, TIM_OC2); timer_enable_counter(TIM3); }
int main(void) { rcc_clock_setup_in_hsi_out_48mhz();
gpio_setup(); pwm_setup();
while (1) { __asm__("nop"); }
return 0; }
|
程式分段說明
引入函式庫
1 2 3
| #include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/timer.h>
|
除了基本的「RCC」及「GPIO」外,由於 PWM 是透過 Timer 所實現的,所以還要引入「Timer」。
PWM 數值宣告
1 2
| #define PWM_FREQUENCY (1000) #define PWM_DUTY_CYCLE (72.5)
|
宣告 PWM 的目標頻率及 Duty Cycle 以供後續計算。
PWM 頻率設定
1 2
| #define PWM_TIMER_PRESCALER (48 - 1) #define PWM_TIMER_PERIOD (((rcc_apb1_frequency * 2) / ((PWM_TIMER_PRESCALER + 1) * PWM_FREQUENCY)) - 1)
|
這裡算是本文的重點之一,也就是如何設定 PWM 的頻率。
首先:
所以:
1
| PER = {f_tim / [(PRS + 1) * f_pwm]} - 1
|
其中:
f_pwm
: PWM frequency,PWM 的頻率.
f_tim
: Timer frequency, Timer 的頻率.
PRS
: Timer prescaler,Timer 的預除頻器數值.
PER
: Timer period,Timer 的週期數值.
透過時鐘樹(Datasheet P.12, Figure 2. Clock tree)可以知道,我們使用的「Timer 3」的時鐘源是「APB 1」,而在本例中,我們會在主程式呼叫 rcc_clock_setup_in_hsi_out_48mhz()
以將系統時鐘設為 48 MHz,這樣將會一併讓「APB 1」的預除頻器(Prescaler)被設定為「除 2」,所以我們的「APB 1」時鐘頻率為 48 MHz / 2 = 24 MHz。
然而,當「APB 1」的預除頻器不等於「除 1」時,「APB 1」的時鐘會先乘 2 再給「Timer 3」,因此「Timer 3」的時鐘頻率 f_tim
為 24 MHz * 2 = 48 MHz。
最後我將 PRS
設定為 48 - 1
,將 PER
以上面的公式帶入。
rcc_apb1_frequency
的數值會在呼叫 rcc_clock_setup_in_hsi_out_48mhz()
時設定。
GPIO 設定
1 2 3 4 5 6 7 8 9
| void gpio_setup(void) { rcc_periph_clock_enable(RCC_GPIOA); gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO7); }
|
設定 GPIO 以輸出 PWM 訊號。注意,每個 Timer 的 Channel 能輸出的 GPIO 腳位都不同,像這裡「Timer 3」的「Channel 2」是對應「PA7」,在使用不同 Timer、Channel 時請參考 STM32 Datasheet。
PWM Timer 設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void pwm_setup(void) { rcc_periph_clock_enable(RCC_TIM3);
timer_set_mode(TIM3, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); timer_disable_preload(TIM3); timer_continuous_mode(TIM3);
timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER); timer_set_period(TIM3, PWM_TIMER_PERIOD);
timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1); timer_set_oc_value(TIM3, TIM_OC2, (PWM_TIMER_PERIOD + 1) * (PWM_DUTY_CYCLE / 100.0));
timer_enable_oc_output(TIM3, TIM_OC2); timer_enable_counter(TIM3); }
|
這裡也是本文的重點之一,在這裡設定好要使用的 Timer 及 PWM Duty Cycle。
timer_set_oc_value()
就是改變 CCR 的值,以調整 Duty Cycle。因為我們使用的是「Channel 2」,所以 Output channel 是TIM_OC2
。
在 TIM_OCM_PWM1
模式下,想要輸出 X
% Duty Cycle 的 PWM,只要把 CCR 的數值也設定成 Timer 週期的 x
% 就可以了,也就是 CCR_Value = (PWM_TIMER_PERIOD + 1) * (PWM_DUTY_CYCLE / 100.0)
。
最後以 timer_enable_oc_output()
啟動 PWM 的輸出,timer_enable_counter()
啟動整個 Timer。
主程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int main(void) { rcc_clock_setup_in_hsi_out_48mhz();
gpio_setup(); pwm_setup();
while (1) { __asm__("nop"); }
return 0; }
|
主程式先將系統時鐘設定為 48 MHz,並依序完成 GPIO 及 PWM 的初始化設定,在執行完 pwm_setup()
後就會開始輸出 PWM 了,最後就進入一個無限空迴圈。
成果
可以看到輸出的波形誤差很小(設定值為:Duty Cycle 72.5%,頻率 1 kHz)。
結語
本次的程式我一樣有放在 GitHub 上,可以使用 PlatformIO 開始專案。
若有什麼問題或錯誤歡迎留言討論。
相關連結