3. 中断系统与NVIC:中断优先级管理、外部中断(EXTI)配置、中断服务函数编写
中断,说白了就是让CPU放下手头的事,先去处理一个更紧急的任务。我刚开始学嵌入式时,总觉得中断是个很玄乎的东西,后来做项目多了才发现,没有中断的系统就像一个人只能排队做事,效率低得可怜。
GD32V的中断系统基于ARM Cortex-M3内核的NVIC(嵌套向量中断控制器)。嗯,这里要注意,NVIC是内核自带的,不是GD32V独有的,但GD32V在它上面做了不少扩展。咱们这节课就把中断优先级管理、外部中断配置和中断服务函数编写这三个核心问题讲透。
3.1 NVIC中断优先级管理
NVIC支持最多240个中断,GD32V实际用到了80多个。每个中断都可以设置抢占优先级和子优先级。你想想看,这就像公司里的紧急程度分级——抢占优先级高的可以打断低的,同级的就看子优先级谁高谁先处理。
核心概念:抢占优先级决定是否允许中断嵌套,子优先级决定同抢占级时的响应顺序。
3.1.1 优先级分组
GD32V通过SCB->AIRCR寄存器来配置优先级分组。说白了就是决定用多少位来表示抢占优先级,多少位表示子优先级。我习惯用以下分组方式:
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 抢占优先级数量 | 子优先级数量 |
|---|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 1 | 16 |
| 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 | 1 |
我个人项目中用得最多的是NVIC_PriorityGroup_2,4级抢占优先级够用,4级子优先级也能满足大部分场景。如果你系统里中断嵌套层级很深,可以考虑Group_3或Group_4。
3.1.2 优先级配置函数
配置优先级分组的代码很简单,但要注意必须在系统初始化时设置,中途改会出问题。我曾经在一个项目里因为中途改了优先级分组,导致中断响应乱套,排查了两天才找到原因。
/* 设置优先级分组为Group_2:4级抢占优先级,4级子优先级 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置具体中断的优先级 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /* 抢占优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; /* 子优先级 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
小技巧:数值越小优先级越高。抢占优先级0是最高的,可以打断任何其他中断。
3.2 外部中断(EXTI)配置
外部中断是GD32V最常用的中断类型之一。它可以把GPIO引脚上的电平变化转换成中断信号。说白了就是:你按个按钮,或者传感器输出一个跳变,CPU就能立刻知道。
3.2.1 EXTI结构框图
先看一张我画的EXTI结构图,帮你理清信号流向:
3.2.2 EXTI配置步骤
配置EXTI其实就五步,我总结了一个口诀:开时钟、配GPIO、选边沿、开中断、写服务函数。咱们一步步来:
- 使能GPIO时钟和AFIO时钟——没有时钟,外设就是死的
- 配置GPIO为输入模式——外部中断必须是输入引脚
- 选择中断触发边沿——上升沿、下降沿还是双边沿
- 使能EXTI中断线——打开中断通道
- 配置NVIC优先级——告诉CPU这个中断有多紧急
/* 完整EXTI配置示例:PA0引脚上升沿触发中断 */
void EXTI_Configuration(void)
{
GPIO_InitPara GPIO_InitStructure;
EXTI_InitPara EXTI_InitStructure;
NVIC_InitPara NVIC_InitStructure;
/* 1. 使能时钟 */
RCC_APB2PeriphClock_Enable(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_AFIO, ENABLE);
/* 2. 配置PA0为浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_PIN_0;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 3. 连接EXTI线0到PA0 */
GPIO_EXTILineConfig(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_0);
/* 4. 配置EXTI线0:上升沿触发 */
EXTI_InitStructure.EXTI_LINE = EXTI_LINE0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LINEEnable = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/* 5. 配置NVIC优先级 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
注意:GD32V的EXTI线0和1共用一个中断向量EXTI0_1_IRQn,线2和3共用EXTI2_3_IRQn,线4到15共用EXTI4_15_IRQn。在中断服务函数里需要判断具体是哪条线触发的。
3.3 中断服务函数编写
中断服务函数(ISR)是中断系统的灵魂。写得好,系统稳如泰山;写得不好,各种奇怪问题就来了。我见过有人把延时函数写在ISR里,结果整个系统卡死——嗯,这就是典型的坑。
3.3.1 ISR编写原则
| 原则 | 说明 | 我的经验 |
|---|---|---|
| 短小精悍 | ISR里只做最必要的事 | 通常只设置标志位,主循环处理 |
| 避免阻塞 | 不要用延时、等待循环 | 曾经踩过坑,ISR里等串口发送完... |
| 保护共享数据 | ISR和主循环共享变量要加volatile | 不加volatile,优化后变量值不对 |
| 清除中断标志 | 退出前必须清除中断标志位 | 否则会一直进中断,系统假死 |
3.3.2 标准ISR模板
下面是我常用的ISR模板,以EXTI0_1为例:
/* 全局标志位,volatile不能少 */
volatile uint8_t g_exti0_flag = 0;
void EXTI0_1_IRQHandler(void)
{
/* 判断是哪条线触发的中断 */
if(EXTI_GetIntBitState(EXTI_LINE0) == SET)
{
/* 设置标志位,告诉主循环有事件发生 */
g_exti0_flag = 1;
/* 清除中断标志位 */
EXTI_ClearIntBitState(EXTI_LINE0);
}
/* 如果还有EXTI1的处理,继续判断 */
if(EXTI_GetIntBitState(EXTI_LINE1) == SET)
{
/* 处理EXTI1的中断 */
EXTI_ClearIntBitState(EXTI_LINE1);
}
}
/* 主循环中处理中断事件 */
int main(void)
{
/* 初始化代码... */
while(1)
{
if(g_exti0_flag)
{
g_exti0_flag = 0; /* 清除标志位 */
/* 在这里处理实际业务逻辑 */
/* 比如:读取传感器、更新显示、发送数据等 */
printf("EXTI0中断触发!\n");
}
/* 其他任务... */
}
}
个人习惯:我通常会在ISR里只做三件事:读状态、设标志、清标志。所有耗时操作都放到主循环里。这样既保证了中断响应速度,又避免了ISR里出问题。
3.3.3 避坑指南
我曾经在一个工业控制项目里,因为中断服务函数里调用了printf,导致系统在中断频繁触发时死机。后来查了三天才发现,printf内部用了互斥锁,中断里调用会导致死锁。从此我定了个规矩:ISR里绝对不调用任何可能阻塞的函数。
另外,共享变量一定要加volatile。我记得有一次优化等级开到O2后,主循环里死活检测不到中断标志位变化,就是因为没加volatile,编译器把变量优化到寄存器里了。
3.4 实战:按键中断控制LED
咱们来个完整的例子:按键PA0触发中断,控制PC13上的LED翻转。这个例子虽然简单,但包含了中断系统的所有核心要素。
/* 按键中断控制LED */
volatile uint8_t led_state = 0;
void EXTI0_1_IRQHandler(void)
{
if(EXTI_GetIntBitState(EXTI_LINE0) == SET)
{
led_state = !led_state; /* 翻转LED状态 */
GPIO_SetBits(GPIOC, GPIO_PIN_13, led_state);
EXTI_ClearIntBitState(EXTI_LINE0);
}
}
int main(void)
{
/* 系统初始化 */
SystemCoreClockUpdate();
SysTick_Config(SystemCoreClock / 1000);
/* 配置LED引脚PC13 */
GPIO_InitPara GPIO_InitStructure;
RCC_APB2PeriphClock_Enable(RCC_APB2PERIPH_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_PIN_13;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUT_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_SPEED_50MHZ;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* 配置EXTI中断 */
EXTI_Configuration();
while(1)
{
/* 主循环可以做其他事情 */
/* 按键中断会自动处理LED翻转 */
}
}
这个例子跑起来后,你按一下按键,LED就翻转一次。中断响应时间通常在几十纳秒级别,比轮询方式快了几个数量级。
总结一下:中断系统是嵌入式开发的基石。优先级分组决定了中断的嵌套能力,EXTI配置让GPIO有了"感知"能力,而ISR编写质量直接决定了系统的稳定性。记住:ISR要短、要快、要干净。