4. 中断与异常管理:别让CPU空转,也别让它卡死
中断,是嵌入式系统的灵魂。没有中断,你的CPU就得像个傻子一样轮询——不停地问“按键按了吗?数据到了吗?定时器到了吗?”效率极低。有了中断,CPU可以安心睡大觉,有事才被叫醒。
但中断也是一把双刃剑。用不好,系统会莫名其妙地死机、数据错乱、优先级反转……我当年第一个产品就因为中断里做了个延时,整得现场工程师差点砸设备。嗯,咱们今天就把这块彻底讲透。
4.1 中断向量表:CPU的“紧急联络簿”
CPU怎么知道某个中断来了该跳到哪里去执行?靠的就是中断向量表。说白了,这就是一张地址表,每个中断源对应一个入口地址。
在OpenHarmony的LiteOS-M内核里,中断向量表通常放在Flash的起始地址。以ARM Cortex-M系列为例,向量表的前16个是系统异常,后面才是外设中断。
典型向量表布局(Cortex-M33):
- 地址0x00000000:栈顶指针
- 地址0x00000004:复位向量 → Reset_Handler
- 地址0x00000008:NMI异常 → NMI_Handler
- 地址0x0000000C:硬错误 → HardFault_Handler
- ……
- 地址0x00000040+:外设中断(UART、TIM、I2C等)
我个人习惯在启动文件里把不用的中断都指向一个空函数,防止野指针飞了。你想想看,如果某个中断意外触发,向量表里却是0xFFFFFFFF,CPU直接跳飞——这锅你背得起吗?
4.2 中断注册与处理函数:把“紧急联系人”写进系统
OpenHarmony的LiteOS-M提供了统一的中断管理接口。注册一个中断,三步走:
- 配置中断控制器:设置触发方式(边沿/电平)、使能中断线
- 注册中断处理函数:调用
LOS_HwiCreate - 使能全局中断:调用
LOS_IntUnlock
#include "los_hwi.h"
// 中断处理函数
static void UART_IRQHandler(uint32_t irqNum, void *arg)
{
// 读取中断状态寄存器
uint32_t status = REG_READ(UART_ISR);
if (status & UART_RX_FLAG) {
char c = REG_READ(UART_RBR);
// 把数据放进环形缓冲区
ringbuf_put(&rx_buf, c);
}
// 清除中断标志
REG_WRITE(UART_ICR, status);
}
void uart_init(void)
{
// 1. 配置UART硬件
uart_hw_config();
// 2. 注册中断:IRQ号、处理函数、触发方式、优先级、参数
LOS_HwiCreate(UART_IRQ_NUM, // 中断号
3, // 抢占优先级
0, // 子优先级
UART_IRQHandler, // 处理函数
NULL); // 参数
// 3. 使能中断
LOS_HwiEnable(UART_IRQ_NUM);
// 4. 开全局中断
LOS_IntUnlock();
}
我在项目中遇到过一个问题:注册中断时忘了使能,结果调试了一下午,最后发现中断根本没开。嗯,这种低级错误,犯过一次就记住了。
4.3 中断优先级配置:谁更重要,谁先跑
中断优先级决定了当多个中断同时发生时,CPU先处理谁。Cortex-M支持最多8~256级优先级(具体看芯片)。
OpenHarmony里,优先级数值越小,优先级越高。0是最高,不能随便用——那是给系统异常留的。
| 中断类型 | 推荐优先级 | 说明 |
|---|---|---|
| 系统Tick中断 | 1 | 系统心跳,必须及时响应 |
| 高实时外设(SPI、DMA) | 2~3 | 数据吞吐量大,不能丢 |
| 普通外设(UART、I2C) | 4~5 | 可以容忍少量延迟 |
| 低优先级(GPIO按键) | 6~7 | 人机交互,慢一点无所谓 |
我的经验:优先级不要铺满所有等级。留几级给后续扩展。我曾经把优先级从0到7全用上了,后来加个新功能,发现没位置了——只能重构,麻烦得很。
4.4 临界区保护与互斥锁:别让数据打架
中断和任务共享数据时,最容易出问题。比如一个全局变量,任务在写,中断在读——写了一半,中断来了,读到的数据是半成品。这就是典型的“竞态条件”。
解决思路很简单:在访问共享资源时,关掉中断。这段代码区域就叫“临界区”。
// 临界区保护示例
uint32_t intSave;
// 进入临界区:关中断,保存当前中断状态
intSave = LOS_IntLock();
// 操作共享数据
g_shared_counter++;
g_buffer[g_shared_counter] = data;
// 退出临界区:恢复中断状态
LOS_IntRestore(intSave);
但注意:临界区里不能做耗时操作!否则系统实时性会崩。我在项目中见过有人把打印日志放在临界区里——串口打印一个字符串要几毫秒,中断全被堵死了,系统直接“假死”。
如果是在任务之间共享资源,用互斥锁更合适:
// 创建互斥锁
LosMutex mutex;
LOS_MuxCreate(&mutex, "myMutex", 0, NULL);
// 任务A
void TaskA(void)
{
while (1) {
LOS_MuxPend(mutex, LOS_WAIT_FOREVER);
// 访问共享资源
do_something();
LOS_MuxPost(mutex);
LOS_TaskDelay(10);
}
}
// 任务B
void TaskB(void)
{
while (1) {
LOS_MuxPend(mutex, LOS_WAIT_FOREVER);
// 访问共享资源
do_something_else();
LOS_MuxPost(mutex);
LOS_TaskDelay(10);
}
}
警告:中断里不要用互斥锁!互斥锁可能导致任务挂起,而中断里不允许阻塞。中断里保护共享数据,只能用关中断的方式。
4.5 避坑指南:中断服务程序不要做耗时操作
这是新手最容易踩的坑,也是我当年摔得最惨的地方。
我曾经在UART中断里直接调用 printf 打印接收到的数据。结果呢?printf内部要等待串口发送完成,而串口发送中断优先级比UART接收低——死锁了!系统直接卡死,连调试器都连不上。
中断服务程序的原则就一句话:快进快出。具体来说:
- 不要做I/O操作:读写文件、打印日志、操作显示屏
- 不要调用阻塞函数:信号量等待、互斥锁获取、延时函数
- 不要做浮点运算:FPU上下文保存开销大,能免则免
- 不要动态分配内存:malloc/free可能触发系统调用,不可重入
那中断里该做什么?只做最必要的事:
- 读取硬件寄存器,保存数据到缓冲区
- 清除中断标志
- 发一个信号量或事件,通知任务去处理
说白了,中断只负责“接单”,任务负责“干活”。
中断处理的最佳实践模式:
// 中断里:只存数据,发信号
void TIM_IRQHandler(uint32_t irqNum, void *arg)
{
// 1. 读取采样值
uint32_t sample = REG_READ(ADC_DR);
// 2. 存入缓冲区
adc_buf[adc_idx++] = sample;
// 3. 通知任务处理
LOS_SemPost(g_adc_sem);
// 4. 清中断标志
REG_WRITE(TIM_ICR, 1);
}
// 任务里:处理数据
void AdcTask(void)
{
while (1) {
LOS_SemPend(g_adc_sem, LOS_WAIT_FOREVER);
// 处理缓冲区数据
process_adc_data();
}
}
这样设计,中断执行时间只有几微秒,任务处理时间几百微秒甚至几毫秒——系统实时性杠杠的。
4.6 知识结构总览
下面这张图,把中断管理的核心脉络串起来了:
从向量表到底层保护,再到避坑指南,这条链路缺一不可。中断管理做得好,系统稳如老狗;做不好,调试到怀疑人生。
好了,中断这块就聊到这儿。记住我说的:中断里别干重活,重活交给任务干。这个原则能帮你避开90%的中断相关bug。