FreeRTOS的学习总结
·
1.ESP32中断
-
中断的类型
- 硬件中断:比如按下按键时的GPIO中断,或者触摸中断(检测到触摸时)
- 软件中断:定时器中断或者看门狗中断(当定时器超时时)
-
ESP32中断相关函数
- attachInterrupt:在Arduino中,可以使用下面这个函数添加一个中断处理逻辑,这个函数有三个参数void attachInterrupt(uint8_t pin, std::function<void(void)> intRoutine, int mode)
- pin:中断触发的中断号。虽然对于esp32,这个参数可以直接写GPIO引脚编号,但是建议尽量调用digitalPinTolnterrupt函数,将原始GPIO引脚转换成中断系统中的对应编号使用,比如digitalPinToInterrupt(23),ESP32能直接使用GPIO编号,是因为对于ESP32,digitalPinToInterrupt函数的返回值就是原样返回,但对于其他类型的单片机,逻辑就不一样了
- intRoutine:中断处理函数,就是当触发中断时,需要执行的操作注意这个操作应该尽可能轻量,不能将繁重的工作写在中断处理函数中,这样会极大降低单片机响应,最糟糕时可能导致单片机重启(看门狗超时),自定义一个函数,一般是void型
- mode:中断触发的模式,一共有以下几种模式可选:

-
detachInterrupt:该函数用于解除某个中断号的中断逻辑,函数具体如下: void detachInterrupt(uint8_t pin),函数只有一个参数:
-
pin:要取消中断程序的中断号,同样地,建议使用digitalPinToInterrupt函数来将GPIO编号转换成中断号
中断实战:
- 外接一个按键来控制LED灯的亮灭,pin选择23引脚,中断触发模式选择下降沿,因为按键默认高电平(上拉),我们按下按键就触发中断。代码实现:

2.FreeRTOS多任务初体验
-
ESP32的Arduino框架里面,已经内置了FreeRTOS框架,并且对ESP32的双核进行了完美的适配,所以我们在使用时,无需引入第三方库就可以直接用了。使用stm32cubemx来运用FreeRTOS我后续会写出来。
-
创建多任务:xTaskCreate函数,一共有6个参数
-
pvTaskCode:创建的任务需要执行的逻辑,是一个自定义函数,该函数返回值必须为void,只有一个参数,参数类型必须为void*
-
pcName:任务的名称,一个字符串,自己定义,为了自己更加理解创建了一个怎么样的任务
- usStackDepth:给任务分配的最大堆栈大小,单位是字节
- pvParameters:传递给任务的参数,类型是void*,一般为NULL
- uxPriority:任务优先级,一个数字,取值范围为[0,24],数字越大,优先级越高
- pxCreatedTask:用于接收该任务的句柄,后续对该任务的操作,需要基于该句柄完成,类型为TaskHandle_t
- 代码实战:


- FreeRTOS延时函数:vTaskDelay,该函数的作用是延迟一段时间,在这段时间内,调用该函数的任务让出CPU资源,其他任务可以借机获取CPU资源
- 该函数就只有一个参数:xTicksToDelay,需要任务延迟的tick数,对于ESP32,一个tick等于1ms,所以要延迟3秒,就应该调用vTaskDelay(3000),但是对于其他芯片,一个tick就不一定等于1ms。
- 我们平常使用的Delay函数,其内部也是调用vTaskDelay函数实现的
- 任务删除函数:vTaskDelete
- 该函数就一个参数,xTaskToDelete表示要删除任务的句柄,这个句柄就是xTaskCreate函数中传入的最后一个参数,如果参数设置为NULL,表示删除当前任务,一般都是填NULL,自己删除自己,不建议跨任务删除,会出现一些不可预知的问题
- 任务暂停函数:vTaskSuspend
- 这个函数就一个参数,填写NULL表示暂停当前任务,函数也可传其他任务句柄
- 任务继续函数:vTaskResume
- 这个函数就一个参数,参数为NULL表示恢复当前任务,函数也可传其他任务句柄
小提醒:大家可能会发现FreeRTOS库中的函数名,有些是以x开头的,有些是以v开头的,其实这与函数的返回值有关,如果函数有返回值,一般以x开头,如果函数没有返回值,一般以v(void)开头
3.FreeRTOS任务绑定
- esp32有2个CPU,我们可以将创建的任务分配到指定的CPU上运行
- 创建任务并绑定CPU函数:xTaskCreatePinnedToCore,该函数和xTaskCreate前6个参数一致,最后一个参数:
- xCoreID:需要将任务分配到的CPU编号,esp32可选编号为0和1,也可以指定常量
- 需要注意的是:esp32中,与无线通信相关的,比如WiFI或者蓝牙协议栈相关的逻辑都在CPU0上处理,所以我们在使用esp32时,如果有用到esp32的蓝牙或者WiFi功能,则不建议将我们自己的比较复杂的任务分配到CPU0上,因为这样有可能会干扰WiFI和蓝牙的正常运行,影响通信质量。而对于简单任务的分配,也建议不指定CPU,让esp32自行调度,除非有特殊需求
4.看门狗
- 什么是看门狗?看门狗就是一个定时器,能够让微控制器在程序发生意外的时候,自动重启单片机,这些意外有什么?
- 看门狗分类:中断看门狗和任务看门狗
- 中断看门狗(IWDT):interrupt watch dof timer,它是用于监控有没有哪一个中断处理程序执行时间过长(默认时间800ms),如果时间过长,直接重启esp32
- 任务看门狗(TWDT):task watch dog timer,它用来看管FreeRTOS的任务,任务如果长时间(5s)得不到CPU资源,就重启esp32
- 看门狗机制:中断开门狗默认开启,esp32有2个CPU,在每一个CPU上,都有一个预先创建的任务,这个任务优先级非常低(0),它有三个作用:
-
当esp32处于空闲状态时,自动降低功耗,增加续航时间
-
当自己CPU下有任务被删除时,主动清理相关内存资源
-
重置看门狗定时器
-
喂狗定义:看门狗本质是定时器,只有定时器超时才触发重启逻辑,我们只需要在超时时间之内去重置定时器,就避免被重启,这个就叫喂狗。
5.如何避免日常错误,导致看门狗重启
- 中断处理函数简单一点,不要在中断处理函数里面进行打印操作
- 主动让出CPU,使用vTaskDelay函数,主动休眠一段时间
6.FreeRTOS信号量的使用
- 类型:二值信号量和计数信号量
- 使用相当多的是二值信号量
- 二值信号量有三种流程:创建,释放,等待

- 创建:semaphore = xSemaphoreCreateBinary()
- 释放:xSemaphoreGive(semaphore)
- 等待:xSemaphoreTake(semaphore, portMAX_DELAY)
7.FreeRTOS互斥信号量使用
- 2个线程并行执行的东西强制转换成串行的,避免出现线程不安全的现象,相当于一把锁,谁拿到钥匙谁就干活,怎么开锁?xSemaphoreTakeRecursive(mutex, portMAX_DELAY) == pdTRUE用if语句来判断是否拿到锁,谁拿到就谁执行
- 互斥信号量,用三个流程:
- 定义自变量SemaphoreHandle_t mutex;
- 然后set up函数里面用定义的变量创建的函数去获取mutex = xSemaphoreCreateMutex();
- 最后一步在自定义函数里面使用使用if语句去判断,然后执行逻辑
举例代码:
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "usart.h"
#include <stdio.h>
/* ============ 1. 定义互斥信号量句柄 ============ */
SemaphoreHandle_t xMutex;
/* ============ 2. 共享资源:UART 打印函数 ============ */
/**
* @brief 线程安全的打印函数(关键!每次打印前都要加锁)
*/
void Safe_Printf(const char *fmt, ...)
{
/* 拿钥匙(最多等待 1000ms,防止永久死锁) */
if (xSemaphoreTakeRecursive(xMutex, pdMS_TO_TICKS(1000)) == pdTRUE)
{
va_list args;
va_start(args, fmt);
vprintf(fmt, args); /* 整段打印完成前,不会被打断 */
va_end(args);
/* 还钥匙 */
xSemaphoreGiveRecursive(xMutex);
}
else
{
/* 等待超时,可能发生死锁,可以在这里报警 */
}
}
/* ============ 3. 任务 A:模拟温度采集 ============ */
void TaskA_Temperature(void *pvParameters)
{
while (1)
{
/* 模拟读温度传感器(耗时操作) */
float temp = 26.5f;
/* 打印前加锁,确保完整输出 */
Safe_Printf("[TaskA] Temperature: %.1f C\r\n", temp);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* ============ 4. 任务 B:模拟湿度采集 ============ */
void TaskB_Humidity(void *pvParameters)
{
while (1)
{
/* 模拟读湿度传感器 */
int humidity = 60;
Safe_Printf("[TaskB] Humidity: %d%%\r\n", humidity);
vTaskDelay(pdMS_TO_TICKS(700));
}
}
/* ============ 5. setup / main 函数 ============ */
int main(void)
{
/* 硬件初始化 */
USART_Init();
/* 创建互斥信号量(相当于创建一把锁) */
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL)
{
printf("Mutex create failed!\r\n");
while (1); /* 创建失败,停下来检查 */
}
/* 创建两个任务 */
xTaskCreate(TaskA_Temperature, "TaskA", 256, NULL, 1, NULL);
xTaskCreate(TaskB_Humidity, "TaskB", 256, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
while (1) { /* 不会到达这里 */ }
}
8.FreeRTOS事件组的使用
- 概念:允许任务等待多个事件的组合每个事件对应事件组中的一个位(bit),任务可以设置、清除或等待这些位的特定组合。
- 怎么运用?首先还是创建出事件组句柄EventGroupHandle_t xEventGroup,然后在setup中创建出事件组xEventGroup=xEventGroupCreate();在任务中使用xEventGroupWaitBits函数
举例代码:
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "usart.h"
#include <stdio.h>
/* ============ 1. 定义事件位(用 bit 的位置表示) ============ */
#define BIT_CARD_OK (1 << 0) // bit0:刷卡成功
#define BIT_PWD_OK (1 << 1) // bit1:密码正确
#define BIT_ALL_OK (BIT_CARD_OK | BIT_PWD_OK) // 两个都满足
/* ============ 2. 创建事件组句柄 ============ */
EventGroupHandle_t xEventGroup;
/* ============ 3. 任务 A:模拟刷卡(200ms 后刷卡成功) ============ */
void Task_SwipeCard(void *pvParameters)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(200));
printf("[刷卡中...] 请刷卡\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
printf("[刷卡] 刷卡成功!\r\n");
/* 设置 bit0:刷卡成功事件发生 */
xEventGroupSetBits(xEventGroup, BIT_CARD_OK);
}
}
/* ============ 4. 任务 B:模拟输密码(500ms 后密码正确) ============ */
void Task_InputPwd(void *pvParameters)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(500));
printf("[密码] 请输入密码\r\n");
vTaskDelay(pdMS_TO_TICKS(2000));
printf("[密码] 密码正确!\r\n");
/* 设置 bit1:密码正确事件发生 */
xEventGroupSetBits(xEventGroup, BIT_PWD_OK);
}
}
/* ============ 5. 门控任务:等两个条件都满足才开门 ============ */
void Task_DoorControl(void *pvParameters)
{
EventBits_t uxBits;
while (1)
{
printf("[门控] 等待:刷卡 + 密码\r\n");
/* 等待 bit0 和 bit1 都置 1(pdTRUE 表示 AND 逻辑) */
uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_ALL_OK, // 等待这些位
pdTRUE, // 满足后自动清除
pdTRUE, // 全部满足才返回(AND)
portMAX_DELAY); // 永久等待
printf("[门控] ✅ 所有条件满足,开门!\r\n\n");
/* 模拟开门动作 2 秒 */
vTaskDelay(pdMS_TO_TICKS(2000));
printf("[门控] 关门,等待下一次\r\n\n");
}
}
/* ============ 6. 主函数 ============ */
int main(void)
{
USART_Init();
/* ① 创建事件组 */
xEventGroup = xEventGroupCreate();
if (xEventGroup == NULL)
{
printf("Event group create failed!\r\n");
while (1);
}
/* ② 创建任务 */
xTaskCreate(Task_SwipeCard, "Card", 256, NULL, 1, NULL);
xTaskCreate(Task_InputPwd, "Pwd", 256, NULL, 1, NULL);
xTaskCreate(Task_DoorControl,"Door", 256, NULL, 1, NULL);
/* ③ 启动调度器 */
vTaskStartScheduler();
while (1);
}
结果:
时间线
0ms [门控] 等待:刷卡 + 密码
200ms [刷卡中...] 请刷卡
500ms [密码] 请输入密码
1200ms [刷卡] 刷卡成功! ← bit0 置 1,但密码还没好
2500ms [密码] 密码正确! ← bit1 置 1,两个都满足!
2500ms [门控] ✅ 所有条件满足,开门!
4500ms [门控] 关门,等待下一次
9.FreeRTOS队列的使用
- 之前学习的信号量和事件组只是任务和任务之间传递信号,相互告诉某件事情发生了没有
- 而队列则是任务和任务之间传递真实的数据
- 创建队列:先创建句柄,然后再在setup函数里面句柄函数的形式来书写:句柄=xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
- 发送数据:
- BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);正常发送
- BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);往后排
- BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);往前插队
- 接受数据:BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
- 检查队列中是否有数据:UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);
举例代码:
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "usart.h"
#include <stdio.h>
/* ============ 1. 定义"订单"结构体 ============ */
typedef struct {
uint8_t table_id; // 几号桌
char dish[20]; // 什么菜
} Order_t;
/* ============ 2. 创建队列句柄 ============ */
/* 队列容量 5 条,每条是 Order_t 大小 */
QueueHandle_t xOrderQueue;
/* ============ 3. 生产者任务:模拟"点餐"(每 1 秒下一个单) ============ */
void Task_Waiter(void *pvParameters)
{
Order_t order;
int cnt = 0;
while (1)
{
cnt++;
order.table_id = (cnt % 5) + 1; // 轮流给 1~5 号桌
sprintf(order.dish, "红烧肉 #%d", cnt);
printf("[服务员] %d号桌点了 %s,扔进队列\r\n",
order.table_id, order.dish);
/* ✅ 发送数据到队尾 */
xQueueSend(xOrderQueue, &order, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* ============ 4. 消费者任务:模拟"做菜"(每 2 秒做一个) ============ */
void Task_Chef(void *pvParameters)
{
Order_t order;
while (1)
{
/* ✅ 从队列里接收一条数据 */
if (xQueueReceive(xOrderQueue, &order, portMAX_DELAY) == pdTRUE)
{
printf("[厨师] 正在做 %s ...\r\n", order.dish);
vTaskDelay(pdMS_TO_TICKS(2000)); // 模拟做菜 2 秒
printf("[厨师] ✅ %s 上桌!\r\n\n", order.dish);
}
}
}
/* ============ 5. 监控任务:每秒看队列里堆了几个单 ============ */
void Task_Monitor(void *pvParameters)
{
UBaseType_t cnt;
while (1)
{
cnt = uxQueueMessagesWaiting(xOrderQueue);
printf("[监控] 当前队列里有 %lu 个订单等待\r\n\n", cnt);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
/* ============ 6. 主函数 ============ */
int main(void)
{
USART_Init();
/* ① 创建队列:容量 5 条,每条 sizeof(Order_t) 字节 */
xOrderQueue = xQueueCreate(5, sizeof(Order_t));
if (xOrderQueue == NULL)
{
printf("Queue create failed!\r\n");
while (1);
}
/* ② 创建任务 */
xTaskCreate(Task_Waiter, "Waiter", 256, NULL, 1, NULL);
xTaskCreate(Task_Chef, "Chef", 256, NULL, 1, NULL);
xTaskCreate(Task_Monitor, "Monitor",256, NULL, 1, NULL);
/* ③ 启动调度器 */
vTaskStartScheduler();
while (1);
}
结果:
[服务员] 1号桌点了 红烧肉 #1,扔进队列
[监控] 当前队列里有 1 个订单等待
[服务员] 2号桌点了 红烧肉 #2,扔进队列
[厨师] 正在做 红烧肉 #1 ...
[监控] 当前队列里有 2 个订单等待
[服务员] 3号桌点了 红烧肉 #3,扔进队列
[监控] 当前队列里有 3 个订单等待
[厨师] ✅ 红烧肉 #1 上桌!
[厨师] 正在做 红烧肉 #2 ...
...
10.FreeRTOS环形缓冲区使用
- 暂存数据达到削峰的效果,比如我们开展下一次对话,我们把上一次对话就暂存在循环缓冲区,由于不需要重新分配内存,所以比队列效率高
- 创建缓冲区:xRingbufferCreate(size_t xBufferSize, RingbufferType_t xBufferType);
- 往缓冲区写入数据:BaseType_t xRingbufferSend(RingbufHandle_t xRingbuffer, const void *pvItem, size_t xItemSize, TickType_t xTicksToWait);
- 从缓冲区接受数据:void *xRingbufferReceive(RingbufHandle_t xRingbuffer, size_t *pxItemSize, TickType_t xTicksToWait);
- 归还内存:void vRingbufferReturnItem(RingbufHandle_t xRingbuffer, void *pvItem);
举例代码:
#include "FreeRTOS.h"
#include "task.h"
#include "ringbuffer.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>
/* ============ 1. 定义句柄 ============ */
RingbufHandle_t xRingBuf;
/* ============ 2. 串口接收任务:模拟"瞬间来一大段数据" ============ */
void Task_UartRx(void *pvParameters)
{
const char *fake_data[] = {
"Hello World!\r\n",
"STM32 is great!\r\n",
"FreeRTOS Ringbuffer demo\r\n"
};
int i = 0;
while (1)
{
const char *p = fake_data[i % 3];
size_t len = strlen(p) + 1; // 含 \0
printf("[串口] 收到 %d 字节: %s", (int)len, p);
/* ✅ 写入环形缓冲区(队尾) */
xRingbufferSend(xRingBuf, p, len, pdMS_TO_TICKS(100));
i++;
vTaskDelay(pdMS_TO_TICKS(800));
}
}
/* ============ 3. 数据处理任务:慢慢取出来解析 ============ */
void Task_Process(void *pvParameters)
{
while (1)
{
size_t recv_size;
char *pItem;
/* ✅ 从缓冲区取出一段数据(自动复制出来) */
pItem = (char *)xRingbufferReceive(xRingBuf, &recv_size, portMAX_DELAY);
if (pItem != NULL)
{
printf("[处理] 取出 %d 字节: %s", (int)recv_size, pItem);
/* ⚠️ 关键:处理完必须归还内存!否则缓冲区会耗尽 */
vRingbufferReturnItem(xRingBuf, (void *)pItem);
vTaskDelay(pdMS_TO_TICKS(1500)); // 模拟处理耗时
}
}
}
/* ============ 4. 监控任务:每秒看缓冲区状态 ============ */
void Task_Monitor(void *pvParameters)
{
while (1)
{
/* getFreeSize() / getCurSize() 查看使用情况(如果支持) */
printf("[监控] 缓冲区状态:...\r\n\n");
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/* ============ 5. 主函数 ============ */
int main(void)
{
USART_Init();
/* ① 创建环形缓冲区:1024 字节,字节流模式(NO_SPLIT 性能高) */
xRingBuf = xRingbufferCreate(
1024, // 容量
RINGBUF_TYPE_BYTEBUF); // 按字节流方式
if (xRingBuf == NULL)
{
printf("Ringbuffer create failed!\r\n");
while (1);
}
/* ② 创建任务 */
xTaskCreate(Task_UartRx, "UartRx", 256, NULL, 2, NULL);
xTaskCreate(Task_Process, "Process", 256, NULL, 1, NULL);
xTaskCreate(Task_Monitor, "Monitor", 256, NULL, 1, NULL);
/* ③ 启动调度器 */
vTaskStartScheduler();
while (1);
}
结果:
[串口] 收到 14 字节: Hello World!
[串口] 收到 18 字节: STM32 is great!
[处理] 取出 14 字节: Hello World!
[串口] 收到 26 字节: FreeRTOS Ringbuffer demo
[处理] 取出 18 字节: STM32 is great!
...
更多推荐


所有评论(0)