从FreeRTOS的Task API出发
前言
笔者注意到,不少教程是真的直接上来就告诉你FreeRTOS的架构是如何的,库库的告诉你要理解什么是 RTOS、FreeRTOS 在嵌入式的作用与应用场景;环境准备。但是实际上,如果我们只是先把宏观的知识塞到期盼学习FreeRTOS的嵌入式人,恐怕只会学的有些难受(尽管后面显然会意识到FreeRTOS到底适合在哪些场景)
所以这篇博客压根不打算跟你起手扯大的,而是带你先体验一圈RTOS的编程范式(或者更加通俗的说——如果你使用RTOS组织你的工程,你的代码看起来像是什么样子的)。这里,我们就能带出来基本的任务相关的API(注意笔者不计划在这片博客疯狂讲RTOS的原理,不在这里讲)
从前后台程序到 RTOS 任务模型
笔者认为,在真正学习 Task API 之前,先建立正确的编程范式认知非常重要。
在最传统的无RTOS的单片机上,也就是在绝大多数 8 位 / 32 位单片机裸机工程中,程序结构通常是:
intmain(void){init();// <- 一部分硬件初始化的工作while(1){TaskA();TaskB();TaskC();}}同时辅以若干中断:
voidSysTick_Handler(void){tick++;}这种大家最开始学习单片机程序的时候,编写的程序就是满足这样的前后台程序架构的,你可以看到,后台就在默默的轮询执行任务,前台则是招待板上硬件的中断,打断我们的执行处理相关的业务代码,当然,中断内一般咱们不会去执行长期的阻塞任务防止单片机挂死,一般都是直接设置标志位。所以简单的说:
- 前台(Foreground):中断服务程序 ISR
- 后台(Background):
main()中的while(1)轮询逻辑
这种前后台程序架构比较直接,我相信大部分嵌入式工程师,对于身兼数职(又是画板子又是写程序又是测试又是拉客户的)的同志,这种结构简单,极易上手,代码出问题了有时候一下子执行逻辑是如何触发这个错误的时序图就蹦出来了(也就是代码可控、可预测),最重要的是——有时候使用的单片机就没法跑啥东西的时候,其实只能这样编写了。
但是慢慢伴随业务的扩大,如何协调这些具体的业务代码本身就构成了一个令人头疼的难题了。举个例子,我们假设真的有突发很重要的任务需要执行(比如说用户触发了按钮需要在漫长的后台轮询任务中打断执行流),但是在没有RTOS的项目中这个事情就比较难搞了——毕竟这种轮询都是我让你 → 你让他 → 他再让回来,如果我们每个函数都“没有分寸感”(这里说的分寸感是指——你不得不让一些函数要有速度执行的下限,也就是不能太慢;或者是必须异步;或者是必须时时刻刻的判断其他任务是否触发的变量),系统就直接整体失效了(比如说用户按了半天按钮才轮循到检查Flag变量的部分,这个体验有时候会很致命)
这种根源就是执行的任务代码是不可以被打断的,或者说,在处理器看来,您给定的所有代码的“重要性”是一样的。这种重要性就是我们后面要谈到的——任务优先级了。
在过去的范式里,我们的代码必须仔细可控的标称函数的行为,不可以过多的越过一些条件(不可以太慢,不可以同步阻塞,随时都可以返回),但是如果我们的业务迭代有些迅速,那么在这样小心翼翼的写代码只会让自己埋没在代码的修改中(改了这个挂了那个)。对于这种复杂的业务场景(这就是笔者不赞同点个灯也要RTOS,除非是教学需求),上RTOS就是合适的。
RTOS 的 Task 编程范式
FreeRTOS引入的核心变化是从“我主动让出 CPU” → “由内核决定谁运行”,意味着我们的代码稍微有了一点点不确定性。但是换来的就是巨大的代码清晰和可维护性,以及最重要的,我们的任务终于有了主次而不是在前后台中成为扁平的一条长链。
在RTOS 中,每一个Task就是我们熟悉的独立执行流,是的,就像你单独为点灯写了一个Main函数,单独为驱动OLED写了一个Main函数一样,现在我们放到一个Task函数放心的while(1)而不用担心程序走不出去了。在这里,调度不是由被执行的代码决定而是由内核调度器决定
很快我们就要写这样的代码了:
voidTaskA(void*param){while(1){DoSomethingA();vTaskDelay(pdMS_TO_TICKS(100));}}💡 这里的while(1)是被内核托管的无限循环,而不是裸奔循环。所以放心大胆的使用
两种模型的本质对比
说了很多,看看这个表格吧:
| 维度 | 前后台程序 | FreeRTOS Task |
|---|---|---|
| 并发模型 | 顺序轮询 | 抢占式调度 |
| 延时方式 | 软件计数 / 阻塞 | 内核阻塞 |
| 时序管理 | 人工维护 | 内核维护 |
| 任务重要性 | 隐式 | 显式(优先级) |
| 可扩展性 | 差 | 强 |
| 可读性 | 依赖经验 | 贴近业务 |
GPT的总结很好——裸机是“人脑调度”,RTOS 是“内核调度”。
快点端上来罢!我等不及了
正常到这里,笔者看到不少的文章和教程就要开始跟你念经——什么是TCB,什么是任务优先级,什么是调度,乱七八糟的东西就来了,但是没有必要。
FreeRTOS在任务创建的时候就写过很长一串注释,这里的注释里的代码,笔者拿过来了:
// 任务函数的标准原型voidvTaskCode(void*pvParameters);// 示例:一个 LED 闪烁任务voidvLedTask(void*pvParameters){// 任务的局部初始化代码// ...vLedInit();// 如果我们打算在任务里初始化// 任务的主体是一个无限循环for(;;){// 1. 执行任务的实际工作// 比如:切换 LED 状态Toggle_LED();// 2. 主动让出 CPU 或进入阻塞状态// 比如:延时 500msvTaskDelay(pdMS_TO_TICKS(500));}// 任务理论上不应该退出 for(;;) 循环。// 如果需要退出,应使用 vTaskDelete(NULL)。}笔者在之前的教程中就谈到了如何在上位机上模拟RTOS:
#include"FreeRTOS.h"#include"task.h"#include<stdio.h>voidTask1(void*pv){for(;;){printf("Task1 running\n");vTaskDelay(pdMS_TO_TICKS(1000));}}voidTask2(void*args_pv){for(;;){printf("Task2 running\n");vTaskDelay(pdMS_TO_TICKS(3000));}}intmain(void){xTaskCreate(Task1,"t1",configMINIMAL_STACK_SIZE,NULL,1,NULL);xTaskCreate(Task2,"Task2",configMINIMAL_STACK_SIZE,NULL,1,NULL);vTaskStartScheduler();return0;}不过也没事,如果你的单片机有移植printf,那太好了,直接CV就能用。
笔者之前的RTOS上位机模拟教程和配置教程:
- 在上位机上熟悉FreeRTOS API-CSDN博客
- 基于STM32F407ZGT6的硬件平台,(可选CubeMX) + PlatformIO软件开发的FreeRTOS部署指南_platformio freertos-CSDN博客
- (1 封私信) 在上位机上熟悉FreeRTOS API——环境配置 - 知乎
- (1 封私信) 基于STM32F407ZGT6的硬件平台,(可选CubeMX) + PlatformIO软件开发的FreeRTOS部署指南 - 知乎