前言
在之前的文章中我們使用輪詢的方式來讀取目前的按鈕狀態,但這種方式的效率不是很好,在需要讀取按鈕狀態等情況下,我們可以使用外部中斷(External Interrupt,EXTI),讓 CPU 可以去忙其它事情,等到按鈕被按下時會產生中斷事件,才去執行按鈕被按下時要處理的事。
這次要我們的目標功能是每次按下按鈕後,LED 的閃爍速度就會變化。
正文
首先一樣以 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
|
#include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/exti.h> #include <libopencm3/cm3/nvic.h>
#define RCC_LED_GPIO (RCC_GPIOA) #define GPIO_LED_PORT (GPIOA) #define GPIO_LED_PIN (GPIO5)
#define RCC_BUTTON_GPIO (RCC_GPIOC) #define GPIO_BUTTON_PORT (GPIOC) #define GPIO_BUTTON_PIN (GPIO13) #define EXTI_BUTTON_SOURCE (EXTI13) #define NVIC_BUTTON_IRQ (NVIC_EXTI15_10_IRQ)
#define DELAY_VALUE_A ((uint32_t)500000) #define DELAY_VALUE_B ((uint32_t)200000)
uint32_t delay_value = DELAY_VALUE_A;
static void delay(uint32_t value) { for (uint32_t i = 0; i < value; i++) { __asm__("nop"); } }
static void led_setup(void) { gpio_mode_setup(GPIO_LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_LED_PIN); gpio_set_output_options(GPIO_LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_LED_PIN); }
static void button_setup(void) { gpio_mode_setup(GPIO_BUTTON_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_BUTTON_PIN);
nvic_enable_irq(NVIC_BUTTON_IRQ); exti_select_source(EXTI_BUTTON_SOURCE, GPIO_BUTTON_PORT); exti_set_trigger(EXTI_BUTTON_SOURCE, EXTI_TRIGGER_FALLING); exti_enable_request(EXTI_BUTTON_SOURCE); }
static void rcc_setup(void) { rcc_periph_clock_enable(RCC_LED_GPIO); rcc_periph_clock_enable(RCC_BUTTON_GPIO); rcc_periph_clock_enable(RCC_SYSCFG); }
int main(void) { rcc_setup(); led_setup(); button_setup();
while (1) { gpio_toggle(GPIO_LED_PORT, GPIO_LED_PIN); delay(delay_value); } return 0; }
void exti15_10_isr(void) { if (exti_get_flag_status(EXTI_BUTTON_SOURCE)) { exti_reset_request(EXTI_BUTTON_SOURCE);
if (delay_value == DELAY_VALUE_A) { delay_value = DELAY_VALUE_B; } else { delay_value = DELAY_VALUE_A; } } }
|
分段說明
Include
1 2 3 4
| #include <libopencm3/stm32/rcc.h> #include <libopencm3/stm32/gpio.h> #include <libopencm3/stm32/exti.h> #include <libopencm3/cm3/nvic.h>
|
除了基本的 rcc.h
和 gpio.h
外,還要引入 exti.h
與 nvic.h
。
exti.h
當然就是包含了 EXTI 的各種函式。nvic.h
是嵌套向量中斷控制器(Nested Vectored Interrupt Controller,NVIC),這是一個 ARM Cortex-M 中負責處理中斷的控制器,有用到中斷的話都會需要它。
注意是 libopencm3/cm3/nvic.h
,而不是 libopencm3/stm32/nvic.h
。
設定腳位
1 2 3 4 5 6 7 8 9 10 11
| #define RCC_LED_GPIO (RCC_GPIOA) #define GPIO_LED_PORT (GPIOA) #define GPIO_LED_PIN (GPIO5)
#define RCC_BUTTON_GPIO (RCC_GPIOC) #define GPIO_BUTTON_PORT (GPIOC) #define GPIO_BUTTON_PIN (GPIO13) #define EXTI_BUTTON_SOURCE (EXTI13) #define NVIC_BUTTON_IRQ (NVIC_EXTI15_10_IRQ)
|
這次處理要定義 RCC 與腳位外,還一併設定了按鈕的 IRQ 與 EXTI 來源。因為按鈕是 PC13,所以 IRQ 是 NVIC_EXTI15_10_IRQ
,它負責處理 EXTI 15 ~ 10,而我們實際會觸發的是 EXTI13
。
設定中斷
1 2 3 4 5 6 7 8 9 10
| static void button_setup(void) {
nvic_enable_irq(NVIC_BUTTON_IRQ); exti_select_source(EXTI_BUTTON_SOURCE, GPIO_BUTTON_PORT); exti_set_trigger(EXTI_BUTTON_SOURCE, EXTI_TRIGGER_FALLING); exti_enable_request(EXTI_BUTTON_SOURCE); }
|
nvic_enable_irq()
:致能指定的 IRQ。
exti_select_source()
:選擇 EXTI 的來源。
exti_set_trigger()
:設定觸發方式。這裡使用的是 EXTI_TRIGGER_FALLING
,即負緣觸發,還可以選擇 EXTI_TRIGGER_RISING
(正緣觸發)或 EXTI_TRIGGER_BOTH
(正/負緣都觸發)。
exti_enable_request()
:致能 EXTI IRQ。
中斷服務程式 ISR
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
void exti15_10_isr(void) { if (exti_get_flag_status(EXTI_BUTTON_SOURCE)) { exti_reset_request(EXTI_BUTTON_SOURCE);
if (delay_value == DELAY_VALUE_A) { delay_value = DELAY_VALUE_B; } else { delay_value = DELAY_VALUE_A; } } }
|
exti_reset_request()
可以用來清除 IRQ flag。
由於 EXTI 15 ~ 10 共用一個 ISR,所以還要再用 exti_get_flag_status()
來讀取 EXTI_PR 暫存器的值,以確定目前是哪一個 EXTI Line 被觸發。
在 LibOpenCM3 中,各個功能的 ISR 函式名稱是固定的,如果打錯的話就無法正常執行。完整的 STM32F4 系列的 ISR 列表在此。
RCC
1 2 3 4 5 6
| static void rcc_setup(void) { rcc_periph_clock_enable(RCC_LED_GPIO); rcc_periph_clock_enable(RCC_BUTTON_GPIO); rcc_periph_clock_enable(RCC_SYSCFG); }
|
比較要注意的是,RCC 除了 GPIO Port 外,還要致能 RCC_SYSCFG
,否則 EXTI 不會工作。
多環境程式(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) { rcc_periph_clock_enable(RCC_LED_GPIO); rcc_periph_clock_enable(RCC_BUTTON_GPIO);
#if defined(STM32F1) rcc_periph_clock_enable(RCC_AFIO); #else rcc_periph_clock_enable(RCC_SYSCFG); #endif }
|
1 2 3 4 5 6 7 8 9 10
| static void led_setup(void) { #if defined(STM32F1) gpio_set_mode(GPIO_LED_PORT, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO_LED_PIN); #else gpio_mode_setup(GPIO_LED_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_LED_PIN); gpio_set_output_options(GPIO_LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_LED_PIN); #endif }
|
1 2 3 4 5 6 7 8 9 10 11
| static void button_setup(void) { #if defined(STM32F1) gpio_set_mode(GPIO_BUTTON_PORT, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_BUTTON_PIN); #else gpio_mode_setup(GPIO_BUTTON_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_BUTTON_PIN); #endif
}
|
小結
這次簡單介紹了 EXTI 的實際程式。中斷是很基本也實用的功能,而外部中斷 EXTI 也是中斷中比較單純且常用的,希望大家看完後也會使用 EXTI 了。
實際上 STM32 的中斷還要許多細節我沒寫到,因為本篇主要還是希望大家可以最快速入門,因此就先省略了。
參考資料
本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽。