4、中断系统与C语言实现:中断向量表与优先级、中断服务函数编写规范、中断嵌套与临界区保护、使用__interrupt关键字

中断系统,说白了就是ECU的「紧急事务处理机制」。CPU正在跑主循环,突然来了个紧急信号——比如曲轴位置传感器跳了个脉冲——CPU就得放下手头的事,先去处理这个信号。处理完再回来接着干。这个机制要是搞不好,整个ECU就废了。

我刚开始做底层开发时,对中断的理解就停留在「打断一下」这个层面。直到有一次,一个客户的车在高速上偶发性熄火,查了三个月,最后发现是中断优先级配错了,低优先级的中断把高优先级的给堵死了。嗯,从那以后,我对中断系统就再也不敢马虎了。

4.1 中断向量表——ECU的「紧急联络簿」

中断向量表是什么?说白了就是一张地址表。每个中断源对应一个入口地址,CPU收到中断信号后,就去这张表里查该跳到哪里去执行。

在嵌入式C语言里,中断向量表通常定义在链接脚本中,或者用编译器特定的语法来声明。以Infineon TC2xx系列为例,它的中断向量表长这样:

/* 中断向量表定义示例(TC2xx系列) */
#define INT_VECTOR_BASE    0x80000000U

/* 中断服务函数声明 */
void ISR_ADC(void);
void ISR_CAN(void);
void ISR_TIMER(void);

/* 中断向量表 —— 实际项目中通常放在链接脚本或启动文件里 */
__attribute__((section(".intvec"))) 
const tIntVectorEntry IntVectorTable[] = 
{
    { 0x0000, (uint32)ISR_ADC   },   /* ADC中断 */
    { 0x0010, (uint32)ISR_CAN   },   /* CAN中断 */
    { 0x0020, (uint32)ISR_TIMER },   /* 定时器中断 */
    /* ... 其他中断向量 */
};

这里有个坑,我必须要说。中断向量表的地址必须对齐到特定的边界,通常是256字节或1KB。我曾经见过一个同事,把向量表定义在了一个未对齐的地址上,结果中断一触发,CPU直接跑飞。查了两天才找到原因。

⚠️ 注意:中断向量表的存放位置和格式,完全取决于具体的芯片架构。ARM Cortex-M系列和TriCore系列的处理方式完全不同。写代码前,一定要先看芯片的参考手册,别想当然。

4.2 中断优先级——谁先谁后,得有个规矩

多个中断同时来了怎么办?谁先处理?这就涉及到优先级了。

在AUTOSAR架构下,中断优先级通常分为硬件优先级和软件优先级两层。硬件优先级由芯片的中断控制器决定,软件优先级则由OS(操作系统)来管理。

我个人习惯把中断优先级分成三个等级:

优先级等级 典型用途 响应时间要求
高优先级(0-1) 曲轴/凸轮轴信号、故障保护 < 5μs
中优先级(2-3) ADC采样、CAN通信 < 50μs
低优先级(4-7) 诊断、非实时数据采集 < 1ms

配置优先级时,有个原则要记住:时间要求越苛刻的中断,优先级越高。发动机的曲轴信号中断,优先级必须最高,因为错过一个脉冲,点火正时就偏了,发动机可能直接抖起来。

💡 核心原则:中断优先级不是越高越好。优先级太高,会阻塞其他中断;优先级太低,又可能被频繁打断导致响应不及时。找到平衡点,才是关键。

4.3 中断服务函数编写规范——别在ISR里干重活

中断服务函数(ISR)怎么写?很多新手容易犯一个错误:在ISR里写了一大堆逻辑,甚至调用printf来调试。这是大忌。

ISR的编写规范,我总结为「三要三不要」:

  • 要短小精悍——ISR里只做最必要的事,比如读取数据、设置标志位
  • 要快进快出——执行时间越短越好,别在里面做复杂计算
  • 要保护现场——进入ISR时保存上下文,退出时恢复
  • 不要调用阻塞函数——比如delay、while循环
  • 不要使用不可重入函数——比如某些库函数
  • 不要访问共享资源不加锁——这个后面临界区会讲

一个规范的ISR示例:

/* 好的ISR写法 —— 只做必要的事 */
__interrupt void ISR_ADC_Handler(void)
{
    /* 1. 读取ADC结果寄存器 */
    uint16 adcValue = ADC_RESULT_REG;
    
    /* 2. 将数据存入缓冲区(用volatile修饰) */
    g_AdcBuffer[g_AdcIndex] = adcValue;
    
    /* 3. 更新索引 */
    g_AdcIndex = (g_AdcIndex + 1) & 0x0F;
    
    /* 4. 设置标志位,通知主循环处理 */
    g_AdcReadyFlag = TRUE;
    
    /* 5. 清除中断标志 */
    ADC_INT_FLAG = 0U;
}

/* 坏的ISR写法 —— 在中断里做复杂处理 */
__interrupt void ISR_ADC_Bad(void)
{
    /* ❌ 不要在ISR里做滤波算法 */
    for(int i = 0; i < 100; i++) { ... }
    
    /* ❌ 不要在ISR里调用printf */
    printf("ADC value: %d\n", value);
    
    /* ❌ 不要在ISR里做延时 */
    delay_ms(10);
}

📌 我的经验:ISR里只做三件事——读硬件、存数据、设标志。剩下的处理,全部交给主循环或任务去干。这样既保证了中断的快速响应,又避免了复杂逻辑带来的风险。

4.4 中断嵌套与临界区保护——别让中断「打架」

中断嵌套,就是高优先级的中断打断了正在执行的低优先级中断。这本身没问题,但问题出在共享资源的访问上。

你想想看,如果两个中断都去修改同一个全局变量,会发生什么?数据竞争!结果就是变量值变得不可预测,程序行为诡异。

临界区,就是那些「不允许被中断打断」的代码段。保护临界区,常用的方法就是关中断和开中断。

/* 临界区保护示例 */
volatile uint32 g_SharedCounter = 0U;

void UpdateSharedCounter(void)
{
    /* 进入临界区 —— 关中断 */
    DisableAllInterrupts();
    
    /* 临界区代码 —— 操作共享资源 */
    g_SharedCounter++;
    g_SharedCounter += 5U;
    
    /* 退出临界区 —— 开中断 */
    EnableAllInterrupts();
}

这里有个细节要注意:关中断的时间不能太长。我曾经在一个项目里,看到有人在临界区里做了个循环等待,结果关中断时间超过了100μs,导致高优先级的中断丢失了。发动机转速信号直接丢了,车就熄火了。

⚠️ 警告:临界区内的代码执行时间,必须严格控制。一般建议不超过10μs。如果确实需要长时间保护,考虑使用信号量或互斥锁,而不是简单地关中断。

在AUTOSAR架构下,临界区保护有更规范的做法:

/* AUTOSAR风格的临界区保护 */
void AUTOSAR_CriticalSection(void)
{
    /* 保存当前中断状态并关中断 */
    uint32 irqState = SuspendAllInterrupts();
    
    /* 临界区代码 */
    g_SharedData = newValue;
    
    /* 恢复中断状态 */
    ResumeAllInterrupts(irqState);
}

为什么要保存中断状态?因为如果进入临界区前中断本来就是关的,退出时就不能简单地开中断,而是要恢复到之前的状态。这个细节,很多新手会忽略。

4.5 使用__interrupt关键字——编译器帮你做的事

不同的编译器,声明中断服务函数的关键字不一样。在Tasking编译器里用__interrupt,在GCC里用__attribute__((interrupt)),在IAR里用__irq

这个关键字到底做了什么?说白了,它告诉编译器:这个函数是中断服务函数,进入和退出时,编译器要自动生成保存和恢复上下文的代码。

/* Tasking编译器 —— 使用__interrupt关键字 */
__interrupt void ISR_Timer(void)
{
    /* 编译器自动生成:
     *   - 保存CPU寄存器
     *   - 保存PSW(程序状态字)
     *   - 切换到中断栈
     */
    
    g_TimerTick++;
    
    /* 编译器自动生成:
     *   - 恢复CPU寄存器
     *   - 恢复PSW
     *   - 返回被中断的代码
     */
}

/* GCC编译器 —— 使用__attribute__((interrupt)) */
__attribute__((interrupt)) void ISR_Timer_GCC(void)
{
    g_TimerTick++;
}

我个人建议,永远不要手动去写保存和恢复上下文的汇编代码。让编译器去做,既安全又省事。你只需要确保ISR函数用对了关键字就行。

💡 关键点:使用__interrupt关键字后,ISR函数不能有参数,也不能有返回值。因为中断是硬件触发的,不是软件调用的。如果你在ISR里写了参数,编译器会报错。

嗯,中断系统这块内容,说复杂也复杂,说简单也简单。核心就三点:向量表要配对、优先级要合理、ISR要短小。把这三点做好了,ECU的中断系统基本就稳了。

我在做AUTOSAR底层驱动时,每次写完中断相关的代码,都会做一件事:用逻辑分析仪抓一下中断响应时间。如果发现某个中断的响应时间超过了预期,就回头检查优先级配置和临界区保护。这个方法,推荐你也试试。