任务为什么创建失败,运行一会儿又为什么崩溃?
从创建接口里暂时略过的栈深度继续:分清任务栈、内核分配池和静态数组,用项目的大数组实例理解容量,再建立有证据的排查顺序。
1 · 同一块 RAM,为什么有几种用法?
RAM 是芯片中运行时可读写的存储空间。任务栈、文件级数组、内核动态分配的对象最终都需要占用它,只是分配和使用方式不同。
| 区域或对象 | 什么时候安排空间 | 典型内容 |
|---|---|---|
| 静态存储区 | 程序链接时安排 | 文件级变量、static 数组;BSS 常用来指未显式初始化或零初始化对象所在的区域。 |
| FreeRTOS 动态分配池 | 内核运行时按需分配 | 通过 xTaskCreate 创建的任务管理信息和栈、动态创建的队列等。 |
| 某个任务的栈 | 随任务一起准备,再由执行过程使用 | 普通局部变量、调用过程的返回信息、保存的执行状态。 |
当前工程采用 heap_4.c,configTOTAL_HEAP_SIZE 为 32768 字节。这是内核这套分配池的容量,不是芯片所有 RAM。C 库的 malloc()、界面库内部的分配区也不应未经核对就视为同一个池。
2 · 栈深度 1536,到底是多少字节?
原生 xTaskCreate() 的深度按 StackType_t 元素数计算。当前 STM32U5 的 Cortex-M33 端口每个元素 4 字节,所以 1536 对应 6144 字节。其他接口层的单位可能不同,不能看到一个类似字段就照抄。
| 当前任务 | 创建时的深度 | 此端口对应栈空间 |
|---|---|---|
| SensorTask | 1536 | 6144 字节 |
| MicTask | 512 | 2048 字节 |
| KWSTask | 1024 | 4096 字节 |
这三项栈空间相加是 12288 字节,内核还需要各任务的管理信息、空闲与服务任务,以及同步对象等空间。因此“总栈大小小于 32768”只是最初估算,不等于一定能够创建成功。
3 · 一个局部数组,怎样把栈用完?
容量计算反例 · 不作为运行实验
void ExampleTask(void *argument)
{
(void)argument;
int16_t window[16384];
/* 仅数组本身就需要 32768 字节。 */
}这个普通局部数组可能占用任务栈,而关键词任务只有 4096 字节栈,还没考虑其他局部变量和调用过程就已经不够。当前关键词实现把大窗口写为函数内的 static 数组:
当前 task_kws.c 的存储方式
static int16_t window_samples[KWS_AUDIO_SAMPLES];函数内 static 数组具有整个程序运行期的存储期,不在每次任务执行到这里时重新占用一份任务栈。它仍要占 RAM,而且同一函数的多次使用会共享它;若以后让多个任务同时调用,不能再假定各自独有。
类似地,main.c 把较大的血氧状态对象 spo2 保存在文件级静态存储区,而不是作为 Sensor_Task 的普通局部变量。这样减少该任务的栈占用,但函数调用链、浮点运算保存和其他局部数组仍需预算。
4 · 创建时怎样发现内存不足?
先检查已经写过的返回值:xTaskCreate() 是否返回 pdPASS,队列、信号量和事件组句柄是否为 NULL。创建失败后不能继续把空句柄交给中断或其他任务。
任务内或调试观察点 · 查看当前与历史最小剩余内核分配池
size_t free_now = xPortGetFreeHeapSize();
size_t free_min = xPortGetMinimumEverFreeHeapSize();free_now 是当前剩余量,free_min 是运行以来最少剩余过多少。它们描述内核池,不包含所有静态数组,也不能仅凭总剩余量证明下一笔连续分配一定成功。heap_4 会合并相邻的空闲块,但仍需要查看实际申请大小。
初始化时每创建一个对象记录一次,更容易发现是哪一步耗尽空间。减少无用对象、调整数据存放位置后再重新核对,而不是先盲目把池和所有栈同时增大。
5 · 运行后怎样看栈余量?
在要测量的任务中执行 · NULL 表示当前任务
UBaseType_t unused_words = uxTaskGetStackHighWaterMark(NULL);
size_t unused_bytes = unused_words * sizeof(StackType_t);这个接口返回该任务栈历史上最少还剩多少未用元素,值越小意味着曾经用得越满。单位仍然是栈元素数。必须覆盖最长调用链、界面刷新、算法和通信叠加的场景,才有参考价值;只在空闲启动后测一次,不代表峰值安全。
当前配置已经包含该查询接口。需要增加栈溢出检查时,要另行启用合适的 configCHECK_FOR_STACK_OVERFLOW 配置并提供匹配的错误钩子。仓库当前没有显式启用这项配置,不能把“内核必然会替我抓住全部溢出”当作前提。
编译器的栈使用报告可辅助估算函数和调用链;动态水位、编译分析与高负载实测结合起来判断。它们也不会替代对所有路径的理解。
6 · 遇到崩溃,沿着证据排查
- 先看停在错误处理、内核断言,还是处理器异常入口。
- 核对对象创建结果,排除空句柄。
- 核对任务参数、队列指针指向的对象是否仍有效。
- 若从中断调用内核,核对优先级门槛和接口类型。
- 若进入较深算法或刷新后出错,再结合栈水位与大局部数组定位。
- 检查一个任务是否始终就绪,导致别的任务看起来“停止”。
分别记录修改前后的结果,每次处理一个原因。任务多了以后,故障可能在后续运行位置才显现,不能只因为崩溃发生在某个函数内,就认定问题一定由它产生。
资料与源码
本章按仓库当前 FreeRTOS V11.2.0 与 STM32U5 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。