前言

  RTOS(Real-Time Operating System,实时操作系统)是一类专为嵌入式系统设计的操作系统,常见的包括 FreeRTOS、uC/OS、RTX、RT-Thread 等。那么,为什么在嵌入式开发中要引入 RTOS 呢?
  初学 MCU 编程时,通常从裸机开发入手。对于功能相对简单、逻辑清晰的系统,裸机编程完全能够胜任,甚至因其资源占用少、执行效率高而成为优选方案。然而,当系统功能变得复杂,需要将整体任务拆解为多个并发执行的子任务时,或者对响应时间、任务调度有严格实时性要求时,裸机开发就显得力不从心了。
  此时,引入 RTOS 就显得尤为必要。通过将系统功能划分为多个独立任务,RTOS 不仅能有效管理任务的调度与同步,还能显著提升程序的模块化程度——使代码结构更清晰、逻辑更易维护,同时也更便于团队协作和功能扩展。此外,RTOS 提供的队列、信号量、事件组等通信与同步机制,大大简化了复杂系统的开发难度。
正因如此,在实际嵌入式应用开发中,一旦项目规模或实时性需求达到一定门槛,开发者往往会倾向于采用 RTOS,以获得更高的开发效率和更强的系统可靠性。

一、任务相关的一些概念

1.1 多任务运行基本机制

  操作系统的主要功能就是实现多任务管理,而FreeRTOS是一个支持多任务的实时操作系统。FreeRTOS将任务称为线程(thread)。
  在FreeRTOS中,一个任务就是实现某个功能的一个函数,任务函数的内部是一个死循环结构。任何时候都不允许从任务函数退出,也就是不能出现return语句。如果需要结束任务,在任务函数里,可以跳出死循环,然后使用函数vTaskDelete()删除任务自己。
  在FreeRTOS里,用户可以创建多个任务,每个任务需要分配一个栈(stack)空间和一个任务控制块(Task Control Block,TCB,存储任务的状态、优先级、栈指针等信息)空间。每个任务还需要设定一个优先级,优先级越小,表示优先级越低。
  在单核处理器上,任何时刻只能有一个任务占用CPU并运行。但是在RTOS系统上,运行多个任务时,运行起来好像多个任务在同时运行,这是由于RTOS的任务调度使得多个任务对CPU实现分时复用的功能。
在这里插入图片描述

图1-1 简单的基于时间片的多任务运行原理

  图1-1所示是最简单的基于时间片的多任务运行原理。这里假设只有2个任务,并且任何人物Task1和Task2具有相同的优先级。圆周表示CPU时间,如同钟表的一圈,RTOS将CPU时间分成基本的时间片(time slice),例如,FreeRTOS默认的时间片长度是1ms,也就是SysTick定时器的定时周期。在一个时间片内,会有一个任务占用CPU并执行,假设当前运行的任务是Task1,在一个时间片结束时(实际是Systick定时器发生中断时)进行时间调度,由于Task1和Task2具有相同的优先级,RTOS会将CPU使用权交给Task2,Task1交出CPU使用权时,会将CPU的当前场景(CPU各个核心寄存器的值)压入字节的占空间。而Task2获取CPU的使用权时,会调用自己栈空间保存的数据恢复CPU场景,因而Task2可以从上次运行的状态继续运行。
  基于时间片的多任务调度就是这样控制多个同等优先级任务实现CPU的分时复用,从而实现多任务运行的。因为时间片的长度很短(默认是1ms),任务切换的速度非常块,所示程序运行时,给用户的感觉就是多个任务在同时运行。
  当多个任务的优先级不同时,FreeRTOS还会使用基本优先级的抢占式任务调度方法,每个任务获取的CPU的使用时间长度可以是不一样的。

1.3 任务的状态

  由单核CPU的多任务运行机制可知,任何时刻,只能有一个任务占用CPU并运行,这个任务得到状态称为运行(running)状态,其他未占用CPU的任务的状态都可以称为非运行(not running)状态。非运行状态又可以细分为3个状态,任务的各个状态以及任务状态之间的转换如图2-2所示。
在这里插入图片描述

图1-2 任务状态以及任务之间的转换

1、就绪状态
  任务被创建之后就处于就绪(ready)状态。RTOS的任务调度器在基础时钟每次中断时进行一次任务调度申请,根据抢占式任务调度的特点,任务调度的结果有以下几种情况。

  • 如果当前没有其他处于运行状态的任务,处于就绪状态的任务进入运行状态。
  • 如果就绪任务的优先级高于或等于当前运行任务的优先级,处于就绪状态任务进入运行状态。
  • 如果就绪任务的优先级低于当前运行任务的优先级,处于就绪状态的任务无法获得CPU使用权,继续处于就绪状态。

  就绪的任务获取CPU的使用权,进入运行状态,这个过程称为切入(switch in)。相应的,处运行状态的人任务被调度器调度为就绪状态,这个过程称为切出(switch out)

2、运行状态
  在单核处理器上,占有CPU并运行的任务就处于运行状态。处于运行状态的高优先级任务如果一直运行,将一直占用CPU,在任务调度时,低优先级的就绪任务就无法获得CPU的使用权。
  处于运行 状态的任务,有两种主动让出CPU使用权的方法,一种是执行函数osThreadSuspend()进入挂起状态,另一种是执行阻塞式函数(比如vTaskDelay())进入阻塞状态。这两种状态都是非运行状态,运行的任务就交出了CPU的使用权,任务调度器就可以使其他就绪状态的任务进入运行状态。
3、阻塞状态
  阻塞(blocked)状态就是任务暂时让出CPU的使用权,处于等待的状态。运行状态的任务可以调用两类函数进入阻塞状态。
  一类是时间延迟函数,如vTaskDelay()和vTaskDelayUntil()。处于运行状态的任务调用这类函数后,就进入了阻塞状态,并延迟指定的时间。延迟时间到了后,又进入了就绪状态,参与任务调度后,又可以进入运行状态。
  另一类是用于进程间通信时,请求信号量、队列等事件。例如,请求信号量的函数xSemaphoreTake()。处于运行状态的任务执行函数xSemaphoreTake()后,就进入了阻塞状态,如果其他任务释放了信号量,或等待的超时时间到了,任务就从阻塞状态进入就绪状态。
在运行状态的任务中调用函数vTaskSuspend(),可以将一个处于阻塞状态的任务转入挂起状态。
4、挂起状态
  挂起状态(suspended)状态的任务就是暂停的任务,不参与调度器的调度,其他3种状态的任务都可以通过函数vTaskSuspend()进入挂起状态。处于挂起状态的任务是不能自动退出挂起状态的,需要在其他任务里面调用函数vTaskResume (),才能使一个挂起的任务变为就绪状态。

二、任务管理相关函数

2.1 相关函数概述

  在FreeRTOS中,任务的管理主要包括任务的创建、删除、挂起、恢复等操作,还包括任务调度器的启动、挂起与恢复,以及任务进入阻塞状态的延迟函数等。任务管理常用的一些函数及其功能描述见表1-1,这里列出了函数名,省略了输入/输出参数。如需要了解每个函数的定义和功能说明,可以查看其源码。

表1-1 任务管理常用的一些函数及其功能描述
分组功能说明FreeRTOS 函数CMSIS-RTOS 函数名
任务管理动态创建一个任务xTaskCreate()osThreadCreate()
任务管理静态创建一个任务(预分配堆栈内存)xTaskCreateStatic()osThreadNew()
任务管理删除指定任务(需任务句柄)vTaskDelete()osThreadTerminate()
任务管理删除当前任务xTaskDelete()osThreadExit()
任务管理挂起指定任务(暂停执行)xTaskSuspend()osThreadSuspend()
任务管理恢复被挂起的任务xTaskResume()osThreadResume()
延时与调度相对延时:当前任务延时指定的节拍数(相对当前时间),进入阻塞状态vTaskDelay()osDelay()
延时与调度绝对延时:当前任务延时到指定的节拍数(绝对时间点),确保周期性执行,进入阻塞状态vTaskDelayUntil()osDelayUntil()
延时与调度获取系统当前节拍计数值xTaskGetTickCount()osKernelGetTickCount()

2.3 FreeRTOS 主要函数功能说明

(1)xTaskCreate()
  xTaskCreate() 函数用于动态创建一个任务,并分配堆栈内存。该函数是 FreeRTOS 中最常用的任务创建方式之一。其函数原型如下:

BaseType_t xTaskCreate(
   TaskFunction_t pxTaskCode,          // 指向任务函数的指针
   const char * const pcName,          // 任务名称(用于调试)
   const uint32_t ulStackDepth,        // 栈大小(单位:字)
   void * const pvParameters,          // 传递给任务函数的参数
   UBaseType_t uxPriority,             // 任务优先级
   TaskHandle_t * const pxCreatedTask  // 返回任务句柄(可选)
);

① 参数说明

  • pxTaskCode:指向任务函数的函数指针。任务函数原型通常为 void vTaskFunction(void *pvParameters)。
  • pcName:任务的名称字符串,主要用于调试和跟踪,不会影响运行时行为。
  • ulStackDepth:任务堆栈的大小,单位为“字”(通常是 4 字节)。需根据任务复杂度合理设置。
  • pvParameters:传递给任务函数的参数,可以是 NULL。
  • uxPriority:任务的优先级,数值越大优先级越高。
  • pxCreatedTask:指向 TaskHandle_t 类型变量的指针,用于保存新创建任务的句柄。若不需要,可设为 NULL。

② 返回值

  • pdPASS:任务创建成功。
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:内存分配失败(堆栈或控制块不足)。
    其他错误码:表示其他创建失败原因(如系统资源限制)。

(2)xTaskCreateStatic()
  xTaskCreateStatic() 函数用于静态创建任务,即在编译时预分配堆栈和任务控制块(TCB),避免动态内存分配。适用于对内存确定性要求高的场景。其函数原型如下:

BaseType_t xTaskCreateStatic(
   TaskFunction_t pxTaskCode,
   const char * const pcName,
   const uint32_t ulStackDepth,
   void * const pvParameters,
   UBaseType_t uxPriority,
   StackType_t * const puxStackBuffer,
   StaticTask_t * const pxTaskBuffer,
   TaskHandle_t * const pxCreatedTask
);

① 参数说明
所有参数与 xTaskCreate() 类似,但新增两个关键参数:

  • puxStackBuffer:指向预分配的堆栈缓冲区的指针。
  • pxTaskBuffer:指向预分配的任务控制块(TCB)的指针。
    这两个缓冲区必须由用户在全局或静态区域预先定义。

② 返回值

  • pdPASS:任务创建成功。
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:虽然静态分配,但仍可能因参数无效导致失败。
    💡 优点:无动态内存开销,适合资源受限或安全关键系统。

(3)xTaskDelete()
  xTaskDelete() 函数用于删除一个任务。如果删除的是当前任务,则立即终止;如果是其他任务,则将其标记为待删除状态,等待调度器处理。其函数原型如下:

void vTaskDelete(TaskHandle_t * const pxTaskToDelete);

① 参数说明

  • pxTaskToDelete:指向要删除任务的句柄。若传入 NULL,则删除调用此函数的当前任务。
    ② 返回值
    无返回值。删除操作完成后,任务不再被调度,其资源会被系统回收(在后续调度周期中)。
    ⚠️ 注意:删除任务后,其使用的堆栈和 TCB 可能不会立即释放,具体取决于内存管理策略。

(4)xTaskSuspend()
  xTaskSuspend() 函数用于挂起指定任务,使其进入“挂起状态”,不再参与调度。只有通过 xTaskResume() 才能恢复执行。其函数原型如下:

void vTaskSuspend(TaskHandle_t * const pxTaskToSuspend);

① 参数说明

  • pxTaskToSuspend:指向要挂起任务的句柄。不能为 NULL。
    ② 返回值
    无返回值。挂起成功后,任务将停止运行,直到被恢复。
    🔁 注意:挂起任务不会释放其堆栈,仅暂停调度。

(5)xTaskResume()
   xTaskResume() 函数用于恢复一个被挂起的任务,使其重新进入就绪状态,等待调度执行。其函数原型如下:

void vTaskResume(TaskHandle_t * const pxTaskToResume);

① 参数说明

  • pxTaskToResume:指向要恢复任务的句柄。该任务必须之前已被 xTaskSuspend() 挂起。
    ② 返回值
    无返回值。恢复成功后,任务会进入就绪队列,等待调度器分配 CPU。
    ✅ 常用于实现“软复位”或“任务启用/禁用”机制。

(6)vTaskDelay()
  vTaskDelay() 函数用于使当前任务延时指定的节拍数(相对延时),并进入阻塞状态。函数原型如下:

void vTaskDelay(const TickType_t xTicksToDelay);

① 参数说明

  • xTicksToDelay:延时的时间,单位为系统节拍(ticks)。例如,若系统节拍为 1000Hz,则 1 tick = 1ms。
    ② 返回值
    无返回值。任务在延时期间不占用 CPU,调度器会切换到其他就绪任务。
    📌 适用场景:普通延时、非周期性任务。

(7)vTaskDelayUntil()
  vTaskDelayUntil() 函数用于实现绝对延时,确保任务在固定周期内执行,常用于周期性任务。其函数原型如下:

void vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement);

① 参数说明

  • pxPreviousWakeTime:指向上次唤醒时间的指针,首次使用时需初始化为 xTaskGetTickCount()。
  • xTimeIncrement:期望的周期间隔(单位:ticks)。
    ② 返回值
    无返回值。任务会延迟到下一个周期点再执行。
    ✅ 示例用途:每 100ms 采集一次传感器数据,保证频率稳定。

(8)xTaskGetTickCount()
  xTaskGetTickCount() 函数用于获取系统当前的节拍计数值,即自系统启动以来经过的节拍数。其函数原型如下:

TickType_t xTaskGetTickCount(void);

① 参数说明
无参数。
② 返回值
返回一个 TickType_t 类型的值,表示当前系统的节拍计数。可用于计算时间差、实现定时器等。
💡 提示:结合 vTaskDelayUntil() 使用,可构建精确周期任务。

2.4 CMSIS-RTOS 主要函数功能说明

(1)osThreadNew()
  osThreadNew() 函数的作用是创建一个新的线程,并初始化它的状态。这个函数的原型如下:

osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);

①参数说明

  • func:指向线程函数的指针。线程函数是线程启动后执行的实际代码。例如,它的原型可能类似于 void ThreadFunction(void *argument)。

  • argument: 传递给线程函数的参数。这可以是一个指针,指向你希望在线程中使用的数据。可以是 NULL,如果线程函数不需要任何参数。

  • attr: 指向 osThreadAttr_t 结构体的指针,用于设置线程的属性。这个结构体包含了一些线程的配置信息,例如线程栈大小、优先级等。你可以设置这个参数来定制线程的行为,也可以将其设置为 NULL,使用默认属性。

②返回值

  • 成功:返回新创建线程的标识符(osThreadId_t)。
  • 失败: 返回 NULL,表示线程创建失败。

(2)osThreadTerminate()
  osThreadTerminate()函数在 Keil RTX5 实时操作系统中用于终止一个已经存在的线程。函数原型如下:

osStatus_t osThreadTerminate(osThreadId_t thread_id);

①参数说明
  thread_id: 需要终止的线程的标识符(osThreadId_t)。可以通过之前调用 osThreadNew 创建线程时获得的线程ID来指定哪个线程需要被终止。

②返回值

  • osOK:线程成功被终止。
  • osErrorParameter:提供的 thread_id 无效(例如,该线程ID不存在或已经被删除)。
  • osError:其他错误情况,通常是由于系统资源问题或线程当前状态导致的。

(3)osThreadExit()
  在 Keil RTX5 实时操作系统中,osThreadExit 函数用于主动退出当前线程,并结束线程的执行,其函数原型如下:

void osThreadExit(void);

  该函数无返回值,无参数,会使当前线程立即终止。

(4)osThreadSuspend()
  osThreadSuspend()函数用于挂起(暂停)一个线程,使其不再被调度执行,直到被显式地恢复。其函数原型如下:

osStatus osThreadSuspend (osThreadId thread_id);

①参数
thread_id:需要挂起的线程的标识符。这个标识符是通过调用osThreadCreate函数创建线程时获得的。

②返回值
  返回值是osStatus类型,表示函数的执行结果。常见的返回值包括:

  • osOK:操作成功。
  • osErrorParameter:传入的线程标识符无效。
  • osError:其他错误。

(5)osThreadResume()
  osThreadResume()函数用于恢复之前被挂起的线程,使其可以重新被调度执行。其函数原型是:

osStatus osThreadResume (osThreadId thread_id);

①参数
osOK thread_id:需要恢复的线程的标识符。这个标识符是通过调用osThreadCreate函数创建线程时获得的。

②返回值
  返回值是osStatus类型,表示函数的执行结果。常见的返回值包括:

  • osOK:操作成功。
  • osErrorParameter:传入的线程标识符无效。
  • osError:其他错误。

(6)osDelay()
  osDelay()函数在Keil RTX5中用于使当前线程暂停执行指定的时间。其函数原型如下:

osStatus_t osDelay(uint32_t ticks);

①参数
ticks:指定线程应暂停的时间,以系统时钟周期(ticks)为单位。ticks表示系统的时钟滴答时间,这通常由系统的时钟频率和调度器配置决定。

②返回值
返回值是osStatus_t类型,表示函数的执行结果。常见的返回值包括:

  • osOK:操作成功,线程已成功进入延迟状态。
  • osErrorValue:传入的参数值无效(例如,非正值或过大的值)。
  • osError:其他错误情况。

(7)osDelayUntil()
  osDelayUntil()函数用于将当前线程挂起,直到指定的绝对时间点。其函数原型如下:

osStatus_t osDelayUntil (uint32_t ticks);

①参数
ticks:指定线程应挂起直到的绝对时间点,以系统节拍(ticks)为单位。这个时间点是系统启动以来经过的节拍数,通常由osKernelGetTickCount函数返回。

②返回值
osStatus_t:表示函数的执行结果。常见的返回值包括:

  • osOK:操作成功,线程已成功进入延迟状态。
  • osErrorValue:传入的参数值无效(例如,非正值或过大的值)。
  • osError:其他错误情况。

(8)osKernelGetTickCount()
  osKernelGetTickCount()函数用于获取系统的滴答计数器的值,其函数原型:

uint32_t osKernelGetTickCount(void);

①参数
这个函数没有参数。

②返回值
osKernelGetTickCount 返回一个 uint32_t 类型的值,表示系统启动以来的滴答次数,是系统时钟的计数器值。

三、任务创建(动态方法)

3.1 示例内容

  本篇文章是基于STM32F407开发板上进行测试的,用到的库是HAL库,但是Keil RTX5操作系统的配置过程不仅仅适用于次开发板。在本次示例中需要设计两个任务,分别是task1_task和task2_task,功能如下:

  • task1_task:此任务会控制LED0的闪烁,同时,当LED0闪烁5次后会调用函数osThreadTerminate()函数来删除任务task2_task。
  • task2_task:普通的应用任务,此任务会控制LED1的闪烁。

3.2 CubeMx配置

3.2.1 FreeRTOS模板工程配置

  我们先生成一个基础工程,后续大部分FreerRTOS的功能分享都会在这个工程上实现。在 CubeMX中,我们选择STM32F407ZG创建一个项目,先完成如下的基本设置。
(1)打开STM32CubeMx,点击新建工程。在这里插入图片描述

图3-1

(2)找到对应的芯片在这里插入图片描述

图3-2

(3)在 STM32CubeMX 中将高速外部时钟(HSE)配置为使用外部晶振(Crystal/Ceramic Resonator),以提供精确的系统主时钟源在这里插入图片描述

图3-3

(4)将 STM32 的系统时间基准源(Timebase Source)配置为 TIM6 定时器
在这里插入图片描述

图3-4

(5)在 STM32CubeMX 中将 PF9 引脚配置为 GPIO_Output 模式,用于作为普通数字输出引脚(用来控制LED灯),根据LED电路连接图,配置引脚PF9和PF10为GPIO推挽输出,无上拉或下拉。
在这里插入图片描述

图3-5

(6)在 STM32CubeMX 中启用了 FreeRTOS 实时操作系统,并将其接口配置为 CMSIS_V2 标准
在这里插入图片描述

图3-6

(7)在 STM32CubeMX 中通过 FreeRTOS 配置界面 创建新任务,正在使用“Tasks and Queues”选项卡添加一个名为 myTask02 的用户任务,并设置其优先级、栈大小和入口函数。

字段含义说明
Task Name(任务名)用户自定义的任务名称,如 myTask02,仅用于调试和日志输出(例如在调试器或 pcName 字段中显示),不影响运行行为。建议命名清晰,便于识别。
Priority(优先级)任务的调度优先级,数值越大优先级越高。
osPriorityLow:低优先级
osPriorityNormal:正常优先级(默认)
osPriorityHigh:高优先级
FreeRTOS 调度器根据此优先级决定任务执行顺序。
Stack Size (Words)(栈大小)任务堆栈的大小,单位为“字”(通常是 4 字节)。例如 128 表示 512 字节(128 × 4)。
⚠️ 栈太小可能导致溢出(HardFault),太大浪费内存,需根据任务复杂度合理设置。
Entry Function(入口函数)任务执行的函数名,例如 StartTask02。CubeMX 会在生成的代码中声明或定义该函数,其原型为:
void StartTask02(void *argument)
用户需在 .c 文件中实现具体逻辑。
Code Generation Option(代码生成方式)控制 CubeMX 如何生成该任务的函数和创建调用:
• Default:自动生成任务函数和 xTaskCreate() 调用(最常用)
• As external:仅声明函数为 extern,不生成创建代码,由用户手动调用 xTaskCreate()
• As weak:生成带 __weak 属性的函数,允许用户在其他文件中重定义同名函数
Parameter(参数)传递给任务入口函数的参数指针(void* 类型),可以是 NULL 或指向结构体、队列句柄、设备地址等数据。
Allocation(内存分配方式)指定任务堆栈和 TCB 的分配方式:
• Dynamic:使用 xTaskCreate(),从 FreeRTOS 堆中动态分配内存
• Static:使用 xTaskCreateStatic(),需用户提供缓冲区(见下两项)
Buffer Name(缓冲区名)当 Allocation = Static 时,必须填写堆栈缓冲区的变量名(如 Task02Stack),该数组需在全局或静态作用域中预定义,类型为 StackType_t
Control Block Name(控制块名)当 Allocation = Static 时,必须填写任务控制块(TCB)的变量名(如 Task02TCB),类型为 StaticTask_t,用于存储任务状态信息。

在这里插入图片描述

图3-7

在这里插入图片描述

图3-8

(8)在 STM32CubeMX 中配置系统时钟,使用 8MHz 外部晶振(HSE)作为输入源,通过 PLL 倍频至 168MHz 作为主系统时钟(HCLK),并启用 CSS(时钟安全系统)以增强稳定性(时钟树根据自己芯片继续配置)。在这里插入图片描述

图3-9

(9)在 STM32CubeMX 的“Project Manager”界面中配置工程基本信息,包括设置项目名称、存储路径、选择 Keil MDK-ARM 作为编译器,并指定其最低版本为 V5.32,可根据实际用到的编译器进行选择。
在这里插入图片描述

图3-10

(10)在 STM32CubeMX 的“Code Generator”设置中,配置生成代码时仅复制必要的库文件,并为每个外设生成独立的 .c 和 .h 文件,以实现模块化和便于用户维护。
在这里插入图片描述

图3-11

在这里插入图片描述

图3-12

在这里插入图片描述

图3-13

(11)打开工程后,在freertos.c中找到 void StartTask02(void *argument)函数,在函数中加入LED灯控制,并将程序下载到开发板中,以此验证生成的程序能否正常跑起来。

HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_RESET);
osDelay(500);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9|GPIO_PIN_10, GPIO_PIN_SET);
osDelay(500);

在这里插入图片描述

图3-14

3.2.2 示例工程配置

  在上面模板工程的基础上,我们在FreeRTOS中再创建两个任务,创建步骤可以参考3.2.1节中的步骤(7)。
  创建好的两个任务的基本参数下图3-15所示。单击图中的 Add或 Delete 按钮,就可以添加或删除任务,双击列表中的一个任务,就可以打开一个设置其属性的对话框。
最终实现的目标就是:在两个线程/任务里面分别控制两个LED灯的闪烁,并且在其中的一个线程里面实现,当该线程里面的灯闪烁4下后关闭另一个线程里面的LED。
在这里插入图片描述

图3-15

在这里插入图片描述

图3-16

  图3-16所示对话框中各个参数的意义可以参考3.2.1节中的步骤(7),本示例的两个任务的设置有如下要点。

  • 两个任务的优先级都设置为osPriorityNormal,也就是具有相同的优先级。

  • 两个任务采用了动态分配内存内存分配方式。如果使用静态分配内存,需要设置作为栈空间的数组名称以及控制块名称。

3.3 程序分析

3.3.1 初始程序分析

我们在 CubeMX 中生成代码,在CubeIDE 中打开项目,主程序文件 main.c的部分代码如下。

在这里插入图片描述
  main()函数的代码结构前面部分是 MCU 软硬件的初始化,包括 HAL初始化、系统时钟配置和外设初始化。
  FreeRTOS的初始化和启动就是执行以下3个函数。

  • osKernelInitialize() 是 CMSIS-RTOS 标准接口函数,用于初始化 FreeRTOS 的调度器。
  • MX FREERTOS Init() 是FreeRTOS对象初始化函数,在freertos.c中实现,用于创建任务、信号量、互斥量等 FreeRTOS 中定义的对象。
  • osKernelStart() 是 CMSIS-RTOS 标准接口函数,用于启动FreeRTOS的任务调度器。执行函数 osKermelStart()启动FreeRTOS的任务调度器后,任务调度器就接管了CPU 的控制权,函数 osKernelStart()是永远不会退出的,所以不会执行后面的while()循环。

3.3.2 任务创建

   文件freertos.c是CubeMX生成代码时生成的文件,是实现用户功能的代码文件在main()函数中调用的函数就是在这个文件里实现的。注意,这个文件没有对应的头文件。
  本示例初始化生成的文件freertos.c的代码如下,此处删除了一些沙箱代码段的注释添加了一些注释:
  在freertos.c的私有变量定义部分,我们定义了两个任务的句柄变量和任务属性变量。
在这里插入图片描述
  MX_FREERTOS_Init() 函数时FreeRTOS的对象初始化函数,用于创建定义的三个任务。

在这里插入图片描述
  CubeMx生成的任务函数如下,一共有三个,后续需要在任务函数里面实现具体的功能。 初始化生成的任务函数都只有一个基本的框架,任务函数主体是一个for死循环,要用户添加功能代码,实现任务2的功能。
在这里插入图片描述

3.3.3 编写用户功能代码

  示例的内容相对简单,task1Task() 函数里面通过 osDelay(500)实现500ms的闪烁,并通过一个局部变量统计灯闪烁的次数,当PF9对应的LED灯闪烁4下后,便会调用osThreadTerminate函数删除task2_taskHandle任务,此时需要传入任务2的句柄,也就是task2_taskHandle。
在这里插入图片描述

四、总结

  上述主要讲述了FreeRTOS中的任务概念,以及关于任务相关的函数,之所以在解释了FreeRTOS 一些任务的主要函数后又介绍了CMSIS-RTOS的相关函数是因为博主最开始接触的是keil TRX5操作系统,也是一种RTOS系统,到后来发现其实只要是RTOS系统,其中的线程/任务、信号量、消息队列、线程标志…都是相通的,就提笔简单解释了不同的函数,可以做一个对比。最后通过一个简单示例去示范在线程中怎么实现功能,当然,这只是最简单的任务实现,在实际工作应用中,一个功能的实现往往需要多个线程配合完成,其中又会涉及到线程之间的有优先级、栈空间、共有资源死锁等,需要不断深入研究。


源码分享:FreeRTOS 系列分享(一):任务管理

参考书籍和文章:
1、《STM32Cube高效开发教程(高级篇)》王维波
2、FreeRTOS源码详解与应用开发


路虽远行则将至,事虽难做则必成。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐