欢迎光临
我们一直在努力

史上最强嵌入式外设篇——EXTI & NVIC(进阶)

目录

一. volatile 关键字:全局变量通信的“护身符”

1.1 Bug演示:

为什么会这样?

volatile 的作用

1.2 决定性作用:

1.3 代码示例

二. “前后台系统”架构设计

2.1 错误示范:ISR 里干重活

2.2 正确示范:ISR 只置标志

2.3 ISR 与主循环的三种通信方式

2.4 前后台系统的局限

为什么必须这样设计?

三. 中断嵌套与 RTOS (如 FreeRTOS) 的优先级规划

中断嵌套机制:

FreeRTOS 的中断接管原则:

避坑指南(优先级规划):

优先级低于阈值的中断(如优先级 5 到 15):

优先级高于阈值的中断(如优先级 0 到 4):

优先级分组设定:


 

一. volatile 关键字:全局变量通信的“护身符”

在中断与主循环(while(1))之间传递信息,最常用的方式就是全局变量(标志位)。但是,如果不加 volatile 关键字,你的程序很可能在开启编译器优化(如 -O2)后出现莫名其妙的死机或无响应(无厘头)。

1.1 Bug演示:

看这段代码,你能看出问题吗?

c

uint8_t g_key_flag = 0;

void EXTI0_IRQHandler(void)
{
if(exti_interrupt_flag_get(EXTI_0) != RESET)
{
exti_interrupt_flag_clear(EXTI_0);
g_key_flag = 1;
}
}

int main(void)
{
/* … 初始化 … */

while(1)
{
if(g_key_flag)
{
g_key_flag = 0;
/* 处理按键 */
}
}
}

逻辑没问题。但是,如果开启编译器优化 -O2,很可能出现:

  • 按键按下,中断确实进了,g_key_flag 确实被写成 1

  • 但主循环里 if(g_key_flag) 永远判断为假

  • LED 不翻转,中断像是“没生效”

为什么会这样?

编译器在优化主循环时,会做这样的推理:

main 函数里没有修改过 g_key_flag, 也没有调用任何可能修改它的函数, 那么 g_key_flag 的值在整个循环中不会变。 干脆把它读到寄存器里,只读一次,后续都用寄存器里的旧值。

于是生成的汇编等价于:

c

register uint8_t cached = g_key_flag; // 只读一次,永远是 0
while(1)
{
if(cached) { … } // 永远不成立
}

编译器不知道中断会在背后修改这个变量。

volatile 的作用

c

volatile uint8_t g_key_flag = 0;

volatile 告诉编译器:

这个变量可能在程序控制流之外被修改, 每次使用都必须从内存重新读取, 不许缓存到寄存器,不许优化掉。

加上 volatile 后,每次 if(g_key_flag) 都会真正去读内存,中断写的新值就能被看到。

1.2 决定性作用:

总结:现代编译器非常聪明。如果它发现主循环里一直在读取一个变量,而主循环本身并没有修改它,编译器为了提高效率,会把这个变量的值缓存在 CPU 的寄存器里。当中断突然发生并修改了内存中该变量的值时,主循环依然在读取寄存器里的“旧值”。volatile 的作用就是警告编译器:“这个变量随时可能被意想不到的硬件或中断改变,每次读取它时,必须老老实实去内存里重新取值!”(强制让编译器每次读取都去内存重新取值)

1.3 代码示例

C

#include "gd32f4xx.h"

// 必须加 volatile,否则主循环可能永远检测不到标志位改变
volatile uint8_t g_KeyPressFlag = 0;

// 中断服务函数(“快进快出”)
void EXTI0_IRQHandler(void) {
if (exti_interrupt_flag_get(EXTI_0) != RESET) {
g_KeyPressFlag = 1; // 仅仅置位标志位
exti_interrupt_flag_clear(EXTI_0);
}
}

int main(void) {
// … 硬件初始化 …
while(1) {
if (g_KeyPressFlag == 1) {
g_KeyPressFlag = 0; // 清除标志位

// 执行耗时的业务逻辑
// 比如:蜂鸣器响、驱动屏幕刷新、记录日志等
}
}
}

二. “前后台系统”架构设计

你上面看到的这种“中断置标志位,主循环处理”的模式,就是嵌入式中经典的前后台系统(Foreground-Background System)架构。

  • 前台(Foreground): 指的是各种中断服务函数 (ISR)。前台要求极高的实时性,它的任务是“抓取”突发事件。比如在智能门禁终端项目中,刷卡瞬间会触发中断,前台 ISR 只需要将读取到的卡号存入缓冲区,并将 Card_Swiped_Flag 设为 1,然后立刻退出。

  • 后台(Background): 指的是 main 函数中的死循环 while(1)。后台负责处理复杂的业务逻辑。它会不断轮询各个标志位,当发现 Card_Swiped_Flag == 1 时,它才会去执行查验卡号权限、驱动继电器开门、更新屏幕显示等耗时操作。

2.1 错误示范:ISR 里干重活

c

void EXTI0_IRQHandler(void)
{
if(exti_interrupt_flag_get(EXTI_0) != RESET)
{
exti_interrupt_flag_clear(EXTI_0);

delay_ms(20); /* ❌ 阻塞 20ms */
if(KEY == 0)
{
uart_send_string("KEY\\r\\n"); /* ❌ 串口打印,慢 */
flash_write_config(); /* ❌ Flash 擦写,极慢 */
}
}
}

后果:

  • 20ms 内其他中断全部被延迟

  • 串口可能丢数据

  • Flash 擦写可能几十毫秒,系统像死机

  • 将来接 RTOS,直接破坏调度

2.2 正确示范:ISR 只置标志

c

volatile uint8_t g_key_down_flag = 0;

void EXTI0_IRQHandler(void)
{
if(exti_interrupt_flag_get(EXTI_0) != RESET)
{
exti_interrupt_flag_clear(EXTI_0);
g_key_down_flag = 1; /* ✅ 只做这一件事 */
}
}

int main(void)
{
/* … 初始化 … */

while(1)
{
if(g_key_down_flag)
{
g_key_down_flag = 0;

/* ✅ 业务逻辑放这里 */
delay_ms(20); /* 即使阻塞 20ms,也不影响中断响应 */
if(KEY == 0)
{
uart_send_string("KEY\\r\\n");
}
}

/* 其他任务继续跑 */
task_led_refresh();
task_uart_parse();
}
}

2.3 ISR 与主循环的三种通信方式

方式适用场景示例
单个标志位 事件通知,只需知道“发生了” g_key_flag
计数器 需要知道“发生了几次” g_rx_count++
环形缓冲区 高速数据流,如串口接收 ringbuf_write(&rb, byte)

标志位适合“按键”、“超时”这类离散事件。 环形缓冲区适合“串口一个字节接一个字节来”的连续数据。 环形缓冲区会在 UART 进阶篇详细讲。

2.4 前后台系统的局限

  • 主循环里如果有多个任务,需要自己调度

  • 任务之间没有优先级,长的任务会拖延短任务

  • 没有栈保护、没有任务隔离

  • 复杂项目会变得难以维护

这就是为什么后来需要 RTOS。

但在接 RTOS 之前,把前后台系统写扎实,是初中级工程师的基本功。

为什么必须这样设计?

如果把查验权限、刷新屏幕这种可能耗时几十毫秒的操作放在中断(前台)里,那么在这几十毫秒内,系统将被阻塞,其他所有同级或低级别的中断(比如串口接收数据)都会被忽略,导致严重的数据丢失。前后台系统完美实现了 ISR 的“快进快出”。

三. 中断嵌套与 RTOS (如 FreeRTOS) 的优先级规划

当你的项目越来越复杂(比如自动售货机主板,既要处理支付串口通信,又要控制电机,还要响应屏幕触摸),单纯的前后台系统可能就不够用了,这时就需要引入实时操作系统(RTOS)。在接入 FreeRTOS 之前,对 NVIC 中断优先级的规划是极其重要的一步。

中断嵌套机制:

Cortex-M4 支持中断嵌套。如果 NVIC 配置中,串口中断的抢占优先级是 1,定时器中断的抢占优先级是 2(在 ARM 架构中,数字越小,优先级越高),那么当系统正在处理定时器中断时,如果串口数据来了,串口中断会强制打断定时器中断,优先执行,执行完后再回到定时器中断继续。

FreeRTOS 的中断接管原则:

FreeRTOS 的内核调度高度依赖硬件中断(通常是 PendSV 和 SysTick)。为了防止 RTOS 内部的队列或信号量操作被随意打断导致数据损坏,FreeRTOS 设定了一个“系统最高中断优先级阈值”(通常宏定义为 configMAX_SYSCALL_INTERRUPT_PRIORITY,假设设为 5)。

避坑指南(优先级规划):

优先级低于阈值的中断(如优先级 5 到 15):

这些中断被视为“受 FreeRTOS 管理”的中断。在这些 ISR 里,可以安全地调用以 FromISR 结尾的 FreeRTOS API 函数(比如 xQueueSendFromISR 来发送消息队列)。

优先级高于阈值的中断(如优先级 0 到 4):

这些中断拥有至高无上的权力,甚至连 RTOS 都无法屏蔽它们。它们用于处理极度紧急的硬件故障或微秒级响应。但是,在这些 ISR 里,绝对禁止调用任何 FreeRTOS 的 API 函数,否则必定触发系统的 HardFault 死机。

优先级分组设定:

在使用 FreeRTOS 的项目中,通常要求将 NVIC 分组配置为全部使用抢占优先级(NVIC_PRIGROUP_PRE4_SUB0),即 4 位全为抢占优先级,没有响应优先级,以确保 RTOS 调度的确定性。

        如果此篇文章对您有所帮助感谢您的三连我们共同进步~~~

        如果此篇文章对您有所帮助感谢您的三连我们共同进步~~~

赞(0)
未经允许不得转载:171主机测评 » 史上最强嵌入式外设篇——EXTI & NVIC(进阶)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址