FreeRTOS任务创建成功却不运行?这7个原因一定要查!从调度器、优先级到栈溢出全面排查

前言
在 STM32、GD32 等 MCU 上使用 FreeRTOS 时,经常会遇到一种非常让人困惑的现象:
BaseType_t ret;
ret = xTaskCreate(
AppTask,
"AppTask",
256,
NULL,
2,
NULL
);
if (ret == pdPASS)
{
printf("Task create success!\\r\\n");
}
串口已经打印:
Task create success!
说明:
xTaskCreate() == pdPASS
但是进入任务以后:
void AppTask(void *argument)
{
printf("AppTask running!\\r\\n");
for (;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
却发现:
没有打印
LED不闪
断点进不去
任务好像根本没有执行
于是很多人的第一反应就是:
xTaskCreate() 明明已经返回 pdPASS 了,为什么任务还是不运行?
这里最重要的一点是:
任务创建成功,不等于任务一定能够运行。
xTaskCreate() 成功主要说明 FreeRTOS 已经为任务创建了任务控制块、任务栈等资源,并把任务加入了相应的任务管理结构。
但任务真正运行还需要满足很多条件:
任务创建成功
↓
调度器已经启动
↓
任务进入Ready状态
↓
没有被更高优先级任务长期压制
↓
没有处于Blocked/Suspended状态
↓
任务栈和任务入口正确
↓
SysTick / PendSV / SVC工作正常
↓
任务真正获得CPU
本文就围绕这个问题,详细讲解 FreeRTOS 中:
任务明明创建成功,却不运行的7个常见原因。
一、先理解:xTaskCreate成功到底意味着什么?
首先看一个典型任务创建代码:
BaseType_t ret;
ret = xTaskCreate(
AppTask, /* 任务函数 */
"AppTask", /* 任务名称 */
256, /* 栈深度 */
NULL, /* 参数 */
2, /* 优先级 */
NULL /* 任务句柄 */
);
如果返回:
pdPASS
一般说明:
FreeRTOS已经成功申请任务相关内存,并建立了任务控制结构。
任务内部通常会对应一个:
TCB
Task Control Block
也就是:
任务控制块
TCB中会记录类似:
任务栈指针
任务优先级
任务状态
任务名称
任务链表节点
任务通知值
运行状态
同时 FreeRTOS 还会给任务准备:
Task Stack
然后将任务加入:
Ready List
或者相应的任务列表。
因此:
xTaskCreate() == pdPASS
只能说明:
任务被成功创建了
不能直接说明:
任务已经获得CPU执行
这两个概念一定要分开。
二、FreeRTOS任务到底是怎么运行起来的?
一个简单 FreeRTOS 工程通常类似:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
xTaskCreate(
AppTask,
"AppTask",
256,
NULL,
2,
NULL
);
vTaskStartScheduler();
while (1)
{
}
}
启动过程可以理解为:
MCU启动
↓
main()
↓
硬件初始化
↓
xTaskCreate()
↓
任务被加入就绪列表
↓
vTaskStartScheduler()
↓
创建Idle Task
↓
必要时创建Timer Task
↓
启动RTOS Tick
↓
启动第一次任务调度
↓
最高优先级Ready任务开始运行
因此:
xTaskCreate只是“创建任务”,vTaskStartScheduler才真正“启动任务调度”。
三、原因1:忘记调用vTaskStartScheduler()
这是最基础,也最容易出现的问题。
例如:
int main(void)
{
HAL_Init();
SystemClock_Config();
xTaskCreate(
AppTask,
"AppTask",
256,
NULL,
2,
NULL
);
while (1)
{
}
}
这里:
xTaskCreate()
完全可能成功。
但是没有:
vTaskStartScheduler();
那么任务永远不会被 FreeRTOS 正常调度执行。
正确写法
int main(void)
{
HAL_Init();
SystemClock_Config();
xTaskCreate(
AppTask,
"AppTask",
256,
NULL,
2,
NULL
);
vTaskStartScheduler();
for (;;)
{
}
}
流程:
xTaskCreate
↓
任务Ready
↓
vTaskStartScheduler
↓
启动调度器
↓
任务运行
为什么任务创建以后不会自动运行?
因为在启动调度器以前:
CPU仍然在执行main()
FreeRTOS只是把任务准备好了。
真正的:
上下文切换
任务调度
Tick中断
优先级管理
还没有正式开始。
所以可以这样理解:
xTaskCreate() 是把运动员放到起跑线上。
vTaskStartScheduler() 才是真正鸣枪开始比赛。
四、怎么快速判断是不是忘了启动调度器?
最简单的方法:
在:
vTaskStartScheduler();
前后分别加打印:
printf("Before scheduler\\r\\n");
vTaskStartScheduler();
printf("Scheduler returned!\\r\\n");
正常情况下:
Before scheduler
应该能打印。
但如果调度器正常启动:
Scheduler returned!
通常不应该继续正常执行。
因此:
Before scheduler
打印后进入任务:
正常
如果根本没有调用:
vTaskStartScheduler()
任务当然不会运行。
五、原因2:vTaskStartScheduler启动失败
第二种情况更加隐蔽。
代码明明有:
vTaskStartScheduler();
但任务还是不运行。
甚至:
vTaskStartScheduler();
执行以后,程序又回到了:
main()
后面的代码。
例如:
vTaskStartScheduler();
printf("ERROR: scheduler failed!\\r\\n");
while (1)
{
}
如果真的打印:
ERROR: scheduler failed!
就说明:
调度器没有正常启动。
六、为什么调度器会启动失败?
FreeRTOS启动调度器时,通常还需要创建:
Idle Task
也就是:
空闲任务
如果启用了:
configUSE_TIMERS
还需要创建:
Timer Service Task
也叫:
Software Timer Task
这些任务也需要:
TCB
+
Stack
+
Heap
如果 FreeRTOS Heap 已经不足,就可能导致调度器无法正常启动。
七、典型原因:configTOTAL_HEAP_SIZE太小
例如:
#define configTOTAL_HEAP_SIZE (5 * 1024)
但你已经创建:
Task1
Task2
Task3
Task4
Queue
Semaphore
Timer
大量RTOS对象。
剩余 Heap 不够创建:
Idle Task
Timer Task
调度器就可能启动失败。
八、如何查看FreeRTOS剩余Heap?
可以使用:
xPortGetFreeHeapSize();
例如:
printf(
"Free Heap = %lu\\r\\n",
(unsigned long)xPortGetFreeHeapSize()
);
还可以查看历史最小剩余值:
xPortGetMinimumEverFreeHeapSize();
例如:
printf(
"Min Ever Free Heap = %lu\\r\\n",
(unsigned long)xPortGetMinimumEverFreeHeapSize()
);
两个值的意义不同。
xPortGetFreeHeapSize()
表示:
当前还有多少FreeRTOS Heap
xPortGetMinimumEverFreeHeapSize()
表示:
从系统启动以来
FreeRTOS Heap最少曾经剩多少
后者非常适合判断:
Heap到底有没有配置得过于紧张
九、建议打开Malloc Failed Hook
FreeRTOS提供:
vApplicationMallocFailedHook()
如果启用:
#define configUSE_MALLOC_FAILED_HOOK 1
就可以实现:
void vApplicationMallocFailedHook(void)
{
taskDISABLE_INTERRUPTS();
printf("FreeRTOS malloc failed!\\r\\n");
for (;;)
{
}
}
如果系统卡在这里:
基本就可以确认:
FreeRTOS动态内存申请失败
需要检查:
configTOTAL_HEAP_SIZE
任务StackDepth
Queue长度
Semaphore
Software Timer
其他动态对象
十、还要检查heap_x.c
FreeRTOS提供多种内存管理方案:
heap_1.c
heap_2.c
heap_3.c
heap_4.c
heap_5.c
最常用之一是:
heap_4.c
如果工程:
忘记加入heap_x.c
或者:
重复加入多个heap_x.c
也会产生问题。
一般一个工程应该选择:
一种合适的Heap实现
而不是把:
heap_1
heap_2
heap_4
同时加入工程。
十一、原因3:任务优先级太低,被高优先级任务“饿死”
这是 FreeRTOS 中非常经典的问题。
假设:
HighTask
优先级 = 5
LowTask
优先级 = 1
HighTask:
void HighTask(void *argument)
{
for (;;)
{
Work();
}
}
LowTask:
void LowTask(void *argument)
{
for (;;)
{
printf("LowTask running\\r\\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
如果:
configUSE_PREEMPTION = 1
而 HighTask:
永远处于Ready状态
又从不:
Delay
等待队列
等待信号量
等待事件
等待通知
那么只要 HighTask 优先级高于 LowTask:
调度器永远优先运行HighTask
LowTask可能一直抢不到CPU。
十二、任务“饿死”是什么意思?
所谓 Starvation:
任务存在
任务创建成功
任务甚至处于Ready状态
但:
永远没有机会获得CPU
流程:
HighTask Ready
↓
优先级5
↓
一直运行
↓
从不阻塞
↓
LowTask优先级1
↓
永远无法成为最高优先级Ready任务
所以 LowTask:
不是不存在
而是:
一直没有被调度
十三、怎么解决高优先级任务长期占CPU?
典型错误:
void HighTask(void *argument)
{
for (;;)
{
Process();
}
}
更合理:
void HighTask(void *argument)
{
for (;;)
{
Process();
vTaskDelay(pdMS_TO_TICKS(1));
}
}
或者更推荐事件驱动:
void HighTask(void *argument)
{
for (;;)
{
xQueueReceive(
QueueHandle,
&data,
portMAX_DELAY
);
Process(data);
}
}
这样任务没有工作时会进入:
Blocked
CPU就可以让给其他任务。
十四、不要通过疯狂提高优先级解决所有问题
例如:
TaskA不运行
→ 把优先级从1改到5
TaskB又不运行
→ 改成6
TaskC不运行
→ 改成7
最后变成:
所有任务都不断提高优先级
这是非常不合理的。
更正确的思路是:
检查高优先级任务有没有正确进入阻塞状态。
FreeRTOS的核心思想并不是:
所有任务不停while(1)抢CPU
而是:
有事情的任务运行
没事情的任务阻塞
十五、原因4:任务其实运行过,但马上进入Blocked状态
这是一个特别容易误判的情况。
例如:
void UartTask(void *argument)
{
uint8_t data;
for (;;)
{
xQueueReceive(
UartQueue,
&data,
portMAX_DELAY
);
printf("RX = %02X\\r\\n", data);
}
}
任务启动以后马上执行:
xQueueReceive()
但队列中:
没有数据
而等待时间:
portMAX_DELAY
于是:
任务进入Blocked状态
如果你把断点打在:
printf()
那么你可能永远进不去。
于是误认为:
UartTask根本没运行。
实际上:
它已经运行了
只是:
正在等Queue
十六、FreeRTOS常见任务状态
理解任务不运行,一定要理解几个状态。
典型包括:
Running
Ready
Blocked
Suspended
Deleted
Running
正在CPU上执行:
Running
单核MCU在某个时刻通常只有一个任务处于 Running。
Ready
已经具备运行条件:
Ready
只是目前CPU可能正在执行:
更高优先级任务
Blocked
正在等待:
时间
Queue
Semaphore
Mutex
Event Group
Task Notification
Stream Buffer
Message Buffer
所以暂时不能运行。
Suspended
被主动挂起:
vTaskSuspend();
需要:
vTaskResume();
才能重新进入调度。
十七、哪些API会让任务进入Blocked状态?
常见:
vTaskDelay();
vTaskDelayUntil();
xQueueReceive();
xQueueSend();
某些情况下:
xSemaphoreTake();
xEventGroupWaitBits();
ulTaskNotifyTake();
xTaskNotifyWait();
等等。
因此当一个任务“没运行”时,要先问:
它是不是其实正在等待某个事件?
十八、典型问题:信号量永远没有Give
例如:
void AppTask(void *argument)
{
for (;;)
{
xSemaphoreTake(
AppSem,
portMAX_DELAY
);
printf("App running\\r\\n");
}
}
但整个程序里:
从来没有
xSemaphoreGive()
那么任务:
第一次运行
↓
进入Semaphore等待
↓
永远Blocked
自然不会继续打印。
十九、典型问题:任务被Suspend以后没人Resume
例如:
vTaskSuspend(AppTaskHandle);
后续却没有:
vTaskResume(AppTaskHandle);
那么任务会一直处于:
Suspended
也会表现成:
任务创建成功但不再运行
二十、如何判断任务到底是什么状态?
FreeRTOS提供:
eTaskGetState();
例如:
eTaskState state;
state = eTaskGetState(AppTaskHandle);
常见返回:
eRunning
eReady
eBlocked
eSuspended
eDeleted
eInvalid
这样就可以快速判断:
任务到底是不被调度
还是正在阻塞
二十一、还可以使用vTaskList()
如果配置允许,可以:
char buffer[512];
vTaskList(buffer);
printf("%s\\r\\n", buffer);
可能得到类似:
Name State Prio Stack Num
AppTask B 2 156 2
CommTask R 3 190 3
IDLE R 0 110 1
状态可能类似:
R = Running/Ready
B = Blocked
S = Suspended
D = Deleted
这对调试非常有帮助。
不过使用:
vTaskList()
需要根据 FreeRTOSConfig.h 开启对应配置。
二十二、原因5:任务函数写法错误
FreeRTOS任务函数并不是普通函数。
典型任务入口:
void AppTask(void *argument)
{
for (;;)
{
Work();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
函数原型通常类似:
void TaskFunction(void *pvParameters);
二十三、任务函数不要直接return
错误示例:
void AppTask(void *argument)
{
InitHardware();
return;
}
这种写法有问题。
FreeRTOS任务函数通常:
不能像普通函数一样直接return。
因为任务启动时,并不是通过普通:
AppTask();
函数调用方式进入。
任务运行环境是由 FreeRTOS 调度器构造出来的。
因此任务函数返回并没有一个普通函数意义上的合法调用者可以返回。
不同FreeRTOS移植层可能通过错误处理、断言或异常路径处理这种情况,总之:
不应该让任务函数自然return。
二十四、如果任务真的需要结束怎么办?
应该调用:
vTaskDelete(NULL);
例如:
void OneShotTask(void *argument)
{
DoSomething();
vTaskDelete(NULL);
}
表示:
当前任务执行完自己的工作
↓
主动删除自己
而不是:
return;
二十五、任务函数应该有无限循环吗?
长期任务一般写成:
void AppTask(void *argument)
{
for (;;)
{
Work();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
或者:
void AppTask(void *argument)
{
for (;;)
{
xQueueReceive(
Queue,
&msg,
portMAX_DELAY
);
ProcessMessage(&msg);
}
}
也就是说:
任务本身通常长期存在,但是不代表它要一直占用CPU。
推荐:
无限生命周期
+
事件驱动/阻塞等待
而不是:
无限忙循环
二十六、注意任务参数生命周期
例如:
void CreateTask(void)
{
int parameter = 100;
xTaskCreate(
AppTask,
"AppTask",
256,
¶meter,
2,
NULL
);
}
如果:
parameter
是局部变量。
CreateTask返回以后:
parameter所在栈空间已经失效
但新任务仍然保存:
¶meter
这个指针。
之后使用:
void AppTask(void *argument)
{
int value = *(int *)argument;
}
就可能访问失效内存。
结果可能:
异常数据
HardFault
任务刚启动就跑飞
看起来也像:
任务不运行
实际上是:
任务刚开始运行就异常了
二十七、原因6:任务栈太小,发生Stack Overflow
这是 FreeRTOS 项目中非常常见,同时也非常危险的问题。
例如:
xTaskCreate(
AppTask,
"AppTask",
128,
NULL,
2,
NULL
);
任务内部却有:
void AppTask(void *argument)
{
uint8_t buffer[500];
float value[100];
printf("Task Start\\r\\n");
for (;;)
{
ComplexFunction();
vTaskDelay(10);
}
}
这时候:
任务虽然创建成功
但是:
栈空间根本不够
可能一运行就:
Stack Overflow
二十八、任务创建成功为什么不能证明Stack够?
因为 xTaskCreate() 只检查:
当前是否有足够内存按照你指定的 StackDepth 创建这个任务。
例如你指定:
StackDepth = 128
FreeRTOS成功申请了:
128个StackType_t单位
那么创建就可能成功。
但 FreeRTOS 并不知道你的任务以后会不会执行:
printf
递归
大数组
多层函数调用
浮点运算
复杂库函数
所以:
任务创建成功
只代表:
分配成功
并不代表:
运行时一定够用
二十九、StackDepth一定是字节数吗?
这是一个非常重要的坑。
xTaskCreate() 中:
usStackDepth
通常以:
StackType_t
为单位,而不是简单理解为字节。
例如某些32位MCU中:
sizeof(StackType_t) = 4
那么:
StackDepth = 256
可能对应约:
256 × 4
=
1024 Bytes
但是具体一定要根据:
FreeRTOS移植层
StackType_t定义
确认。
不要机械认为:
256 = 256字节
三十、栈溢出会有什么表现?
可能出现:
HardFault
也可能:
程序随机重启
甚至:
打印乱码
或者:
其他任务突然异常
还有一种最迷惑的表现:
任务创建成功
但是断点一直看起来进不去
实际上可能:
刚进入任务
↓
栈已经损坏
↓
立即异常
三十一、打开FreeRTOS栈溢出检测
建议在:
FreeRTOSConfig.h
中配置:
#define configCHECK_FOR_STACK_OVERFLOW 2
然后实现:
void vApplicationStackOverflowHook(
TaskHandle_t xTask,
char *pcTaskName)
{
taskDISABLE_INTERRUPTS();
printf(
"Stack overflow: %s\\r\\n",
pcTaskName
);
for (;;)
{
}
}
如果任务发生栈溢出,就更容易快速定位。
三十二、使用uxTaskGetStackHighWaterMark()
这是分析任务栈非常有价值的API。
例如:
UBaseType_t watermark;
watermark =
uxTaskGetStackHighWaterMark(
AppTaskHandle
);
printf(
"Stack High Water Mark = %lu\\r\\n",
(unsigned long)watermark
);
它表示:
从任务创建到当前时刻,任务栈曾经剩余的最小安全空间。
简单理解:
值很大
→ 栈比较宽裕
值很小
→ 栈比较危险
注意它返回的单位通常仍与:
StackType_t
有关。
三十三、哪些代码特别吃任务栈?
常见包括:
1. 大型局部数组
void Task(void *arg)
{
uint8_t buffer[1024];
}
2. printf
尤其涉及:
浮点格式化
时可能明显增加栈需求。
3. 深层函数调用
例如:
Task
↓
FunctionA
↓
FunctionB
↓
FunctionC
↓
FunctionD
每一层函数调用都可能继续使用栈。
4. 浮点运算
某些架构和库配置下:
浮点上下文
库函数
可能明显增加栈压力。
5. 局部大型结构体
例如:
BigStruct_t data;
如果结构体本身数百字节甚至几KB:
直接放在任务栈
非常危险。
三十四、原因7:SysTick、PendSV或中断配置异常
这是最底层,也是相对难排查的一类。
FreeRTOS任务调度离不开几个重要异常/中断机制,例如 Cortex-M 平台经常涉及:
SysTick
PendSV
SVC
可以简单理解:
SysTick
→ 提供RTOS时间节拍
PendSV
→ 完成任务上下文切换
SVC
→ 与调度器启动等底层机制有关
如果这些机制异常:
任务创建完全可能成功
但:
任务切换可能完全失败
三十五、SysTick有什么作用?
FreeRTOS需要一个:
Tick
周期性节拍。
例如:
#define configTICK_RATE_HZ 1000
表示:
每秒1000个Tick
也就是:
1 Tick = 1ms
对于常见配置而言。
很多:
vTaskDelay()
vTaskDelayUntil()
xQueueReceive(..., timeout)
都依赖 Tick。
三十六、如果SysTick没有正常工作会发生什么?
例如:
void AppTask(void *argument)
{
for (;;)
{
printf("Task\\r\\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
第一次可能执行:
Task
然后进入:
Blocked
等待:
1000ms
但如果:
FreeRTOS Tick根本不增加
这个任务可能:
永远无法解除阻塞
于是看起来像:
任务只运行了一次,然后彻底不动了。
三十七、为什么STM32 HAL和FreeRTOS容易在SysTick上冲突?
STM32 HAL本身也常使用:
系统Tick
完成:
HAL_Delay
HAL_GetTick
超时判断
而 FreeRTOS 同样需要:
RTOS Tick
因此在 STM32 + FreeRTOS 工程中:
SysTick由谁管理
HAL Tick使用哪个Timer
FreeRTOS Tick入口是否正确
必须配置清楚。
CubeMX生成的工程通常会根据配置处理这些关系。
但如果:
手动移植FreeRTOS
修改中断文件
自己重写SysTick_Handler
就特别容易出问题。
三十八、检查SysTick_Handler
某些移植中可能存在:
void SysTick_Handler(void)
{
xPortSysTickHandler();
}
或者底层通过 FreeRTOS 端口宏:
vPortSVCHandler
xPortPendSVHandler
xPortSysTickHandler
与芯片异常入口对应。
具体名称和映射方式取决于:
MCU
FreeRTOS Port
CMSIS
CubeMX
工程配置
不能机械照搬别的芯片代码。
但核心原则是:
FreeRTOS的Tick、PendSV和SVC入口必须正确接入。
三十九、PendSV异常会发生什么?
FreeRTOS 在 Cortex-M 上通常利用:
PendSV
完成任务上下文切换。
如果 PendSV:
没有正确进入FreeRTOS Handler
可能出现:
任务1一直运行
任务2永远切不过去
甚至:
第一次任务就无法正常启动
四十、中断优先级配置为什么非常重要?
Cortex-M 中:
优先级数字越小
通常代表实际优先级越高
例如:
Priority 0
通常比:
Priority 5
更高。
这和很多初学者直觉正好相反。
FreeRTOS通常配置:
configPRIO_BITS
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
或者:
configMAX_SYSCALL_INTERRUPT_PRIORITY
等参数。
如果配置错误,就可能导致:
中断调用FreeRTOS API异常
调度异常
链表损坏
随机HardFault
任务无法切换
四十一、ISR中不能随便调用普通FreeRTOS API
例如中断里错误使用:
xQueueSend();
而不是:
xQueueSendFromISR();
就可能破坏系统行为。
中断环境应使用对应:
FromISR
版本API,例如:
xQueueSendFromISR();
xSemaphoreGiveFromISR();
vTaskNotifyGiveFromISR();
并根据返回情况触发:
portYIELD_FROM_ISR();
或者对应端口宏。
四十二、长时间关闭中断也会让任务“不运行”
例如:
taskENTER_CRITICAL();
while (1)
{
Process();
}
如果一直不:
taskEXIT_CRITICAL();
就可能导致:
Tick无法正常处理
PendSV无法正常切换
其他中断受影响
最后:
系统看起来像死在某一个任务里
所以如果任务切换突然停止,要检查:
有没有长期关中断
有没有临界区忘记退出
有没有BASEPRI/PRIMASK配置异常
四十三、除了7个原因,还要检查任务句柄和静态创建
虽然前面7个是最常见问题,但实际工程还有一些值得注意的地方。
例如使用:
xTaskCreateStatic()
时,需要自己提供:
Stack Buffer
StaticTask_t
如果这些变量:
生命周期错误
例如定义成某个函数中的普通局部变量:
void CreateTasks(void)
{
StackType_t stack[256];
StaticTask_t tcb;
xTaskCreateStatic(
Task,
"Task",
256,
NULL,
2,
stack,
&tcb
);
}
函数返回以后:
stack
tcb
对应存储生命周期结束。
后面任务行为就完全不可靠。
使用静态创建时,这些对象必须长期有效。
四十四、任务优先级有没有超出configMAX_PRIORITIES?
FreeRTOS中:
#define configMAX_PRIORITIES 8
通常意味着有效优先级范围类似:
0 ~ 7
其中:
0
通常是最低任务优先级。
任务优先级配置必须遵守:
configMAX_PRIORITIES
的范围和对应API要求。
四十五、任务刚创建后什么时候开始运行?
这是理解FreeRTOS调度非常重要的一点。
调度器还没启动
例如:
xTaskCreate(Task1, ...);
xTaskCreate(Task2, ...);
vTaskStartScheduler();
Task1和Task2虽然已经创建:
都不会真正运行
直到:
vTaskStartScheduler()
调度器已经运行
如果某个运行中的任务调用:
xTaskCreate(NewTask, ..., priority, ...);
而新任务的优先级:
高于当前任务
那么在抢占式调度中,新任务可能:
创建完成后很快立即获得CPU
所以运行时创建任务和启动前创建任务的行为需要区分。
四十六、为什么printf并不是判断任务运行的最佳方式?
很多人判断任务有没有运行,只看:
printf("Task Running\\r\\n");
但是如果:
printf本身没有线程安全
UART被多个任务同时使用
printf被重定向到阻塞UART
串口锁死
UART初始化异常
你可能看不到打印。
但任务实际上已经运行。
因此更可靠的方式包括:
GPIO翻转
任务计数器
Debugger断点
Trace工具
FreeRTOS任务列表
Runtime Stats
例如:
volatile uint32_t task_run_count = 0;
void AppTask(void *argument)
{
for (;;)
{
task_run_count++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
在调试器 Watch 窗口观察:
task_run_count
是否增长。
四十七、推荐给每个任务增加“生命计数器”
例如:
volatile uint32_t AppTaskAlive = 0;
volatile uint32_t CommTaskAlive = 0;
volatile uint32_t LedTaskAlive = 0;
任务:
void AppTask(void *argument)
{
for (;;)
{
AppTaskAlive++;
App_Process();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
这样调试时:
AppTaskAlive不断增加
说明任务还活着。
如果:
突然停止
就能知道:
任务什么时候卡死了
这在嵌入式工程中非常实用。
四十八、推荐实现一个FreeRTOS任务监控命令
如果项目有CLI,可以设计:
task
命令。
输出:
Name State Prio Stack RunCount
——————————————-
AppTask B 3 185 1200
CommTask R 4 230 5320
LedTask B 1 90 120
IDLE R 0 118 20000
这样现场遇到:
某功能不工作
就不用猜任务有没有死。
直接查看:
任务状态
栈余量
运行次数
即可。
四十九、如何使用uxTaskGetSystemState()
FreeRTOS还可以获取系统中多个任务的信息。
例如:
UBaseType_t task_count;
task_count = uxTaskGetNumberOfTasks();
配合:
uxTaskGetSystemState();
可以获得:
Task Name
Task Number
Task State
Priority
Stack High Water Mark
Runtime Counter
对于复杂项目非常适合。
五十、完整排查流程
如果遇到:
xTaskCreate返回pdPASS
但是任务不运行
推荐严格按照下面顺序。
第1步:确认任务真的创建成功
不要直接忽略返回值。
BaseType_t ret;
ret = xTaskCreate(...);
if (ret != pdPASS)
{
printf("Create task failed!\\r\\n");
}
第2步:确认vTaskStartScheduler被执行
检查:
vTaskStartScheduler();
有没有真正被调用。
第3步:判断vTaskStartScheduler有没有异常返回
vTaskStartScheduler();
printf("Scheduler failed!\\r\\n");
for (;;)
{
}
如果真的走到了:
Scheduler failed
重点查:
Heap
Idle Task
Timer Task
Port配置
第4步:检查任务状态
使用:
eTaskGetState();
或者:
vTaskList();
判断任务到底是:
Ready
Blocked
Suspended
第5步:检查优先级
确认是否有:
更高优先级任务
一直:
while(1)
占CPU。
第6步:检查任务是不是阻塞在某个API
例如:
xQueueReceive(..., portMAX_DELAY);
xSemaphoreTake(..., portMAX_DELAY);
ulTaskNotifyTake(...);
第7步:检查任务入口函数
确认:
void Task(void *argument)
原型正确。
确认没有:
return;
第8步:检查任务栈
查看:
uxTaskGetStackHighWaterMark();
并开启:
configCHECK_FOR_STACK_OVERFLOW
第9步:检查Heap
查看:
xPortGetFreeHeapSize();
以及:
xPortGetMinimumEverFreeHeapSize();
第10步:检查SysTick
确认:
Tick在增长
例如周期查看:
xTaskGetTickCount();
如果:
数值一直不变化
就应该重点检查:
SysTick
RTOS Tick
中断
第11步:检查PendSV/SVC
尤其是:
自己移植FreeRTOS
更换MCU
修改startup文件
修改中断入口
以后出现的问题。
第12步:检查中断优先级
确认:
FreeRTOSConfig.h
中的优先级参数与:
NVIC
MCU __NVIC_PRIO_BITS
匹配。
五十一、一个典型错误案例
代码:
void HighTask(void *argument)
{
while (1)
{
HAL_GPIO_TogglePin(
GPIOB,
GPIO_PIN_0
);
}
}
void LowTask(void *argument)
{
while (1)
{
printf("LowTask running\\r\\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
创建:
xTaskCreate(
HighTask,
"High",
256,
NULL,
5,
NULL
);
xTaskCreate(
LowTask,
"Low",
256,
NULL,
1,
NULL
);
现象:
HighTask运行
LowTask从不打印
很多人会怀疑:
LowTask创建失败
但实际:
LowTask已经Ready
真正问题:
HighTask优先级更高
+
HighTask永不阻塞
五十二、修改方法
void HighTask(void *argument)
{
while (1)
{
HAL_GPIO_TogglePin(
GPIOB,
GPIO_PIN_0
);
vTaskDelay(
pdMS_TO_TICKS(1)
);
}
}
现在:
HighTask
运行
↓
进入Blocked
↓
LowTask获得CPU
系统开始正常调度。
五十三、另一个典型案例:任务只运行一次
任务:
void AppTask(void *argument)
{
printf("App Start\\r\\n");
vTaskDelay(pdMS_TO_TICKS(1000));
printf("App End\\r\\n");
return;
}
现象:
可能只打印一次
之后任务消失/异常
问题:
任务函数错误return
修改:
void AppTask(void *argument)
{
for (;;)
{
printf("App running\\r\\n");
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
如果任务本身只需要执行一次:
void AppTask(void *argument)
{
DoOnce();
vTaskDelete(NULL);
}
五十四、典型案例:vTaskDelay以后再也不回来
代码:
void AppTask(void *argument)
{
for (;;)
{
printf("APP\\r\\n");
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
只打印:
APP
一次。
以后永远不再打印。
重点检查:
xTaskGetTickCount()
是否增长。
如果 Tick 不增长:
vTaskDelay等待时间永远到不了
重点检查:
SysTick_Handler
xPortSysTickHandler
FreeRTOS Tick配置
中断屏蔽
五十五、FreeRTOS任务不运行常见现象对照表
| 所有任务都不运行 | Scheduler |
| Scheduler调用后返回 | Heap/Idle/Timer Task |
| 只有低优先级任务不运行 | 高优先级任务霸占CPU |
| 任务运行一次后不动 | return/Blocked/Tick |
| 任务卡在QueueReceive | Queue没有数据 |
| 任务卡在SemaphoreTake | 信号量没人释放 |
| 任务刚运行就HardFault | Stack/参数/非法指针 |
| vTaskDelay不恢复 | SysTick/Tick |
| 一个任务一直运行 | PendSV/高优先级忙循环 |
| 随机任务异常 | Stack Overflow/内存破坏 |
| 创建任务偶尔失败 | Heap不足 |
| 多任务后开始异常 | Heap/Stack/优先级 |
| ISR后系统异常 | ISR API/中断优先级 |
五十六、推荐打开的FreeRTOS调试配置
开发阶段可以考虑:
#define configUSE_MALLOC_FAILED_HOOK 1
#define configCHECK_FOR_STACK_OVERFLOW 2
根据项目需求还可以打开:
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define INCLUDE_eTaskGetState 1
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
这样可以使用更多调试功能。
实际宏名称和可用功能应以当前使用的 FreeRTOS 版本和配置文件为准。
五十七、推荐增加assert
FreeRTOS很多内部错误都可以通过:
configASSERT()
快速发现。
例如:
#define configASSERT(x) \\
if ((x) == 0) \\
{ \\
taskDISABLE_INTERRUPTS();\\
for (;;) \\
{ \\
} \\
}
实际工程最好进一步打印:
File
Line
这样发现:
错误优先级
非法API参数
内核状态异常
时,比系统直接莫名其妙 HardFault 好排查得多。
五十八、任务设计的一个重要原则:尽量事件驱动
不推荐:
void Task(void *argument)
{
while (1)
{
if (flag)
{
Process();
}
}
}
因为任务会:
不停轮询
长期占CPU
更推荐:
void Task(void *argument)
{
for (;;)
{
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
Process();
}
}
或者:
xQueueReceive();
这样:
没事
→ Blocked
有事
→ Ready
运行
→ 处理
处理完成
→ 再Block
这才是 RTOS 更高效的使用方式。
五十九、FreeRTOS“任务创建成功却不运行”的7个原因总结
可以浓缩为:
1. 没启动Scheduler
xTaskCreate成功
但是没有vTaskStartScheduler
2. Scheduler启动失败
重点:
Heap不足
Idle Task
Timer Task
Port配置
3. 优先级太低,被高优先级任务饿死
重点检查:
高优先级任务有没有一直Ready
有没有主动阻塞
4. 任务已经进入Blocked/Suspended
检查:
Queue
Semaphore
Mutex
Event
Notification
Delay
Suspend
5. 任务函数写法错误
重点:
函数原型正确
不要直接return
需要退出时vTaskDelete
参数生命周期正确
6. Stack太小
重点:
Stack Overflow
High Water Mark
大数组
printf
深层函数
7. SysTick/PendSV/中断配置异常
重点:
RTOS Tick
SysTick
PendSV
SVC
NVIC优先级
临界区
ISR API
六十、最推荐的排查顺序
以后看到:
xTaskCreate = pdPASS
但是Task不运行
直接按照下面顺序:
① Scheduler启动了吗?
↓
② vTaskStartScheduler有没有返回?
↓
③ Heap还够吗?
↓
④ Task是什么状态?
↓
⑤ 有没有更高优先级任务霸占CPU?
↓
⑥ Task是不是Blocked/Suspended?
↓
⑦ 任务函数有没有return?
↓
⑧ Stack够不够?
↓
⑨ Tick是否正常增长?
↓
⑩ SysTick / PendSV / SVC是否正确?
↓
⑪ 中断优先级有没有配置错误?
这个顺序基本可以覆盖绝大多数:
FreeRTOS任务创建成功但不运行
的问题。
六十一、面试题:xTaskCreate返回pdPASS为什么任务还可能不运行?
可以这样回答:
xTaskCreate() 返回 pdPASS 只能说明任务控制块和任务栈等资源创建成功,任务已经被FreeRTOS建立起来,但并不能保证任务一定获得CPU执行。
任务真正运行还要求:
因此调试时要结合:
任务状态
优先级
Heap
Stack High Water Mark
Tick
RTOS异常入口
逐层排查。
六十二、面试题:高优先级任务为什么可能导致低优先级任务永远不运行?
因为 FreeRTOS 在抢占式调度下通常优先运行:
最高优先级的Ready任务
如果高优先级任务:
永远保持Ready
并且:
不Delay
不等待Queue
不等待Semaphore
不等待Event
那么低优先级任务就无法成为:
最高优先级Ready任务
因此可能长期得不到 CPU。
解决方法是:
让高优先级任务在没有工作时正确进入Blocked状态,而不是单纯调整所有任务优先级。
六十三、面试题:如何判断任务栈够不够?
可以回答:
首先可以启用:
configCHECK_FOR_STACK_OVERFLOW
实现:
vApplicationStackOverflowHook()
捕获栈溢出。
其次使用:
uxTaskGetStackHighWaterMark()
查看任务历史最小剩余栈空间。
如果 High Water Mark 已经非常小,就应该:
适当增加栈
或者减少:
大型局部数组
深层函数调用
printf
浮点格式化
等栈消耗。
六十四、最终总结
FreeRTOS中最容易产生的一个误区就是:
xTaskCreate() == pdPASS
所以:
任务一定正在运行
实际上完全不是。
更准确的流程是:
xTaskCreate
↓
任务资源创建成功
↓
进入Ready或相应任务状态
↓
Scheduler正常启动
↓
调度器根据优先级选择任务
↓
上下文切换
↓
任务获得CPU
↓
Task Function真正执行
因此:
任务创建成功,只是任务“存在了”;任务真正运行,还取决于调度器、优先级、任务状态、栈空间以及系统Tick和中断机制。
遇到问题时最值得先检查的7个方向:
1. 有没有启动Scheduler
2. Scheduler有没有启动失败
3. 有没有被高优先级任务饿死
4. 是不是处于Blocked/Suspended
5. Task函数有没有写错
6. Stack有没有溢出
7. SysTick/PendSV/中断配置是否异常
最终记住一条 FreeRTOS 调试思路:
不要只问“任务有没有创建”,而要继续问:任务现在是什么状态?为什么它没有成为当前最高优先级的Ready任务?
只要顺着:
创建
↓
调度
↓
状态
↓
优先级
↓
栈
↓
Tick
逐层检查,大多数 FreeRTOS“任务创建成功却不运行”的问题都可以快速定位。

