[LibOpenCM3 × STM32教學-4] 輸出PWM並控制Duty Cycle及頻率

系列:LibOpenCM3 × STM32教學 Posted on 2022-04-26

我在 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 * @file   main.c
 3 * @brief  Basic PWM(Pulse-width modulation) output example.
 4 */
 5
 6#include <libopencm3/stm32/rcc.h>
 7#include <libopencm3/stm32/gpio.h>
 8#include <libopencm3/stm32/timer.h>
 9
10#define PWM_FREQUENCY (1000)  /* PWM frequency in Hz. */
11#define PWM_DUTY_CYCLE (72.5) /* PWM duty-cycle in %. */
12
13/*
14 * PER = {f_tim / [(PRS + 1) * f_pwm]} - 1
15 * 
16 * f_pwm: PWM frequency.
17 * f_tim: Timer frequency. The value is 'rcc_apb1_frequency * 2' equal 48MHz in this case.
18 * PRS:   PWM timer prescaler.
19 * PER:   PWM timer period.
20 */
21#define PWM_TIMER_PRESCALER (48 - 1)
22#define PWM_TIMER_PERIOD (((rcc_apb1_frequency * 2) / ((PWM_TIMER_PRESCALER + 1) * PWM_FREQUENCY)) - 1)
23
24void gpio_setup(void)
25{
26  /* Timer3-Channel2 on PA7 (NUCLEO-F103RB). */
27  rcc_periph_clock_enable(RCC_GPIOA);
28  gpio_set_mode(GPIOA,
29                GPIO_MODE_OUTPUT_50_MHZ,
30                GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
31                GPIO7);
32}
33
34/**
35 * @brief Setup PWM on Timer3-Channel2.
36 */
37void pwm_setup(void)
38{
39  rcc_periph_clock_enable(RCC_TIM3);
40
41  timer_set_mode(TIM3,
42                 TIM_CR1_CKD_CK_INT,
43                 TIM_CR1_CMS_EDGE,
44                 TIM_CR1_DIR_UP);
45  timer_disable_preload(TIM3);
46  timer_continuous_mode(TIM3);
47
48  timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER);
49  timer_set_period(TIM3, PWM_TIMER_PERIOD);
50
51  timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
52  timer_set_oc_value(TIM3,
53                     TIM_OC2,
54                     (PWM_TIMER_PERIOD + 1) * (PWM_DUTY_CYCLE / 100.0));
55
56  timer_enable_oc_output(TIM3, TIM_OC2);
57  timer_enable_counter(TIM3);
58}
59
60int main(void)
61{
62  /* Setup system clock = 48MHz. */
63  rcc_clock_setup_in_hsi_out_48mhz();
64
65  gpio_setup();
66  pwm_setup();
67
68  /* Halt. */
69  while (1)
70  {
71    __asm__("nop");
72  }
73
74  return 0;
75}

程式分段說明

引入函式庫

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

除了基本的「RCC」及「GPIO」外,由於 PWM 是透過 Timer 所實現的,所以還要引入「Timer」。

PWM 數值宣告

1#define PWM_FREQUENCY (1000)  /* PWM frequency in Hz. */
2#define PWM_DUTY_CYCLE (72.5) /* PWM duty-cycle in %. */

宣告 PWM 的目標頻率及 Duty Cycle 以供後續計算。

PWM 頻率設定

1#define PWM_TIMER_PRESCALER (48 - 1)
2#define PWM_TIMER_PERIOD (((rcc_apb1_frequency * 2) / ((PWM_TIMER_PRESCALER + 1) * PWM_FREQUENCY)) - 1)

這裡算是本文的重點之一,也就是如何設定 PWM 的頻率。

首先:

1f_pwm = f_tim / [(PRS + 1) * (PER + 1)]

所以:

1PER = {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 設定

1void gpio_setup(void)
2{
3  /* Timer3-Channel2 on PA7 (NUCLEO-F103RB). */
4  rcc_periph_clock_enable(RCC_GPIOA);
5  gpio_set_mode(GPIOA,
6                GPIO_MODE_OUTPUT_50_MHZ,
7                GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
8                GPIO7);
9}

設定 GPIO 以輸出 PWM 訊號。注意,每個 Timer 的 Channel 能輸出的 GPIO 腳位都不同,像這裡「Timer 3」的「Channel 2」是對應「PA7」,在使用不同 Timer、Channel 時請參考 STM32 Datasheet。

PWM Timer 設定

 1void pwm_setup(void)
 2{
 3  rcc_periph_clock_enable(RCC_TIM3);
 4
 5  timer_set_mode(TIM3,
 6                 TIM_CR1_CKD_CK_INT,
 7                 TIM_CR1_CMS_EDGE,
 8                 TIM_CR1_DIR_UP);
 9  timer_disable_preload(TIM3);
10  timer_continuous_mode(TIM3);
11
12  timer_set_prescaler(TIM3, PWM_TIMER_PRESCALER);
13  timer_set_period(TIM3, PWM_TIMER_PERIOD);
14
15  timer_set_oc_mode(TIM3, TIM_OC2, TIM_OCM_PWM1);
16  timer_set_oc_value(TIM3,
17                     TIM_OC2,
18                     (PWM_TIMER_PERIOD + 1) * (PWM_DUTY_CYCLE / 100.0));
19
20  timer_enable_oc_output(TIM3, TIM_OC2);
21  timer_enable_counter(TIM3);
22}

這裡也是本文的重點之一,在這裡設定好要使用的 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。

主程式

 1int main(void)
 2{
 3  /* Setup system clock = 48MHz. */
 4  rcc_clock_setup_in_hsi_out_48mhz();
 5
 6  gpio_setup();
 7  pwm_setup();
 8
 9  /* Halt. */
10  while (1)
11  {
12    __asm__("nop");
13  }
14
15  return 0;
16}

主程式先將系統時鐘設定為 48 MHz,並依序完成 GPIO 及 PWM 的初始化設定,在執行完 pwm_setup() 後就會開始輸出 PWM 了,最後就進入一個無限空迴圈。

成果

▲ 輸出波形圖

▲ 輸出波形圖

可以看到輸出的波形誤差很小(設定值爲:Duty Cycle 72.5%,頻率 1 kHz)。

結語

本次的程式我一樣有放在 GitHub 上,可以使用 PlatformIO 開始專案。

若有什麼問題或錯誤歡迎留言討論。

相關連結



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

comments powered by Disqus