SMARTWATCH 学习手册有 C 基础 · 嵌入式从零开始
第二章 / FreeRTOS / 第 7 节

任务为什么创建失败,运行一会儿又为什么崩溃?

从创建接口里暂时略过的栈深度继续:分清任务栈、内核分配池和静态数组,用项目的大数组实例理解容量,再建立有证据的排查顺序。

接续:6 · 声音帧与多个使用者先看问题,再增加对应代码

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 字节。其他接口层的单位可能不同,不能看到一个类似字段就照抄。

当前任务创建时的深度此端口对应栈空间
SensorTask15366144 字节
MicTask5122048 字节
KWSTask10244096 字节

这三项栈空间相加是 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 · 遇到崩溃,沿着证据排查

  1. 先看停在错误处理、内核断言,还是处理器异常入口。
  2. 核对对象创建结果,排除空句柄。
  3. 核对任务参数、队列指针指向的对象是否仍有效。
  4. 若从中断调用内核,核对优先级门槛和接口类型。
  5. 若进入较深算法或刷新后出错,再结合栈水位与大局部数组定位。
  6. 检查一个任务是否始终就绪,导致别的任务看起来“停止”。

分别记录修改前后的结果,每次处理一个原因。任务多了以后,故障可能在后续运行位置才显现,不能只因为崩溃发生在某个函数内,就认定问题一定由它产生。

资料与源码

本章按仓库当前 FreeRTOS V11.2.0 与 STM32U5 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。