就好比是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)。

工作机制

  1. 计算“下次应该被唤醒的绝对时间” =*pxPreviousWakeTime + xTimeIncrement。

  2. 如果现在的系统节拍还没到这个时间,就让任务 阻塞;到了就立即返回。

  3. 返回之前把*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]

 

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐