4. 外部中断与按键响应:NVIC中断优先级配置、EXTI外部中断触发方式、中断服务函数编写规范
好,咱们接着聊。电梯召唤面板里,按键检测是最基础的功能。但你想想看,如果CPU一直在那轮询按键状态,是不是太浪费了?就像你一直盯着电梯按钮看它亮了没,多累啊。
所以,我们得用中断。按键按下去,CPU才去处理。这就是外部中断的典型应用场景。今天我就把这块掰开了讲清楚。
4.1 NVIC中断优先级配置
NVIC,全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。名字挺长,说白了就是ARM Cortex-M内核里管中断的“大管家”。
我个人习惯,拿到一个新项目,第一件事就是先把NVIC的优先级分组搞清楚。为什么?因为一旦你搞错了,中断嵌套的时候就会出各种诡异的问题。
4.1.1 优先级分组
STM32里,中断优先级分为抢占优先级和响应优先级(也叫子优先级)。
- 抢占优先级:数值越小,优先级越高。高抢占优先级可以打断低抢占优先级的中断服务。
- 响应优先级:数值越小,优先级越高。但响应优先级不能打断,只在同一抢占优先级下决定谁先执行。
配置函数是 NVIC_PriorityGroupConfig()。有5种分组方式:
| 分组 | 抢占优先级位数 | 响应优先级位数 |
|---|---|---|
| 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.1.2 配置示例
看代码吧,这样更直观:
// 设置优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置外部中断(比如EXTI0)
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
嗯,这里要注意:整个系统里,NVIC_PriorityGroupConfig() 只能调用一次。你想想看,如果不同模块各调各的,那不乱套了?
4.2 EXTI外部中断触发方式
EXTI,External Interrupt,外部中断。它负责检测GPIO引脚上的电平变化。
触发方式有三种:
- 上升沿触发:从低电平变高电平时触发。适合检测按键松开。
- 下降沿触发:从高电平变低电平时触发。适合检测按键按下。
- 双边沿触发:上升沿和下降沿都触发。适合需要同时检测按下和松开的场景。
4.2.1 配置步骤
配置EXTI,一般分三步走:
- 使能GPIO时钟和AFIO时钟:AFIO是复用功能IO,用来把GPIO映射到EXTI。
- 配置GPIO为输入模式:一般是上拉输入或下拉输入。
- 配置EXTI线路:选择触发方式,并使能中断。
看个例子:
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置EXTI
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
你可能会问:为什么用上拉输入?因为按键另一端接地,按下时引脚变低电平,松开时被拉高。这样下降沿触发正好对应按键按下。
4.3 中断服务函数编写规范
中断服务函数,ISR(Interrupt Service Routine)。这是中断系统的核心。写不好,整个系统都可能崩溃。
我总结了几个原则,你记一下:
4.3.1 短小精悍
中断服务函数里,不要做复杂运算,不要调用延时函数,不要做打印输出。为什么?因为中断是在打断主程序执行的,你处理时间越长,主程序响应就越慢。
黄金法则: 中断里只做三件事——读取数据、设置标志位、清除中断标志。其他事情,交给主循环去处理。
4.3.2 清除中断标志
这个太重要了。不清除中断标志,中断会反复触发,CPU就卡死在中断里了。
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 设置按键按下标志
KeyPressFlag = 1;
// 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
4.3.3 使用volatile修饰共享变量
中断和主程序都会访问的变量,一定要加 volatile 关键字。不然编译器优化时,可能会把变量值缓存到寄存器里,导致主程序读不到中断里修改的值。
volatile uint8_t KeyPressFlag = 0; // 中断和主程序共享
4.3.4 中断嵌套要小心
如果允许中断嵌套,高优先级中断可以打断低优先级中断。但嵌套层数太多,栈空间可能不够。我一般建议,除非必要,否则不要开太多嵌套。
4.4 实战:按键消抖与中断结合
前面说了,按键有抖动。硬件上可以加RC滤波,但软件消抖更灵活。我常用的方法是:中断触发后,延时10-20ms再检测一次电平状态。
volatile uint8_t KeyPressFlag = 0;
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 延时消抖
Delay_ms(20);
// 再次检测按键状态
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET)
{
KeyPressFlag = 1; // 确认按键按下
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
你想想看,如果不用延时,直接在中断里设置标志位,那抖动期间可能会触发多次中断,标志位被反复设置,主程序根本分不清是按下一次还是多次。
好了,外部中断这块就讲这么多。说白了,就是配置好NVIC优先级,选对EXTI触发方式,写好短小精悍的中断服务函数。再加上软件消抖,按键响应就稳了。
下一章,咱们聊聊定时器。电梯面板里,定时器用来做啥?嘿嘿,到时候你就知道了。