STM32 LibOpenCM3:PWM 脈波寬度調變

系列:簡單入門 LibOpenCM3 STM32 嵌入式系統開發 Posted on 2022-09-27

前言

在之前的內容中已經介紹過基本的 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 * @file   main.c
 3 * @brief  PWM(Pulse-width modulation) example for STM32 Nucleo-F446RE.
 4 */
 5
 6#include <libopencm3/stm32/rcc.h>
 7#include <libopencm3/stm32/gpio.h>
 8#include <libopencm3/stm32/timer.h>
 9
10#define PWM_GOAL_FREQUENCY (1000)  /* f_goal, PWM goal frequency in Hz. */
11#define PWM_GOAL_DUTY_CYCLE (72.5) /* dc_goal, PWM goal duty-cycle in %. */
12
13#define PWM_TIMER_CLOCK (rcc_apb1_frequency * 2) /* f_timer. */
14#define PWM_COUNTER_CLOCK (1000000) /* f_counter (CK_CNT). */
15
16#define PWM_TIMER_PRESCALER (PWM_TIMER_CLOCK / PWM_COUNTER_CLOCK - 1) /* PSC. */
17#define PWM_TIMER_PERIOD (((PWM_TIMER_CLOCK) / ((PWM_TIMER_PRESCALER + 1) * PWM_GOAL_FREQUENCY)) - 1) /* ARR. */
18#define PWM_TIMER_OC_VALUE ((PWM_TIMER_PERIOD + 1) * PWM_GOAL_DUTY_CYCLE / 100) /* CCR. */
19
20#define RCC_PWM_GPIO (RCC_GPIOA)
21#define GPIO_PWM_PORT (GPIOA)
22#define GPIO_PWM_PIN (GPIO7)   /* D11. */
23#define GPIO_PWM_AF (GPIO_AF2) /* Ref: Table-11 in DS10693. */
24
25static void rcc_setup(void)
26{
27  rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
28
29  rcc_periph_clock_enable(RCC_PWM_GPIO);
30  rcc_periph_clock_enable(RCC_TIM3);
31  rcc_periph_reset_pulse(RST_TIM3); /* Reset TIM3 to defaults. */
32}
33
34static void pwm_setup(void)
35{
36  /* Set PWM pin to alternate function push-pull. */
37  gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN);
38  gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN);
39  gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN);
40
41  timer_set_mode(TIM3, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
42  timer_disable_preload(TIM3);
43  timer_continuous_mode(TIM3);
44
45  timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER);        /* Setup TIMx_PSC register. */
46  timer_set_period(TIM3, PWM_TIMER_PERIOD);              /* Setup TIMx_ARR register. */
47  timer_set_oc_value(TIM3, TIM_OC2, PWM_TIMER_OC_VALUE); /* Setup TIMx_CCRx register. */
48  timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
49
50  timer_enable_oc_output(TIM3, TIM_OC2);
51  timer_enable_counter(TIM3);
52}
53
54int main(void)
55{
56  rcc_setup();
57  pwm_setup();
58
59  while (1)
60  { /* Halt. */ }
61  return 0;
62}

分段說明

Include

1#include <libopencm3/stm32/rcc.h>
2#include <libopencm3/stm32/gpio.h>
3#include <libopencm3/stm32/timer.h>

Timer 時相比只少了中斷的 nvic.h,要使用 PWM 就只需要這 3 個功能就可以了。

計算並設計 Timer 參數(PSC、ARR、CCR 暫存器)

1#define PWM_GOAL_FREQUENCY (1000)  /* f_goal, PWM goal frequency in Hz. */
2#define PWM_GOAL_DUTY_CYCLE (72.5) /* dc_goal, PWM goal duty-cycle in %. */
3
4#define PWM_TIMER_CLOCK (rcc_apb1_frequency * 2) /* f_timer. */
5#define PWM_COUNTER_CLOCK (1000000) /* f_counter (CK_CNT). */
6
7#define PWM_TIMER_PRESCALER (PWM_TIMER_CLOCK / PWM_COUNTER_CLOCK - 1) /* PSC. */
8#define PWM_TIMER_PERIOD (((PWM_TIMER_CLOCK) / ((PWM_TIMER_PRESCALER + 1) * PWM_GOAL_FREQUENCY)) - 1) /* ARR. */
9#define PWM_TIMER_OC_VALUE ((PWM_TIMER_PERIOD + 1) * PWM_GOAL_DUTY_CYCLE / 100) /* CCR. */

這部分的 PWM_TIMER_CLOCKPWM_COUNTER_CLOCKPWM_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

1static void rcc_setup(void)
2{
3  rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
4
5  rcc_periph_clock_enable(RCC_PWM_GPIO);
6  rcc_periph_clock_enable(RCC_TIM3);
7  rcc_periph_reset_pulse(RST_TIM3); /* Reset TIM3 to defaults. */
8}

這部分還是和 Timer 一樣。重點一樣是指定時鐘源爲 8 MHz 的 HSE,並設定系統時鐘爲 168 MHz。

PWM 與 Timer 設定

 1static void pwm_setup(void)
 2{
 3  /* Set PWM pin to alternate function push-pull. */
 4  gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN);
 5  gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN);
 6  gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN);
 7
 8  timer_set_mode(TIM3, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
 9  timer_disable_preload(TIM3);
10  timer_continuous_mode(TIM3);
11
12  timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER);        /* Setup TIMx_PSC register. */
13  timer_set_period(TIM3, PWM_TIMER_PERIOD);              /* Setup TIMx_ARR register. */
14  timer_set_oc_value(TIM3, TIM_OC2, PWM_TIMER_OC_VALUE); /* Setup TIMx_CCRx register. */
15  timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
16
17  timer_enable_oc_output(TIM3, TIM_OC2);
18  timer_enable_counter(TIM3);
19}

要使 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

 1static void rcc_setup(void)
 2{
 3#if defined(STM32F1)
 4  rcc_clock_setup_in_hse_8mhz_out_72mhz();
 5#elif defined(STM32F4)
 6  rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
 7#endif
 8
 9  rcc_periph_clock_enable(RCC_PWM_GPIO);
10  rcc_periph_clock_enable(RCC_TIM3);
11  rcc_periph_reset_pulse(RST_TIM3); /* Reset TIM3 to defaults. */
12}
 1static void pwm_setup(void)
 2{
 3  /* Set PWM pin to alternate function push-pull. */
 4#if defined(NUCLEO_F103RB)
 5  gpio_set_mode(GPIO_PWM_PORT,
 6                GPIO_MODE_OUTPUT_50_MHZ,
 7                GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
 8                GPIO_PWM_PIN);
 9#else
10  gpio_mode_setup(GPIO_PWM_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PWM_PIN);
11  gpio_set_output_options(GPIO_PWM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PWM_PIN);
12  gpio_set_af(GPIO_PWM_PORT, GPIO_PWM_AF, GPIO_PWM_PIN);
13#endif
14  /* 省略部分程式 */
15}

成果

我使用兩組開發板並分別設定爲頻率 1kHz, Duty Cycle 72.5% 以及頻率 2kHz, Duty Cycle 15.0%
可以看到 PWM 的輸出結果是相當精準的。

小結

這次介紹了 STM32 的 PWM 用法,PWM 是 Timer 的延伸功能,因此大部分的設定都和 Timer 有關,如果 Timer 有理解的話 PWM 應該不會太難。

參考資料

本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽



留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)

comments powered by Disqus