前言
在之前的內容中已經介紹過基本的 Timer 用法,及 PWM 的計算。
在使用 PWM 時我們會需要控制兩種參數:頻率與 Duty Cycle(佔空比)。頻率的部分和 Timer 一樣,由 TIMx_PSC 與 TIMx_ARR 暫存器的值來設定,而 Duty Cycle 則由 TIMx_CCRx 暫存器來指定。
這篇的目標是寫出一個可以設定 PWM 頻率與 Duty Cycle 的程式,並讓 STM32 輸出 PWM 訊號。
正文
首先一樣以 Nucleo-F446RE 做示範。
首先建立一個 PIO 的專案,選擇 Framework 為「libopencm3」,並在 src/
資料夾中新增並開啓 main.c
檔案。
完整程式
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
|
#include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/timer.h>
#define PWM_GOAL_FREQUENCY (1000) #define PWM_GOAL_DUTY_CYCLE (72.5)
#define PWM_TIMER_CLOCK (rcc_apb1_frequency * 2) #define PWM_COUNTER_CLOCK (1000000)
#define PWM_TIMER_PRESCALER (PWM_TIMER_CLOCK / PWM_COUNTER_CLOCK - 1) #define PWM_TIMER_PERIOD (((PWM_TIMER_CLOCK) / ((PWM_TIMER_PRESCALER + 1) * PWM_GOAL_FREQUENCY)) - 1) #define PWM_TIMER_OC_VALUE ((PWM_TIMER_PERIOD + 1) * PWM_GOAL_DUTY_CYCLE / 100)
#define RCC_PWM_GPIO (RCC_GPIOA) #define GPIO_PWM_PORT (GPIOA) #define GPIO_PWM_PIN (GPIO7) #define GPIO_PWM_AF (GPIO_AF2)
static void rcc_setup(void) { rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
rcc_periph_clock_enable(RCC_PWM_GPIO); rcc_periph_clock_enable(RCC_TIM3); rcc_periph_reset_pulse(RST_TIM3); }
static void pwm_setup(void) { gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN); gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN); gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN);
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_value(TIM3, TIM_OC2, PWM_TIMER_OC_VALUE); timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
timer_enable_oc_output(TIM3, TIM_OC2); timer_enable_counter(TIM3); }
int main(void) { rcc_setup(); pwm_setup();
while (1) { } return 0; }
|
分段說明
Include
1 2 3
| #include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/timer.h>
|
和 Timer 時相比只少了中斷的 nvic.h
,要使用 PWM 就只需要這 3 個功能就可以了。
計算並設計 Timer 參數(PSC、ARR、CCR 暫存器)
1 2 3 4 5 6 7 8 9
| #define PWM_GOAL_FREQUENCY (1000) #define PWM_GOAL_DUTY_CYCLE (72.5)
#define PWM_TIMER_CLOCK (rcc_apb1_frequency * 2) #define PWM_COUNTER_CLOCK (1000000)
#define PWM_TIMER_PRESCALER (PWM_TIMER_CLOCK / PWM_COUNTER_CLOCK - 1) #define PWM_TIMER_PERIOD (((PWM_TIMER_CLOCK) / ((PWM_TIMER_PRESCALER + 1) * PWM_GOAL_FREQUENCY)) - 1) #define PWM_TIMER_OC_VALUE ((PWM_TIMER_PERIOD + 1) * PWM_GOAL_DUTY_CYCLE / 100)
|
這部分的 PWM_TIMER_CLOCK
、PWM_COUNTER_CLOCK
、PWM_TIMER_PRESCALER
(PSC)、 PWM_TIMER_PERIOD
(ARR) 和 Timer 的部分一樣,就不再贅述。
這次的重點是 CCR 暫存器。在上一篇中已經說明其關係式為:
Duty_Cycle% = CCR / (ARR + 1) * 100%
所以
CCR = (ARR + 1) * Duty_Cycle% / 100%
因此這裡以 PWM_TIMER_OC_VALUE
為名定義 CCR 的計算公式 (PWM_TIMER_PERIOD + 1) * PWM_GOAL_DUTY_CYCLE / 100
。
RCC
1 2 3 4 5 6 7 8
| static void rcc_setup(void) { rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
rcc_periph_clock_enable(RCC_PWM_GPIO); rcc_periph_clock_enable(RCC_TIM3); rcc_periph_reset_pulse(RST_TIM3); }
|
這部分還是和 Timer 一樣。重點一樣是指定時鐘源為 8 MHz 的 HSE,並設定系統時鐘為 168 MHz。
PWM 與 Timer 設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static void pwm_setup(void) { gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN); gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN); gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN);
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_value(TIM3, TIM_OC2, PWM_TIMER_OC_VALUE); timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
timer_enable_oc_output(TIM3, TIM_OC2); timer_enable_counter(TIM3); }
|
要使 GPIO 可以輸出 PWM 訊號的話,要將 Timer 的 Channel 對應的 GPIO 設定為 Alternate function。我們使用 TIM3 的 Channel 2。
Timer 大部分的設定都和和上一篇的一樣,主要差異為要使用 timer_set_oc_mode()
指定使用 Channel 2(TIM_OC2
),並設定為 TIM_OCM_PWM1
模式。
使用 timer_set_oc_value()
函式將 CCR 的值傳給 TIMx_CCRx 暫存器。
多環境程式(F446RE + F103RB)
由於 STM32F1 的部分函式不同,所以 F103RB 沒辦法直接使用上面的 F446RE 的程式。
以下列出主要的差異部分,也就是 RCC 與 GPIO 的部分。完整的程式請看 GitHub repo。
1 2 3 4 5 6 7 8 9 10 11 12
| static void rcc_setup(void) { #if defined(STM32F1) rcc_clock_setup_in_hse_8mhz_out_72mhz(); #elif defined(STM32F4) rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]); #endif
rcc_periph_clock_enable(RCC_PWM_GPIO); rcc_periph_clock_enable(RCC_TIM3); rcc_periph_reset_pulse(RST_TIM3); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static void pwm_setup(void) { #if defined(NUCLEO_F103RB) gpio_set_mode(GPIO_PWM_PORT, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_PWM_PIN); #else gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN); gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN); gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN); #endif }
|
成果
我使用兩組開發板並分別設定為頻率 1kHz
, Duty Cycle 72.5%
以及頻率 2kHz
, Duty Cycle 15.0%
。
可以看到 PWM 的輸出結果是相當精準的。
小結
這次介紹了 STM32 的 PWM 用法,PWM 是 Timer 的延伸功能,因此大部分的設定都和 Timer 有關,如果 Timer 有理解的話 PWM 應該不會太難。
參考資料
本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽。