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.c,pvPortMalloc() 会包装标准库 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 语言语义看,sum 和 temp 是自动存储期对象。实际生成代码时:
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
);
}
函数返回后,control 和 storage 都已失效,返回的 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 默认复制元素内容,但如果元素本身是指针,指针目标的生命周期必须由应用程序管理。