前言
在前面的篇章中,我們已經學會使用 Timer 來精確定時了,而在使用 MCU 的過程中最常會需要精確定時的莫過於 delay()
函式,在此之前我都是單純的讓 MCU 空跑一定的次數,但這樣很難知道它實際上到底 delay 了多久的時間,而已同樣的數值在不同的 Clock Tree 設定下 delay 的長度也不同,因此我們可以使用 Timer 來做出一個更好的 delay()
。
但是如果只是要實現 delay()
功能的話,並不用像之前的 Timer 那樣計算並設定一大堆數值,因爲 ARM Cortex M3 有一個特殊的計時器——SysTick,我們可以使用它來完成 delay()
函式。
這次的目標是使用 SysTick 來實現一個 delay_ms()
函式,它可以以毫秒爲單位進行 delay,並且用來寫 LED 閃爍的程式。
正文
首先一樣以 Nucleo-F446RE 做示範。
首先建立一個 PIO 的專案,選擇 Framework 爲「libopencm3」,並在 src/
資料夾中新增並開啓 main.c
檔案。
完整程式
1/**
2 * @file main.c
3 * @brief SysTick delay example for STM32 Nucleo-F446RE
4 */
5
6#include <libopencm3/stm32/rcc.h>
7#include <libopencm3/stm32/gpio.h>
8#include <libopencm3/cm3/systick.h>
9#include <libopencm3/cm3/nvic.h>
10
11#define RCC_LED_GPIO (RCC_GPIOA)
12#define GPIO_LED_PORT (GPIOA)
13#define GPIO_LED_PIN (GPIO5) /* D13. */
14
15static volatile uint32_t systick_delay = 0;
16
17static void delay_ms(uint32_t ms)
18{
19 systick_delay = ms;
20 while (systick_delay != 0)
21 {
22 /* Wait. */
23 }
24}
25
26static void rcc_setup(void)
27{
28 rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
29 rcc_periph_clock_enable(RCC_LED_GPIO);
30}
31
32static void led_setup(void)
33{
34 /* Set LED pin to output push-pull. */
35 gpio_mode_setup(GPIO_LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_LED_PIN);
36 gpio_set_output_options(GPIO_LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_LED_PIN);
37}
38
39static void systick_setup(void)
40{
41 systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8);
42 systick_set_reload(rcc_ahb_frequency / 8 / 1000 - 1);
43
44 systick_counter_enable();
45 systick_interrupt_enable();
46}
47
48int main(void)
49{
50 rcc_setup();
51 systick_setup();
52 led_setup();
53
54 while (1)
55 {
56 gpio_toggle(GPIO_LED_PORT, GPIO_LED_PIN); /* LED on/off. */
57 delay_ms(500);
58 }
59
60 return 0;
61}
62
63/**
64 * @brief SysTick handler.
65 */
66void sys_tick_handler(void)
67{
68 if (systick_delay != 0)
69 {
70 systick_delay--;
71 }
72}
分段說明
Include
1#include <libopencm3/stm32/rcc.h>
2#include <libopencm3/stm32/gpio.h>
3#include <libopencm3/cm3/systick.h>
4#include <libopencm3/cm3/nvic.h>
重點在於要記得引入 systick.h
。值得注意的是如果不引入 nvic.h
的話,程式應該也可以完成編譯甚至執行,但 SysTick 的 ISR 函式原型其實是宣告在這裡面的,所以我還是把它加入。
設定 SysTick
1static void systick_setup(void)
2{
3 systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8);
4 systick_set_reload(rcc_ahb_frequency / 8 / 1000 - 1);
5
6 systick_counter_enable();
7 systick_interrupt_enable();
8}
SysTick(System tick timer)是 ARM Cortex M3 系列內建的功能,這是一個 24 位元的下數計數器,擁有自動裝載與中斷功能。
從 Clock Tree 中可以看到 SysTick 在 AHB 底下,並且前面有一個可程式設定的預分頻器(圖上雖然看起來是固定 /8
,但根據我實測的結果與 STM32CubeMX 中顯示的設定,這應該是可以選擇 /1
或 /8
)。
這裡使用 systick_set_clocksource()
來指定 SysTick 時鐘源爲 AHB 並啓用 /8
預分頻器,因此目前的 SysTick 頻率爲 rcc_ahb_frequency / 8
。
然後要設定 SysTick 的 RVR(Reload Value Register)暫存器,這個數值決定了 SysTick 發生中斷的頻率,SysTick 每次下數到 0 時都會自動載入 RVR 的值到 CVR(Current Value Register)中,所以 SysTick 會計數 RVR~0 共 RVR + 1 次。
所以 SysTick 中斷發生的頻率爲:f_int = f_systick / (RVR + 1)
因此RVR = f_systick / f_int - 1
最後只要套用上面的公式,並用 systick_set_reload()
設定 RVR 的值就好。因爲這裡預計要實現 ms 等級的 delay,所以我希望 SysTick 可以每 1 ms 就中斷一次,也就是中斷頻率爲 1 kHz,故設定 RVR 的值爲 rcc_ahb_frequency / 8 / 1000 - 1
。
RVR 是一個 24 位元的暫存器,它的容許範圍爲
0x000001
~0xFFFFFF
,實際在設定時要注意一下。官方說明
Delay 與 SysTick ISR
1static volatile uint32_t systick_delay = 0;
2
3static void delay_ms(uint32_t ms)
4{
5 systick_delay = ms;
6 while (systick_delay != 0)
7 {
8 /* Wait. */
9 }
10}
11
12/**
13 * @brief SysTick handler.
14 */
15void sys_tick_handler(void)
16{
17 if (systick_delay != 0)
18 {
19 systick_delay--;
20 }
21}
首先宣告一個全域變數 systick_delay
,並加上 volatile
以防止編譯器優化它。
delay_ms()
要做的就是把其參數 ms
傳遞給 systick_delay
,然後等待 sys_tick_handler()
將 systick_delay
的值減到 0。
而 sys_tick_handler()
是 SysTick 的 ISR,它只要負責每次都把 systick_delay
減 1 即可。
多環境程式(F446RE + F103RB)
由於 STM32F1 的部分函式不同,所以 F103RB 沒辦法直接使用上面的 F446RE 的程式。
以下列出主要的差異部分。完整的程式請看 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_LED_GPIO);
10}
1static void led_setup(void)
2{
3 /* Set LED pin to output push-pull. */
4#if defined(STM32F1)
5 gpio_set_mode(GPIO_LED_PORT,
6 GPIO_MODE_OUTPUT_2_MHZ,
7 GPIO_CNF_OUTPUT_PUSHPULL,
8 GPIO_LED_PIN);
9#else
10 gpio_mode_setup(GPIO_LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_LED_PIN);
11 gpio_set_output_options(GPIO_LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_LED_PIN);
12#endif
13}
成果
這裡使用兩個 STM32,並分別設定 LED 開關的 delay 爲 500ms 和 5ms,結果也是滿精準的。
請注意 LED 要切換 2 次才是一個完整的波形。
小結
delay_ms()
是在用 MCU 時非常常用到的功能,而這次介紹如何使用 SysTick 來實現它,這樣就可以得到一個相對精準的 delay,也不用大費周章去設定一般的 Timer。
參考資料
- Cortex-M3 Devices Generic User Guide
- libopencm3/libopencm3-examples
- platformio/platform-ststm32
- STM32F446RE datasheet (DS10693)
- STM32F446xx reference manual (RM0390)
- STM32F103RB datasheet (DS5319)
- STM32 Nucleo-64 board user manual (UM1724)
本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽。
留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)
comments powered by Disqus