一个中断把整个系统搞死了——FreeRTOS优先级踩坑实录
一个中断把整个系统搞死了——FreeRTOS优先级踩坑实录
void UART_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xTaskToNotify, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
这代码看起来没啥问题对吧?网上教程都这么写,官方demo也这么写。
之前一个项目里,整个系统跑了大概十几分钟就死一次——不是崩溃,是卡死,所有任务集体罢工,只剩那个最高优先级的UART处理任务在疯狂跑。但问题是,UART上根本没数据进来。
折腾了两天我才反应过来——问题根本就不在代码逻辑上,在任务优先级安排上。
递归优先级地狱
我那个系统的任务优先级大概是这样的:
| 优先级 | 任务 |
|---|---|
| 5 (最高) | uart_task |
| 4 | sensor_task |
| 3 | led_task |
| 2 | watchdog_task |
| 1 | idle_task |
uart_task我给了最高优先级,因为要响应串口命令嘛,怕丢数据。sensor_task跑传感器采集,led_task只是闪个灯。看起来很合理对吧?
问题是uart_task里我用了vTaskDelay(10)——就是跑完一轮处理逻辑之后delay 10ms再继续。结果你猜怎么着?因为它是最高优先级,delay结束了还是它跑,别的任务根本抢不过它。
但这不是死机的原因,真正的原因是中断。
传感器用的是I2C挂了个外部中断脚,每次数据准备好就触发中断。中断服务函数里调了xTaskNotifyFromISR,通知sensor_task去读数据。
一切看起来都很正常,直到我想来想去,拿逻辑分析仪看了一眼那个中断脚——
真相是中断在狂喷
中断脚每秒触发将近1000次。每次触发,ISR都试图唤醒sensor_task。但sensor_task优先级只有4,而uart_task优先级5一直占着CPU。sensor_task根本跑不了,它的notify pending flag就永远为true。每次中断再来,xTaskNotifyFromISR又在pending的基础上再加notification。
你以为这最多就是浪费点CPU?不,要命的是——FreeRTOS在调度器里有个逻辑:如果一个task有pending notification,并且它的优先级高于当前运行的任务,调度器会强行切换到它。问题是我所有中断都在UART_IRQHandler里,这个handler跑在中断上下文中,调度器只在中断退出时做一次context switch。
好戏来了:中断结束时,调度器一看有任务要切换,切出来,发现uart_task优先级最高,又切回uart_task。sensor_task虽然也有pending notification,但优先级不够,始终抢不到CPU。
但中断还在不断地触发。
FreeRTOS的tick中断会定期检查有没有更高优先级的任务就绪——但uart_task本来就已经是最高了。所以tick来了也白来,什么也不做。
然而问题出现在一个隐蔽的地方:xTaskNotifyGiveFromISR如果被频繁调用,会导致notification value溢出(不是数值溢出,是内部的pending状态机卡住)。FreeRTOS的notification机制在taskNOTIFICATION_WAITING和taskNOTIFICATION_RECEIVING之间来回跳,在高频中断下会产生一种"死锁"效果——task的TCB被标记为pending,但调度器永远无法正确地把它切进来。
所以最后的现象就是:这个任务既不在running状态,也不在ready状态,卡在两个状态的缝隙里。系统就"死"了。
怎么救的
三个改动:
第一,降低uart_task优先级。
降到2,跟watchdog_task平级。这样sensor_task(优先级4)在收到notification时能被调度器切进去。uart_task靠delay让出CPU,delay结束再抢回来——反正串口数据有buffer,十毫秒的延迟死不了人。
第二,中断里加去抖。
void EXTI_Sensor_IRQHandler(void)
{
static uint32_t last_trigger = 0;
uint32_t now = HAL_GetTick();
if (now - last_trigger < 10) return; // 10ms去抖
last_trigger = now;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xSensorTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
传感器数据刷新率才20Hz,中断每秒1000次显然是干扰。加了去抖之后正常了。
第三,终极武器——在ISR里不要直接用notification唤醒高优先级任务,改成用队列。
void EXTI_Sensor_IRQHandler(void)
{
uint8_t dummy = 1;
xQueueSendFromISR(xSensorQueue, &dummy, NULL);
}
任务那头在队列上阻塞等待:
void vSensorTask(void *pvParameters)
{
uint8_t dummy;
while(1) {
if(xQueueReceive(xSensorQueue, &dummy, portMAX_DELAY) == pdPASS) {
// 读传感器
HAL_I2C_Master_Receive(...);
}
}
}
队列的好处是,它不会像notification那样在高频场景下产生pending状态机的混乱。队列要么有数据要么没有,状态单一,不存在"半pending"这种尴尬情况。
一些后话
改完之后跑了三天没出问题。后来我把所有用到xTaskNotifyGiveFromISR的地方都重新审了一遍——发现有3个地方也有类似的隐患。那两个地方因为中断频率低所以没出问题,但天晓得哪天客户环境一变就炸了。
后来我跟一个做RTOS内核的朋友聊,他说其实FreeRTOS的notification设计初衷是"轻量单次通知",官方文档里也写了"only one notification state per task"——一个task只有1个notification状态位。高频触发的时候两个中断之间可能产生race,而队列是FIFO buffer,天然能扛住burst。
所以我现在有个习惯:中断频率 > 100Hz的场景,一律用队列,不用notification。 低频的触发(按键、报警量)才用notification。
其实吧,FreeRTOS的坑远不止这一个。光任务优先级这玩意儿,你在纸上画怎么都画得通,一上硬件就各种想不到的情况。我后来养了个习惯——每次调优先级都开着串口打印任务状态,用vTaskList或者uxTaskGetSystemState看一下每个任务的运行时间占比。看到某个任务占了99%的CPU但干的活很少的时候,心里就有数了。
更多推荐



所有评论(0)