4、中断管理:中断向量表配置、中断嵌套与优先级、中断服务程序(ISR)设计规范
中断管理,说白了就是嵌入式系统的“应急响应机制”。你想想看,CPU正在那儿吭哧吭哧算数据呢,突然来了个紧急事件——比如转速超限、油压报警——这时候怎么办?总不能等CPU把手头的活干完再处理吧?那黄花菜都凉了。
所以我们需要中断。我做了这么多年航空发动机控制,可以负责任地告诉你:中断系统设计的好坏,直接决定了EEC控制器的实时性和可靠性。今天咱们就掰开揉碎了聊聊中断管理的三个核心问题。
4.1 中断向量表配置
中断向量表,你可以把它理解成一个“紧急电话本”。每个中断源都有一个对应的电话号码——也就是中断服务程序的入口地址。CPU收到中断信号后,就翻这个电话本,找到对应的号码,然后跳过去执行。
在EEC控制器里,我们常用的MCU(比如Infineon TC系列、NXP MPC系列)都有几百个中断源。但实际用到的,也就那么几十个。我个人习惯是:先梳理清楚系统里到底有哪些中断需求,再去做向量表配置。
中断向量表配置要点:
- 向量表基地址:通常放在Flash或RAM的起始位置。我建议放在RAM里,因为运行时可以动态修改——这在Bootloader升级时特别有用。
- 向量表大小:根据MCU型号确定。比如TC275有256个中断向量,每个向量占4字节(一个地址指针),总共1KB。
- 中断号分配:每个中断源对应一个唯一的中断号。这个号不能乱填,必须和硬件手册一致。
举个例子,在Infineon TC系列上配置中断向量表:
/* 中断向量表配置示例 - Infineon TC275 */
/* 将中断向量表放在RAM起始地址 0x70000000 */
#define INT_VECTOR_BASE 0x70000000
/* 配置中断向量表基地址 */
SCU_EICON.B.SVECT = 0x01; /* 使能向量表重定位 */
SCU_EICON.B.SVECTSEL = 0; /* 选择向量表基地址寄存器 */
/* 设置向量表基地址寄存器 */
__mtcr(CPU_BIV, INT_VECTOR_BASE);
/* 注册中断服务程序 */
/* 中断号 10:定时器中断 */
/* 中断号 20:CAN接收中断 */
/* 中断号 30:AD转换完成中断 */
InstallInterruptHandler(10, Timer_ISR);
InstallInterruptHandler(20, CAN_Rx_ISR);
InstallInterruptHandler(30, ADC_Conv_ISR);
我的经验:我在某型涡扇发动机的FADEC项目中,遇到过中断向量表配置错误导致系统启动就死机的问题。排查了两天才发现——向量表基地址没对齐到256字节边界。嗯,这个坑我替你们踩过了,记得检查对齐要求。
4.2 中断嵌套与优先级
中断嵌套,说白了就是“中断里套中断”。高优先级的中断可以打断低优先级的中断服务程序。这在EEC控制器里很常见——比如转速信号中断(优先级高)可以打断CAN通信中断(优先级低)。
但这里有个问题:中断嵌套层级越多,系统确定性越差。你想想看,如果A中断正在执行,B中断来了打断它,B还没执行完呢,C中断又来了...这就像开会时不断有人插话,最后谁的话都听不清。
我建议的优先级设计原则:
| 优先级等级 | 中断类型 | 典型中断源 | 响应时间要求 |
|---|---|---|---|
| 最高(0-1) | 安全关键中断 | 转速超限、油压过低、看门狗 | < 10 μs |
| 高(2-3) | 实时控制中断 | 角度同步、AD转换完成、PWM更新 | < 50 μs |
| 中(4-5) | 通信中断 | CAN接收、SPI传输完成 | < 200 μs |
| 低(6-7) | 后台任务中断 | 定时器、系统滴答 | < 1 ms |
注意:我曾经在一个项目中,把CAN接收中断设成了最高优先级。结果呢?CAN总线上一有数据,CPU就跑去处理中断,把转速采样给耽误了。发动机控制周期乱了,差点导致喘振。所以记住:安全关键中断永远比通信中断优先级高。
中断嵌套的配置代码示例:
/* 中断优先级配置 - 以TC275为例 */
/* 每个中断源有8级优先级(0最高,7最低) */
/* 配置转速超限中断(中断号5)为最高优先级 */
SRC_5.B.SRPN = 0; /* 服务请求优先级 = 0 */
SRC_5.B.SRE = 1; /* 使能中断请求 */
/* 配置CAN接收中断(中断号20)为中等优先级 */
SRC_20.B.SRPN = 4; /* 服务请求优先级 = 4 */
SRC_20.B.SRE = 1; /* 使能中断请求 */
/* 配置系统滴答中断(中断号100)为最低优先级 */
SRC_100.B.SRPN = 7; /* 服务请求优先级 = 7 */
SRC_100.B.SRE = 1; /* 使能中断请求 */
4.3 中断服务程序(ISR)设计规范
ISR怎么写?很多人觉得不就是个函数嘛,随便写写就行。但我要告诉你:ISR写不好,系统迟早要崩。
我在EEC控制器项目中总结了几条铁律:
- ISR要短小精悍——最好控制在50行以内,执行时间不超过10 μs。为什么?因为ISR执行期间,其他中断都在排队等着呢。
- 不要在ISR里做复杂运算——比如浮点运算、三角函数、查表插值。这些活交给后台任务去做。ISR只做“标记”和“搬运”。
- ISR里不要调用不可重入函数——比如printf、malloc、某些标准库函数。你想想看,ISR里调printf,万一打印到一半又被中断了,那输出就乱套了。
- 注意数据共享保护——ISR和主循环共享的变量,要用volatile修饰,必要时加临界区保护。
ISR设计黄金法则:
- ISR里只做三件事:读硬件状态、清中断标志、设置事件标志
- 具体处理逻辑放在主循环或后台任务中
- 使用“生产者-消费者”模式:ISR是生产者,主循环是消费者
来看一个规范的ISR示例:
/* 规范的ISR设计示例 - 转速测量中断 */
/* 全局变量 - 用volatile修饰 */
volatile uint32_t g_RPM_Value; /* 转速值 */
volatile uint8_t g_RPM_Updated; /* 转速更新标志 */
/* 转速中断服务程序 */
void RPM_ISR(void)
{
/* 第一步:读取硬件状态 */
uint32_t capture_val = GPT12_T3CAPVAL; /* 读取捕获值 */
/* 第二步:清中断标志 */
GPT12_T3INTCLR.B.T3ICLR = 1; /* 清中断标志位 */
/* 第三步:更新共享数据 */
g_RPM_Value = CalculateRPM(capture_val); /* 简单计算 */
g_RPM_Updated = 1; /* 设置事件标志 */
/* 注意:这里没有复杂运算,没有打印,没有延时 */
}
/* 主循环中处理转速数据 */
void MainLoop(void)
{
while(1)
{
if(g_RPM_Updated)
{
g_RPM_Updated = 0; /* 清标志 */
/* 在这里做复杂处理:滤波、限幅、故障诊断等 */
ProcessRPMData(g_RPM_Value);
}
/* 其他任务... */
}
}
我的小技巧:我习惯在ISR入口和出口加两个宏——ISR_ENTER()和ISR_EXIT()。入口宏里做:保存上下文、记录中断发生时间戳;出口宏里做:恢复上下文、检查是否有更高优先级中断等待。这样调试时特别方便,能清楚看到每个中断的执行时间和发生频率。
4.4 中断管理的避坑指南
做了十几年EEC控制器,我踩过的坑不少。分享几个典型的:
- 中断标志没清干净——有一次我忘了清一个外设的中断标志,结果ISR执行完立刻又被触发,CPU卡死在中断里。嗯,从那以后我每次写ISR第一件事就是清标志。
- 中断优先级反转——低优先级中断占用了高优先级中断需要的资源,导致高优先级中断被阻塞。解决办法:使用优先级继承协议或禁止中断嵌套。
- 中断延迟过大——ISR里关了全局中断做临界区保护,结果关中断时间太长,其他紧急中断响应不过来。我建议:临界区保护用“关中断+开中断”的方式,时间控制在1 μs以内。
最后说一句:中断管理没有银弹。每个项目的需求不同,MCU的特性也不同。我的建议是:先理解硬件手册,再结合系统需求做设计,最后通过实际测试验证。别偷懒,该做的中断延迟测试、中断吞吐量测试,一个都不能少。