中断系统与PIE模块:中断向量表解析
中断,说白了就是CPU正在干活时,突然被一个紧急事件打断。CPU得停下手中的活,去处理这个紧急事件,处理完再回来继续干活。这个机制在嵌入式系统里太重要了。我刚开始接触C2000时,就被它的中断系统搞得有点晕——因为它不是传统单片机那种简单的中断控制器,而是用了PIE(外设中断扩展)模块。
为什么要搞这么复杂?因为C2000的外设太多了。ADC、ePWM、eCAP、eQEP、SCI、SPI、I2C……每个外设都可能产生中断。如果所有中断都直接连到CPU,那CPU得有多少个中断引脚?所以TI设计了一个分层的中断架构。
核心思路:外设中断 → PIE模块(分组/优先级管理)→ CPU中断(INT1~INT14)
中断向量表:CPU的“紧急联系人名单”
中断向量表,就是一张表。表里记录了每个中断号对应的服务函数地址。CPU收到中断后,会查这张表,找到对应的函数,跳过去执行。
C2000的中断向量表默认放在0x0000~0x003F这段地址。但实际项目中,我们通常会把向量表重定位到RAM里。为什么?因为在RAM里可以动态修改,调试时特别方便。我记得有个项目,需要在运行时切换中断服务函数,如果向量表在Flash里,那就得擦写Flash,太麻烦了。
来看一个典型的向量表定义:
// 中断向量表结构
typedef struct {
Uint32 RESERVED; // 0x00: 复位向量
Uint32 INT1; // 0x02: INT1
Uint32 INT2; // 0x04: INT2
// ... 其他中断向量
Uint32 INT14; // 0x1C: INT14
Uint32 DLOG; // 0x1E: 数据日志
Uint32 RTOS; // 0x20: RTOS
Uint32 RESERVED2; // 0x22: 保留
Uint32 NMI; // 0x24: 不可屏蔽中断
Uint32 ILLEGAL; // 0x26: 非法指令
Uint32 USER1; // 0x28: 用户定义
// ...
} PIE_VECT_TABLE;
嗯,这里要注意:每个向量占2个字(32位),但实际地址是16位对齐的。所以你在写链接脚本时,一定要确保向量表是128字对齐的。我曾经见过有人忘了这个,结果中断跳转到了奇怪的地方,查了两天才发现是地址对齐问题。
PIE模块工作原理:中断的“交通警察”
PIE模块,你可以把它想象成一个交通警察。外设产生的中断请求,就像路上的车辆,PIE负责指挥它们有序地进入CPU。
PIE把96个外设中断分成了12组(INT1~INT12),每组8个中断。每组对应一个CPU中断。也就是说,CPU只处理12个中断源,但每个中断源下面挂了8个外设中断。
我画了一张图,帮你理解这个结构:
PIE模块内部有三个关键寄存器:
- PIEIER(中断使能寄存器):决定哪个中断被允许通过
- PIEIFR(中断标志寄存器):记录哪个中断正在请求
- PIEACK(中断应答寄存器):CPU处理完中断后,需要写1来清除对应组的应答位
这里有个坑:很多人忘了清PIEACK。中断服务函数执行完后,如果不写PIEACK,同组的下一个中断就进不来。我刚开始也犯过这个错,调试时发现某个中断只触发一次,后面就没了。查了半天,原来是PIEACK没清。
避坑指南:在中断服务函数末尾,一定要写 PieCtrlRegs.PIEACK.all = PIEACK_GROUPx; 来清除对应组的应答位。否则同组其他中断无法响应。
中断优先级配置:谁先谁后?
中断优先级,说白了就是多个中断同时发生时,CPU先处理谁。C2000的优先级规则其实很简单:
- CPU中断优先级:INT1最高,INT14最低。INT1~INT14的优先级是固定的,不能改。
- 组内中断优先级:每组8个中断,INTx.1最高,INTx.8最低。这也是固定的。
- 跨组中断:如果INT1.1和INT2.1同时发生,INT1.1先处理,因为INT1的CPU优先级更高。
但实际项目中,我们经常需要调整优先级。比如ADC采样中断需要比串口中断优先级高。怎么办?
方法一:把ADC中断放在INT1组里,串口中断放在INT2组里。因为INT1优先级高于INT2。
方法二:如果两个中断在同一组,那就只能通过软件来实现了。比如在低优先级中断里,判断是否有高优先级中断挂起,如果有就先处理高优先级的。
我个人习惯把实时性要求高的中断(比如ADC、ePWM)放在INT1~INT3,把通信类中断(SCI、SPI、I2C)放在INT4~INT6,把辅助功能中断放在后面。这样分配,基本能满足大部分项目需求。
| CPU中断 | 优先级 | 推荐用途 | 典型外设 |
|---|---|---|---|
| INT1 | 最高 | 实时控制 | ePWM、ADC |
| INT2~INT3 | 高 | 定时器、捕获 | CPU Timer、eCAP |
| INT4~INT6 | 中 | 通信 | SCI、SPI、I2C |
| INT7~INT12 | 低 | 辅助功能 | GPIO、eQEP |
| INT13~INT14 | 最低 | 非实时 | 看门狗、NMI |
中断服务函数编写规范
中断服务函数(ISR)怎么写?我总结了几个要点:
个人经验:ISR里只做最必要的事。把数据处理、协议解析这些耗时操作放到主循环或任务里。ISR里只做标志位设置、数据搬移、状态更新。
来看一个标准的ISR模板:
// 中断服务函数模板
interrupt void adc_isr(void)
{
// 1. 读取中断源,确认是谁触发了中断
Uint16 intSource = PieCtrlRegs.PIEIFR1.all;
// 2. 清除外设中断标志
AdcRegs.ADCINTFLGCLR.bit.ADCINT1 = 1;
// 3. 读取数据(尽快搬走)
adc_result = AdcResult.ADCRESULT0;
// 4. 设置标志位,通知主循环处理
adc_data_ready = 1;
// 5. 清除PIE应答位(非常重要!)
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
// 6. 返回(CPU自动恢复现场)
}
写ISR时,有几个铁律必须遵守:
- 不要用延时函数:ISR里调用DELAY_US()?那是找死。整个系统都会卡住。
- 不要调用printf:printf太慢了,而且可能重入。调试时可以用,正式代码里别用。
- 不要做浮点运算:除非你的C2000有FPU,否则浮点运算在ISR里会拖死系统。
- 变量加volatile:ISR和主循环共享的变量,一定要加volatile关键字。否则编译器优化后,主循环可能永远看不到ISR更新的值。
避坑指南:我曾经在一个项目里,ISR里调用了某个库函数,结果那个库函数内部用了全局变量,而且没加保护。两个中断同时调用时,全局变量被写乱了,导致系统崩溃。从那以后,我坚持ISR里只用局部变量和volatile全局变量。
最后,说一个很多人忽略的点:中断嵌套。C2000默认不支持中断嵌套,也就是说,一个ISR执行时,其他中断都被屏蔽了。如果你需要高优先级中断打断低优先级中断,得手动修改IER寄存器。但说实话,我很少用中断嵌套。因为中断嵌套会大大增加系统的复杂度和不确定性。除非你有非常明确的需求,否则保持ISR简短、不嵌套,是最稳妥的做法。
嗯,中断系统这块内容不少,但核心就三点:向量表怎么配、PIE怎么用、ISR怎么写。把这三点搞清楚了,C2000的中断系统你就掌握了七八成。剩下的,就是在实际项目中慢慢积累了。