FreeRTOS 中变量、任务栈、队列和信号量的内存分配与生命周期


FreeRTOS 不会改变 C 语言变量的基本存储规则。全局变量和静态变量仍由链接器放入 .data.bss 或只读区;普通函数局部变量通常使用当前执行上下文的栈;任务、队列、信号量等内核对象则根据创建方式位于 FreeRTOS Heap 或用户提供的静态内存中。理解“句柄在哪里”“对象在哪里”“数据复制到哪里”是避免越界、悬空指针和栈溢出的关键。

关键词: FreeRTOS、任务栈、全局变量、局部变量、Queue、Semaphore、TCB、Heap、生命周期、静态分配


一、先给出核心结论

1. 全局变量不在任务栈中

全局变量、文件级静态变量和静态局部变量具有静态存储期,通常位于:

  • .data
  • .bss
  • .rodata
  • 用户在链接脚本中定义的特殊段。

它们从系统完成 C 运行环境初始化后一直存在到系统复位或掉电。

2. 普通函数局部变量通常使用当前任务栈

任务函数及其调用函数中的普通局部变量,通常位于当前任务自己的栈中,但编译器也可能把它们保存在寄存器中或直接优化掉。

3. Queue 句柄和 Queue 对象不是一回事

QueueHandle_t queue;

这里只定义了一个指针形式的句柄。真正的 Queue_t 控制块和队列数据缓冲区由 xQueueCreate() 动态分配,或者由 xQueueCreateStatic() 的调用者提供。

4. 信号量和互斥量底层通常也是 Queue 对象

它们的元素大小为 0,不保存普通消息内容,而是利用 Queue 的计数、等待链表和互斥量字段表达同步状态。

5. 对象不会因为创建它的任务退出而自动消失

动态创建的 Queue、Semaphore、EventGroup、Timer 和用户通过 pvPortMalloc() 申请的内存,通常都需要显式删除或释放。删除创建任务并不会自动释放这些对象。


二、典型 MCU 内存布局

以使用 heap_4.c 的 Cortex-M 系统为例:

Flash
┌─────────────────────────────┐
│ .isr_vector                 │
│ .text:程序代码              │
│ .rodata:只读常量            │
│ .data 的初始值镜像           │
└─────────────────────────────┘

RAM
┌─────────────────────────────┐ 高地址
│ MSP / 中断栈                │
├─────────────────────────────┤
│ 其他链接器预留区域           │
├─────────────────────────────┤
│ FreeRTOS Heap               │
│ ├── 动态任务 TCB            │
│ ├── 动态任务栈              │
│ ├── Queue_t + Queue Buffer  │
│ ├── Semaphore / Mutex       │
│ └── 用户 pvPortMalloc 内存   │
├─────────────────────────────┤
│ .bss:零初始化全局/静态变量  │
├─────────────────────────────┤
│ .data:非零初始化全局变量    │
└─────────────────────────────┘ 低地址

这里的布局只是概念图,真实顺序由链接脚本决定。

使用 heap_4.c 时,FreeRTOS Heap 通常来自:

static uint8_t ucHeap[configTOTAL_HEAP_SIZE];

ucHeap 自身一般位于 .bss,但它内部再被 FreeRTOS 分割成任务、队列和用户对象所需的块。

如果使用 heap_3.cpvPortMalloc() 会包装标准库 malloc(),此时实际内存来自 C 运行库管理的 Heap,而不是 ucHeap


三、全局变量怎样分配

1. 未初始化或零初始化全局变量

uint32_t g_counter;
uint8_t g_buffer[128];
bool g_ready = false;

这些变量具有静态存储期,通常进入 .bss。启动代码在进入 main() 前把 .bss 清零。

它们不属于任何任务,所有任务看到的是同一份对象。

2. 非零初始化全局变量

uint32_t g_baudrate = 115200U;
uint8_t g_address[4] = { 1U, 2U, 3U, 4U };

通常位于 RAM 的 .data。其初始值镜像保存在 Flash,启动代码将初值复制到 RAM。

3. 只读全局变量

const uint16_t g_sine_table[] = { 0U, 100U, 200U };

通常位于 .rodata 或其他 Flash 只读段。

但 C 语言中的 const 本身不保证对象一定在 Flash,最终位置仍由编译器和链接脚本决定。

4. 全局变量的并发问题

全局变量虽然不在任务栈中,但会被多个任务或 ISR 共享:

uint32_t g_counter;

void TaskA(void *arg)
{
    g_counter++;
}

void TaskB(void *arg)
{
    g_counter++;
}

g_counter++ 是读、修改、写三个步骤,不保证原子性。可能需要:

  • 临界区;
  • 互斥量;
  • 原子变量;
  • 单写者设计;
  • 任务通知或消息队列。

volatile 不能代替互斥或原子操作。


四、static 变量怎样分配

1. 文件级静态变量

static uint32_t module_state;
static uint32_t module_timeout = 1000U;
  • module_state 通常在 .bss
  • module_timeout 通常在 .data
  • 生命周期贯穿整个程序;
  • 只在当前源文件内可见。

2. 函数静态局部变量

void protocol_poll(void)
{
    static uint32_t call_count;
    call_count++;
}

虽然 call_count 的作用域只在函数内部,但它不在任务栈上:

  • 通常位于 .bss
  • 整个系统只有一份;
  • 函数返回后仍然存在;
  • 多个任务调用同一函数时共享它。

如果多个任务同时调用带静态局部变量的函数,该函数可能不具备可重入性。


五、普通函数局部变量在哪里

例如:

void calculate(void)
{
    uint32_t sum = 0U;
    uint8_t temp[64];

    process(temp, &sum);
}

从 C 语言语义看,sumtemp 是自动存储期对象。实际生成代码时:

  • sum 可能放在 CPU 寄存器中;
  • temp 通常放在当前任务栈中;
  • 对变量取地址后,变量更可能被放到栈中;
  • 优化后未使用的变量可能完全不存在;
  • 函数调用保存的寄存器和返回地址也会消耗栈空间。

因此“局部变量一定在栈中”并不完全准确,更准确的说法是:

自动局部变量由当前调用上下文管理;需要内存实体时,通常使用当前任务栈。


六、每个任务都有自己的栈

假设创建三个任务:

TaskA → StackA
TaskB → StackB
TaskC → StackC

任务切换时,FreeRTOS 会保存当前任务的栈顶指针,并恢复下一个任务的栈顶指针:

TaskA 运行

保存 StackA 顶部到 TaskA TCB

从 TaskB TCB 取出 StackB 顶部

TaskB 继续运行

因此即使多个任务调用同一个函数:

void worker(void)
{
    uint8_t local_buffer[32];
}

每个任务也拥有自己的 local_buffer 实例,因为它们分别位于各自任务栈中。

但如果改成:

void worker(void)
{
    static uint8_t local_buffer[32];
}

所有任务就会共享同一个数组。


七、任务栈中具体保存什么

任务栈可能包含:

  • 自动局部变量;
  • 函数参数的一部分;
  • 函数返回地址;
  • 编译器保存的寄存器;
  • 临时表达式;
  • 嵌套函数调用栈帧;
  • FreeRTOS 上下文切换保存的寄存器;
  • 异常进入时硬件压入的上下文。

在 Cortex-M 中,任务通常在 Thread Mode 使用 PSP。中断发生时,硬件会把被中断任务的基本上下文压入该任务 PSP:

R0~R3、R12、LR、PC、xPSR

PendSV 再保存 R4~R11,必要时还保存 FPU 上下文。

中断处理程序本身通常使用 MSP,因此 ISR 中 C 函数的局部变量一般消耗 MSP 中断栈,而不是任务 PSP。不过被中断任务的硬件异常帧仍保存在任务栈中。


八、任务栈怎样创建

1. 动态创建任务

xTaskCreate(
    WorkerTask,
    "worker",
    512U,
    NULL,
    3U,
    &worker_handle
);

启用动态分配时,FreeRTOS 通常通过 pvPortMalloc() 分配:

  • TCB_t
  • 任务栈数组。

概念上:

FreeRTOS Heap
├── Worker TCB
└── Worker Stack

不同 FreeRTOS 版本和 Port 的分配顺序可能不同,不应依赖两个块在内存中连续。

2. 静态创建任务

static StaticTask_t worker_tcb;
static StackType_t worker_stack[512];

TaskHandle_t worker_handle = xTaskCreateStatic(
    WorkerTask,
    "worker",
    512U,
    NULL,
    3U,
    worker_stack,
    &worker_tcb
);

这里:

  • worker_tcb 通常位于 .bss
  • worker_stack 通常位于 .bss
  • FreeRTOS 不再从 Heap 为它们分配内存;
  • 它们的生命周期贯穿整个程序。

3. 栈深度通常不是字节数

xTaskCreate() 的栈深度参数通常以 StackType_t 为单位:

实际字节数 = StackDepth × sizeof(StackType_t)

在 32 位 Cortex-M 上:

StackDepth = 512
实际空间通常约为 512 × 4 = 2048 字节

具体仍应检查当前 FreeRTOS Port 和类型定义。


九、任务局部变量的生命周期

1. 普通函数局部变量

void helper(void)
{
    uint8_t buffer[32];
}

buffer 的语言生命周期从进入其作用域开始,到离开作用域结束。函数返回后,原栈空间可以被后续函数调用复用。

因此不能返回局部变量地址:

uint8_t *get_buffer(void)
{
    uint8_t buffer[32];
    return buffer;          // 错误:返回悬空指针
}

2. 任务入口函数中的局部变量

void WorkerTask(void *argument)
{
    uint8_t buffer[128];

    for (;;) {
        process(buffer);
    }
}

只要任务函数没有返回,buffer 的生命周期就持续存在。但它仍然属于任务栈:

  • 删除任务后失效;
  • 栈溢出时可能被破坏;
  • 其他模块不应在任务删除后继续保存它的地址。

3. 循环块内局部变量

for (;;) {
    uint8_t packet[64];
    receive(packet);
}

从语言语义看,每次进入块都会开始一个新的生命周期;从生成代码看,编译器通常复用任务栈帧中的同一段空间。


十、任务参数保存在哪里

创建任务时:

xTaskCreate(
    WorkerTask,
    "worker",
    512U,
    parameter,
    3U,
    NULL
);

FreeRTOS 保存的是 parameter 指针值,不会自动复制指针所指向的对象。

错误示例:

void create_worker(void)
{
    worker_config_t config;

    config.channel = 1U;
    xTaskCreate(WorkerTask, "worker", 512U,
                &config, 3U, NULL);
}

create_worker() 返回后,config 已经失效,任务得到的是悬空指针。

安全方式包括:

  • 参数对象使用全局或静态存储期;
  • 从 Heap 分配并明确释放者;
  • 在创建前把配置复制到任务长期对象;
  • 使用静态任务上下文结构。

例如:

static worker_config_t worker_config = {
    .channel = 1U
};

xTaskCreate(WorkerTask, "worker", 512U,
            &worker_config, 3U, NULL);

十一、Queue 句柄放在哪里

QueueHandle_t queue;

QueueHandle_t 通常是指向 Queue_t 的指针。句柄变量本身的位置取决于声明方式:

QueueHandle_t g_queue;        // 通常在 .bss

static QueueHandle_t s_queue; // 通常在 .bss

void function(void)
{
    QueueHandle_t queue;      // 句柄变量通常在当前任务栈
}

无论句柄变量放在哪里,都不能据此推断真正 Queue 对象的位置。


十二、动态 Queue 对象和数据缓冲区

创建队列:

QueueHandle_t queue = xQueueCreate(
    10U,
    sizeof(sensor_data_t)
);

动态创建时,FreeRTOS 通常调用 pvPortMalloc(),分配:

sizeof(Queue_t) + 队列长度 × 单项大小

概念布局:

FreeRTOS Heap 中的一块内存
┌──────────────────────────┐
│ Queue_t 控制块            │
│ ├── 写指针                │
│ ├── 读指针                │
│ ├── 当前消息数            │
│ ├── 等待发送任务链表       │
│ └── 等待接收任务链表       │
├──────────────────────────┤
│ Item 0                   │
│ Item 1                   │
│ ...                      │
│ Item 9                   │
└──────────────────────────┘

不同 FreeRTOS 版本的具体布局可能变化,但结论相同:动态 Queue 实例和数据缓冲区位于 FreeRTOS Heap,而不是创建者任务栈。


十三、静态创建 Queue

#define QUEUE_LENGTH 10U

static StaticQueue_t queue_control;
static uint8_t queue_storage[
    QUEUE_LENGTH * sizeof(sensor_data_t)
];

QueueHandle_t queue = xQueueCreateStatic(
    QUEUE_LENGTH,
    sizeof(sensor_data_t),
    queue_storage,
    &queue_control
);

这里:

  • queue_control 保存 Queue 控制块;
  • queue_storage 保存消息副本;
  • 两者通常位于 .bss
  • 不使用 FreeRTOS Heap;
  • 对象的有效期由用户提供内存的有效期决定。

不要这样创建长期使用的静态队列:

QueueHandle_t create_queue_wrong(void)
{
    StaticQueue_t control;
    uint8_t storage[128];

    return xQueueCreateStatic(
        8U, 16U, storage, &control
    );
}

函数返回后,controlstorage 都已失效,返回的 Queue 句柄成为悬空对象。


十四、Queue 数据是复制还是引用

FreeRTOS 普通 Queue 默认采用按值复制

假设:

typedef struct {
    uint32_t id;
    int16_t temperature;
} sensor_data_t;

sensor_data_t data = {
    .id = 1U,
    .temperature = 250
};

xQueueSend(queue, &data, portMAX_DELAY);

FreeRTOS 会把 sizeof(sensor_data_t) 字节复制到 Queue 内部缓冲区。

发送成功后,即使调用者修改原变量:

data.temperature = 999;

也不会改变已经进入队列的那份数据副本。

接收时:

sensor_data_t received;
xQueueReceive(queue, &received, portMAX_DELAY);

FreeRTOS 再把队列内部数据复制到 received

Queue 中数据的生命周期

发送成功

数据副本存在于 Queue Buffer

被接收、覆盖、队列复位或队列删除

该内部数据副本生命周期结束

十五、Queue 传递指针时的生命周期

为了避免复制大数据,有时 Queue 中保存指针:

QueueHandle_t pointer_queue = xQueueCreate(
    8U,
    sizeof(message_t *)
);

发送:

message_t *message = get_message();
xQueueSend(pointer_queue, &message, portMAX_DELAY);

这时 Queue 只复制指针值,不复制 message 指向的数据。

Queue Buffer
┌────────────────┐
│ message 指针    │ ──────► 真正的 message 对象
└────────────────┘

必须明确真正对象的生命周期和所有权。

错误示例:发送局部数组指针

void send_packet(void)
{
    uint8_t packet[128];
    uint8_t *ptr = packet;

    build_packet(packet);
    xQueueSend(pointer_queue, &ptr, 0U);
}

函数返回后,packet 失效。接收任务以后访问该指针属于未定义行为。

任务长期栈缓冲也可能有问题

void ProducerTask(void *arg)
{
    uint8_t packet[128];

    for (;;) {
        build_packet(packet);
        uint8_t *ptr = packet;
        xQueueSend(pointer_queue, &ptr, portMAX_DELAY);
    }
}

任务虽然没有返回,packet 仍然存在,但生产者下一轮可能在消费者读取前覆盖它。

安全设计方式

方式一:Queue 按值复制

适合小型固定长度消息。

方式二:固定块内存池

生产者从内存池申请块

填充数据

通过 Queue 发送指针

消费者获得所有权

处理完成后归还内存池

方式三:静态缓冲区池加所有权状态

每个缓冲区只能处于:

FREE → PRODUCER_OWNED → QUEUED → CONSUMER_OWNED → FREE

十六、信号量和互斥量放在哪里

FreeRTOS 中 Semaphore 和 Mutex 通常基于 Queue 机制实现:

typedef QueueHandle_t SemaphoreHandle_t;

1. 动态创建信号量

SemaphoreHandle_t sem = xSemaphoreCreateBinary();
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

真正内核对象通常通过 pvPortMalloc() 分配在 FreeRTOS Heap 中。

因为 Semaphore 的项目大小为 0,它不需要像普通 Queue 那样保存消息副本。控制块主要保存:

  • 当前计数;
  • 等待获取的任务链表;
  • 等待释放相关状态;
  • Mutex 持有者;
  • 递归计数;
  • 优先级继承相关信息。

2. 静态创建信号量

static StaticSemaphore_t sem_memory;

SemaphoreHandle_t sem =
    xSemaphoreCreateBinaryStatic(&sem_memory);

sem_memory 通常位于 .bss,不使用 FreeRTOS Heap。

3. 信号量的生命周期

创建

任务或 ISR Give/Take

显式调用 vSemaphoreDelete()

删除前必须确保没有任务仍在等待或使用它。

4. Mutex 的所有权不是内存所有权

Mutex 会记录哪个任务持有锁,并支持优先级继承。但它不会自动管理被保护对象的内存生命周期。


十七、任务通知存在哪里

任务通知不需要单独创建 Queue 或 Semaphore 对象。通知值和通知状态直接存放在目标任务的 TCB 中。

概念上:

Task TCB
├── pxTopOfStack
├── 优先级
├── 状态链表节点
├── 通知值
└── 通知状态

因此任务通知:

  • 不需要额外动态对象;
  • 生命周期与目标任务一致;
  • 通常比 Queue 或 Semaphore 更轻量;
  • 适合一对一事件、计数和简单数据传递。

目标任务删除后,对应通知状态也随 TCB 一起消失。


十八、EventGroup、软件定时器和 StreamBuffer

1. EventGroup

EventGroupHandle_t event = xEventGroupCreate();

动态创建时,EventGroup 控制块位于 FreeRTOS Heap。静态版本使用用户提供的 StaticEventGroup_t

2. 软件定时器

TimerHandle_t timer = xTimerCreate(...);

动态 Timer 对象位于 FreeRTOS Heap,静态版本使用 StaticTimer_t

定时器回调不是在创建 Timer 的任务中执行,而是在 Timer Service Task 中执行。因此回调函数中的普通局部变量使用的是 Timer Service Task 的任务栈

定时器 ID 通常只是一个 void * 指针,FreeRTOS 不复制它指向的对象,也不自动管理其生命周期。

3. StreamBuffer 和 MessageBuffer

动态创建时通常分配:

  • 控制块;
  • 环形数据缓冲区。

静态创建时由用户提供控制块和数据缓冲区。

StreamBuffer 和 MessageBuffer 默认针对单生产者、单消费者设计;多生产者或多消费者需要额外串行化。


十九、用户动态内存的生命周期

message_t *message = pvPortMalloc(sizeof(message_t));

message 句柄变量可能在任务栈中,但它指向的 message_t 对象位于 FreeRTOS Heap。

任务栈
┌─────────────────┐
│ message 指针     │ ─────► FreeRTOS Heap 中的 message_t
└─────────────────┘

函数返回后,指针变量消失,但 Heap 对象仍然存在,直到:

vPortFree(message);

如果失去最后一个指向它的指针,就发生内存泄漏。

删除任务不会自动释放用户内存

void WorkerTask(void *arg)
{
    void *buffer = pvPortMalloc(1024U);

    /* ... */

    vTaskDelete(NULL);
}

如果任务删除前没有调用 vPortFree(buffer),该内存通常仍被占用,FreeRTOS 不会自动扫描任务局部指针并释放用户对象。


二十、删除任务时 TCB 和任务栈怎样释放

1. 动态创建的任务

调用:

vTaskDelete(task_handle);

后,动态任务的 TCB 和任务栈不会一定在调用点立即释放。FreeRTOS 通常把任务放入待清理列表,由 Idle Task 完成内存回收。

因此 Idle Task 必须有运行机会。

2. 静态创建的任务

静态 TCB 和静态任务栈由用户提供。FreeRTOS 删除任务后不会把这些数组“释放回 Heap”,它们仍然是用户的静态内存。

重新使用这些内存前,应确保任务删除和内核清理流程已经完成,并遵循当前 FreeRTOS 版本的约束。

3. 不会自动释放的关联对象

删除任务通常不会自动删除该任务创建的:

  • Queue;
  • Semaphore;
  • Mutex;
  • EventGroup;
  • Timer;
  • pvPortMalloc() 用户块;
  • 驱动缓冲区;
  • 文件或外设资源。

应用需要定义明确的资源清理顺序。


二十一、不同执行上下文的局部变量使用哪个栈

执行上下文 普通局部变量通常使用
普通任务函数 当前任务栈
任务调用的普通函数 当前任务栈
软件定时器回调 Timer Service Task 栈
Idle Hook Idle Task 栈
Timer/Daemon Task Startup Hook Timer Service Task 栈
中断服务函数 通常使用 MSP 中断栈
PendSV/SVC/SysTick Handler Handler Mode 栈及被切换任务栈帧

因此一个普通函数究竟使用哪个任务栈,不取决于函数写在哪个源文件,而取决于当前是谁调用它

例如:

void parse_packet(void)
{
    uint8_t temp[256];
}
  • 被 TaskA 调用时,temp 消耗 TaskA 栈;
  • 被 TaskB 调用时,消耗 TaskB 栈;
  • 被 Timer 回调调用时,消耗 Timer Service Task 栈;
  • 被 ISR 调用时,通常消耗 MSP。

二十二、对象和数据生命周期汇总

对象 常见存储位置 生命周期结束条件
全局变量 .data/.bss 系统复位或掉电
全局 const .rodata/Flash 系统复位或掉电
静态局部变量 .data/.bss 系统复位或掉电
普通局部变量 寄存器或当前栈 离开作用域
动态任务 TCB/栈 FreeRTOS Heap 删除后由 Idle Task 回收
静态任务 TCB/栈 用户静态内存 由用户存储期决定
动态 Queue FreeRTOS Heap vQueueDelete()
静态 Queue 用户提供内存 用户存储期结束
Queue 中按值数据 Queue Buffer 接收、覆盖、复位或删除
Queue 中的指针 Queue Buffer 指针项被接收等;目标对象独立管理
动态 Semaphore/Mutex FreeRTOS Heap vSemaphoreDelete()
任务通知 目标任务 TCB 任务删除
动态 EventGroup FreeRTOS Heap vEventGroupDelete()
动态软件 Timer FreeRTOS Heap xTimerDelete() 完成
pvPortMalloc() 对象 FreeRTOS Heap vPortFree()

二十三、常见错误示例

1. 在任务栈上放置过大数组

void ProtocolTask(void *arg)
{
    uint8_t frame_buffer[8192];
}

如果任务栈只有 2 KB,会立即或很快溢出。

大缓冲区可以考虑:

  • 静态全局专用缓冲;
  • 固定块内存池;
  • Heap;
  • 外部 RAM;
  • DMA 专用段。

2. Queue 发送了局部变量指针

void function(void)
{
    message_t message;
    message_t *ptr = &message;
    xQueueSend(pointer_queue, &ptr, 0U);
}

函数返回后指针失效。

3. 把 Queue 句柄误认为 Queue 对象

QueueHandle_t queue;

只占一个指针大小,真正对象可能尚未创建。

必须检查:

queue = xQueueCreate(...);

if (queue == NULL) {
    /* 处理内存不足 */
}

4. 删除创建任务后继续使用其栈数据

如果另一个任务保存了被删除任务栈中对象的地址,该地址会在任务栈回收后失效。

5. 多任务共享静态局部变量

int parse(void)
{
    static uint8_t scratch[128];
}

多个任务同时调用可能相互覆盖,应增加互斥、改为调用者提供缓冲区,或把状态放入各自实例对象。

6. 忘记释放用户 Heap 对象

删除任务只回收动态任务自身的 TCB 和栈,不会自动释放任务申请的其他对象。


二十四、怎样检查任务栈是否足够

1. 栈高水位

UBaseType_t remain = uxTaskGetStackHighWaterMark(task_handle);

该值通常表示任务运行历史中最少剩余的栈单元数量,不一定是字节数。

2. 启用栈溢出检测

#define configCHECK_FOR_STACK_OVERFLOW 2

并实现:

void vApplicationStackOverflowHook(
    TaskHandle_t task,
    char *task_name
)
{
    disable_interrupts_and_report(task_name);
}

栈检测只能提高发现概率,不能替代合理的栈预算、静态分析和最坏路径测试。

3. 测试最坏路径

需要覆盖:

  • 最深函数调用;
  • 最大局部数组;
  • printf 等库函数;
  • 浮点格式化;
  • 异常嵌套;
  • FPU 上下文;
  • 错误处理和日志路径。

二十五、推荐的内存设计原则

小型消息

使用 Queue 按值复制:

typedef struct {
    uint32_t id;
    uint32_t value;
} event_t;

优点是所有权简单,不容易产生悬空指针。

大型数据

使用预分配缓冲池加指针 Queue:

Memory Pool → Producer → Pointer Queue → Consumer → Memory Pool

单任务私有状态

放入任务上下文结构,由任务参数传入:

typedef struct {
    parser_t parser;
    uint8_t buffer[256];
    uint32_t error_count;
} protocol_task_context_t;

多任务共享状态

优先考虑:

  • 单一所有者任务;
  • 消息传递;
  • 明确的 Mutex;
  • 原子变量;
  • 不可变配置对象。

内核对象

安全和实时性要求高时优先静态创建:

xTaskCreateStatic();
xQueueCreateStatic();
xSemaphoreCreateBinaryStatic();
xEventGroupCreateStatic();
xTimerCreateStatic();

需要灵活创建和删除时使用动态分配,但必须检查失败返回值并设计清理流程。


二十六、最终理解框架

判断 FreeRTOS 中某个“变量”放在哪里,可以依次问四个问题。

1. 它是变量、句柄还是对象

QueueHandle_t queue;

是句柄变量,不是 Queue 对象。

2. 它使用什么存储期

  • 全局或 static:静态存储期;
  • 普通局部变量:自动存储期;
  • pvPortMalloc():动态存储期;
  • FreeRTOS 静态创建:由用户提供内存的存储期决定。

3. 当前函数在哪个上下文执行

  • 普通任务:当前任务栈;
  • Timer 回调:Timer Service Task 栈;
  • ISR:通常是 MSP;
  • 静态变量:与调用者栈无关。

4. API 是复制数据还是只复制指针

  • Queue 元素是结构体:复制整个结构体;
  • Queue 元素是指针:只复制地址;
  • 任务参数:只保存指针;
  • Timer ID:只保存指针;
  • 任务通知:值保存在 TCB。

最重要的结论是:

FreeRTOS 中只有普通任务函数局部变量通常使用任务栈。全局变量和静态变量不在任务栈;动态任务、队列和信号量对象通常在 FreeRTOS Heap;静态创建的内核对象位于用户提供的内存;Queue 默认复制元素内容,但如果元素本身是指针,指针目标的生命周期必须由应用程序管理。