第4章 中断服务程序编写:ISR编写规则、上下文限制、可重入性

好,咱们进入中断管理的核心环节——写ISR。说实话,很多VxWorks开发者对中断服务程序的认知,停留在“不就是个回调函数嘛”的层面。但实际项目中,ISR写不好,轻则系统卡死,重则设备“炸机”。我见过不止一次,因为ISR里调了个printf,整个系统直接挂掉。

这一章,咱们就把ISR的编写规则、上下文限制、可重入性这几个硬骨头啃下来。你想想看,中断来了,CPU正在干着活呢,突然被拽去执行你的ISR。这时候环境有多敏感?稍有不慎就是灾难。

4.1 ISR编写规则——这些红线不能碰

先列几条硬性规则。这些都是VxWorks内核强制的,违反了直接报错或者死机。

⚠️ 核心红线:ISR中绝对禁止调用可能引起阻塞的函数

什么叫阻塞函数?比如:

  • semTake()semGive()(带超时的信号量操作)
  • msgQSend()msgQReceive()(消息队列操作)
  • malloc()free()(动态内存分配)
  • printf()fprintf()(标准I/O)
  • taskDelay()taskSuspend()(任务调度相关)

为什么?因为ISR运行在中断上下文中,没有任务控制块(TCB)。你调用阻塞函数,内核找不到当前任务,直接panic。我早期做通信设备时,有个同事在ISR里调了printf调试,结果一上电就重启,查了两天才找到原因。

💡 个人习惯:我写ISR时,会先列一个“禁止调用清单”贴在代码注释里。每次review时对照检查,省得踩坑。

那ISR里能干什么?说白了,就三件事:

  1. 读取硬件状态——从寄存器拿数据
  2. 清除中断标志——告诉硬件“我处理完了”
  3. 通知任务层——通过信号量、事件、消息队列等机制,把数据交给任务处理

嗯,这里要注意:通知任务层时,只能用非阻塞的API。比如semGive()(不带超时)、msgQSend()(不带超时)、eventSend()。这些函数不会阻塞,可以安全调用。

4.2 上下文限制——ISR不是任务,别搞混了

ISR运行在中断上下文中,和任务上下文有本质区别。我刚开始学VxWorks时,总觉得“都是代码,有啥不一样?”后来被现实狠狠教育了一回。

特性 任务上下文 中断上下文
拥有TCB ✅ 是 ❌ 否
可被抢占 ✅ 是 ❌ 否(除非嵌套中断)
可调用阻塞函数 ✅ 是 ❌ 否
可访问浮点寄存器 ✅ 是(需配置) ❌ 默认不行
栈空间 任务栈(通常几KB) 中断栈(通常1-2KB)

你看,中断上下文就是个“轻量级”的执行环境。没有任务控制块,没有任务栈,连浮点运算都默认不支持。为什么会这样?因为中断处理要快,要轻,不能拖泥带水。

🔑 关键点:ISR的栈空间非常有限。我见过有人把大数组定义在ISR里,结果栈溢出,系统随机崩溃。排查这种问题,比找bug还痛苦。

关于浮点寄存器,这里多说一句。如果你的ISR里确实需要做浮点运算(比如处理传感器数据),必须在系统启动时调用intCtxFpSave()来使能中断上下文浮点保存。但说实话,我个人不建议这么做——浮点保存恢复的开销太大,违背了ISR“快进快出”的原则。

4.3 可重入性——同一个ISR,能同时跑吗?

可重入性,说白了就是:同一个ISR,在第一次调用还没返回时,又被第二次调用了,会不会出问题?

在VxWorks中,默认情况下,同优先级的中断不会嵌套。但不同优先级的中断可以嵌套。也就是说,一个低优先级的ISR正在执行,高优先级的中断来了,低优先级的ISR会被打断。

这时候,可重入性就变得至关重要。如果你的ISR里用了全局变量,而且没有做保护,那嵌套进来时数据就乱了。

举个例子:

/* ❌ 不可重入的ISR */
int global_counter = 0;

void isr_bad(void)
{
    global_counter++;  /* 危险!嵌套时数据不一致 */
    /* 处理中断... */
}

/* ✅ 可重入的ISR */
void isr_good(void)
{
    int local_counter;  /* 局部变量,每次调用独立 */
    local_counter = 0;
    /* 只操作局部变量或硬件寄存器 */
}

我曾经在一个多串口通信项目中,ISR里用了全局变量记录接收字节数。结果高优先级中断一嵌套,计数全乱套了。后来改成每个中断实例用独立的局部变量,问题解决。

💡 避坑指南:我曾经在ISR里使用volatile修饰全局变量,以为这样就能保证可重入。其实volatile只告诉编译器不要优化,它不解决并发访问的问题。真正保证可重入的方法是:只用局部变量,或者用硬件寄存器

那如果确实需要共享数据怎么办?我的做法是:在ISR里把数据写入一个环形缓冲区(ring buffer),然后在任务层去读取。环形缓冲区的读写指针用原子操作维护,这样既安全又高效。

4.4 实战:一个规范的ISR模板

说了这么多规则,咱们看个实际例子。这是我个人常用的ISR模板,经过多个项目验证,比较靠谱。

/* 中断服务程序模板 */
#include <vxWorks.h>
#include <intLib.h>
#include <semLib.h>

/* 全局信号量,用于通知任务 */
SEM_ID g_semUartRx;

/* 环形缓冲区 */
#define RING_BUF_SIZE 256
static char g_ringBuf[RING_BUF_SIZE];
static volatile int g_writeIdx = 0;
static volatile int g_readIdx = 0;

/* ISR:UART接收中断处理 */
void uartRxIsr(void)
{
    char data;
    
    /* 1. 读取硬件数据 */
    data = REG_READ(UART_RX_REG);
    
    /* 2. 写入环形缓冲区 */
    int nextIdx = (g_writeIdx + 1) % RING_BUF_SIZE;
    if (nextIdx != g_readIdx)  /* 缓冲区未满 */
    {
        g_ringBuf[g_writeIdx] = data;
        g_writeIdx = nextIdx;
    }
    
    /* 3. 清除中断标志 */
    REG_WRITE(UART_INT_CLR, 0x01);
    
    /* 4. 通知任务层处理 */
    semGive(g_semUartRx);  /* 非阻塞,安全 */
}

/* 任务:处理接收数据 */
void uartRxTask(void)
{
    while (1)
    {
        semTake(g_semUartRx, WAIT_FOREVER);
        
        while (g_readIdx != g_writeIdx)
        {
            char data = g_ringBuf[g_readIdx];
            g_readIdx = (g_readIdx + 1) % RING_BUF_SIZE;
            /* 处理数据... */
        }
    }
}

你看这个模板,ISR里只做了四件事:读数据、写缓冲区、清标志、发信号。没有阻塞调用,没有动态内存分配,没有浮点运算。干净利落。

🎯 核心原则:ISR要“快进快出”。我的经验是,一个ISR的执行时间不要超过100微秒。如果处理逻辑复杂,就通过信号量或消息队列交给任务层去做。

4.5 总结一下

写ISR,说白了就是戴着镣铐跳舞。规则多,限制严,但掌握了规律就不难。

  • 规则:不调阻塞函数,不用动态内存,不做浮点运算
  • 上下文:中断栈小,没有TCB,不能任务切换
  • 可重入:多用局部变量,少用全局变量,用环形缓冲区做数据中转

嗯,这些内容看起来条条框框很多,但等你真正写过几个ISR,踩过几次坑,就会明白——这些规则背后,都是血的教训。我当年调试一个中断嵌套导致的数据错乱问题,整整花了一周。从那以后,我写ISR就格外小心。

下一章,咱们聊聊中断与任务的同步机制。信号量、事件、消息队列,到底该用哪个?到时候我会结合项目经验,给你讲清楚。