外观
1.9 FreeRTOS 核心 API
这节要搞懂:实战项目会用到的 19 个函数,分别是什么、怎么用、参数填什么。 需要基础:1.8
这一节是"字典"
不用背,也不用一次看完。 这里列的就是第三章天气时钟实际用到的全部 FreeRTOS 函数,写代码时想不起参数怎么填,回来翻一下就行。
零、先认识函数名的规律
FreeRTOS 的函数名有严格的命名约定,认识规律就不用死记:
| 开头 | 含义 | 例子 |
|---|---|---|
v | void,没有返回值 | vTaskDelay |
x | 有返回值,要判断 | xQueueReceive |
ul | 返回 unsigned long(32 位) | ulTaskNotifyTake |
pd | 是个常量或宏 | pdTRUE、pdMS_TO_TICKS |
带 FromISR | 中断里专用的版本 | xQueueSendFromISR |
一、任务管理
1. xTaskCreate —— 创建一个任务
作用:创建一个任务,创建完立刻就开始参与调度。
c
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务函数
const char *pcName, // 任务名字
uint32_t usStackDepth, // 栈大小(字节)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 输出:任务句柄
);| 参数 | 填什么 | 新手建议 |
|---|---|---|
pxTaskCode | 任务函数名,不加括号 | 写 task_led |
pcName | 任意字符串,调试时显示 | 写 "led",别超过 16 字符 |
usStackDepth | 栈大小,ESP-IDF 单位是字节 | 先填 2048,联网任务填 6144 |
pvParameters | 要传进去的数据地址 | 没有就填 NULL |
uxPriority | 优先级,数字越大越优先 | 一般填 5,按键填 6 |
pxCreatedTask | 用来接收任务句柄 | 用不到就填 NULL,后续如果不需要操作这个任务就填NULL(挂起、恢复、修改优先级等) |
返回值:pdPASS 表示成功,其他值表示内存不够创建失败。
c
BaseType_t ret = xTaskCreate(task_led, "led", 2048, NULL, 5, NULL);
if (ret != pdPASS) {
printf("create task failed\n"); // 内存不够了
}要传参数怎么办
传进去的地址必须一直有效,所以不能传局部变量的地址:
c
// ❌ 错误:函数一返回,cfg 就没了
led_config_t cfg = { .gpio = 2 };
xTaskCreate(task_led, "led", 2048, &cfg, 5, NULL);
// ✅ 正确:加 static,或者用全局变量
static led_config_t cfg = { .gpio = 2 };
xTaskCreate(task_led, "led", 2048, &cfg, 5, NULL);2. xTaskCreatePinnedToCore —— 创建任务并指定跑在哪个核
作用:ESP32-S3 有两个 CPU 核心,这个函数可以指定任务固定在哪个核上跑。
c
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char *pcName,
uint32_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pvCreatedTask,
BaseType_t xCoreID // ⭐ 比 xTaskCreate 多了这个参数
);前六个参数和 xTaskCreate 完全一样,只多了最后一个:
xCoreID 填什么 | 含义 |
|---|---|
0(PRO_CPU_NUM) | 固定在 0 号核 |
1(APP_CPU_NUM) | 固定在 1 号核 |
tskNO_AFFINITY | 不固定,哪个核空就用哪个 |
c
// 把界面任务固定在 1 号核,避免被 WiFi 打扰
xTaskCreatePinnedToCore(task_ui, "ui", 4096, NULL, 5, NULL, 1);新手用哪个
直接用 xTaskCreate 就行 —— 它等价于 xTaskCreatePinnedToCore(..., tskNO_AFFINITY),系统会自动安排。
只有当你发现某个任务被别的任务干扰(比如刷屏被 WiFi 卡顿)时,才需要把它钉到另一个核上。
3. vTaskDelete —— 删除任务
作用:删掉一个任务,回收它占用的栈和内存。
c
void vTaskDelete(TaskHandle_t xTaskToDelete);| 参数 | 填什么 |
|---|---|
xTaskToDelete | 要删的任务句柄;填 NULL 表示删自己 |
c
void task_once(void *pvParameters)
{
do_something_once(); // 干一次性的活
vTaskDelete(NULL); // ⭐ 干完删掉自己,函数不能直接 return
}两个注意点
- 任务函数不能自然结束,要么死循环,要么最后调
vTaskDelete(NULL) - 任务里
malloc的内存不会自动释放,删任务前要自己free
4. vTaskDelay —— 延时(让出 CPU)
作用:让当前任务睡一会儿,这段时间 CPU 让给别的任务用。
c
void vTaskDelay(const TickType_t xTicksToDelay);| 参数 | 填什么 |
|---|---|
xTicksToDelay | 睡多少个 tick,必须用 pdMS_TO_TICKS 换算 |
c
vTaskDelay(pdMS_TO_TICKS(500)); // ✅ 睡 500 毫秒
vTaskDelay(500); // ❌ 这是 500 个 tick,不是 500 毫秒!它和空循环的区别
c
for (int i = 0; i < 1000000; i++); // ❌ 傻等,CPU 被你占着
vTaskDelay(pdMS_TO_TICKS(500)); // ✅ 睡过去,CPU 让给别人5. pdMS_TO_TICKS —— 毫秒转成 tick
作用:把毫秒换算成 FreeRTOS 内部的时间单位(tick)。
c
#define pdMS_TO_TICKS(xTimeInMs) // 是个宏,不是函数| 参数 | 填什么 |
|---|---|
xTimeInMs | 毫秒数 |
为什么必须用它:一个 tick 是多少毫秒取决于系统配置(可能是 1ms,也可能是 10ms)。用这个宏换算,换个配置代码也不用改。
c
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1 秒
xQueueReceive(queue, &data, pdMS_TO_TICKS(100)); // 最多等 100 毫秒
xSemaphoreTake(mutex, pdMS_TO_TICKS(50)); // 最多等 50 毫秒凡是"等多久"的参数,都用它包一下。
6. xTaskGetTickCount —— 取当前的 tick 计数
作用:返回系统从启动到现在过了多少个 tick。
c
TickType_t xTaskGetTickCount(void);没有参数。返回值是当前的 tick 数。
主要用途:配合 vTaskDelayUntil 实现严格周期的任务。
c
void task_clock(void *pvParameters)
{
// 记下"上一次醒来的时刻"
TickType_t last_wake_time = xTaskGetTickCount();
while (1) {
update_clock(); // 干活,耗时不确定
// 严格每 1000 毫秒执行一次,不受干活耗时影响
vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(1000));
}
}vTaskDelay vs vTaskDelayUntil
vTaskDelay(1000):干完活再睡 1 秒 → 实际周期 = 干活时间 + 1 秒,会越走越偏vTaskDelayUntil:严格每 1 秒一次,干活花多久都不影响
做时钟这种要准的,必须用 vTaskDelayUntil。
二、队列
7. xQueueCreate —— 创建队列
作用:创建一条队列,用于任务之间传数据。
c
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 最多能放几个
UBaseType_t uxItemSize // 每个多大(字节)
);| 参数 | 填什么 | 说明 |
|---|---|---|
uxQueueLength | 队列容量 | 传状态用 1,传消息用 5~10 |
uxItemSize | 单个元素的大小 | 一律写 sizeof(你的类型) |
返回值:队列句柄;返回 NULL 表示内存不够,创建失败。
c
// 存放 ui_event_t 类型的消息,最多排 10 条
QueueHandle_t ui_queue = xQueueCreate(10, sizeof(ui_event_t));
if (ui_queue == NULL) {
printf("create queue failed\n");
}8. xQueueReceive —— 从队列取数据
作用:从队列里取走一个数据。队列空的时候任务会睡着等,不占 CPU。
c
BaseType_t xQueueReceive(
QueueHandle_t xQueue, // 队列句柄
void *pvBuffer, // 数据放到哪
TickType_t xTicksToWait // 最多等多久
);| 参数 | 填什么 |
|---|---|
xQueue | xQueueCreate 返回的句柄 |
pvBuffer | 接收变量的地址,要写 &变量 |
xTicksToWait | portMAX_DELAY = 一直等;pdMS_TO_TICKS(100) = 最多等 100ms;0 = 不等 |
返回值:pdTRUE 取到了,pdFALSE 超时没取到。
c
void task_ui(void *pvParameters)
{
ui_event_t event;
while (1) {
// 最多等 1 秒
if (xQueueReceive(ui_queue, &event, pdMS_TO_TICKS(1000)) == pdTRUE) {
ui_handle_event(&event); // 取到消息了
} else {
ui_handle_timeout(); // 1 秒没消息,当超时处理
}
}
}这个"超时"很有用
它让任务可以既等消息、又能按时干周期性的活 —— 有消息就处理消息,没消息就当"时间到了",做一次定时任务。
不用为了"每隔 N 秒做一次"再单独开一个任务。
9. xQueueOverwrite —— 覆盖式写入
作用:往队列里写数据,如果队列满了就直接覆盖掉旧的,不等待。
c
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue, // 队列句柄
const void *pvItemToQueue // 要写入的数据地址
);| 参数 | 填什么 |
|---|---|
xQueue | 长度必须是 1 的队列(xQueueCreate(1, ...)) |
pvItemToQueue | 要发送的数据地址,写 &变量 |
返回值:永远是 pdTRUE(因为它从不失败)。
只能用在长度为 1 的队列上
xQueueOverwrite 要求队列长度必须是 1,用在更长的队列上会触发断言崩溃。
什么时候用它:传"最新状态"而不是"消息流"的场合。
c
// 队列长度必须是 1
QueueHandle_t temp_queue = xQueueCreate(1, sizeof(float));
// 采集任务:只关心最新温度,旧的没人看了就直接覆盖
float temperature = 26.5;
xQueueOverwrite(temp_queue, &temperature);| 需求 | 用哪个 |
|---|---|
| 每一条都不能丢(按键事件) | xQueueSend,队列长度 5~10 |
| 只要最新的一份(当前温度) | xQueueOverwrite,队列长度 1 |
顺带认识 xQueueSend
xQueueSend(queue, &data, timeout) 是普通发送 —— 队列满了就等,等不到就返回 pdFALSE,数据会丢,所以要判断返回值。
用法和 xQueueOverwrite 几乎一样,只多一个超时参数。
三、互斥锁与信号量
10. xSemaphoreCreateMutex —— 创建互斥锁
作用:创建一把互斥锁,用于保护共享资源。
c
SemaphoreHandle_t xSemaphoreCreateMutex(void);没有参数。返回值是锁的句柄,NULL 表示创建失败。
c
SemaphoreHandle_t state_mutex = xSemaphoreCreateMutex();创建出来就是"可用"的
互斥锁刚创建时钥匙在挂钩上,第一次 Take 立刻就能拿到。
11. xSemaphoreCreateBinary —— 创建二值信号量
作用:创建一个二值信号量,用于任务间通知。
c
SemaphoreHandle_t xSemaphoreCreateBinary(void);没有参数。返回值是信号量句柄,NULL 表示创建失败。
c
SemaphoreHandle_t key_sem = xSemaphoreCreateBinary();创建出来是"空"的!
和互斥锁不一样 —— 二值信号量刚创建时计数是 0,也就是"铃还没响"。
第一次 xSemaphoreTake 会一直等,直到有人 Give 一下。这符合"等通知"的语义,但新手常在这里卡住。
12. xSemaphoreTake —— 获取(拿锁 / 等通知)
作用:拿互斥锁,或者等信号量。拿不到就睡着等。
c
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore, // 锁或信号量的句柄
TickType_t xTicksToWait // 最多等多久
);| 参数 | 填什么 |
|---|---|
xSemaphore | 创建时返回的句柄 |
xTicksToWait | portMAX_DELAY = 一直等;pdMS_TO_TICKS(100) = 最多等 100ms;0 = 不等 |
返回值:pdTRUE 拿到了,pdFALSE 超时没拿到。
c
// 用法一:拿锁保护数据(建议给超时,别死等)
if (xSemaphoreTake(state_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
s_state.temperature = 26.5;
xSemaphoreGive(state_mutex); // ⚠️ 必须还
} else {
printf("take mutex timeout\n");
}
// 用法二:等通知(一直等,等到才干活)
xSemaphoreTake(key_sem, portMAX_DELAY);
handle_key();13. xSemaphoreGive —— 释放(还锁 / 发通知)
作用:还回互斥锁,或者发出一次信号量通知。
c
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);| 参数 | 填什么 |
|---|---|
xSemaphore | 创建时返回的句柄 |
返回值:pdTRUE 成功。
忘了 Give 会卡死整个程序
拿了锁不还 → 别人永远拿不到 → 全卡住。
写 Take 的时候就顺手把 Give 写好。
中断里要用 FromISR 版本
c
// 中断服务函数里
void IRAM_ATTR key_isr_handler(void *arg)
{
xSemaphoreGiveFromISR(key_sem, NULL); // ⭐ 带 FromISR
}在中断里用 xSemaphoreGive 会直接崩溃。
四、事件组
14. xEventGroupCreate —— 创建事件组
作用:创建一组"标志位"(可以理解成一排小旗子),用来标记若干件事是否发生。
c
EventGroupHandle_t xEventGroupCreate(void);没有参数。返回值是事件组句柄,NULL 表示创建失败。
c
#define BIT_WIFI_OK BIT0 // 第 0 面旗:联网成功
#define BIT_TIME_OK BIT1 // 第 1 面旗:时间同步成功
EventGroupHandle_t app_events = xEventGroupCreate();一个事件组能放几面旗
24 面(BIT0 ~ BIT23),足够用了。
15. xEventGroupSetBits —— 升旗(标记事件发生)
作用:把指定的标志位置 1,表示"这件事发生了"。
c
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToSet // 要置 1 的位
);| 参数 | 填什么 |
|---|---|
xEventGroup | 创建时返回的句柄 |
uxBitsToSet | 要升的旗,多面用 | 连接 |
c
xEventGroupSetBits(app_events, BIT_WIFI_OK); // 升一面
xEventGroupSetBits(app_events, BIT_WIFI_OK | BIT_TIME_OK); // 同时升两面16. xEventGroupClearBits —— 降旗(清除标记)
作用:把指定的标志位清 0,表示"这件事不成立了"。
c
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToClear // 要清 0 的位
);c
// WiFi 断开了,把"联网成功"这面旗降下来
void on_wifi_disconnected(void)
{
xEventGroupClearBits(app_events, BIT_WIFI_OK);
}17. xEventGroupWaitBits —— 等旗(等事件发生)
作用:等待一面或多面旗升起来。等不到就睡着,不占 CPU。
c
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToWaitFor, // 等哪几面旗
const BaseType_t xClearOnExit, // 等到后要不要自动降旗
const BaseType_t xWaitForAllBits, // 全部满足 还是 任一满足
TickType_t xTicksToWait // 最多等多久
);五个参数,重点是中间两个:
| 参数 | 填什么 | 说明 |
|---|---|---|
uxBitsToWaitFor | 要等的旗,多面用 | | 如 BIT_WIFI_OK | BIT_TIME_OK |
xClearOnExit | pdTRUE = 等到后自动降旗pdFALSE = 保持升着 | 状态标志填 pdFALSE |
xWaitForAllBits | pdTRUE = 全部满足才返回(且)pdFALSE = 任一满足就返回(或) | 按需求填 |
xTicksToWait | portMAX_DELAY = 一直等 | 建议给个超时 |
返回值:返回等待结束时所有位的状态,需要自己判断到底哪几面旗升起来了。
c
// 等"联网成功"和"时间同步"都完成,最多等 60 秒
EventBits_t bits = xEventGroupWaitBits(
app_events,
BIT_WIFI_OK | BIT_TIME_OK,
pdFALSE, // 不降旗,别的任务还要看
pdTRUE, // 两面都要升起来才返回
pdMS_TO_TICKS(60000));
if ((bits & (BIT_WIFI_OK | BIT_TIME_OK)) == (BIT_WIFI_OK | BIT_TIME_OK)) {
start_business(); // 都好了,开工
} else {
printf("init timeout\n"); // 超时了,降级运行
}两个高频坑
- 超时和成功都会返回,必须用返回值再判断一次,不能假定一定成功
xClearOnExit填pdFALSE时旗子一直升着,下次再 Wait 会立刻返回 —— 如果放在while(1)里会变成空转
五、任务通知
任务通知是什么
任务通知(Task Notification)是 FreeRTOS 提供的轻量级通知机制。每个任务自带一个通知值,不需要额外创建对象,因此比信号量更快、更省内存。
可以理解成"每个任务自带的一个信号量",用法几乎一样。
18. xTaskNotifyGive —— 通知某个任务
作用:给指定任务发一次通知(相当于给它"按铃")。
c
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);| 参数 | 填什么 |
|---|---|
xTaskToNotify | 要通知的任务句柄 |
要拿到任务句柄
和信号量不同,任务通知必须知道通知谁,所以创建任务时要保存句柄:
c
TaskHandle_t g_worker_handle; // 全局保存句柄
xTaskCreate(task_worker, "worker", 2048, NULL, 5, &g_worker_handle);
// ⭐ 最后一个参数别填 NULL 了c
// 通知它去干活
xTaskNotifyGive(g_worker_handle);
// 中断里用这个版本
void IRAM_ATTR some_isr(void *arg)
{
vTaskNotifyGiveFromISR(g_worker_handle, NULL);
}19. ulTaskNotifyTake —— 等待通知
作用:等别人来通知自己。没通知就睡着等。
c
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, // 取走后是清零还是减一
TickType_t xTicksToWait // 最多等多久
);注意:它等的永远是"自己",所以不需要传任务句柄。
| 参数 | 填什么 | 说明 |
|---|---|---|
xClearCountOnExit | pdTRUE = 取走后清零pdFALSE = 每次减一 | 当信号量用就填 pdTRUE |
xTicksToWait | portMAX_DELAY = 一直等 | 同前面 |
返回值:收到的通知次数;返回 0 表示超时没等到。
c
void task_worker(void *pvParameters)
{
while (1) {
// 一直等通知,取到后清零
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0) {
do_work(); // 被通知了,干活
}
}
}和二值信号量怎么选
| 二值信号量 | 任务通知 | |
|---|---|---|
| 要不要创建对象 | 要 xSemaphoreCreateBinary() | 不用,任务自带 |
| 速度 | 快 | 更快(约快 45%) |
| 内存 | 多占一份 | 省 |
| 能通知几个任务 | 谁来拿都行 | 只能一对一 |
一对一通知优先用任务通知;一对多、或者不知道谁来拿,就用信号量。
六、速查总表
| 分类 | 函数 | 一句话 |
|---|---|---|
| 任务 | xTaskCreate | 创建任务 |
xTaskCreatePinnedToCore | 创建任务并指定 CPU 核 | |
vTaskDelete | 删除任务(NULL = 删自己) | |
vTaskDelay | 延时,让出 CPU | |
pdMS_TO_TICKS | 毫秒转 tick | |
xTaskGetTickCount | 取当前 tick,配合 vTaskDelayUntil | |
| 队列 | xQueueCreate | 创建队列 |
xQueueReceive | 取数据(空则等待) | |
xQueueOverwrite | 覆盖写入(队列长度必须为 1) | |
| 锁/信号量 | xSemaphoreCreateMutex | 创建互斥锁(创建即可用) |
xSemaphoreCreateBinary | 创建二值信号量(创建时是空的) | |
xSemaphoreTake | 拿锁 / 等通知 | |
xSemaphoreGive | 还锁 / 发通知 | |
| 事件组 | xEventGroupCreate | 创建事件组 |
xEventGroupSetBits | 升旗 | |
xEventGroupClearBits | 降旗 | |
xEventGroupWaitBits | 等旗(返回值要再判断) | |
| 任务通知 | xTaskNotifyGive | 通知某任务(要任务句柄) |
ulTaskNotifyTake | 等自己的通知 |
附:调试时会用到的
不属于上面 19 个,但排查问题时很有用(1.10 会细讲):
c
uxTaskGetStackHighWaterMark(NULL); // 当前任务栈还剩多少字节
esp_get_free_heap_size(); // 剩余内存
esp_get_minimum_free_heap_size(); // 历史最低剩余内存(查泄漏)最后一条:判断返回值 ⭐
c
// ❌ 不判断,出错了你都不知道
xQueueReceive(queue, &data, portMAX_DELAY);
// ✅ 判断一下
if (xQueueReceive(queue, &data, portMAX_DELAY) != pdTRUE) {
printf("receive failed\n");
}凡是 x 开头的函数都有返回值,都该判断。这是新手和熟手最明显的差别。
动手试试
- 用
xTaskCreate创建两个任务,故意把栈填成256,观察程序报什么错。 - 创建一个二值信号量,先不 Give 直接 Take,验证任务确实卡住不动了。
- 创建一个长度为 1 的队列,用
xQueueOverwrite连写三次不同的值,再xQueueReceive一次,看看取到的是第几个。 - 用
xEventGroupWaitBits等两面旗,只升其中一面,验证它确实在超时后才返回。