第4章 中断服务程序编写:ISR编写规则、上下文限制、可重入性
好,咱们进入中断管理的核心环节——写ISR。说实话,很多VxWorks开发者对中断服务程序的认知,停留在“不就是个回调函数嘛”的层面。但实际项目中,ISR写不好,轻则系统卡死,重则设备“炸机”。我见过不止一次,因为ISR里调了个printf,整个系统直接挂掉。
这一章,咱们就把ISR的编写规则、上下文限制、可重入性这几个硬骨头啃下来。你想想看,中断来了,CPU正在干着活呢,突然被拽去执行你的ISR。这时候环境有多敏感?稍有不慎就是灾难。
4.1 ISR编写规则——这些红线不能碰
先列几条硬性规则。这些都是VxWorks内核强制的,违反了直接报错或者死机。
什么叫阻塞函数?比如:
semTake()、semGive()(带超时的信号量操作)msgQSend()、msgQReceive()(消息队列操作)malloc()、free()(动态内存分配)printf()、fprintf()(标准I/O)taskDelay()、taskSuspend()(任务调度相关)
为什么?因为ISR运行在中断上下文中,没有任务控制块(TCB)。你调用阻塞函数,内核找不到当前任务,直接panic。我早期做通信设备时,有个同事在ISR里调了printf调试,结果一上电就重启,查了两天才找到原因。
那ISR里能干什么?说白了,就三件事:
- 读取硬件状态——从寄存器拿数据
- 清除中断标志——告诉硬件“我处理完了”
- 通知任务层——通过信号量、事件、消息队列等机制,把数据交给任务处理
嗯,这里要注意:通知任务层时,只能用非阻塞的API。比如semGive()(不带超时)、msgQSend()(不带超时)、eventSend()。这些函数不会阻塞,可以安全调用。
4.2 上下文限制——ISR不是任务,别搞混了
ISR运行在中断上下文中,和任务上下文有本质区别。我刚开始学VxWorks时,总觉得“都是代码,有啥不一样?”后来被现实狠狠教育了一回。
| 特性 | 任务上下文 | 中断上下文 |
|---|---|---|
| 拥有TCB | ✅ 是 | ❌ 否 |
| 可被抢占 | ✅ 是 | ❌ 否(除非嵌套中断) |
| 可调用阻塞函数 | ✅ 是 | ❌ 否 |
| 可访问浮点寄存器 | ✅ 是(需配置) | ❌ 默认不行 |
| 栈空间 | 任务栈(通常几KB) | 中断栈(通常1-2KB) |
你看,中断上下文就是个“轻量级”的执行环境。没有任务控制块,没有任务栈,连浮点运算都默认不支持。为什么会这样?因为中断处理要快,要轻,不能拖泥带水。
关于浮点寄存器,这里多说一句。如果你的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里用了全局变量记录接收字节数。结果高优先级中断一嵌套,计数全乱套了。后来改成每个中断实例用独立的局部变量,问题解决。
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里只做了四件事:读数据、写缓冲区、清标志、发信号。没有阻塞调用,没有动态内存分配,没有浮点运算。干净利落。
4.5 总结一下
写ISR,说白了就是戴着镣铐跳舞。规则多,限制严,但掌握了规律就不难。
- 规则:不调阻塞函数,不用动态内存,不做浮点运算
- 上下文:中断栈小,没有TCB,不能任务切换
- 可重入:多用局部变量,少用全局变量,用环形缓冲区做数据中转
嗯,这些内容看起来条条框框很多,但等你真正写过几个ISR,踩过几次坑,就会明白——这些规则背后,都是血的教训。我当年调试一个中断嵌套导致的数据错乱问题,整整花了一周。从那以后,我写ISR就格外小心。
下一章,咱们聊聊中断与任务的同步机制。信号量、事件、消息队列,到底该用哪个?到时候我会结合项目经验,给你讲清楚。