protothreads协程与FreeRTOS操作系统
就好比是东厂和锦衣卫
就好比是FreeRTOS说:你PT协程干的了的事我们FreeRTOS一样干,你PT协程干不了的事我们FreeRTOS更可以干,既然这样,皇上,您为什么还要重用PT协程哇?
我:咳咳,因为穷,国库空虚,阁下可听说过富则火力覆盖,穷则战术穿插?
其实,protothreads协程作为一款轻量级的协程系统,它没有任务栈,所有协程共享一个栈,只用极少的状态变量和一套宏机制实现伪并发,资源占用极低,而FreeRTOS呢,每个任务需要独立的栈(最少数百字节))和内核调度管理结构,内存占用较高。因此,当单片机RAM充裕,MCU 有几十 KB RAM时,可以上FreeRTOS,反之,就是protothreads协程比较合适了。
一、PT协程有什么作用?和FreeRTOS的区别是什么
PT协程是“写起来像同步阻塞,但实际上是非阻塞”的编程方式。能让代码不写状态机就实现异步逻辑。FreeRTOS适合多任务系统,把每个功能写成一个 任务函数;设定它的 优先级 ;FreeRTOS 自动切换、调度、同步、延时、通信。
pt 协程 | FreeRTOS | |
---|---|---|
多任务数量 | 支持多个协程 | 支持多个任务 |
执行方式 | 手动轮询,每次只跑一点点 | 自动调度,可抢占切换 |
并发性 | 伪并发(轮流执行) | 真并发(系统调度器切换) |
实时性 | 差,取决于轮询频率 | 好,支持优先级抢占调度 |
注意:PT协程是协作式多任务,没有优先级。因此每个任务以轮询的方式顺序运行,而FreeRTOS有优先级,相关任务之间可以按优先级排序进行切换。
eg:实现一个逻辑“等用户按下按钮,等待500ms,再亮灯”
1.单纯的用delay阻塞500ms:
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) {
delay_ms(500); // 阻塞函数
LED_ON();
}
这500ms内mcu啥也不干,死等。
2.使用协程库:
PT_THREAD(ButtonLedTask(struct pt *pt)) {
static uint32_t timer;
PT_BEGIN(pt);
while (1) {
PT_WAIT_UNTIL(pt, key_press_detected());
timer = millis();
PT_WAIT_UNTIL(pt, millis() - timer > 500);
LED_ON();
}
PT_END(pt);
}
解析:
对于PT_WAIT_UNTIL(pt, condition),它在协程库长这样:
#define PT_WAIT_UNTIL(pt, condition) \
do { \
LC_SET((pt)->lc); \
if(!(condition)) { \
return PT_WAITING; \
} \
} while(0)
也就是说,如果不满足condition这个条件,则会立马返回PT_WAITING协程挂起等待状态,放在代码中,意思就是:
如果还没到 500ms,协程暂时挂起,记录现在的位置,并且立即返回,让调度器去跑别的协程,以后每次调度器再次调用ButtonLedTask,代码会直接从这一行继续,再检查同一个条件。只要条件仍然不满足,就继续返回;
一旦 millis() - timer > 500 为真,宏就不再返回,程序“穿过”这一行,顺序执行下一条语句 led_on();
整个期间 MCU 没有卡死,只是这个协程暂时挂起等待条件成立。
因此是非阻塞式的等待,不会一直占用mcu,在此期间呢,可以执行其他协程或主循环任务。
这个RTOS有异曲同工之妙,下面是FreeRTOS实现上述历程的相关代码:
3.使用FreeRTOS:
void vButtonLedTask(void *pvParameters)
{
while (1)
{
if (key_press_detected())
{
vTaskDelay(pdMS_TO_TICKS(500));
LED_ON();
}
}
}
vTaskDelay(500);FreeRTOS提供的延时函数,当任务处于阻塞态时,会根据优先级自动搜寻其它任务并执行,因此也不会是一直死等阻塞的模式。
二,PT协程与FreeRTOS任务创建与删除
2.1、PT协程的任务创建与删除:
在开始PT协程的任务创建与删除前,我们先来看一下PT协程的源码:
2.1.1protothreads源码分析
/**
* \addtogroup pt
* @{
* 将以下内容添加到 pt 模块组
*/
/**
* \file
* Protothreads 实现
* \author
* Adam Dunkels <adam@sics.se>
*/
#ifndef __PT_H__
#define __PT_H__
#include "lc.h" // 局部续延(Local Continuations)实现
/* Protothread 控制结构 */
struct pt {
lc_t lc; // 局部续延变量,保存执行位置
};
/* Protothread 状态常量 */
#define PT_WAITING 0 // 等待状态
#define PT_EXITED 1 // 已退出状态
#define PT_ENDED 2 // 已结束状态
#define PT_YIELDED 3 // 已让出状态
/**
* \name 初始化
* @{
*/
/**
* 初始化 protothread
* \param pt 指向 protothread 控制结构的指针
*/
#define PT_INIT(pt) LC_INIT((pt)->lc) // 初始化局部续延
/** @} */
/**
* \name 声明和定义
* @{
*/
/**
* 声明 protothread 函数
* \param name_args 函数名和参数
*/
#define PT_THREAD(name_args) char name_args // protothread 返回 char 类型状态
/**
* 在函数中声明 protothread 起点
* \param pt 指向 protothread 控制结构的指针
*/
#define PT_BEGIN(pt) { \
char PT_YIELD_FLAG = 1; /* 让出标志 */ \
LC_RESUME((pt)->lc) /* 恢复执行位置 */
/**
* 声明 protothread 终点
* \param pt 指向 protothread 控制结构的指针
*/
#define PT_END(pt) \
LC_END((pt)->lc); /* 设置结束标签 */ \
PT_YIELD_FLAG = 0; \
PT_INIT(pt); /* 重置控制结构 */ \
return PT_ENDED; /* 返回结束状态 */ \
}
/** @} */
/**
* \name 阻塞等待
* @{
*/
/**
* 阻塞直到条件为真
* \param pt 控制结构指针
* \param condition 等待条件
*/
#define PT_WAIT_UNTIL(pt, condition) \
do { \
LC_SET((pt)->lc); /* 设置续延点 */ \
if(!(condition)) { /* 条件不满足 */ \
return PT_WAITING; /* 返回等待状态 */ \
} \
} while(0)
/**
* 当条件为真时阻塞
* \param pt 控制结构指针
* \param cond 等待条件
*/
#define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond))
/** @} */
/**
* \name 层级式 protothreads
* @{
*/
/**
* 等待子线程完成
* \param pt 父线程控制结构
* \param thread 子线程调用
*/
#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))
/**
* 创建并等待子线程
* \param pt 父线程控制结构
* \param child 子线程控制结构
* \param thread 子线程调用
*/
#define PT_SPAWN(pt, child, thread) \
do { \
PT_INIT((child)); /* 初始化子线程 */ \
PT_WAIT_THREAD((pt), (thread)); /* 等待完成 */ \
} while(0)
/** @} */
/**
* \name 退出和重启
* @{
*/
/**
* 重启 protothread
* \param pt 控制结构指针
*/
#define PT_RESTART(pt) \
do { \
PT_INIT(pt); /* 重置控制结构 */ \
return PT_WAITING; /* 返回等待状态 */ \
} while(0)
/**
* 退出 protothread
* \param pt 控制结构指针
*/
#define PT_EXIT(pt) \
do { \
PT_INIT(pt); /* 重置控制结构 */ \
return PT_EXITED; /* 返回退出状态 */ \
} while(0)
/** @} */
/**
* \name 调用 protothread
* @{
*/
/**
* 调度 protothread
* \param f protothread 函数调用
*/
#define PT_SCHEDULE(f) ((f) == PT_WAITING) // 检查是否处于等待状态
/** @} */
/**
* \name 让出执行
* @{
*/
/**
* 主动让出 CPU
* \param pt 控制结构指针
*/
#define PT_YIELD(pt) \
do { \
PT_YIELD_FLAG = 0; /* 重置标志 */ \
LC_SET((pt)->lc); /* 设置续延点 */ \
if(PT_YIELD_FLAG == 0) { /* 未设置标志 */ \
return PT_YIELDED; /* 返回让出状态 */ \
} \
} while(0)
/**
* 让出直到条件满足
* \param pt 控制结构指针
* \param cond 等待条件
*/
#define PT_YIELD_UNTIL(pt, cond) \
do { \
PT_YIELD_FLAG = 0; \
LC_SET((pt)->lc); \
if((PT_YIELD_FLAG == 0) || !(cond)) { \
return PT_YIELDED; \
} \
} while(0)
/** @} */
#endif /* __PT_H__ */
/** @} */ // 结束 pt 模块组
2.1.2 利用PT协程实现任务创建与删除
因为PT协程库本身不提供“删除协程”的函数,因此:
创建协程 定义函数 +PT_INIT()
删除协程 使用标志控制 + PT_EXIT()
简单示例:
int task_enabled = 1;
struct pt pt_task;
PT_THREAD(task(struct pt *pt)) {
PT_BEGIN(pt);
while (1) {
if (!task_enabled)
PT_EXIT(pt);
// do something
PT_YIELD(pt);
}
PT_END(pt);
}
// main loop
int main(void)
{
PT_INIT(&pt_task);
while(1)
{
if (task_enabled)
task(&pt_task); // 手动轮询调用
}
}
代码解析:如何创建一个任务:
“创建任务” = 写协程函数 ➜ 准备struct pt ➜ PT_INIT();
• 定义任务函数(必须接受一个状态结构体指针)PT_THREAD(task(struct pt *pt))
• 准备保存任务状态的变量 struct pt pt_task;
• 初始化任务状态,相当于“创建任务” PT_INIT(&pt_task);
• 手动调用任务函数,相当于“调度一次” task(&pt_task);
补充:
PT_THREAD是一个宏,#define PT_THREAD(name_args) char name_args
PT_THREAD(task(struct pt *pt))等价于char task(struct pt *pt) ;pt 是任务的 状态结构体,里面记录了这个协程上次停在哪里(即保存的行号)。
PT_INIT(&pt_task);初始化 pt_task状态结构体;把里面的 pt->lc(line counter,行号)清零;表示这个任务是 “新建/重启” 状态。
task(&pt_task);调用 task()
协程函数,把 pt_task状态结构体传进去,函数内部用 pt->lc
来跳转和恢复运行,相当于手动给这个任务“调度一次”。
task(&pt_task);task是“任务逻辑”(函数),pt_task是“任务状态”(变量),task(&pt_task) 就是 “让这个任务用这块状态运行一次”。
注意,在这个任务的循环中一定要加 PT_YIELD(pt);PT_YIELD会保存当前位置并return,模拟“挂起”;没有 PT_YIELD,这个任务在一次调用中会 无限循环,永不返回;
2.2FreeRTOS的任务创建与删除:
任务创建函数:
xTaskCreate() //动态方式创建任务
BaseType_t xTaskCreate
(
TaskFunction_t pxTaskCode, /* 指向任务函数的指针 */
const char * const pcName, /* 任务名字,最大长度
configMAX_TASK_NAME_LEN */
const configSTACK_DEPTH_TYPE usStackDepth, /* 任务堆栈大小,默认单位
4 字节 */
void * const pvParameters, /* 传递给任务函数的参数
*/
UBaseType_t uxPriority, /* 任务优先级,范围:0 ~
configMAX_PRIORITIES - 1 */
TaskHandle_t * const pxCreatedTask /* 任务句柄,就是任务的任
务控制块 */
)
vTaskDelete() //删除任务
void vTaskDelete(TaskHandle_t xTaskToDelete);
下面一个例程简单介绍了任务的创建与删除:
TaskHandle_t xHandle = NULL;
void vTask1(void *pvParameters)
{
while(1) {
// do something
}
}
void vTask2(void *pvParameters)
{
// 删除任务1
vTaskDelete(xHandle);
vTaskDelete(NULL); // 自己也删除自己
}
int main(void)
{
xTaskCreate(vTask1, "Task1", 100, NULL, 1, &xHandle);
xTaskCreate(vTask2, "Task2", 100, NULL, 1, NULL);
vTaskStartScheduler();
}
工程大致流程:
A[freertos_start] --> B[创建启动任务]
B --> C[启动调度器]
C --> D[执行启动任务]
D --> E[创建任务1/2/3]
E --> F[启动任务自删除]
F --> G[任务并行运行]
注意:“任务配置”必须在创建任务之前完成,创建任务本身使用 xTaskCreate()
,不属于“定义配置”部分。这里以宏定义配置任务是为了让代码更加优雅,最终要为xTaskCreate()服务。
/* 启动任务的配置 */
#define START_TASK_STACK 128 // 启动任务的堆栈大小(单位:字)
#define START_TASK_PRIORITY 1 // 启动任务的优先级
TaskHandle_t start_task_handle; // 启动任务句柄(用于引用任务)
void start_task(void *pvParameters); // 启动任务函数声明
/* 任务1的配置 */
#define TASK1_STACK 128 // 任务1的堆栈大小
#define TASK1_PRIORITY 2 // 任务1的优先级
TaskHandle_t task1_handle; // 任务1句柄
void task1(void *pvParameters); // 任务1函数声明
/* 任务2的配置 */
#define TASK2_STACK 128 // 任务2的堆栈大小
#define TASK2_PRIORITY 3 // 任务2的优先级
TaskHandle_t task2_handle; // 任务2句柄
void task2(void *pvParameters); // 任务2函数声明
/* 任务3的配置 */
#define TASK3_STACK 128 // 任务3的堆栈大小
#define TASK3_PRIORITY 4 // 任务3的优先级
TaskHandle_t task3_handle; // 任务3句柄
void task3(void *pvParameters); // 任务3函数声明
TaskHandle_t本质为一个 void*
指针,typedef void * TaskHandle_t;在xTaskCreate()中创建任务并返回句柄。
2.2.1创建一个启动任务
void freertos_start(void)
{
/* 1.创建一个启动任务 */
xTaskCreate(
(TaskFunction_t)start_task, // 任务函数的地址
(char *)"start_task", // 任务名字(用于调试)
(configSTACK_DEPTH_TYPE)START_TASK_STACK, // 任务堆栈大小
(void *)NULL, // 传递给任务的参数
(UBaseType_t)START_TASK_PRIORITY, // 任务的优先级
(TaskHandle_t *)&start_task_handle // 任务句柄的地址(用于后续操作)
);
/* 2.启动调度器: 会自动创建空闲任务 */
vTaskStartScheduler();
}
只要启动调度器,就会开始启用任务。并自动创建空闲任务。
空闲任务是FreeRTOS自动创建的最低优先级任务,负责系统资源清理和维持系统运行稳定,也可用于低功耗和后台任务处理。
当调用 vTaskDelete()
删除任务时,任务并不会立即释放其内存资源。
系统会立即将该任务状态标记为“删除中”,从调度器中移除,不再调度运行;
但是:资源(堆栈、TCB 等)不会立即释放,会等 空闲任务运行时释放;
实际上,是空闲任务检查是否有需要被清理的任务,并释放其栈空间和TCB(任务控制块)等资源。只有当所有高优先级任务都在等待(阻塞、挂起、延时)时,才会运行空闲任务。
2.2.2创建任务1/2/3
/**
* @description: 启动任务:用来创建其他task
* @param {void} *pvParameters
* @return {*}
*/
void start_task(void *pvParameters)
{
/* 进入临界区: 保护临界区里的代码不会被打断 */
taskENTER_CRITICAL();
/* 创建3个应用任务 */
xTaskCreate(
(TaskFunction_t)task1,
(char *)"task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle
);
xTaskCreate(
(TaskFunction_t)task2,
(char *)"task2",
(configSTACK_DEPTH_TYPE)TASK2_STACK,
(void *)NULL,
(UBaseType_t)TASK2_PRIORITY,
(TaskHandle_t *)&task2_handle
);
xTaskCreate(
(TaskFunction_t)task3,
(char *)"task3",
(configSTACK_DEPTH_TYPE)TASK3_STACK,
(void *)NULL,
(UBaseType_t)TASK3_PRIORITY,
(TaskHandle_t *)&task3_handle
);
/* 启动任务只需要执行一次即可,用完就删除自己 */
vTaskDelete(NULL); // NULL参数表示删除当前任务自身
/* 退出临界区 */
taskEXIT_CRITICAL();
}
注意:这三个任务的优先级分别是task3>task2>task1>start_task,因此我们希望看到的现象是task3任务优先执行,其次是task2,最后是task1,而start_task被我们删除了,所以不会进行,这里涉及到一个很重要的概念,正如代码中所写的:
/* 进入临界区: 保护临界区里的代码不会被打断 */ taskENTER_CRITICAL();
.......
/* 退出临界区 */ taskEXIT_CRITICAL();
若没有临界区,则启动task1任务后,由于task1任务优先级大于start_task任务,因此会一直运行task1任务,而忽略掉task2和task3任务。
2.2.3任务并行运行
void task1(void *pvParameters)
{
while (1) // 任务通常为无限循环
{
printf("task1运行....\r\n");
vTaskDelay(500); // 延迟500ms(需计算tick数)
}
}
/**
* @description: 任务二:实现LED2每500ms闪烁一次
* @param {void} *pvParameters
* @return {*}
*/
void task2(void *pvParameters)
{
while (1)
{
printf("task2运行....\r\n");
vTaskDelay(500);
}
}
/**
* @description: 任务三:判断按键KEY1是否按下,按下则删除task1
* @param {void} *pvParameters
* @return {*}
*/
void task3(void *pvParameters)
{
uint8_t detect = 0;
while (1)
{
printf("task3运行....\r\n");
detect = Detect(); // 检测按键状态
if (detect== 1)
{
/* 判断是否已经删除过,避免重复执行删除 */
if (task1_handle != NULL) // 检查任务句柄是否有效
{
/* key1按下,删除task1 */
printf("执行删除task1....\r\n");
vTaskDelete(task1_handle); // 通过句柄删除任务1
task1_handle = NULL; // 将句柄置NULL防止重复删除
}
}
vTaskDelay(500);
}
}
vTaskDelay(500);FreeRTOS提供的延时函数,当高优先级任务处于阻塞态时,会根据优先级自动搜寻其它任务并执行,因此我们看到的现象是:
task3运行....task2运行....
task1运行....
task3运行....
task2运行....
task1运行....
当某一条件触发,在任务3中通过句柄删除任务1执行时,现象又变成了
task3运行....
task2运行....
task3运行....
task2运行....
注意:
为什么创建任务时参数为(void *pvParameters)?
因为FreeRTOS 规定任务函数的格式必须是:
void TaskFunction(void *pvParameters);
pvParameters
是创建任务时传进去的参数,FreeRTOS 会把它传递给这个任务函数使用;即使你不使用参数,也必须保留这个形式。
eg:
void MyTask(void *pvParameters)
{
int value = *(int *)pvParameters; // 把传进来的指针转成int指针,再解引用
while (1)
{
printf("参数值: %d\n", value);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// 创建任务时传入参数
int myValue = 42;
xTaskCreate(MyTask, "TaskA", 128, &myValue, 1, NULL);
完整源码:
#include "freertos_demo.h"
/* freertos相关的头文件,必须的 */
#include "FreeRTOS.h" // FreeRTOS核心头文件
#include "task.h" // 任务管理API头文件
/* 需要用到的其他头文件 */
#include "LED.h" // 自定义LED控制库
#include "Key.h" // 自定义按键检测库
/* 启动任务的配置 */
#define START_TASK_STACK 128 // 启动任务的堆栈大小(单位:字)
#define START_TASK_PRIORITY 1 // 启动任务的优先级
TaskHandle_t start_task_handle; // 启动任务句柄(用于引用任务)
void start_task(void *pvParameters); // 启动任务函数声明
/* 任务1的配置 */
#define TASK1_STACK 128 // 任务1的堆栈大小
#define TASK1_PRIORITY 2 // 任务1的优先级
TaskHandle_t task1_handle; // 任务1句柄
void task1(void *pvParameters); // 任务1函数声明
/* 任务2的配置 */
#define TASK2_STACK 128 // 任务2的堆栈大小
#define TASK2_PRIORITY 3 // 任务2的优先级
TaskHandle_t task2_handle; // 任务2句柄
void task2(void *pvParameters); // 任务2函数声明
/* 任务3的配置 */
#define TASK3_STACK 128 // 任务3的堆栈大小
#define TASK3_PRIORITY 4 // 任务3的优先级
TaskHandle_t task3_handle; // 任务3句柄
void task3(void *pvParameters); // 任务3函数声明
/**
* @description: 启动FreeRTOS
* @return {*}
*/
void freertos_start(void)
{
/* 1.创建一个启动任务 */
xTaskCreate(
(TaskFunction_t)start_task, // 任务函数的地址
(char *)"start_task", // 任务名字(用于调试)
(configSTACK_DEPTH_TYPE)START_TASK_STACK, // 任务堆栈大小
(void *)NULL, // 传递给任务的参数
(UBaseType_t)START_TASK_PRIORITY, // 任务的优先级
(TaskHandle_t *)&start_task_handle // 任务句柄的地址(用于后续操作)
);
/* 2.启动调度器: 会自动创建空闲任务 */
vTaskStartScheduler(); // 注意:原代码写的是vTaskScheduler(),实际应为vTaskStartScheduler()
}
/**
* @description: 启动任务:用来创建其他task
* @param {void} *pvParameters
* @return {*}
*/
void start_task(void *pvParameters)
{
/* 进入临界区: 保护临界区里的代码不会被打断 */
taskENTER_CRITICAL();
/* 创建3个应用任务 */
xTaskCreate(
(TaskFunction_t)task1,
(char *)"task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle
);
xTaskCreate(
(TaskFunction_t)task2,
(char *)"task2",
(configSTACK_DEPTH_TYPE)TASK2_STACK,
(void *)NULL,
(UBaseType_t)TASK2_PRIORITY,
(TaskHandle_t *)&task2_handle
);
xTaskCreate(
(TaskFunction_t)task3,
(char *)"task3",
(configSTACK_DEPTH_TYPE)TASK3_STACK,
(void *)NULL,
(UBaseType_t)TASK3_PRIORITY,
(TaskHandle_t *)&task3_handle
);
/* 启动任务只需要执行一次即可,用完就删除自己 */
vTaskDelete(NULL); // NULL参数表示删除当前任务自身
/* 退出临界区 */
taskEXIT_CRITICAL();
}
/**
* @description: 任务一:实现LED1每500ms闪烁一次
* @param {void} *pvParameters
* @return {*}
*/
void task1(void *pvParameters)
{
while (1) // 任务通常为无限循环
{
printf("task1运行....\r\n");
LED_Toggle(LED1_Pin); // 切换LED1状态
vTaskDelay(500 / portTICK_PERIOD_MS); // 延迟500ms(需计算tick数)
}
}
/**
* @description: 任务二:实现LED2每500ms闪烁一次
* @param {void} *pvParameters
* @return {*}
*/
void task2(void *pvParameters)
{
while (1)
{
printf("task2运行....\r\n");
LED_Toggle(LED2_Pin); // 切换LED2状态
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
/**
* @description: 任务三:判断按键KEY1是否按下,按下则删除task1
* @param {void} *pvParameters
* @return {*}
*/
void task3(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
printf("task3运行....\r\n");
key = Key_Detect(); // 检测按键状态
if (key == KEY1_PRESS)
{
/* 判断是否已经删除过,避免重复执行删除 */
if (task1_handle != NULL) // 检查任务句柄是否有效
{
/* key1按下,删除task1 */
printf("执行删除task1....\r\n");
vTaskDelete(task1_handle); // 通过句柄删除任务1
task1_handle = NULL; // 将句柄置NULL防止重复删除
}
}
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
三、PT协程与FreeRTOS任务挂起与恢复
3.1 PT协程任务挂起与恢复
PT协程库任务挂起运行并子线程:
#define PT_SPAWN(pt, child, thread) \
do { \
PT_INIT((child)); \
PT_WAIT_THREAD((pt), (thread)); \
} while(0)
意思是在父任务(PT)中,创建并运行一个“子协程” thread
,使用 child
保存它的状态,直到这个子协程运行“结束(PT_END/return PT_ENDED)”,父任务才继续往下执行。
参数名 | 代表什么 | 示例值 | 解释 |
---|---|---|---|
pt |
父协程的状态 | pt |
当前这个主协程自己 |
child |
子协程的状态控制块 | &pt_task1 |
保存子任务的运行位置(即子协程自己的状态) |
thread |
子协程的函数调用 | task1(&pt_task1) |
真正运行子任务,用的是上面这个状态 |
示例:
static struct pt mainPt, subPt;
static int counter = 0;
// 子协程函数
static PT_THREAD(SubPt(struct pt *pt)) {
PT_BEGIN(pt);
for (int i = 0; i < 3; i++) {
printf("SubPt: 执行第 %d 次任务\n", i + 1);
// 模拟等待(这里用条件等价于延迟)
counter = 0;
PT_WAIT_UNTIL(pt, counter++ > 100000); // 假设延迟
}
printf("SubPt: 执行完成,返回主协程\n");
PT_END(pt);
}
// 主协程函数
static PT_THREAD(MainPt(struct pt *pt)) {
PT_BEGIN(pt);
printf("MainPt: 启动,调用子协程...\n");
// 挂起直到子协程执行完成
PT_SPAWN(pt, &subPt, SubPt(&subPt));
printf("MainPt: 子协程执行完成,继续执行主协程的后续逻辑\n");
PT_END(pt);
}
int main() {
PT_INIT(&mainPt);
PT_INIT(&subPt);
while (PT_SCHEDULE(MainPt(&mainPt))) {
// 主协程调度执行
}
printf("所有协程执行完毕\n");
return 0;
}
输出结果:
MainPt: 启动,调用子协程...
SubPt: 执行第 1 次任务
SubPt: 执行第 2 次任务
SubPt: 执行第 3 次任务
SubPt: 执行完成,返回主协程
MainPt: 子协程执行完成,继续执行主协程的后续逻辑
所有协程执行完毕
咦,为什么退出这个while循环了呢?
首先我们来看PT_SCHEDULE()宏,定义如下:
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
只要 f() 的返回值是 PT_WAITING 或 PT_ENDED(即 < PT_EXITED),就继续执行;
一旦返回值是 PT_EXITED,调度就终止(不再调用这个协程)。
我们再来看PT_END():
#define PT_END(pt) \
do { \
(pt)->lc = 0; \
return PT_ENDED; \
} while (0)
PT_END() 会让协程返回 PT_ENDED
所以 MainPt() 在执行完后会返回 PT_ENDED
#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED 2
#define PT_ENDED 3
因此程序可以说只运行了一次就退出了。
3.2 FreeRTOS任务挂起与恢复
3.2.1API函数介绍:
1. vTaskSuspend(TaskHandle_t xTaskToSuspend)
作用:挂起一个任务,使其停止运行,不再被调度。参数:
xTaskToSuspend:要挂起的任务句柄。如果传入 NULL,表示挂起当前任务。
注意事项:
被挂起的任务无法自动恢复,只能通过 vTaskResume() 或 xTaskResumeFromISR() 进行恢复。
2. vTaskResume(TaskHandle_t xTaskToResume)
作用:恢复被 vTaskSuspend() 挂起的任务,使其重新进入就绪态。参数:
xTaskToResume:要恢复的任务句柄。
注意事项:
如果任务没有被挂起,调用 vTaskResume() 无效。
如果优先级高于当前任务,恢复后可能立即抢占执行。
3. vTaskSuspendAll(void)
作用:挂起任务调度器,防止上下文切换。用法场景:
临界区保护:在不希望被打断的代码段中使用。
特点:
不会屏蔽中断(ISR 仍会运行)。
多次调用需配对调用 xTaskResumeAll() 才会真正恢复。
4. xTaskResumeAll(void)
作用:恢复调度器,重新允许任务调度。返回值:
pdTRUE:恢复后发生了上下文切换(有高优先级任务就绪)。
pdFALSE:没有任务就绪,不需要上下文切换。
注意事项:
与 vTaskSuspendAll() 配套使用。
调用后会一次性处理挂起期间产生的任务切换。
3.2.2简单示例:
调度器挂起与恢复(临界区保护):
int sharedVar = 0;
void CriticalTask(void *pvParameters)
{
while (1)
{
vTaskSuspendAll(); // 暂停调度器(防止任务切换)
// 临界区操作(不会被其他任务打断)
sharedVar++;
printf("sharedVar = %d\n", sharedVar);
xTaskResumeAll(); // 恢复调度器
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
单个任务挂起与恢复(任务控制):
#include "FreeRTOS.h"
#include "task.h"
#include "stdio.h"
TaskHandle_t Task2Handle;
void Task1(void *pvParameters)
{
while (1)
{
printf("Task1: 准备挂起 Task2\n");
vTaskSuspend(Task2Handle); // 挂起 Task2
printf("Task1: 已挂起 Task2,等待 3 秒...\n");
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 3 秒
printf("Task1: 恢复 Task2\n");
vTaskResume(Task2Handle); // 恢复 Task2
vTaskDelay(pdMS_TO_TICKS(2000)); // 再等 2 秒后重复
}
}
void Task2(void *pvParameters)
{
while (1)
{
printf("Task2: 正在运行...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 每秒打印一次
}
}
int main(void)
{
xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", 128, NULL, 1, &Task2Handle);
vTaskStartScheduler(); // 启动调度器
while (1); // 不会执行到这里
}
输出示意:
Task1: 准备挂起 Task2
Task1: 已挂起 Task2,等待 3 秒...
(等待3秒后)
Task1: 恢复 Task2
Task2: 正在运行... // 恢复后立即执行
(等待1秒后)
Task2: 正在运行... // 第2次执行
Task1: 准备挂起 Task2
Task1: 已挂起 Task2,等待 3 秒...
(等待3秒后)
Task1: 恢复 Task2
Task2: 正在运行... // 恢复后立即执行
(等待1秒后)
Task2: 正在运行... // 第2次执行
Task1: 准备挂起 Task2
Task1: 已挂起 Task2,等待 3 秒...
...(后续循环重复)
3.2.3FreeRTOS任务状态:
FreeRTOS 中任务共存在 4 种状态:
运行态:当任务实际执行时,它被称为处于运行状态。如果运行 RTOS 的处理器
只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。注意在 STM32
中,同一时间仅一个任务处于运行态。
就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前
没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。阻塞态:如果任务当前正在等待延时或外部事件,则该任务被认为处于阻塞状态。
挂起态:类似暂停,调用函数 vTaskSuspend() 进入挂起态,需要调用解挂函数
vTaskResume()才可以进入就绪态
只有就绪态可转变成运行态,其他状态的任务想运行,必须先转变成就绪态。转换关
系如下:
四、时间片调度
4.1什么是时间片调度?
FreeRTOS 默认使用固定优先级的抢占式调度策略,对同等优先级的任务执行时间片轮询调度:在相同优先级的任务之间,FreeRTOS 采用时间片轮转策略。每个任务执行一个时间片,如果有其他同优先级的任务等待执行,则切换到下一个任务.
PT(Protothread)协程本身 没有时间片调度 的机制,由主循环顺序调度多个协程,轮转的周期时长取决于调度循环一圈所花的时间。
假设有三个小朋友轮流玩一个游戏机,每人玩 5 分钟:
A 玩 5 分钟 → B 玩 5 分钟 → C 玩 5 分钟 → A 玩下一轮 → ...
这就像时间片调度,每个任务(小朋友)都有一个时间片(5 分钟)。
4.2 PT协程模拟时间片轮转调度
PT(Protothread)协程本身 没有时间片调度 的机制,它和 FreeRTOS支持“抢占式多任务”和“时间片轮转调度” 不一样。但是!我们用 PT 实现“时间片轮转”的感觉,
代码示例:
static struct pt pt1, pt2, pt3;
/* ---------- 任务 1 ---------- */
PT_THREAD(task1(struct pt *pt))
{
PT_BEGIN(pt);
while (1) {
printf("task1 running...\n");
PT_YIELD(pt); /* 让出执行权,下一轮再继续 */
}
PT_END(pt);
}
/* ---------- 任务 2 ---------- */
PT_THREAD(task2(struct pt *pt))
{
PT_BEGIN(pt);
while (1) {
printf("task2 running...\n");
PT_YIELD(pt);
}
PT_END(pt);
}
/* ---------- 任务 3 ---------- */
PT_THREAD(task3(struct pt *pt))
{
PT_BEGIN(pt);
while (1) {
printf("task3 running...\n");
PT_YIELD(pt);
}
PT_END(pt);
}
/* ---------- 主函数 ---------- */
int main(void)
{
/* 初始化线程控制块 */
PT_INIT(&pt1);
PT_INIT(&pt2);
PT_INIT(&pt3);
/* 轮询调度:依次调度三个协程 */
while (1) {
PT_SCHEDULE(task1(&pt1));
PT_SCHEDULE(task2(&pt2));
PT_SCHEDULE(task3(&pt3));
/* 这里没有真正的“时间片”,
但每个协程让出一次 CPU,就形成类似轮转的效果。*/
}
return 0;
}
这里的 PT_YIELD(pt); 就是“让出 CPU” 的关键,相当于时间片用完了自动跳出,让主循环去跑下一个任务。
4.3 FreeRTOS实现时间片调度
4.3.1API函数介绍:
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
TickType_t xTimeIncrement );
简单来说,vTaskDelayUntil(&xLastWakeTime, xDelay),将任务挂起,直到指定的唤醒时间。
参数 | 含义 |
---|---|
pxPreviousWakeTime | 指向上一次唤醒时间的变量,一般叫 xLastWakeTime;第一次调用前要先用xTaskGetTickCount() 初始化。 |
xTimeIncrement | 要等待的固定时间间隔(tick)。 |
工作机制
计算“下次应该被唤醒的绝对时间” =*pxPreviousWakeTime + xTimeIncrement。
如果现在的系统节拍还没到这个时间,就让任务 阻塞;到了就立即返回。
返回之前把*pxPreviousWakeTime 更新成这次的绝对唤醒时间。
-
周期性精准:不管循环里其他代码花了多久,只要不超过
xTimeIncrement
,每一轮总周期都恒定,不会累积漂移。 -
和 vTaskDelay 区别
vTaskDelay vTaskDelayUntil 等待“相对当前时刻”的时间 等待“相对于上次周期起点”的时间 可能因代码执行耗时而漂移 保证硬周期,适合采样、控制回路
4.3.2创建一个FreeRTOS实现时间片调度例程:
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// 任务函数声明
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);
// 任务堆栈大小
#define TASK_STACK_SIZE 2048
// 任务优先级(相同优先级触发时间片调度)
#define TASK_PRIORITY 2
// 时间片长度(毫秒)
#define TIME_SLICE_MS 500
void app_main() {
// 创建两个相同优先级的任务
xTaskCreate(vTask1, "Task1", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL);
xTaskCreate(vTask2, "Task2", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL);
}
// 任务1实现
void vTask1(void *pvParameters) {
TickType_t xLastWakeTime;
const TickType_t xDelay = pdMS_TO_TICKS(TIME_SLICE_MS);
xLastWakeTime = xTaskGetTickCount();
for (;;) {
// 打印任务1运行信息
printf("Task1 running on core %d\n", xPortGetCoreID());
// 精确延迟到下个时间片开始
vTaskDelayUntil(&xLastWakeTime, xDelay);
}
}
// 任务2实现
void vTask2(void *pvParameters) {
TickType_t xLastWakeTime;
const TickType_t xDelay = pdMS_TO_TICKS(TIME_SLICE_MS);
xLastWakeTime = xTaskGetTickCount();
for (;;) {
// 打印任务2运行信息
printf("Task2 running on core %d\n", xPortGetCoreID());
// 精确延迟到下个时间片开始
vTaskDelayUntil(&xLastWakeTime, xDelay);
}
}
代码分析:
当多个任务优先级相同时,FreeRTOS自动启用时间片轮转
每个时间片长度 = 1个系统节拍(tick)
本例中 configTICK_RATE_HZ=1000→ 时间片=1ms
结果:
时序
- 时间 0 毫秒:
- Task1 运行,打印 "Task1 running on core 0",然后挂起至 500 毫秒。
- Task2 运行,打印 "Task2 running on core 0",然后挂起至 500 毫秒。
- 时间 500 毫秒:
- Task1 运行,打印 "Task1 running on core 0",然后挂起至 1000 毫秒。
- Task2 运行,打印 "Task2 running on core 0",然后挂起至 1000 毫秒。
- 时间 1000 毫秒:
- 重复上述过程。
每次周期中,Task1 和 Task2 的打印几乎是连续的(间隔仅为任务切换和打印执行的时间,通常远小于 1 毫秒),但整个周期以 500 毫秒为间隔重复。
五、任务通知
任务通知是 FreeRTOS 中一种用于任务间通信的机制,它允许一个任务向其他任务发
送简单的通知或信号,以实现任务间的同步和协作。向任务发送“任务通知” 会将目标任务通知设为“挂起”状态。 正如任务可以阻塞中间对象 (如等待信号量可用的信号量),任务也可以阻塞任务通知, 以等待通知状态变为“挂起”。
5.1 PT协程模拟任务通知
Protothread(PT 协程) 中实现“任务通知”或“等待某个条件完成后再执行”的功能,其实它本质上就是用一个条件变量来模拟通知,配合 PT_WAIT_UNTIL() 来完成。
PT_WAIT_UNTIL(pt, condition);
pt:协程的状态结构体指针,通常就是 struct pt *pt
condition:要等待的条件表达式
用来挂起当前协程,直到某个条件成立为止。这相当于一种非阻塞的等待机制,协程会在每次调度器调用它时检查条件是否满足。
代码示例:
#include "pt.h"
static struct pt pt_taskA;
volatile int notified = 0; // 用作“任务通知”标志位
PT_THREAD(TaskA(struct pt *pt)) {
PT_BEGIN(pt);
while (1) {
// 等待 notified 被设置为 1,相当于任务被通知
PT_WAIT_UNTIL(pt, notified == 1);
// 被“通知”后执行的任务逻辑
DoSomething();
// 重置标志,准备下一次等待
notified = 0;
}
PT_END(pt);
}
// 在某处调用这个函数,模拟通知 TaskA
void NotifyTaskA() {
notified = 1;
}
notified 相当于“任务通知槽”,谁想通知就写 notified = 1;
TaskA 就会在 PT_WAIT_UNTIL() 中发现条件满足,继续执行;
完成后重置 notified = 0,继续等待下一次通知。
5.2 FreeRTOS实现任务通知
5.2.1 API函数介绍:
ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait)
- 功能:使当前任务阻塞,等待通知到达。
- 参数:
- xClearCountOnExit:若为pdTRUE,在退出时清除通知值;若为pdFALSE,在退出时将通知值减1。
- xTicksToWait:等待通知的最长时间(以tick为单位,例如portMAX_DELAY表示无限等待)。
- 返回值:如果在超时时间内接收到通知,返回通知值;否则返回0。
- 用途:用于实现等待条件成立的阻塞机制。
xTaskNotifyGive(TaskHandle_t xTaskToNotify)
- 功能:从任务中发送通知,增加目标任务的通知值。
- 参数:
- xTaskToNotify:目标任务的句柄(通过xTaskCreate创建任务时获取)。
- 用途:在条件满足时通知等待的任务。
5.2.2代码示例:
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
volatile uint32_t condition = 0;
TaskHandle_t xTaskToNotify;
// 等待任务
void vTaskFunction(void *pvParameters) {
uart_printf("[WaitTask] 启动,等待condition变为1\r\n");
for (;;) {
while (condition == 0) {
printf("[WaitTask] 进入等待状态...\r\n");
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
// condition为真时的后续逻辑
printf("[WaitTask] 检测到condition=1,开始处理\r\n");
vTaskDelay(pdMS_TO_TICKS(100));
// 执行具体操作
static int counter = 0;
printf("[WaitTask] 正在处理数据#%d...\r\n", ++counter);
vTaskDelay(pdMS_TO_TICKS(50));
// 重置条件以便下次等待
condition = 0;
printf("[WaitTask] 处理完成,重置condition=0\r\n\r\n");
}
}
// 发送通知的任务
void vOtherTask(void *pvParameters) {
printf("[NotifyTask] 启动,周期设置condition=1\r\n");
for (;;) {
vTaskDelay(pdMS_TO_TICKS(500));
// 设置条件变量
condition = 1;
printf("[NotifyTask] 设置condition=1,发送通知\r\n");
// 发送通知给等待任务
if (xTaskToNotify != NULL) {
xTaskNotifyGive(xTaskToNotify);
}
vTaskDelay(pdMS_TO_TICKS(100)); // 模拟其他处理
}
}
int main(void) {
// 创建任务
xTaskCreate(vTaskFunction, "WaitTask", 1000, NULL, 2, &xTaskToNotify);
xTaskCreate(vOtherTask, "NotifyTask", 1000, NULL, 1, NULL);
vTaskStartScheduler();
for(;;);
return 0;
}
代码说明:
- 等待任务(vTaskFunction):
- 使用while (condition == 0)循环检查条件。
- 调用ulTaskNotifyTake(pdTRUE, portMAX_DELAY)等待通知:
- pdTRUE表示在接收到通知后清除通知值。
- portMAX_DELAY表示无限等待,直到通知到达。
- 当条件为真时,跳出循环,执行后续代码。
- 发送通知任务(vOtherTask):
- 当condition变为真时,调用xTaskNotifyGive(xTaskToNotify)发送通知。
- xTaskToNotify是等待任务的句柄,在创建任务时获取。
- 任务句柄:
- 通过xTaskCreate创建vTaskFunction任务时,将句柄保存到全局变量xTaskToNotify,以便其他任务可以通知它。
运行结果:
时间(ms) | 事件
---------------|----------------------------------
0 | [系统启动]
0 | [WaitTask] 启动,等待condition变为1
0 | [WaitTask] 进入等待状态...
0 | [NotifyTask] 启动,周期设置condition=1
0-500 | [NotifyTask] 延时500ms (阻塞)
0-500 | [WaitTask] 阻塞等待通知500 | [NotifyTask] 设置condition=1,发送通知
500 | [WaitTask] 被唤醒,检测到condition=1
500-600 | [WaitTask] 延时100ms (阻塞)
500-600 | [NotifyTask] 延时100ms (阻塞)600 | [WaitTask] 正在处理数据#1...
600-650 | [WaitTask] 延时50ms (阻塞)
600-1100 | [NotifyTask] 进入500ms主延时 (阻塞)650 | [WaitTask] 处理完成,重置condition=0
650 | [WaitTask] 进入等待状态...
650-1100 | [WaitTask] 阻塞等待通知1100 | [NotifyTask] 设置condition=1,发送通知
1100 | [WaitTask] 被唤醒,检测到condition=1
1100-1200 | [WaitTask] 延时100ms (阻塞)
1100-1200 | [NotifyTask] 延时100ms (阻塞)1200 | [WaitTask] 正在处理数据#2...
1200-1250 | [WaitTask] 延时50ms (阻塞)
1200-1700 | [NotifyTask] 进入500ms主延时 (阻塞)1250 | [WaitTask] 处理完成,重置condition=0
1250 | [WaitTask] 进入等待状态...[循环继续,周期600ms]
更多推荐
所有评论(0)