tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第7章:按键与中断系统 + 配套代码05~06" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:中断就像你正在写代码,突然电话响了——你暂停写代码、接电话、处理完、挂电话、继续写代码。按键中断就是"按键按下 → 暂停主程序 → 处理按键 → 恢复主程序"。NVIC 就像一个智能电话秘书,知道哪个电话更重要、该先接哪个。
如果没有中断,CPU 只能轮询检测按键状态——就像一直盯着电话看它响不响,浪费 CPU 时间。中断机制允许外设在需要时主动通知 CPU,CPU 不用一直等。
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 |
规则:抢占优先级高的可以打断低的;相同抢占优先级下,子优先级高的先执行但不打断。
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 寄存器描述)
开发板上有 4 个按键:
| 按键 | 对应引脚 | 连接方式 | 按下时电平 |
|---|---|---|---|
| KEY_UP (SW4) | PF10 | 上拉输入 | 低变高 |
| KEY_1 (SW2) | ? | 外部电路 | 取决于设计 |
| KEY_2 | ? | 外部电路 | 取决于设计 |
| KEY_3 (SW5) | PF10? | 上拉输入 | 低变高 |
说明:PF10 对应的 KEY 为上拉输入模式——按键未按下时 PF10=低(下拉到GND),按下时 PF10=高(上拉到 VCC)。
消抖:机械按键在按下/释放瞬间会产生约 5~10ms 的抖动(电平快速跳变),需要软件消抖:
理想波形: __┃┃________
实际波形: __┃_┃_┃┃_┃_┃____ ← 抖动
软件消抖: 检测到边沿 → 等待15ms → 再次确认电平
按下 KEY_1(PF10),翻转 LED1 的开关状态。使用外部中断 EXTI15_10 实现。
项目路径:stm32/05_key_led_register
文件:stm32/05_key_led_register/Hardware/KEY/key.h
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
void KEY_Init(void);
#endif
文件:stm32/05_key_led_register/Hardware/KEY/key.c
#include "key.h"
#include "delay.h"
#include "led.h"
// KEY初始化:配置PF10为上拉输入 + EXTI中断
void KEY_Init(void)
{
/* ========== 1. 开启时钟 ========== */
// GPIOF时钟(操作PF10需要)
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN; // |=: IOPFEN位置1
// AFIO时钟(配置中断线重映射需要)
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // |=: AFIOEN位置1
/* ========== 2. 配置PF10为上拉输入 ========== */
// MODE10[1:0] = 00 → 输入模式
GPIOF->CRH &= ~GPIO_CRH_MODE10; // &=~: MODE10位清0
// CNF10[1:0] = 10 → 上拉/下拉输入
GPIOF->CRH |= GPIO_CRH_CNF10_1; // |=: CNF10_1位置1
GPIOF->CRH &= ~GPIO_CRH_CNF10_0; // &=~: CNF10_0位清0
// 开启上拉电阻(ODR对应位置1 = 上拉)
// PF10 ODR位=1 → 上拉;ODR位=0 → 下拉
GPIOF->ODR &= ~GPIO_ODR_ODR10; // 选择下拉(按键未按时低电平)
/* ========== 3. AFIO选择EXTI中断线 ========== */
// EXTI10连接到PF10(EXTICR[2]的第8~11位控制EXTI10~EXTI13)
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF;
/* ========== 4. 配置EXTI ========== */
// RTSR: 上升沿触发选择寄存器 — PF10从低变高(按键按下)时触发
EXTI->RTSR |= EXTI_RTSR_TR10; // |=: EXTI10上升沿触发使能
// IMR: 中断屏蔽寄存器 — 允许EXTI10产生中断
EXTI->IMR |= EXTI_IMR_MR10; // |=: EXTI10中断不屏蔽
/* ========== 5. 配置NVIC ========== */
// 设置优先级分组为Group_3:3位抢占(8级) + 1位子优先(2级)
NVIC_SetPriorityGrouping(3);
// 设置EXTI15_10中断的抢占优先级=2
NVIC_SetPriority(EXTI15_10_IRQn, 2);
// 使能EXTI15_10中断通道
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(15); // 等待15ms避开抖动期
/* 3. 再次确认按键确实按下 */
// 读取IDR判断PF10是否仍为高电平(屏蔽状态)
if ((GPIOF->IDR & GPIO_IDR_IDR10) != 0) // &测试: IDR10位是否为1
{
// 翻转LED1状态
LED_Toggle(LED_1);
}
}
文件:stm32/05_key_led_register/User/main.c
#include "key.h"
#include "led.h"
int main(void)
{
LED_Init(); // LED初始化
KEY_Init(); // KEY初始化(含中断配置)
while (1)
{
// 主循环什么都不用做,等待中断发生
// 按键按下 → EXTI中断 → ISR翻转LED → 返回继续等
}
}
项目路径:stm32/06_key_led_hal
CubeMX 配置要点:
GPIO_EXTI10 + GPIO_PULLUP(上拉输入)EXTI15_10_IRQnstm32f1xx_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(用户代码部分)
/* USER CODE BEGIN 0 */
// HAL库的中断回调函数(由HAL_GPIO_EXTI_IRQHandler调用)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == KEY1_Pin) // 判断是哪个引脚触发的中断
{
HAL_Delay(15); // 消抖
if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_SET) // 再次确认
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); // 翻转LED
}
}
}
/* USER CODE END 0 */
对比寄存器版:HAL 库将中断的底层操作(PR清除、中断线判断)封装在
HAL_GPIO_EXTI_IRQHandler中,用户只需要重写 Callback 函数。
| 函数/操作 | 代码 | 说明 |
|---|---|---|
| KEY初始化 | KEY_Init() |
时钟+GPIO+AFIO+EXTI+NVIC |
| 清除中断标志 | `EXTI->PR | = EXTI_PR_PR10` |
| 读取引脚 | GPIOF->IDR & GPIO_IDR_IDR10 |
&测试:测量PF10电平 |
| 软件消抖 | Delay_ms(15) |
避开机械抖动期 |
| HAL函数 | 说明 |
|---|---|
HAL_GPIO_ReadPin(GPIOx, pin) |
读取引脚电平 |
HAL_GPIO_TogglePin(GPIOx, pin) |
翻转引脚电平 |
HAL_GPIO_EXTI_IRQHandler(pin) |
EXTI中断入口(清标志+调Callback) |
HAL_GPIO_EXTI_Callback(pin) |
用户重写的回调函数 |
| 函数 | 说明 |
|---|---|
NVIC_EnableIRQ(IRQn) |
使能中断通道 |
NVIC_DisableIRQ(IRQn) |
禁止中断通道 |
NVIC_SetPriority(IRQn, priority) |
设置中断优先级 |
NVIC_SetPriorityGrouping(group) |
设置优先级分组 |