STM32 零基础可移植教程 06:外部中断按键,不用一直在 while 里盯着它
STM32 零基础可移植教程 06外部中断按键不用一直在 while 里盯着它前面两篇我们已经把按键这件事讲了两层第 04 篇用轮询方式读按键电平第 05 篇用软件消抖让按下一次只触发一次事件。这两种写法都需要主循环不断调用按键扫描函数。这一篇我们换一种方式外部中断。也就是让按键引脚自己“提醒”CPU按键电平发生变化了你过来看一下不过这里先说清楚外部中断不是万能药。机械按键会抖换成中断以后仍然会抖。中断只是改变“怎么发现按键变化”不代表自动解决消抖。所以这一篇只做一个明确目标按键触发 EXTI 中断主循环收到按下事件后翻转 LED中断里不直接翻转 LED只记录事件。业务动作仍然放在主循环里。本篇目标最终现象按下一次按键LED 翻转一次 按住不放LED 不会一直翻转 松开后再按才会再次翻转本篇用到的外设GPIO Input with EXTI GPIO Output NVIC SysTick / HAL_GetTick本篇跑通标准Keil 编译通过程序能下载到开发板CubeMX 里按键引脚配置为 EXTINVIC 中对应 EXTI 中断已经打开按下一次按键LED 只翻转一次能说清楚 EXTI 中断函数、HAL 回调函数、主循环事件处理之间的关系。准备工作你需要准备|项目|说明|| — | — ||STM32 开发板|任意 STM32 开发板||下载器|ST-LINK/V2 或板载 ST-LINK||LED|沿用第 02 篇 LED 工程||按键|沿用第 04/05 篇按键硬件||原理图|确认按键引脚、上拉/下拉、有效电平|建议从上一篇05_key_debounce复制一份改名为06_key_exti这样 LED、按键引脚、有效电平这些基础内容都能保留我们只把按键配置从普通输入改成外部中断。外部中断到底是什么先别急着看 CubeMX 里的 EXTI、NVIC 这些词。我们先把“中断”这个概念讲明白。前面几篇用的是轮询方式。轮询就像 CPU 在主循环里一遍遍问按键按下了吗 按键按下了吗 按键按下了吗只要你想及时发现按键变化主循环就要经常回来检查它。中断的思路不一样。中断不是 CPU 一直去问外设而是外设或硬件事件主动提醒 CPU先停一下我这里有事要处理CPU 收到这个提醒后会先暂停当前正在执行的主循环代码跳到一个专门的中断入口函数里处理这件事。处理完以后再回到刚才被打断的位置继续往下跑。可以把它理解成这样的流程主循环正常运行 -按键电平发生变化 -EXTI 产生中断请求 -NVIC 判断这个中断是否允许响应 -CPU 跳到对应的中断服务函数 -HAL 调用用户回调函数 -记录按键事件 -回到主循环继续运行这里有两个点新手一定要先记住。第一中断不是另一个while在旁边同时跑。它更像是临时打断主循环先处理一件急事处理完再回来。第二中断里不要写太多业务。比如 LED 翻转、串口打印、复杂判断这些都可以放到主循环里做。中断里最好只做一件轻量的事记录一下按键事件发生了然后主循环看到这个事件再去翻转 LED。这样程序会更稳后面加串口、定时器、ADC、DMA 时也不容易乱。理解了“中断”之后再看“外部中断”就简单了。轮询方式像这样while循环一直问按键按下了吗外部中断方式像这样按键电平变化时硬件主动通知 CPU对 STM32 来说按键接在某个 GPIO 引脚上。这个 GPIO 可以被配置成 EXTI也就是外部中断/事件线。当引脚电平出现指定变化时比如从高变低或者从低变高EXTI 就会触发中断。常见触发方式有三种|触发方式|适合场景|| — | — ||Falling Edge|高电平变低电平触发常用于低电平有效按键||Rising Edge|低电平变高电平触发常用于高电平有效按键||Rising/Falling Edge|上升沿和下降沿都触发适合同时关心按下和松开|本篇默认按键是低电平有效也就是松开高电平 按下低电平所以我们优先选择External Interrupt Mode with Falling edge trigger detection硬件连接按键硬件还是沿用前两篇。常见低电平有效按键GPIO ---- 按键 ---- GND这种情况下通常使用上拉松开读到1按下读到0常见高电平有效按键GPIO ---- 按键 ----3.3V这种情况下通常使用下拉松开读到0按下读到1如果你的按键是低电平有效本篇用 Falling Edge。如果你的按键是高电平有效就应该改成 Rising Edge并且代码里的APP_KEY_PRESSED_LEVEL也要改成GPIO_PIN_SET。CubeMX 配置步骤1. 保留 LED 输出配置LED 沿用第 02 篇配置|配置项|推荐值|| — | — ||GPIO mode|GPIO_Output||User Label|LED||初始电平|按 LED 有效电平设置为默认灭|这一篇仍然用 LED 来显示按键事件。2. 把按键引脚改成 EXTI找到按键对应的 GPIO 引脚。假设按键接在PA0点击PA0不要再选普通GPIO_Input而是选择GPIO_EXTI0或者在不同 CubeMX 版本里显示为GPIO_EXTI0 / External Interrupt Mode如果你的按键接在PB12就会对应 EXTI12。注意EXTI 是按“线号”来的。PA0、PB0、PC0都属于 EXTI0但同一个 EXTI 线通常不能同时给多个端口一起用。3. 配置按键 GPIO 参数进入System Core -GPIO找到按键引脚重点确认这些项|配置项|低电平有效按键推荐值|| — | — ||GPIO mode|External Interrupt Mode with Falling edge trigger detection||GPIO Pull-up/Pull-down|Pull-up||User Label|KEY|如果你的按键是高电平有效通常改成|配置项|高电平有效按键推荐值|| — | — ||GPIO mode|External Interrupt Mode with Rising edge trigger detection||GPIO Pull-up/Pull-down|Pull-down||User Label|KEY|4. 打开 NVIC 中断配置 EXTI 后还要打开 NVIC。在 CubeMX 左侧找到System Core -NVIC然后根据你的按键引脚勾选对应的 EXTI 中断。常见对应关系|按键引脚|常见中断名|| — | — ||Px0|EXTI line0 interrupt||Px1|EXTI line1 interrupt||Px2|EXTI line2 interrupt||Px3|EXTI line3 interrupt||Px4|EXTI line4 interrupt||Px5 ~ Px9|EXTI line[9:5] interrupts||Px10 ~ Px15|EXTI line[15:10] interrupts|比如按键是PB12就要打开EXTI line[15:10]interrupts如果你忘了打开 NVICGPIO 配成 EXTI 也没用中断函数不会进。5. 生成 Keil 工程配置完成后点击GENERATE CODE打开 Keil 工程先编译一次确认没有错误。Keil 工程生成和编译打开 Keil 后先编译Build / F7确认输出里没有错误0Error(s)然后可以打开 CubeMX 生成的中断文件看一眼Core/Src/stm32f1xx_it.c不同芯片系列文件名会不一样比如stm32f4xx_it.c stm32g4xx_it.c如果按键是PA0你会看到类似void EXTI0_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(KEY_Pin);}如果按键是PB12可能是void EXTI15_10_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(KEY_Pin);}这段是 CubeMX 自动生成的一般不要手动乱改。完整代码这一篇我们继续保留 LED 模块Core/Inc/app_led.h Core/Src/app_led.c按键模块改成 EXTI 事件版本Core/Inc/app_key.h Core/Src/app_key.c中断里只做一件事记录“按键按下事件”。LED 翻转仍然放在主循环里做。1. 更新Core/Inc/app_key.h打开Core/Inc/app_key.h替换为下面代码#ifndef APP_KEY_H#define APP_KEY_H#include main.htypedef enum{APP_KEY_EVENT_NONE0, APP_KEY_EVENT_PRESSED}App_KeyEvent;void App_Key_EXTI_Init(void);App_KeyEvent App_Key_GetEvent(void);#endif这里我们只保留“按下事件”。松开事件以后再扩展也可以但本篇目标是按下一次翻转 LED一篇不要贪多。2. 更新Core/Src/app_key.c打开Core/Src/app_key.c替换为下面代码#include app_key.h#ifndef KEY_GPIO_Port#error KEY_GPIO_Port is not defined. Set the key pin User Label to KEY in CubeMX.#endif#ifndef KEY_Pin#error KEY_Pin is not defined. Set the key pin User Label to KEY in CubeMX.#endif/* * Default: active-low key. * If your key is active-high, change this macro to GPIO_PIN_SET * andsetCubeMX EXTI trigger to Rising edge. */#ifndef APP_KEY_PRESSED_LEVEL#define APP_KEY_PRESSED_LEVEL GPIO_PIN_RESET#endif#ifndef APP_KEY_EXTI_DEBOUNCE_MS#define APP_KEY_EXTI_DEBOUNCE_MS 20u#endifstatic volatile uint8_t s_key_pressed_event0u;static uint32_t s_last_exti_tick0u;void App_Key_EXTI_Init(void){s_key_pressed_event0u;s_last_exti_tickHAL_GetTick();}App_KeyEvent App_Key_GetEvent(void){App_KeyEvent eventAPP_KEY_EVENT_NONE;__disable_irq();if(s_key_pressed_event!0u){s_key_pressed_event0u;eventAPP_KEY_EVENT_PRESSED;}__enable_irq();returnevent;}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){uint32_t now;if(GPIO_Pin!KEY_Pin){return;}nowHAL_GetTick();if((now - s_last_exti_tick)APP_KEY_EXTI_DEBOUNCE_MS){return;}s_last_exti_ticknow;if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin)APP_KEY_PRESSED_LEVEL){s_key_pressed_event1u;}}这里用了 __disable_irq() / __enable_irq() 临时保护事件变量避免主循环读取时被中断打断。它属于“临界区”问题本篇先按固定写法使用后面会单独开一篇解释它的原理和使用边界。这段代码有几个重点HAL_GPIO_EXTI_Callback()是 HAL 提供的外部中断回调函数。CubeMX 生成的中断函数会先进入EXTIx_IRQHandler()。EXTIx_IRQHandler()里会调用HAL_GPIO_EXTI_IRQHandler(KEY_Pin)。HAL 处理完中断标志后会调用我们的HAL_GPIO_EXTI_Callback()。回调里不直接翻转 LED只设置s_key_pressed_event。主循环通过App_Key_GetEvent()取走事件再执行 LED 翻转。为什么不在中断里直接翻转 LED因为实际工程里中断函数越短越好。中断里适合做“记录事件”不适合做一堆业务动作。现在只是翻 LED 看起来没事后面如果换成串口打印、Flash 写入、复杂状态机就容易把工程搞乱。3. 确认app_key.c加入 Keil 工程如果你是从上一篇复制工程过来app_key.c应该已经在 Keil 工程里。还是建议确认一下Application/User/Core里面应该能看到app_key.c app_led.c如果没有就右键Application/User/Core选择Add Existing Files to GroupApplication/User/Core然后添加Core/Src/app_key.cmain.c 调用方式1. Includes 区域找到/*USERCODE BEGIN Includes */ /*USERCODE END Includes */改成/*USERCODE BEGIN Includes */#include app_led.h#include app_key.h/*USERCODE END Includes */2. 初始化区域确保MX_GPIO_Init()已经在前面执行MX_GPIO_Init();然后在USER CODE BEGIN 2里添加/*USERCODE BEGIN2*/ App_LED_Init();App_Key_EXTI_Init();/*USERCODE END2*/3. while 循环区域主循环只做事件处理while(1){/*USERCODE END WHILE */ /*USERCODE BEGIN3*/if(App_Key_GetEvent()APP_KEY_EVENT_PRESSED){App_LED_Toggle();}/*USERCODE END3*/}这里不需要一直HAL_GPIO_ReadPin()读按键也不需要放HAL_Delay(5)扫描。主循环可以很快地转等中断来了以后事件标志就会被置位。编译、下载和验证代码加完后先编译Build / F7如果没有错误再下载Download下载后观察现象按下一次按键 -LED 翻转一次 按住不放 -LED 不连续翻转 松开再按 -LED 再翻转一次如果按键没有反应不要马上改代码。先查 CubeMX 的 EXTI 模式和 NVIC 是否打开。移植到其他板子的修改点这篇的移植点主要有 7 个。|要改的地方|为什么要改|在哪里改|| — | — | — ||按键引脚|不同板子的按键接到不同 GPIO|CubeMX Pinout 页面||EXTI 线号|Px0/Px1/Px12 对应不同 EXTI 中断|CubeMX 自动生成对照 NVIC||触发边沿|低电平有效用 Falling高电平有效用 Rising|CubeMX GPIO mode||Pull-up/Pull-down|按键接 GND 还是 3.3V 不同|CubeMX GPIO Pull-up/Pull-down||User Label|代码依赖KEY_Pin和KEY_GPIO_Port|CubeMX GPIO 页面标签设为KEY||有效电平|回调里会再次确认当前是否真按下|APP_KEY_PRESSED_LEVEL||消抖时间|不同按键抖动时间不同|APP_KEY_EXTI_DEBOUNCE_MS|推荐移植顺序看原理图确认按键接到哪个 MCU 引脚确认按下时是高电平还是低电平CubeMX 把该引脚设置成GPIO_EXTI按硬件选择 Rising 或 Falling Edge按硬件选择 Pull-up 或 Pull-downUser Label 填KEYNVIC 勾选对应 EXTI line interrupt修改APP_KEY_PRESSED_LEVEL编译下载用 LED 验证。常见问题排查1. 按键完全没反应优先检查|优先检查|具体方法|| — | — ||是否配置成 EXTI|CubeMX Pinout 里不是普通GPIO_Input||NVIC 是否打开|System Core - NVIC勾选对应 EXTI||触发边沿是否正确|低电平有效通常 Falling高电平有效通常 Rising||User Label 是否为KEY|main.h里应有KEY_Pin和KEY_GPIO_Port||app_key.c是否加入工程|Keil 工程树里确认有app_key.c||App_Key_EXTI_Init()是否调用|放在MX_GPIO_Init()后面|2. 进了中断但 LED 不翻转优先检查主循环里是否调用App_Key_GetEvent()是否只在中断里设置了事件但主循环没有处理app_led.c是否加入工程LED 有效电平是否配置正确App_LED_Init()是否在MX_GPIO_Init()后面调用。3. 按一次还是触发好几次机械按键在中断方式下仍然会抖。优先调整#define APP_KEY_EXTI_DEBOUNCE_MS 20u如果还是多次触发可以试#define APP_KEY_EXTI_DEBOUNCE_MS 30u或者#define APP_KEY_EXTI_DEBOUNCE_MS 50u注意消抖时间太长会让快速连按变迟钝。一般先从 20 ms 开始。4. 编译报HAL_GPIO_EXTI_Callback重复定义说明你的工程里已经有另一个文件实现了void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)一个工程里不能有两个同名非 weak 函数。解决方法保留一个统一的HAL_GPIO_EXTI_Callback()在这个回调里按GPIO_Pin分发给不同模块不要在多个.c文件里各写一个同名回调。比如可以统一写成void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){App_Key_EXTI_Callback(GPIO_Pin);}这种结构后面外设多了会更清楚。本篇为了新手少绕一层直接把回调写在app_key.c里。5. 编译报KEY_GPIO_Port is not defined说明 CubeMX 没有生成KEY_GPIO_Port KEY_Pin去Core/Inc/main.h看一下。如果是KEY0_Pin或USER_KEY_Pin就说明 User Label 不是KEY。解决方法回 CubeMX找到按键 GPIO把 User Label 改成KEY重新 Generate Code。6. 短按偶尔没反应优先检查触发边沿是否选对按键硬件是否接触不良消抖时间是否设置太长是否在别的地方长时间关闭中断是否在中断里做了太多耗时操作。本篇代码没有在中断里做耗时动作这是一个好习惯。本篇小结这一篇我们把按键从“主循环扫描”升级到了“外部中断触发”。你现在至少应该知道EXTI 是外部中断/事件线用来捕捉 GPIO 电平变化低电平有效按键通常选 Falling Edge高电平有效按键通常选 Rising EdgeCubeMX 里除了 GPIO_EXTI还要打开 NVICEXTIx_IRQHandler()是中断入口HAL_GPIO_EXTI_Callback()是 HAL 给用户留的回调中断里尽量只记录事件主循环里再处理业务机械按键换成中断以后仍然需要消抖。下一篇我们开始进入串口STM32 USART 串口打印从 CubeMX 配置到 printf 重定向。串口是后面调试所有外设的基础。有了串口输出很多问题就不用靠猜了。