跳到主要内容

第五章 协作式系统与FreeRTOS

上一章流水灯和按键状态机的“非阻塞编程”实验,可以让多个任务在单核MCU上“齐头并进”。但这好比一位开发者同时负责多个项目,难免会手忙脚乱,担心某个任务被“饿死”。有没有一种方法,找一个可靠的“项目负责人”,自动、合理地分配CPU时间,确保每个任务都能按时完成?这就是本章的主角——FreeRTOS(Free Real-Time Operating System)!作为最流行的嵌入式RTOS之一,它将带领用户从“逻辑轮询”的原始社会,步入“多任务调度”的现代操作系统时代。

5.1协作式系统原型

协作式系统(Cooperative System)是一种任务调度机制,其核心特征为:

  1. 自主挂起:每个任务主动判断执行条件,自行决定是否让出CPU资源;
  2. 无优先级调度:所有任务具有平等执行权,不依赖优先级队列;
  3. 非抢占机制:运行中的任务持续占用CPU,直至主动释放控制权。 通过自行构建建议协作式操作系统的方式,来深入理解这一概念。首先第一步,将系统定时器中断周期从20ms缩短至1ms,提供更精细的时间片粒度(1ms的基准单位),支持任务以毫秒级精度设置执行周期,增强多任务调度的时效性控制。通过CubeMX将定时器定时器周期从20ms改为1ms,如图5-1所示。

图5-1 配置定时器定时周期

新建一个task.c和task.h文件,添加到工程中去,如图5-2所示。

图5-2 新建任务程序文件 首先,在task.h头文件中设计任务管理模块: 1、定义任务控制块(Tcb) 构建结构体Tcb(Task Control Bank)包含三个成员: 1)函数的指针:指向任务的可执行函数; 2)周期时长(period):任务的理论执行实践(单位:毫秒) 3)时间戳(remainingTime):动态更新的任务剩余执行倒计时。

typedef struct {
void (*function)(void); // 任务函数指针
uint32_t period; // 执行周期(ms)
uint32_t remainingTime; // 剩余时间计数器
} Tcb;

2、构建任务管理器(TcbList) 用结构体封装任务列表和状态跟踪:

  1. 任务计数器(taskCount):实时记录当前系统中已注册有效任务数量。
  2. 任务数组:固定容量(10个Tcb槽位),存储用户注册的任务。
#define TICK_TASK_MAX_NUM  10  // 最大任务容量

typedef struct {
uint8_t taskCount; // 当前已注册任务数(0 ≤ taskCount ≤ MAX_TASKS)
Tcb infoTask[TICK_TASK_MAX_NUM];; // 任务存储池
} TcbList;

整个task.h文件如下代码所示。


#ifndef __TASK_H
#define __TASK_H
#include "main.h"
#define TICK_TASK_MAX_NUM 10 //定时任务最大数量

typedef struct {
void (*fuction)(void);//函数指针
uint32_t period;//周期时长
uint32_t remainingTime;//时间戳
}Tcb;//任务控制块
typedef struct {
uint8_t taskCount;//指示列表占用情况
Tcb infoTask[TICK_TASK_MAX_NUM];//存储任务信息
}TcbList;//任务控制块列表

//声明函数
uint8_t createLoopTask(void (*task)(void),uint32_t period);
void scanTask(void);
void initTask(void);

#endif

在task.c文件中: 1.初始化:创建taskList(基于TcbList)存储任务信息。 2.createTask函数:添加任务

1)输入:函数地址,运行周期 2)检查任务数未超限 3)保存函数地址、周期、当前时间戳这三项到列表中 4)任务技术加1 3.scanTask函数:调度函数,这个函数的核心机制是通过时间戳差检测周期任务,超时则执行。 1)主循环调用,检查各任务是否超时 2)判断:(系统时间mySysTick-任务时间戳)是否大于任务周期 3)若超时:执行任务函数,并更新时间戳 task.c代码如下所示。

#include "task.h"
#include <string.h>

volatile uint32_t mySysTick = 0,mySysTicklast=0;
TcbList taskList;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6){
mySysTick += 1;//中断服务函数只有变量+1操作
}
}

uint8_t createLoopTask(void (*task)(void),uint32_t period)
{
if(taskList.taskCount+1>=TICK_TASK_MAX_NUM){//超出最大任务量
return 0;//创建任务失败
}
taskList.infoTask[taskList.taskCount].fuction=task;//写入函数指针
taskList.infoTask[taskList.taskCount].period=period;//写入周期时长
taskList.infoTask[taskList.taskCount].remainingTime=mySysTick;//记录时刻
taskList.taskCount+=1;//任务总量+1
return 1;
}

void scanTask(void)
{
uint16_t i=0;
if(mySysTick-mySysTicklast>=1)//时钟滴答
{
for(;i<taskList.taskCount;i++){
if(mySysTick-taskList.infoTask[i].remainingTime>=taskList.infoTask[i].period){
taskList.infoTask[i].fuction();//执行任务函数
taskList.infoTask[i].remainingTime=mySysTick;//更新时间戳
}
}
mySysTicklast=mySysTick;
}
}

void initTask(void)
{
taskList.taskCount = 0;
memset(taskList.infoTask, 0x00, sizeof(taskList.infoTask));
}

在main.c文件中,首先进行初始化,而后将三个任务用createLoopTask函数添加到任务中去,打开定时器,在while(1)主循环中扫描任务,如下所示。


/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "Key.h"
#include "LEDBUZ.h"
#include "task.h"

/* USER CODE END Includes */
int main()
{
/* USER CODE BEGIN 2 */
keyInit();
initTask();//初始化
createLoopTask(keyScan,KEY_Period);//
createLoopTask(keyAction,KEY_Period);
createLoopTask(LED_Running2,LED_Period);//任务创建
HAL_TIM_Base_Start_IT(&htim6);//开启定时器
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
scanTask();//任务切换
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
}

通过上述实现,完成了一个轻量级时间片轮转协作式系统。该框架在实际开发中具备较高实时性,核心优势如下:

1.高扩展性,任务创建灵活便捷; 2.资源高效,系统开销小,内存占用少,可以适配资源受限的MCU; 3.低耦合架构,与底层驱动解耦,便于集成。

但是这种轻量级时间片轮转协作式系统对于RTOS来说还有一定的局限性: 1.功能精简,缺乏高级调度机制; 2.驱动需遵循协作式开发规范; 3.禁止阻塞式代码; 4.需采用状态机模式开发,开发复杂度高。

5.2状态机到RTOS的思维转变

在单核CPU上实现多任务时,虽然能从逻辑上划分出多个独立的任务循环,但是CPU无法真正同时运行它们。为此,开发者通常需要将每个循环任务拆解为“基于状态机的非阻塞片段”,通过条件判断在不同片段间切换,从而模拟并发执行。

回顾上一章的按键扫描程序,其本质就是将完整的按键处理流程分解为“检测”、“消抖”、“确认”等离散状态,并通过状态迁移取代单一的顺序执行。然而,对于逻辑简单的任务,引入状态机反而会增加设计的复杂性。

而RTOS的优势在于,它极大的契合了开发者的直觉性思维。编程时,用户无需再手动拆分任务和管理状态变迁,只需专注于实现每个任务自身的完整循环逻辑。RTOS的内核调度器会接管CPU资源的分配,通过快速切换任务,营造出多任务“并行”执行的假象。

此外,以FreeRTOS为代表的系统还提供了更强大的并发控制机制,一是优先级抢占调度,确保高优先级任务能及时响应,类似于抢占优先级但是不完全一样;二是任务间通信机制,如队列、信号量等,能让任务高效、安全的同步和通信。

综上所述,ROTS将开发者从手动管理状态机的复杂工作中解放出来,使其能更高效的构建复杂的多任务应用。

5.3调度的艺术:RTOS的“并发”魔术

下面用一张图片来阐释RTOS环境下基于时间片轮转的任务调度与事件响应机制,如图5-3所示。

图5-3 时间片轮转任务调度示意图

设定在图5-3的系统中共存在三个独立任务,功能分工如下:

  1. 任务1:监控事件A,发生后通知任务3;
  2. 任务2:监控事件B,发生后通知任务3;
  3. 任务3:负责信息显示处理。

在宏观视角下,任务1、任务2和任务3呈现为三条并行的色带,仿佛在时间轴T上持续同步运行,体现了RTOS所提供的“逻辑并发”能力。 然而在微观视角下,单核CPU在任一时刻只能执行一个任务。RTOS调度器将CPU时间划为极小的时间片(例如1ms),并通过快速轮转执行(任务1->任务2->任务3...)营造出宏观并发的效果。

以5-3图为例,详细分析一个事件从发生到被任务处理的完整流程,该场景设定三个任务优先级相同、均不挂起(持续就绪),且系统采用1ms的时间片轮转调度算法。

  1. 事件发生 事件B在t0时刻于硬件层面产生(例如,某个GPIO引脚电平发生变化)。

  2. 响应延迟 由于事件源未连接中断,因此事件B仅被记录在硬件寄存器中。若此时 正在执行任务1或任务3,则无法立即响应此事件。系统必须等待监控该事件的任务2获得CPU的使用权。

  3. 调度响应 1)在当前运行的任务时间片用尽后,RTOS调度器被触发,进行任务切换。 2)调度器根据轮转规则,将CPU使用权赋予给就绪队列中的下一个任务。 3)如果上一个任务是任务1,那么当前应该执行任务2;如果上一个任务是任务3,当前执行任务1;执行完了任务1后,下一个时间片轮转触发调度器执行任务2。 4)任务2开始执行,读取硬件寄存器,从而检测到事件B已经发生,随即执行相应的处理逻辑(任务2:监控事件B,发生后通知任务3)。

  4. 延迟分析 1)最大延迟:在特定场景下,任务2最大响应延迟为一个调度周期。即事件B在任务2刚被切换出去后发生,此时它必须等待任务1和任务3依次执行完各自的时间片后,才能再次被调度。最大延迟时间为(N-1)*时间片,其中N为同等级优先级的就绪任务数,本例中为(3-2)*1ms = 2ms。 2)平均延迟:任务2的平均响应延迟约为一个调度周期的一半,本例中约为1.5ms。

总结下来,RTOS的时间片指的是每个任务被分配到的、依次连续执行的最长时间,可以理解成时间片是每个任务的CPU时间配额,通常是1ms。

当然,RTOS也没有那么“死板”必须固定的执行1ms,因此在实际场景中,可以这样去理解RTOS的整体运行机制:首先,调度器选择一个当前最高优先级的任务让他运行,并且启动一个定时器(系统节拍Tick),这个任务会连续的占用CPU,直到发生以下情况之一:

  1. 用完了它的时间片:定时器超时(1ms到),调度器强行收回CPU控制权。

  2. 主动放弃CPU:当任务执行提前完成,可以主动放弃CPU(挂起),调度器会立即进行任务切换,不会等到当前时间片用完,新任务即刻运行,这样可以避免CPU空转,提升实时性。挂起可通过调用延迟函数(如vTaskDelay)或等待信号量、队列等资源。 这也是RTOS区别于裸机轮询的核心优势,实现了零延迟任务切换、100%CPU有效利用以及硬实时响应能力。

  3. 被更高优先级任务抢占:有更高优先级任务就绪,发生抢占。

只有在情况1下,才真正体现“时间片轮转”的调度,情况2和情况3 是由任务行为或者优先级决定的。

5.4CubeMX配置FreeRTOS

新建一个工程文件夹命名为XXXX,直接将XXXXXXXX复制到,打开CubeMX工程后,需要先去掉TIM6的Activated选项,不再需要继续采用TIM6定时器中断,如图5-4所示。

图5-4 取消TIM6使能 在左侧菜单栏中单击SYS选项,将其中的Timebase Source由默认的SysTick修改为TIM6(TIM6和TIM7属于基本定时器,适合作为HAL库的时钟源)。这样修改的原因是,许多实时操作系统(RTOS)默认使用SysTick作为系统时钟源,若HAL库也使用SysTick,会造成资源争用,导致代码生成时出现冲突警告。通过将HAL时基独立配置为TIM6,可有效避免该问题,如图5-5所示。

图5-5 更换HAL库系统时钟源 从右侧菜单栏找到“Middleware and Software Packs”,选择其中的“FREERTOS”,选择“CMSIS_V1”版本(目前阶段本教材推荐RTOS内核的原生API,V2版本为ARM主推,更加抽象的封装),如图5-6所示。

图5-6 选择配置RTOS 其余选项全部使用默认配置,而后单击“GENERATE CODE”导出MDK工程,如图5-7所示。

图5-7 导出工程 打开MDK工程可以看到以下代码结构,多了一份FreeRTOS的源码文件夹,存放着FreeRTOS的实现代码,如图5-8所示。

图5-8 FreeRTOS源码

打开main.c文件,可以看到代码的注释结构也增加了一些RTOS相关的内容,在编写代码的过程中也需要遵循注释提示的规范,如图5-9所示。

图5-9 FreeRTOS源码

5.5FreeRTOS单任务创建

由于FreeRTOS是广泛应用的实时操作系统,其原生API比CMSIS-OS封装层更加直观,便于学习与应用。因此,本教材选择直接调用FreeRTOS API进行开发。为提升代码的可读性,创建Application/Task和Application/Drivers两个代码目录,分别用于存放任务实现和硬件驱动代码,如图5-10、5-11和5-12所示。

图5-10 单击“Manager Project Items”选项

图5-10 新建“Application/Task”和“Application/Drivers”组

图5-10 项目管理列表

打开工程文件夹,新建两个文件夹,分别命名为Task和UserDrivers用于存放任务代码和用户编写的驱动代码。在Task和UserDrivers文件夹下,分别各自再新建两个命名为Inc(include)和Src(source)的文件夹,用来存放.h头文件和.c源代码,如图5-11所示。

图5-11 任务代码和用户代码文件夹

单击Keil软件的“Option for target”图标,单击“C/C++”菜单,将Task/Inc和UserDriver/Inc两个路径添加到Keil的头文件搜索路径中,具体步骤如图5-12所示。

图5-11 添加Task和UserDriver头文件路径

将之前章节编写的key.c、LEDBUZ.c复制到UserDriver/Src目录下,将key.h、LEDBUZ.h复制到UserDriver/Inc目录下。 双击Project栏下的Application/Driver文件夹进入“Add File to Group ‘Application/Driver’”,将key.c和LEDBUZ.c添加到工程分组,如图5-12所示。

图5-12 将源程序添加进工程

新建文件(快捷键ctrl+n),保存(快捷键ctrl+s)到Task/Src目录下,命名为myTask.c,双击Application/Task分组,将myTask.c添加到这个分组。再新建一个文件,命名为myTask.h,添加到Task/Inc目录下,如图5-13所示。

图5-12 新建myTask.c和myTask.h

将CubeMX生成的CMSIS-OS接口任务创建代码屏蔽(注释/删除),如图5-13所示。这是因为本项目直接使用FreeRTOS API,若保留此代码,系统会创建一个无用的空任务,徒然消耗资源。请注意,后续使用CubeMX重新生成代码时,此段代码会被回复,需要再次手动屏蔽。

图5-13 屏蔽CMSIS-OS接口任务代码

myTask.c文件的程序代码如下所示:


#include "mytask.h"
#include "Key.h"
#include "LEDBUZ.h"

void myTaskCode( void * pvParameters )
{
for( ;; )
{
/* Task code goes here. */
LED_Running2();
keyScan();
keyAction();
vTaskDelay(20);//调用系统延时,单位是ms
}
}

在myTask.c文件中实现myTaskCode函数,该函数将直接调用上一节的功能代码,并使用vTaskDelay()实现延时。(也可以用vTaskDelayUntil()延时,两者区别不大,用vTaskDelayUntil更精准一些) 要明确的是,myTaskCode是一个任务,里边执行了4个函数。也就是根据RTOS时间片轮转的规则,myTaskCode这个任务每次执行时间为1ms。但是这里还用到了一个延时20ms的函数vTaskDelay(20),需要重点说明。 vTaskDelay与之前使用的HAL_Delay有本质区别:

  1. HAL_Delay通过空循环消耗CPU资源实现延时,会持续占用处理器。
  2. vTaskDelay则会将当前的任务挂起,让出CPU给其他就绪任务,待延时结束后再回复执行。

事实上,即使误用HAL_Delay也不会导致系统崩溃。由于FreeRTOS采用抢占式调度机制,虽然HAL_Delay会阻塞当前任务,但系统调度器仍可中断此阻塞状态,切换到其他任务:

  1. 更高优先级任务,可正常抢占执行;
  2. 同优先级任务,可通过时间片轮转获得执行权;
  3. 更低优先级任务,将被阻塞,无法运行。

这种设计体现了FreeRTOS调度器的健壮性,但为了避免资源浪费和低优先级任务“饿死”,在实际开发中仍要选用vTaskDelay。如果选用vTaskDelay函数,在延时的过程中,低优先级也可以获得执行的机会。

myTask.h文件代码如下:


#ifndef __MYTASK_H
#define __MYTASK_H
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#include "queue.h"
#include "semphr.h"
#include "event_groups.h"

#define myTask_STACK_SIZE 200

void myTaskCode( void * pvParameters );

#endif

在这个头文件程序中,首先需要将FreeRTOS相关的头文件都添加进入来。其中只要用到FreeRTOS,FreeRTOS.h和task.h就必须包含,而其他几个头文件在本小节程序中用不到,但是依然添加进来,后续用到时再解释。头文件的增加并不会增加程序代码,只是会增加一点编译时间。

此外,在这个头文件中,还需要通过宏定义制定任务的栈大小,设置合适的栈空间是RTOS编程的关键环节,如果分配不合理会出现以下两种情况:

栈空间分配过小:当任务函数调用层次过深(如递归调用)或定义过多局部变量(如大型局部数据)时,可能导致栈溢出,引发程序异常崩溃。

栈空间分配过大:虽然避免溢出风险,但会浪费宝贵的内存资源,影响系统的整体性能。 因此,为每个任务分配合适的栈空间至关重要。在后续的课程中,将逐步详细介绍如何通过实际测量方法确定最优的栈大小配置,此处暂不展开说明。此处设置栈空间是200,但指的是 StackType_t类型的变量个数,在32位的架构上,相当于800个字节。

main.c文件程序代码如下:


/* USER CODE BEGIN Includes */
#include "mytask.h"

/* USER CODE END Includes */
/*省略其他*/
/* USER CODE BEGIN PV */
StaticTask_t myTaskTCB;
StackType_t myTaskStack[ STACK_SIZE ];
TaskHandle_t myTaskHandle = NULL;
/* USER CODE END PV */
/*省略其他*/

int main()
{
/*省略其他*/
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
myTaskHandle = xTaskCreateStatic(
myTaskCode,/*Function that implements the task. */
"myTaskCode", /* Text name for the task. */
myTask_STACK_SIZE, /* Number of indexes in the xStack array. */
NULL, /* Parameter passed into the task. */
tskIDLE_PRIORITY,/* Priority at which the task is created. */
myTaskStack, /* Array to use as the task's stack. */
&myTaskTCB ); /* Variable to hold the task's data structure. */
/* USER CODE END RTOS_THREADS */
/*省略其他*/
osKernelStart();//操作系统启动,调度器调度任务运行。
while(1){//程序永远也运行不到此处。

}
}

在本例的 main函数中,使用 xTaskCreateStatic来创建一个静态任务。对于初学者,推荐优先掌握静态任务的概念。它的核心特点是任务内存(如任务栈)在程序启动前就已预先分配好,这带来了绝佳的确定性、安全性和可靠性,是理解 RTOS 多任务机制的理想起点。尽管这种方式灵活性不足,但能让用户先专注于任务调度等核心概念。关于更具灵活性但也更复杂的动态任务,将在后续课程中深入探讨其与静态任务的异同。 xTaskCreateStatic函数的参数列表如下所示:

序号参数功能描述备注
1pvTaskCode任务入口函数指针指向要运行的函数
2pcName任务名称不超过16byte
3ulStackDepth任务栈的深度 StackType_t个数
4pvParameters任务函数参数指针不用可设为NULL
5uxPriority任务优先级数字越大优先级越高0~32
6puxStackBuffer指向任务栈内存的指针指向用户分配的数组
7pxTaskBuffer指向任务控制块内存的指针任务块信息,包括栈顶指针、优先级等

在main.c文件中,每一个变量的声明、本质、用途和关联API整合如下:

  1. StaticTask_t myTaskTCB:任务控制块变量,静态分配内存,用于存储任务状态、优先级及栈指针等核心信息。其地址需通过 xTaskCreateStatic()的 pxTaskBuffer参数传入。
  2. StackType_t myTaskStack[STACK_SIZE]:任务栈的静态数组,用于保存任务执行的上下文与局部变量。STACK_SIZE以字为单位(通常1字=4字节),数组首地址需通过 puxStackBuffer参数传入。
  3. TaskHandle_t myTaskHandle = NULL:任务句柄指针,本质是 TCB_t*。在调用 xTaskCreateStatic()成功后,通过其返回值赋值,作为后续操作任务(如删除、挂起)的引用凭证。

5.6FreeRTOS多任务创建与任务控制

在5.5小节单任务实现的基础上,本小节将程序重构为两个独立的任务:流水灯任务和按键任务。通过按键任务对流水灯任务进行挂起与恢复操作,深入演示RTOS多任务管理机制,具体包括任务创建、任务间通信以及任务状态转换等核心概念,从而加深对RTOS实时性和多任务优势的理解。

myTask.c文件代码如下所示:

void ledTaskCode( void * pvParameters )
{
for( ;; )
{
for(uint8_t i =0;i<CODE_STEP;i++){//程序步
HAL_GPIO_WritePin(My_Gpio[i].GPIOx,My_Gpio[i].GPIO_Pin,My_Gpio[i].PinStateOpen);//点亮
vTaskDelay(My_Gpio[i].Delay_ms);
HAL_GPIO_TogglePin(My_Gpio[i].GPIOx,My_Gpio[i].GPIO_Pin);//反转电平
}
}
}


void keyTaskCode( void * pvParameters )
{
for( ;; )
{
/* Task code goes here. */
keyScan();
keyAction();
vTaskDelay(20);
}
}

key.c程序代码如下所示:

extern TaskHandle_t ledTaskHandle ;
/*省略其他*/
void keyAction()
{
/*省略其他*/
switch (Keys[KET_Ok].event) {
case KEY_EVENT_NONE:
break;

case KEY_EVENT_CLICK:
vTaskSuspend(ledTaskHandle);//挂起LED任务
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_DOUBLE_CLICK:
vTaskResume(ledTaskHandle);//恢复LED任务

Keys[KET_Ok].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_LONG_PRESS:
vTaskResume(ledTaskHandle);//恢复LED任务

Keys[KET_Ok].event = KEY_EVENT_NONE;

break;
case KEY_EVENT_COMBO:
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_RELEASE:
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;
}
}

main.c程序代码如下所示:


StaticTask_t ledTaskTCB;
StackType_t ledTaskStack[ STACK_SIZE ];
TaskHandle_t ledTaskHandle = NULL;
StaticTask_t keyTaskTCB;
StackType_t keyTaskStack[ STACK_SIZE ];
TaskHandle_t keyTaskHandle = NULL;
int main ()
{
ledTaskHandle = xTaskCreateStatic(
ledTaskCode, /* Function that implements the task. */
"ledTaskCode", /* Text name for the task. */
STACK_SIZE, /* Number of indexes in the xStack array. */
NULL, /* Parameter passed into the task. */
tskIDLE_PRIORITY,/* Priority at which the task is created. */
ledTaskStack, /* Array to use as the task's stack. */
&ledTaskTCB ); /* Variable to hold the task's data structure. */

keyTaskHandle = xTaskCreateStatic(
keyTaskCode, /* Function that implements the task. */
"keyTaskCode", /* Text name for the task. */
STACK_SIZE, /* Number of indexes in the xStack array. */
NULL, /* Parameter passed into the task. */
tskIDLE_PRIORITY,/* Priority at which the task is created. */
keyTaskStack, /* Array to use as the task's stack. */
&keyTaskTCB ); /* Variable to hold the task's data structure. */
/* Start scheduler */
osKernelStart();
while(1){

}
}

这段代码包含两个相互独立且优先级相同的任务,因此采用时间片轮转调度方式运行。同时,代码中使用了三个操作系统 API 函数,具体说明如下:

  1. vTaskDelay():任务级延时函数,调用后将挂起当前任务,直到指定的延时结束,期间其他任务可继续执行。

  2. vTaskSuspend():无条件挂起指定任务,需传入任务句柄(TaskHandle_t),被挂起的任务需通过 vTaskResume() 恢复才能继续运行。

  3. vTaskResume():用于恢复被挂起的任务,使其重新进入就绪状态。

有关 FreeRTOS API 函数的详细说明,可参考其官方网站文档。

5.7 RTOS下编程思维框架

RTOS 提供了强大的任务管理能力,因此在编程时应转变传统裸机开发的思维模式。为帮助更好地适应操作系统下的开发,以下几点建议供参考:

  1. 任务划分与调度:在操作系统环境下,各任务在宏观上是并发执行的。每个功能可拆分为独立任务,每个任务均可设计为类似主函数的循环结构。建议按实时性需求合理分配优先级,并注意任务会占用 RAM 资源,因此任务划分应适度,以逻辑清晰、易于维护为目标,避免过度拆分。

  2. 使用系统接口管理任务:尽量通过操作系统提供的任务管理接口(如挂起、恢复、延时等)实现对任务的控制,从而方便地实现任务的启动、暂停等行为。

  3. 复杂逻辑考虑状态机​:在处理多分支或复杂流程时,可引入状态机机制,以提高代码的结构性和可读性。

  4. 资源共享需同步​:多个任务访问共享硬件资源时,必须使用信号量、互斥锁等同步机制,防止出现资源冲突导致系统错误(后续章节将详细说明)。

5.8 练习题

尝试增加一个新的任务,使用按键对其进行挂起和恢复。