Skip to content

1.9 FreeRTOS 核心 API ​

这节要搞懂:实战项目会用到的 19 个函数,分别是什么、怎么用、参数填什么。 需要基础:1.8

这一节是"字典"

不用背,也不用一次看完。 这里列的就是第三章天气时钟实际用到的全部 FreeRTOS 函数,写代码时想不起参数怎么填,回来翻一下就行。

零、先认识函数名的规律 ​

FreeRTOS 的函数名有严格的命名约定,认识规律就不用死记:

开头含义例子
vvoid,没有返回值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
}

两个注意点

  1. 任务函数不能自然结束,要么死循环,要么最后调 vTaskDelete(NULL)
  2. 任务里 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     // 最多等多久
);
参数填什么
xQueuexQueueCreate 返回的句柄
pvBuffer接收变量的地址,要写 &变量
xTicksToWaitportMAX_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创建时返回的句柄
xTicksToWaitportMAX_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
xClearOnExitpdTRUE = 等到后自动降旗
pdFALSE = 保持升着
状态标志填 pdFALSE
xWaitForAllBitspdTRUE = 全部满足才返回(且)
pdFALSE = 任一满足就返回(或)
按需求填
xTicksToWaitportMAX_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");             // 超时了,降级运行
}

两个高频坑

  1. 超时和成功都会返回,必须用返回值再判断一次,不能假定一定成功
  2. 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           // 最多等多久
);

注意:它等的永远是"自己",所以不需要传任务句柄。

参数填什么说明
xClearCountOnExitpdTRUE = 取走后清零
pdFALSE = 每次减一
当信号量用就填 pdTRUE
xTicksToWaitportMAX_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 开头的函数都有返回值,都该判断。这是新手和熟手最明显的差别。

动手试试 ​

  1. 用 xTaskCreate 创建两个任务,故意把栈填成 256,观察程序报什么错。
  2. 创建一个二值信号量,先不 Give 直接 Take,验证任务确实卡住不动了。
  3. 创建一个长度为 1 的队列,用 xQueueOverwrite 连写三次不同的值,再 xQueueReceive 一次,看看取到的是第几个。
  4. 用 xEventGroupWaitBits 等两面旗,只升其中一面,验证它确实在超时后才返回。