一、写在前面:为什么需要外部中断
在嵌入式开发中,轮询(Polling)是最直观的输入检测方式:主循环里不断读取按键电平,判断是否被按下。这种方式实现简单,但存在两个明显问题:一是 CPU 被无效占用,二是响应存在延迟,尤其在主循环中存在长耗时任务时,按键可能被漏检。
外部中断(External Interrupt,EXTI)则把"检测电平变化"这件事交给硬件完成。当指定引脚上出现上升沿、下降沿或双边沿时,硬件自动触发中断请求,CPU 暂停当前任务跳转到中断服务函数处理,处理完再返回。对于按键、编码器、传感器脉冲、外部唤醒信号等场景,外部中断几乎是标配方案。
本文以 STM32C562RET6 为例,介绍其外部中断体系,并重点讲解 GPIO 外部中断的配置流程、代码实现与常见问题。
二、STM32C562RET6 芯片与中断体系概览
2.1 芯片基本特性
STM32C562RET6 属于 STM32C5 系列,基于 Arm Cortex-M33 内核,主频可达 144 MHz,内置 512 KB Flash 与 128 KB SRAM,封装为 LQFP64,即"R"代表 64 引脚、"E"代表 512 KB Flash、"T"代表 LQFP 封装、"6"代表工业级温度范围。该系列面向通用型应用,具备丰富的定时器、ADC、通信外设与 GPIO 资源。
在中断方面,Cortex-M33 内核提供 NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器),支持中断优先级分组、抢占与子优先级、尾链优化和迟到中断处理。所有外设中断最终都要经过 NVIC 才能送达内核。
2.2 中断从产生到响应的完整链路
理解外部中断,关键是理清信号从引脚到 CPU 的完整路径。以 GPIO 外部中断为例,大致分为四级:
这条链路中任何一环没有配置正确,中断都不会被触发。实际调试时,建议按"引脚复用 → EXTI 边沿 → NVIC 使能 → 标志清除"的顺序逐级排查。
2.3 STM32C5 的 EXTI 控制器
STM32C5 系列的 EXTI 控制器相比早期 F1 系列有明显增强,主要特点包括:
- 通道数量多:支持 GPIO 线 0~15 映射,同时包含若干内部信号线(如 RTC 闹钟、比较器输出、低功耗唤醒等)。
- 可配置触发边沿:每个通道可独立选择上升沿、下降沿或双边沿触发。
- 独立的中断与事件模式:中断模式会向 NVIC 发请求,事件模式只产生脉冲用于触发其他外设(如 ADC、定时器),不进入中断。
- 软件中断触发:可通过写 SWIER 寄存器软件触发一次中断,便于调试。
- 挂起标志可查询:通过 PR 寄存器读取当前挂起状态,便于轮询式处理或调试定位。
三、EXTI 与 GPIO 的映射关系
3.1 为什么是"线"而不是"引脚"
EXTI 的 GPIO 通道按"线号"组织,共 16 条线(EXTI0~EXTI15),每条线对应引脚编号相同的 GPIO。例如 PA0、PB0、PC0 都连接到 EXTI0,但同一时刻只能选择其中一个端口接入。这个选择由 SYSCFG(系统配置控制器)的 EXTICR 寄存器完成。
这意味着一个常见限制:不能同时把 PA0 和 PB0 都配置为外部中断,因为它们共用 EXTI0。若确实需要,只能把其中一个改为轮询,或换用其他引脚编号。
3.2 端口映射配置
以 PA5 为例,需要把 EXTI5 的输入源选择为端口 A。相关寄存器为 SYSCFG_EXTICR2,其中每 4 位对应一条线:
| 寄存器 | 位域 | 对应 EXTI 线 | 取值示例 |
|---|---|---|---|
| SYSCFG_EXTICR1 | EXTI0~EXTI3 | 线 0~3 | 0x0=PA,0x1=PB,0x2=PC |
| SYSCFG_EXTICR2 | EXTI4~EXTI7 | 线 4~7 | 同上 |
| SYSCFG_EXTICR3 | EXTI8~EXTI11 | 线 8~11 | 同上 |
| SYSCFG_EXTICR4 | EXTI12~EXTI15 | 线 12~15 | 同上 |
使用 HAL 库时,这一步由 HAL_GPIO_Init() 在检测到 GPIO_MODE_IT_xxx 模式时自动完成,无需手动写寄存器。但了解底层映射,在排查"中断不触发"问题时非常有用。
四、GPIO 外部中断的配置流程
4.1 整体步骤
- 使能 GPIO 端口时钟与 SYSCFG 时钟。
- 将目标引脚配置为输入模式,并设置上下拉。
- 设置 EXTI 触发边沿(上升沿、下降沿或双边沿)。
- 在 NVIC 中使能对应的 EXTI 中断通道,并设置优先级。
- 编写中断服务函数,清除挂起标志并执行用户逻辑。
4.2 使用 STM32CubeMX 快速配置
在 CubeMX 中,选中目标引脚(例如 PA5),将其模式设置为 GPIO_EXTI5。随后在 GPIO 配置面板中设置:
- GPIO mode:External Interrupt Mode with Rising edge trigger detection(或 Falling / Rising-Falling)。
- GPIO Pull-up/Pull-down:按键接地时选 Pull-up,按键接电源时选 Pull-down。
- User Label :建议命名为
KEY1之类,便于生成宏定义。
接着在 NVIC 配置页勾选 EXTI line 5 interrupt,设置抢占优先级与子优先级。生成代码后,CubeMX 会自动完成时钟使能、GPIO 初始化与 NVIC 配置。
4.3 寄存器级配置示例
如果不使用 HAL,直接操作寄存器可以更清楚地看到每一步。以下代码以 PA5 下降沿触发为例:
c
#include "stm32c5xx.h"
void EXTI5_GPIO_Init(void)
{
/* 1. 使能 GPIOA 与 SYSCFG 时钟 */
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
/* 2. PA5 配置为输入,上拉 */
GPIOA->MODER &= ~(0x3U << (5 * 2));
GPIOA->PUPDR &= ~(0x3U << (5 * 2));
GPIOA->PUPDR |= (0x1U << (5 * 2)); /* 01: 上拉 */
/* 3. 将 EXTI5 映射到端口 A */
SYSCFG->EXTICR[1] &= ~(0xFU << 4); /* EXTI5 位于 EXTICR2 的 bit[7:4] */
SYSCFG->EXTICR[1] |= (0x0U << 4); /* 0x0 = GPIOA */
/* 4. 配置下降沿触发 */
EXTI->RTSR1 &= ~(1U << 5); /* 关闭上升沿 */
EXTI->FTSR1 |= (1U << 5); /* 使能下降沿 */
EXTI->IMR1 |= (1U << 5); /* 使能中断,非事件 */
/* 5. NVIC 使能并设置优先级 */
NVIC_SetPriority(EXTI5_IRQn, 2);
NVIC_EnableIRQ(EXTI5_IRQn);
}
/* 6. 中断服务函数 */
void EXTI5_IRQHandler(void)
{
if (EXTI->PR1 & (1U << 5))
{
EXTI->PR1 = (1U << 5); /* 写 1 清除挂起标志 */
/* 用户逻辑:例如翻转 LED、记录时间戳 */
}
}
注意 EXTI->PR1 的清除方式是"写 1 清零",写 0 无效。这是初学者最容易踩的坑之一。
4.4 HAL 库实现
使用 HAL 库时,初始化代码更简洁,中断处理则通过回调函数完成:
c
#include "main.h"
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI5_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI5_IRQn);
}
/* HAL 已提供 EXTI5_IRQHandler,内部会调用 HAL_GPIO_EXTI_Callback */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_5)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
HAL 的 HAL_GPIO_EXTI_IRQHandler() 内部已经完成了挂起标志清除,因此回调函数中不需要再手动清标志。但要注意:不要在回调里做耗时操作,例如延时、串口打印长字符串、等待外设就绪等,这些会显著拉长中断响应时间。
五、中断优先级与 NVIC 配置要点
5.1 优先级分组
Cortex-M33 的优先级寄存器为 8 位,但实际实现位数由芯片决定。STM32C5 通常实现 4 位优先级,即 16 级。通过 HAL_NVIC_SetPriorityGrouping() 可以把这 4 位拆分为抢占优先级与子优先级。常用分组为 NVIC_PRIORITYGROUP_4,即 4 位全部用于抢占优先级,子优先级为 0。
抢占优先级决定能否打断正在执行的中断;子优先级只在同时挂起时决定谁先执行,不能抢占。对于按键这类低速输入,优先级设为中等即可;对于编码器、故障保护信号,则应设为较高优先级。
5.2 优先级数值越小优先级越高
这一点与直觉相反,需要特别注意。HAL_NVIC_SetPriority(EXTI5_IRQn, 0, 0) 表示最高优先级。若把按键中断设为 0,而系统滴答定时器设为 15,则按键中断可以打断 SysTick,可能影响 HAL_Delay() 的计时精度。
5.3 共享中断向量
EXTI0~EXTI4 各有独立中断向量(EXTI0_IRQHandler 至 EXTI4_IRQHandler),而 EXTI5~EXTI9 共享 EXTI9_5_IRQHandler,EXTI10~EXTI15 共享 EXTI15_10_IRQHandler。因此在共享向量中必须判断具体是哪条线触发:
c
void EXTI9_5_IRQHandler(void)
{
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_5) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_5);
/* 处理 EXTI5 */
}
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_7) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_7);
/* 处理 EXTI7 */
}
}
六、按键消抖:外部中断绕不开的问题
机械按键在按下和松开的瞬间会产生 5~20 ms 的抖动,表现为电平快速跳变。若直接使用外部中断,一次按键可能触发多次中断。常见处理方式有三种:
6.1 硬件消抖
在按键两端并联 0.1 μF 电容,或使用 RC 低通滤波,配合施密特触发器输入。硬件消抖不占用 CPU,但会增加成本与布板面积。
6.2 中断内简单延时
在中断服务函数中延时 10~20 ms 后再次读取引脚电平。这种方式实现简单,但会阻塞中断,不推荐在复杂系统中使用。
6.3 定时器 + 状态机消抖(推荐)
中断中只记录时间戳或置标志,把消抖逻辑放到定时器中断或主循环中处理:
c
volatile uint32_t key_last_tick = 0;
volatile uint8_t key_pressed_flag = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_5)
{
uint32_t now = HAL_GetTick();
if (now - key_last_tick > 20) /* 20 ms 内忽略重复触发 */
{
key_last_tick = now;
key_pressed_flag = 1;
}
}
}
/* 主循环中处理 */
void App_Loop(void)
{
if (key_pressed_flag)
{
key_pressed_flag = 0;
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET)
{
/* 确认按键仍处于按下状态,执行动作 */
}
}
}
这种"中断只做标记、业务在主循环处理"的模式,是嵌入式开发中非常通用的设计原则,能有效降低中断延迟并避免竞态。
七、低功耗唤醒与事件模式
7.1 用 EXTI 唤醒 STOP 模式
STM32C5 支持多种低功耗模式。在 STOP 模式下,大部分时钟关闭,但 EXTI 仍可工作。配置 GPIO 外部中断后,调用 HAL_PWR_EnterSTOPMode() 进入低功耗,当引脚出现指定边沿时芯片被唤醒,程序从进入点之后继续执行。
c
/* 进入 STOP 模式前确保 EXTI 已配置并使能 NVIC */
HAL_SuspendTick();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 唤醒后需要重新配置系统时钟 */
SystemClock_Config();
HAL_ResumeTick();
注意唤醒后系统时钟通常会回退到 HSI,必须重新配置时钟树,否则串口、定时器等外设的波特率和计时都会出错。
7.2 事件模式与中断模式的区别
EXTI 的 EMR 寄存器控制事件模式。事件模式不产生中断请求,而是输出一个脉冲,可用于触发 ADC 转换、启动定时器、触发 DMA 等。典型用法是把外部信号作为 ADC 的硬件触发源,实现"信号到来即采样",避免软件轮询带来的抖动。
八、常见问题排查清单
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 中断完全不触发 | SYSCFG 时钟未使能,EXTICR 未配置 | 检查 __HAL_RCC_SYSCFG_CLK_ENABLE() 是否调用 |
| 中断完全不触发 | NVIC 未使能或优先级分组错误 | 在调试器中查看 NVIC ISER 寄存器 |
| 中断只触发一次 | 挂起标志未清除 | 确认 ISR 中写 1 清除 PR 寄存器 |
| 一次按键触发多次 | 机械抖动 | 增加软件消抖或硬件 RC 滤波 |
| 配置 PB0 后 PA0 失效 | 共用 EXTI0 线 | 改用不同编号引脚,或改为轮询 |
| 进入中断后程序跑飞 | ISR 中调用了不可重入函数 | 避免在 ISR 中使用 printf、malloc |
| 低功耗唤醒后外设异常 | 唤醒后时钟未重新配置 | 唤醒后调用 SystemClock_Config() |
九、实践建议与总结
外部中断是 STM32 开发中的基础能力,用好它需要把握几个关键点:
- 先理清链路:引脚 → EXTI → NVIC → 内核,任何一环缺失都不会触发,排查时按顺序逐级验证。
- 中断要短:ISR 中只做标志置位、数据搬运等轻量操作,复杂逻辑放到主循环或任务中。
- 标志必须清:EXTI 挂起标志写 1 清零,漏清会导致中断反复进入或只进一次。
- 注意共享线:同一编号的不同端口引脚共用一条 EXTI 线,设计原理图时就要规划好。
- 消抖要重视:机械按键必须消抖,推荐"中断标记 + 主循环确认"的组合方案。
- 低功耗场景:EXTI 是唤醒 STOP 模式的主要手段,唤醒后记得恢复时钟配置。
掌握以上内容后,无论是按键输入、编码器计数、外部脉冲捕获还是低功耗唤醒,都可以基于同一套 EXTI 配置思路快速实现。建议先在开发板上用按键点亮 LED 跑通最小示例,再逐步加入消抖、优先级调整和低功耗功能,循序渐进地加深理解。