--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第7章:按键与中断系统 + 配套代码05~06" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # GPIO输入与NVIC中断系统 > **用生活理解**:中断就像你正在写代码,突然电话响了——你暂停写代码、接电话、处理完、挂电话、继续写代码。按键中断就是"按键按下 → 暂停主程序 → 处理按键 → 恢复主程序"。NVIC 就像一个智能电话秘书,知道哪个电话更重要、该先接哪个。 --- ## 中断系统概述 ### 为什么需要中断 如果没有中断,CPU 只能轮询检测按键状态——就像一直盯着电话看它响不响,浪费 CPU 时间。中断机制允许外设在需要时**主动通知 CPU**,CPU 不用一直等。 ### STM32中断体系架构 STM32F103 的中断体系由两部分组成: | 部件 | 功能 | 参考手册章节 | | ------------------------------ | ---------------------------------- | ------------ | | **NVIC** (嵌套向量中断控制器) | 中断优先级管理、使能/屏蔽 | §8 NVIC | | **EXTI** (外部中断/事件控制器) | 管理 GPIO 引脚的边沿检测与中断请求 | §9 EXTI | 中断响应过程: ``` GPIO引脚电平变化 → EXTI检测边沿 → NVIC判断优先级 → CPU暂停当前任务 → 跳转到中断向量表 → 执行中断服务函数(ISR) → 返回主程序 ``` ### 中断优先级 NVIC 支持 16 级可编程中断优先级(0 最高,15 最低)。 **优先级分组**:4 位优先级可拆分为"抢占优先级 + 子优先级" | 分组 | 分配 | 抢占优先级位数 | 子优先级位数 | | -------------------- | ------------------- | -------------- | ------------ | | NVIC_PriorityGroup_0 | 0位抢占 + 4位子优先 | 0 | 4 | | NVIC_PriorityGroup_1 | 1位抢占 + 3位子优先 | 2级 | 8级 | | NVIC_PriorityGroup_2 | 2位抢占 + 2位子优先 | 4级 | 4级 | | NVIC_PriorityGroup_3 | 3位抢占 + 1位子优先 | 8级 | 2级 | | NVIC_PriorityGroup_4 | 4位抢占 + 0位子优先 | 16级 | 0 | > **规则**:抢占优先级高的可以打断低的;相同抢占优先级下,子优先级高的先执行但不打断。 ### EXTI 外部中断 STM32F103 有 20 个 EXTI 通道(EXTI0~EXTI19),每个 GPIO 引脚都可以配置为 EXTI 输入。 **EXTI配置流程**: ``` 1. GPIO配置(输入模式) → 2. AFIO选择中断线 → 3. EXTI配置(触发边沿+使能) → 4. NVIC配置(优先级+使能) ``` **EXTI 关键寄存器**: | 寄存器 | 功能 | 位说明 | | ------ | ------------------ | ---------------------- | | IMR | 中断屏蔽寄存器 | 1=允许中断请求,0=禁止 | | EMR | 事件屏蔽寄存器 | 1=允许事件请求,0=禁止 | | RTSR | 上升沿触发选择 | 1=允许上升沿触发 | | FTSR | 下降沿触发选择 | 1=允许下降沿触发 | | SWIER | 软件中断事件寄存器 | 写1=软件触发中断 | | PR | 挂起寄存器 | 1=有中断请求,写1清除 | > **参考**:参考手册 §9(EXTI 寄存器描述)+ §8(NVIC 寄存器描述) --- ## 按键输入硬件电路 ![KEY电路原理图](assets/key_电路原理图.png) 开发板上有 4 个按键: | 按键 | 对应引脚 | 连接方式 | 按下时电平 | | ------------ | -------- | -------- | ---------- | | KEY_UP (SW4) | PF10 | 上拉输入 | 低变高 | | KEY_1 (SW2) | ? | 外部电路 | 取决于设计 | | KEY_2 | ? | 外部电路 | 取决于设计 | | KEY_3 (SW5) | PF10? | 上拉输入 | 低变高 | > **说明**:PF10 对应的 KEY 为上拉输入模式——按键未按下时 PF10=低(下拉到GND),按下时 PF10=高(上拉到 VCC)。 **消抖**:机械按键在按下/释放瞬间会产生约 5~10ms 的抖动(电平快速跳变),需要软件消抖: ``` 理想波形: __┃┃________ 实际波形: __┃_┃_┃┃_┃_┃____ ← 抖动 软件消抖: 检测到边沿 → 等待15ms → 再次确认电平 ``` --- ## 实验:按键控制LED(中断方式) ### 需求描述 按下 KEY_1(PF10),翻转 LED1 的开关状态。使用外部中断 EXTI15_10 实现。 ### 软件设计(寄存器版) **项目路径**:`stm32/05_key_led_register` **文件:`stm32/05_key_led_register/Hardware/KEY/key.h`** ```c #ifndef __KEY_H #define __KEY_H #include "stm32f10x.h" void Key_Init(void); #endif ``` **文件:`stm32/05_key_led_register/Hardware/KEY/key.c`** ```c #include "key.h" #include "delay.h" #include "led.h" // KEY初始化:配置PF10为上拉输入 + EXTI中断 void Key_Init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPFEN; RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; GPIOF->CRH &= ~GPIO_CRH_MODE10; GPIOF->CRH |= GPIO_CRH_CNF10_1; GPIOF->CRH &= ~GPIO_CRH_CNF10_0; GPIOF->ODR &= ~GPIO_ODR_ODR10; AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF; EXTI->RTSR |= EXTI_RTSR_TR10; EXTI->IMR |= EXTI_IMR_MR10; NVIC_SetPriorityGrouping(3); NVIC_SetPriority(EXTI15_10_IRQn, 3); NVIC_EnableIRQ(EXTI15_10_IRQn); } // EXTI15_10中断服务函数(10~15共用一个中断入口) void EXTI15_10_IRQHandler(void) { /* 1. 清除中断标志位 */ // PR寄存器的对应位写1清除挂起标志 // 如果不清除,退出中断后立即再次进入 EXTI->PR |= EXTI_PR_PR10; // |=: PR10写1清除 /* 2. 软件消抖 */ Delay_ms(10); // 等待10ms避开抖动期 /* 3. 再次确认按键确实按下 */ // 读取IDR判断PF10是否仍为高电平(屏蔽状态) if ((GPIOF->IDR & GPIO_IDR_IDR10) != 0) // &测试: IDR10位是否为1 { // 翻转LED1状态 LED_Toggle(LED1); } } ``` **文件:`stm32/05_key_led_register/User/main.c`** ```c #include "key.h" #include "led.h" int main(void) { LED_Init(); // LED初始化 Key_Init(); // KEY初始化(含中断配置) while (1) { // 主循环什么都不用做,等待中断发生 // 按键按下 → EXTI中断 → ISR翻转LED → 返回继续等 } } ``` --- ### 软件设计(HAL库版) **项目路径**:`stm32/06_key_led_hal` CubeMX 配置要点: 1. PF10 配置为 `GPIO_EXTI10` + `GPIO_PULLUP`(上拉输入) 2. NVIC 中使能 `EXTI15_10_IRQn` 3. 在 `stm32f1xx_it.c` 的 `EXTI15_10_IRQHandler()` 中添加用户代码 HAL 库中断处理流程: ``` 外部中断 → HAL库的 HAL_GPIO_EXTI_IRQHandler() 调用 → HAL_GPIO_EXTI_Callback() → 用户重写Callback函数 ``` **文件:`stm32/06_key_led_hal/Core/Src/main.c`**(用户代码部分) ```c /* USER CODE BEGIN 0 */ // HAL库的中断回调函数(由HAL_GPIO_EXTI_IRQHandler调用) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == KEY3_Pin) // PF10 对应 KEY3_Pin { HAL_Delay(10); // 消抖 if (HAL_GPIO_ReadPin(GPIOF, KEY3_Pin) == GPIO_PIN_SET) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 翻转 LED1 } } } /* USER CODE END 0 */ ``` > **对比寄存器版**:HAL 库将中断的底层操作(PR清除、中断线判断)封装在 `HAL_GPIO_EXTI_IRQHandler` 中,用户只需要重写 Callback 函数。 --- ## 核心函数速查表 ### 寄存器版 | 函数/操作 | 代码 | 说明 | | ------------ | ----------------------------- | ------------------------ | | KEY初始化 | `Key_Init()` | 时钟+GPIO+AFIO+EXTI+NVIC | | 清除中断标志 | `EXTI->PR | = EXTI_PR_PR10` | 写1清除挂起位 | | 读取引脚 | `GPIOF->IDR & GPIO_IDR_IDR10` | &测试:测量PF10电平 | | 软件消抖 | `Delay_ms(15)` | 避开机械抖动期 | ### HAL库版 | HAL函数 | 说明 | | -------------------------------- | --------------------------------- | | `HAL_GPIO_ReadPin(GPIOx, pin)` | 读取引脚电平 | | `HAL_GPIO_TogglePin(GPIOx, pin)` | 翻转引脚电平 | | `HAL_GPIO_EXTI_IRQHandler(pin)` | EXTI中断入口(清标志+调Callback) | | `HAL_GPIO_EXTI_Callback(pin)` | 用户重写的回调函数 | ### NVIC函数(core_cm3.h) | 函数 | 说明 | | ---------------------------------- | -------------- | | `NVIC_EnableIRQ(IRQn)` | 使能中断通道 | | `NVIC_DisableIRQ(IRQn)` | 禁止中断通道 | | `NVIC_SetPriority(IRQn, priority)` | 设置中断优先级 | | `NVIC_SetPriorityGrouping(group)` | 设置优先级分组 | --- ## 常见问题与避坑 1. **中断不触发** → 检查 NVIC 是否使能、EXTI IMR 是否允许、GPIO 模式是否正确(不能是模拟输入) 2. **中断反复触发** → EXTI 的 PR 标志位未清除(必须写1清除) 3. **按键抖动导致误触发** → 加软件延时消抖(10~15ms) 4. **高了优先级的任务被卡死** → 中断服务函数要尽量简短(只设标志位,不处理复杂逻辑) 5. **多个中断共用一个入口** → 在 ISR 中判断 PR 寄存器的不同位区分来源 6. **HAL 库中中断不工作** → 检查 HAL_GPIO_EXTI_IRQHandler 是否在启动文件中正确注册