大家好,我是良许。
在嵌入式开发中,中断是一个非常重要的概念。
它允许 MCU 在执行主程序的同时,能够及时响应外部事件,比如按键按下、传感器信号变化等。
今天我们就来深入学习 STM32 的 EXTI 外部中断事件控制器,这是每个 STM32 开发者都必须掌握的核心知识。
1. EXTI 外部中断事件控制器概述
1.1 什么是 EXTI
EXTI 是 STM32 中用于管理外部中断和事件的控制器。
它可以检测 GPIO 引脚上的电平变化,并在满足触发条件时产生中断或事件。
简单来说,EXTI 就像是一个"门卫",时刻监视着外部世界的变化,一旦发现符合条件的信号,就立即通知 CPU 去处理。
在实际项目中,我曾经用 EXTI 来处理紧急停止按钮。
当操作人员按下急停按钮时,系统必须在几微秒内做出响应,停止所有运动部件。
如果用轮询的方式去检测按钮状态,可能会因为主程序正在执行其他任务而延迟响应,但使用 EXTI 中断就能保证最快的响应速度。
1.2 EXTI 的主要特性
STM32 的 EXTI 控制器具有以下特性:
- 支持多达 23 条外部中断/事件线(具体数量因芯片型号而异)
- 每条中断线都可以独立配置触发方式:上升沿、下降沿或双边沿触发
- 每个 GPIO 引脚都可以配置为外部中断源
- 支持软件触发中断
- 具有独立的挂起状态位和屏蔽位
- 可以产生中断请求或事件请求
需要注意的是,STM32 的 EXTI 有一个重要的限制:相同编号的 GPIO 引脚共享同一条 EXTI 线。
比如 PA0、PB0、PC0 都连接到 EXTI0 线,这意味着你不能同时将 PA0 和 PB0 都配置为外部中断,只能选择其中一个。
2. EXTI 工作原理
2.1 EXTI 的内部结构
EXTI 控制器主要由以下几个部分组成:
- 边沿检测电路:负责检测输入信号的上升沿、下降沿或双边沿
- 软件中断事件寄存器:允许通过软件触发中断
- 挂起请求寄存器:记录哪些中断线有挂起的中断请求
- 中断屏蔽寄存器:控制哪些中断线被使能
- 事件屏蔽寄存器:控制哪些事件线被使能
当外部信号满足触发条件时,EXTI 会将对应的挂起位置 1,如果该中断线没有被屏蔽,就会向 NVIC(嵌套向量中断控制器)发送中断请求。
2.2 中断与事件的区别
EXTI 可以产生两种类型的输出:中断和事件。
很多初学者容易混淆这两个概念。
中断:会触发 CPU 执行中断服务程序(ISR),需要软件介入处理。
当中断发生时,CPU 会暂停当前任务,跳转到中断服务函数执行,处理完成后再返回主程序。
事件:不会触发 CPU 中断,而是产生一个脉冲信号,可以触发其他外设的操作,比如启动 ADC 转换、触发 DMA 传输等,整个过程不需要 CPU 参与,实现了硬件级的联动。
在我做汽车电子项目时,经常使用事件模式来触发 ADC 采样。
比如每隔固定时间需要采集传感器数据,我会用定时器产生 EXTI 事件,然后这个事件直接触发 ADC 开始转换,整个过程不占用 CPU 资源,效率非常高。
3. EXTI 配置步骤
3.1 使用 HAL 库配置 EXTI 的基本流程
使用 STM32 HAL 库配置 EXTI 外部中断主要包括以下步骤:
- 使能 GPIO 时钟
- 配置 GPIO 引脚为输入模式
- 配置 EXTI 中断线
- 配置 NVIC 中断优先级
- 编写中断服务函数
下面我用一个实际的按键中断例子来说明整个配置过程。
3.2 按键外部中断配置示例
假设我们使用 PA0 引脚连接一个按键,按键按下时引脚电平为低,松开时为高(上拉输入)。
我们希望在按键按下(下降沿)时触发中断。
/* 1. GPIO初始化配置 */
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能GPIOA时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 配置PA0为输入模式,上拉,外部中断模式 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发中断
GPIO_InitStruct.Pull = GPIO_PULL_UP; // 上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* 配置NVIC中断优先级 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
/* 使能EXTI0中断 */
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
/* 2. 中断服务函数 */
void EXTI0_IRQHandler(void)
{
/* 调用HAL库的中断处理函数 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
/* 3. 中断回调函数 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
/* 按键按下,执行相应操作 */
// 这里可以添加你的业务逻辑
// 比如翻转LED状态
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
3.3 配置参数详解
在上面的代码中,有几个关键的配置参数需要理解:
GPIO_MODE_IT_FALLING:这个参数指定了中断触发方式。
HAL 库提供了以下几种选择:
GPIO_MODE_IT_RISING:上升沿触发GPIO_MODE_IT_FALLING:下降沿触发GPIO_MODE_IT_RISING_FALLING:双边沿触发
GPIO_PULL_UP:配置 GPIO 的上拉/下拉电阻。
选项包括:
GPIO_NOPULL:无上拉下拉GPIO_PULLUP:上拉GPIO_PULLDOWN:下拉
HAL_NVIC_SetPriority:设置中断优先级。
第二个参数是抢占优先级,第三个参数是子优先级。
抢占优先级高的中断可以打断抢占优先级低的中断,而子优先级只在抢占优先级相同时才起作用。
4. EXTI 中断优先级管理
4.1 NVIC 中断优先级分组
STM32 使用 NVIC 来管理所有中断,包括 EXTI 中断。
NVIC 支持中断优先级分组,通过 HAL_NVIC_SetPriorityGrouping() 函数来配置。
/* 配置中断优先级分组为组2 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
不同的优先级分组方式决定了抢占优先级和子优先级的位数分配:
NVIC_PRIORITYGROUP_0:0 位抢占优先级,4 位子优先级NVIC_PRIORITYGROUP_1:1 位抢占优先级,3 位子优先级NVIC_PRIORITYGROUP_2:2 位抢占优先级,2 位子优先级NVIC_PRIORITYGROUP_3:3 位抢占优先级,1 位子优先级NVIC_PRIORITYGROUP_4:4 位抢占优先级,0 位子优先级
4.2 合理设置中断优先级
在实际项目中,合理设置中断优先级非常重要。
一般遵循以下原则:
- 紧急程度高的中断设置高优先级:比如急停按钮、故障检测等
- 执行时间短的中断可以设置高优先级:避免长时间占用 CPU
- 相关性强的中断设置相近的优先级:便于管理和调试
在我做的一个电机控制项目中,优先级设置如下:
/* 急停按钮 - 最高优先级 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
/* 编码器脉冲 - 高优先级 */
HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 0);
/* 普通按键 - 中等优先级 */
HAL_NVIC_SetPriority(EXTI2_IRQn, 2, 0);
/* 通信接收 - 较低优先级 */
HAL_NVIC_SetPriority(USART1_IRQn, 3, 0);
5. EXTI 使用注意事项
5.1 按键消抖处理
在使用 EXTI 处理按键输入时,必须考虑按键抖动问题。
机械按键在按下或松开的瞬间,触点会产生多次通断,导致产生多次中断。
有两种常用的消抖方法:
方法一:软件延时消抖
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
/* 简单延时消抖 */
HAL_Delay(10); // 延时10ms
/* 再次检测按键状态 */
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
/* 确认按键按下,执行操作 */
// 你的业务逻辑
}
}
}
但是这种方法有个问题:在中断服务函数中使用延时会阻塞其他中断,不推荐在实际项目中使用。
方法二:定时器消抖(推荐)
uint32_t last_interrupt_time = 0;
#define DEBOUNCE_TIME 50 // 50ms消抖时间
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
uint32_t current_time = HAL_GetTick();
/* 检查距离上次中断的时间间隔 */
if((current_time - last_interrupt_time) > DEBOUNCE_TIME)
{
last_interrupt_time = current_time;
/* 执行按键处理 */
// 你的业务逻辑
}
}
}
这种方法利用系统滴答定时器来判断时间间隔,不会阻塞其他中断,是更好的选择。
5.2 中断服务函数的编写原则
编写 EXTI 中断服务函数时,需要遵循以下原则:
- 尽量简短:中断服务函数应该尽快执行完毕,避免长时间占用 CPU
- 避免使用延时函数:不要在中断中使用
HAL_Delay()等阻塞函数 - 避免复杂运算:复杂的计算应该在主程序中完成
- 使用标志位:可以在中断中设置标志位,在主程序中检测标志位并处理
volatile uint8_t button_pressed = 0; // 按键按下标志
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
/* 只设置标志位,不做复杂处理 */
button_pressed = 1;
}
}
int main(void)
{
/* 系统初始化 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while(1)
{
/* 在主循环中检测标志位 */
if(button_pressed)
{
button_pressed = 0; // 清除标志
/* 执行复杂的处理逻辑 */
process_button_event();
}
/* 其他任务 */
}
}
5.3 多个 EXTI 中断的处理
当使用多个外部中断时,需要注意中断线的分配。
STM32 的 EXTI0 到 EXTI4 各有独立的中断向量,而 EXTI5 到 EXTI9 共享一个中断向量(EXTI9_5_IRQn),EXTI10 到 EXTI15 共享另一个中断向量(EXTI15_10_IRQn)。
/* EXTI5-9共享中断处理函数 */
void EXTI9_5_IRQHandler(void)
{
/* 检查是哪个引脚触发的中断 */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_5) != RESET)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5);
}
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_6) != RESET)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_6);
}
// 其他引脚的处理...
}
/* 回调函数中区分不同的引脚 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case GPIO_PIN_5:
/* 处理PIN5的中断 */
break;
case GPIO_PIN_6:
/* 处理PIN6的中断 */
break;
default:
break;
}
}
6. EXTI 实战应用案例
6.1 旋转编码器接口
旋转编码器是嵌入式系统中常用的输入设备,通常有 A、B 两相输出。
通过检测 A、B 相的相位关系可以判断旋转方向和速度。
使用 EXTI 可以很好地实现编码器接口。
#define ENCODER_A_PIN GPIO_PIN_0
#define ENCODER_B_PIN GPIO_PIN_1
#define ENCODER_PORT GPIOA
volatile int32_t encoder_count = 0;
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 配置A相为外部中断 */
GPIO_InitStruct.Pin = ENCODER_A_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(ENCODER_PORT, &GPIO_InitStruct);
/* 配置B相为普通输入 */
GPIO_InitStruct.Pin = ENCODER_B_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(ENCODER_PORT, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == ENCODER_A_PIN)
{
/* 读取A相和B相的状态 */
uint8_t a_state = HAL_GPIO_ReadPin(ENCODER_PORT, ENCODER_A_PIN);
uint8_t b_state = HAL_GPIO_ReadPin(ENCODER_PORT, ENCODER_B_PIN);
/* 根据相位关系判断旋转方向 */
if(a_state == b_state)
{
encoder_count++; // 正转
}
else
{
encoder_count--; // 反转
}
}
}
6.2 红外遥控接收
红外遥控器发送的是脉宽调制信号,通过测量脉冲宽度可以解码出按键信息。
使用 EXTI 配合定时器可以实现红外信号的解码。
#define IR_PIN GPIO_PIN_2
#define IR_PORT GPIOA
volatile uint32_t ir_start_time = 0;
volatile uint32_t ir_pulse_width = 0;
volatile uint8_t ir_data_ready = 0;
void IR_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = IR_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(IR_PORT, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI2_IRQn);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == IR_PIN)
{
uint32_t current_time = HAL_GetTick();
if(ir_start_time == 0)
{
/* 记录起始时间 */
ir_start_time = current_time;
}
else
{
/* 计算脉冲宽度 */
ir_pulse_width = current_time - ir_start_time;
ir_start_time = current_time;
ir_data_ready = 1;
/* 根据脉冲宽度解码数据 */
// 这里添加解码逻辑
}
}
}
7. 总结
EXTI 外部中断事件控制器是 STM32 中非常重要的外设,掌握它对于开发响应式的嵌入式系统至关重要。
通过本文的学习,我们了解了 EXTI 的工作原理、配置方法以及实际应用技巧。
在实际开发中,使用 EXTI 需要注意以下几点:首先要合理设置中断优先级,确保重要的中断能够及时响应;其次要注意按键消抖等实际问题,避免误触发;最后要遵循中断服务函数简短高效的原则,复杂的处理逻辑应该在主程序中完成。
我在多年的嵌入式开发经验中,EXTI 几乎是每个项目都会用到的功能。
从简单的按键检测到复杂的编码器接口、红外遥控接收,EXTI 都能很好地胜任。
希望这篇文章能帮助大家更好地理解和使用 STM32 的 EXTI 功能,在实际项目中灵活运用。
更多编程学习资源