目录
一. 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 调度的确定性。

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

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


