一、FreeRTOs介绍

什么是FreeRTOS?

Free即免费的,RTOS的全称是Real time operating system,中文就是实时操作系统
注意:RTOS不是指某一个确定的系统,而是指一类操作系统。比如:uc/OS,FreeRTOS,
RTX,RT-Thread等这些都是RTOS类操作系统。
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。
由于RTOS需占用一定的系统资源(优其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-11、embOs等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为10.4.4版。
(以上来自百度百科)


为什么选择FreeRTOS?
  • FreeRTOS是免费的;
  • 很多半导体厂商产品的SDK(Software Development Kit)软件开发工具包,就使用FreeRTOS作为其操作系统,尤其是WIFI、蓝牙这些带有协议栈的芯片或模块。
  • 简单,因为FreeRTOS的文件数量很少。FreeRTOS资料与源码下载
FreeRTOs资料与源码下载

最好的资料就是官网提供的资料!FreeRTOS™ - FreeRTOS™

裸机开发与FreeRTOS
1、裸机开发(Bare Metal)

特点:

  • 没有操作系统。主循环(while(1))是核心,一切任务靠轮询中断驱动。

  • 程序结构:初始化 → 主循环(或状态机) → 中断响应。

  • 资源管理:全靠开发者手动管理(CPU时间、内存、外设等)。

  • 适用场景:逻辑简单、实时要求高的小型系统,如LED控制、简单传感器采集、基础通讯。

  • 优点

    • 运行效率高,没有系统调度开销;

    • 程序占用内存极小;

    • 上电即跑,响应快。

  • 缺点

    • 难以扩展和维护;

    • 多任务之间容易“抢资源”;

    • 复杂逻辑下容易陷入“中断地狱”或“状态机迷宫”。

2、FreeRTOS(实时操作系统)

特点:

  • 提供多任务(task)调度机制,能同时管理多个逻辑任务;

  • 每个任务都有独立栈空间,系统通过调度器切换任务;

  • 提供任务优先级、消息队列、信号量、互斥锁等;

  • 时间管理统一(Tick中断),能精确控制任务延时与超时。

适用场景:

  • 逻辑复杂的系统;

  • 存在多种独立但需协调的任务(如通信、显示、传感、控制);

  • 有一定资源的MCU(如STM32、GD32、ESP32等)。

优点:

  • 程序结构清晰,可模块化;

  • 各任务独立运行,调试方便;

  • 更接近工业级嵌入式系统架构;

  • 方便以后移植到更复杂的RTOS或Linux。
    缺点:

  • 存在上下文切换开销;

  • 需要RAM更多;

  • 系统设计复杂度上升(尤其是任务同步与资源共享)。
     

对比点 裸机开发 FreeRTOS
主体结构 主循环 + 中断 多任务 + 调度器
时间控制 延时函数/定时器中断 vTaskDelay / 系统Tick
任务切换 人工控制流程 系统自动调度
共享资源 全靠程序员小心 信号量、互斥锁
适用复杂度 简单应用 中等以上复杂系统

总结一句话

裸机是直接操控硬件的“手工模式”,FreeRTOS是带“调度与分工”的协作系统。

但CPU是个无情的战斗机器,可以快速在两个乃至多个任务间快速切换,并且不觉得劳累,
实现二者兼顾。


FreeRTOS实现多任务的原理

严格来说FreeRTOS并不是实时操作系统,因为它是分时复用的。
系统将时间分割成很多时间片,然后轮流执行各个任务。
每个任务都是独立运行的,互不影响,由于切换的频率很快,就感觉像是同时运行的一样。

二、移植freeRTOS

手动移植参考这篇文章:【FreeRTOS移植到STM32F103C8T6超详细教程-->>>基于标准库】_freertos stm32f103c8t6-CSDN博客

使用CubeMX快速移植

快速移植流程

1.在SYS选项里,将Debug设为Serial Wire,并且将Timebase Source设为TIM2(其它定时器也行)。为何要如此配置?下文解说。

2.将 RCC里的 HSE设置为Crystal/Ceramic Resonator。

3.时钟按下图配置

4.选择FREERTOS选项,并将Interface改为CMSIS_V1。V1和V2有啥区别?下文解释。

5.配置项目信息,并导出代码。

一些常见问题

意思是当使用RTOS时,强烈建议给HAL库使用一个Systick以外的定时器作为时钟基准,这是因为FreeRTOS会以Systick中断作为时钟基准,因而会将Systick的中断优先级设置得比较低甚至有时会关闭其中断,这可能会导致HAL库的定时发生错乱。

1.Timebase Source为什么不能设置为SysTick?

裸机的时钟源默认是 SysTick,但是开启 FreeRTOs,FreeRTOS会占用 SysTick(用来生成1ms定时,用于任务调度),所以需要需要为其他总线提供另外的时钟源。

2. FreeRTOS版本问题

V2的内核版本更高,功能更多,在大多数情况下V1版本的内核完全够用。


3.FreeRTOS各配置选项卡的解释
  • Events:事件相关的创建
  • Task and Queues:任务与队列的创建
  • Timers and Semaphores:定时器和信号量的创建
  • Mutexes:互斥量的创建
  • FreeRTOS Heap Usage:用于查看堆使用情况
  • config parameters:内核参数设置,用户根据自己的实际应用来裁剪定制FreeRTOS内核
  • Include parameters:FreeRTOS部分函数的使能
  • User Constants:相关宏的定义,可以自建一些常量在工程中使用
  • Advanced settings:高级设置
4.内核配置、函数使能的一些翻译

freeRTOS内核配置的相关说明,可以参考下面这篇文章。

FreeRTOS系列第6篇---FreeRTOS内核配置说明_vassertcalled-CSDN博客

三、任务的创建与删除

1.什么是任务?


任务可以理解为进程/线程,创建一个任务,就会在内存开辟一个空间。
比如:
玩游戏、陪女朋友,都可以视为任务
Windows 系统中的MarkText、谷歌浏览器、记事本,都是任务。
任务通常都含有While(1)死循环。

2.任务创建与删除相关函数


任务创建与删除相关函数有如下三个:


任务动态创建与静态创建的区别: 

动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。
通常情况下使用动态方式创建任务。


xTaskCreate函数原型

1.pvTaskCode:指向任务函数的指针,任务必须实现为永不返回(即连续循环);
2.pcName:任务的名字,主要是用来调试,默认情况下最大长度是16;
3. pvParameters:指定的任务栈的大小;

4.uxPriority:任务优先级,数值越大,优先级越大;
5.pxCreatedTask:用于返回已创建任务的句柄可以被引用。

官方提供的案例:

/* Task to be created. */
void vTaskCode( void * pvParameters )
{
  /* The parameter value is expected to be 1 as 1 is passed in the
  pvParameters value in the call to xTaskCreate() below.
  configASSERT( ( ( uint32_t ) pvParameters ) == 1 );

  for( ;; )
  {
    /* Task code goes here. */
  }
}

/* Function that creates a task. */
void vOtherFunction( void )
{
BaseType_t xReturned;
TaskHandle_t xHandle = NULL;

  /* Create the task, storing the handle. */
  xReturned = xTaskCreate(
           vTaskCode,       /* Function that implements the task. */
           "NAME",          /* Text name for the task. */
           STACK_SIZE,      /* Stack size in words, not bytes. */
           ( void * ) 1,    /* Parameter passed into the task. */
           tskIDLE_PRIORITY,/* Priority at which the task is created. */
           &xHandle );      /* Used to pass out the created task's handle. */

  if( xReturned == pdPASS )
  {
    /* The task was created. Use the task's handle to delete the task. */
    vTaskDelete( xHandle );
  }
}

vTaskDelete函数原型

void vTaskDelete(TaskHandle_t xTaskToDelete);

只需将待删除的任务句柄传入该函数,即可将该任务删除。
当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。

3.实操

 
四、任务的状态

什么是任务调度?
调度器就是使用相关的调度算法来决定当前需要执行的哪个任务。
FreeRTOS中开启任务调度的函数是vTaskStartScheduler(),但在CubeMX中被封装为
osKernelStart()。


FreeRTOS的任务调度规则是怎样的?
FreeRTOS是一个实时操作系统,它所奉行的调度规则:

  1. 高优先级抢占低优先级任务,系统永远执行最高优先级的任务(即抢占式调度)
  2. 同等优先级的任务轮转调度(即时间片调度)

还有一种调度规则是协程式调度,但官方已明确表示不更新,主要是用在小容量的芯片上,用得也不多。

抢占式调度运行过程

Task 1:玩游戏
Task 2:老妈喊你吃饭
Task 3:女朋友喊你看电视


总结:
1.高优先级任务,优先执行;
2.高优先级任务不停止,低优先级任务无法执行;
3.被抢占的任务将会进入就绪态

时间片调度运行过程

总结:
1.同等优先级任务,轮流执行,时间片流转
2.一个时间片大小,取决为滴答定时器中断周期;
3.注意没有用完的时间片不会再使用,下次任务Task3得到执行,还是按照一个时间片的
时钟节拍运行

五、任务的状态

FreeRTOS中任务共存在4种状态:

  • Running 运行态

当任务处于实际运行状态称之为运行态,即CPU的使用权被这个任务占用(同一时间仅一个
任务处于运行态)。

  • Ready 就绪态

处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因
为同优先级或更高优先级的任务正在运行。

  • Blocked 阻塞态

如果一个任务因延时,或等待信号量、消息队列、事件标志组等而处于的状态被称之为阻塞
态。

  • Suspended 挂起态

类似暂停,通过调用函数vTaskSuspend()对指定任务进行挂起,挂起后这个任务将不被执
行,只有调用函数xTaskResume)才可以将这个任务从挂起态恢复。

总结:
1.仅就绪态可转变成运行态
2.其他状态的任务想运行,必须先转变成就绪态

任务综合小实验:

实验需求
创建4个任务:taskLED1,taskLED2,taskKEY1,taskKEY2,任务要求如下:
taskLED1:间隔500ms闪烁LED1;
taskLED2:间隔1000ms 闪烁LED2;
taskKEY1:如果taskLED1存在,则按下KEY1后删除 taskLED1,否则创建taskLED1;
taskKEY2:如果taskLED2正常运行,则按下KEY2后挂起taskLED2,否则恢复taskLED2

cubeMX配置:

代码实现:

/* USER CODE END Header_StartTaskKEY01 */
void StartTaskKEY01(void const * argument)
{
  /* USER CODE BEGIN StartTaskKEY01 */
  /* Infinite loop */
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0 == GPIO_PIN_RESET))
		{
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0 == GPIO_PIN_RESET))
			{
				printf("KEY1按下!\r\n");
				if(taskLED01Handle == NULL)//判断按键1有没有按下
				{
					printf("任务1不存在,准备创建任务1\r\n");
					osThreadDef(taskLED01, StartTaskLED01, osPriorityNormal, 0, 128);
					taskLED01Handle = osThreadCreate(osThread(taskLED01), NULL);
					if(taskLED01Handle !=NULL )
						printf("任务1创建完成!\r\n");
				}
				else
				{
					printf("删除任务1\r\n");
					osThreadTerminate(taskLED01Handle);
					taskLED01Handle=NULL;
				}
			}
			while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET );
		}
    osDelay(10);
  }
  /* USER CODE END StartTaskKEY01 */
}

核心功能

循环检测按键(KEY01)是否被按下,通过按键状态控制taskLED01任务的创建与删除:

  • 当按键按下且taskLED01任务不存在时,创建该任务;
  • 当按键按下且taskLED01任务已存在时,删除该任务。

代码细节分析

  • 无限循环
    任务函数通过for(;;)实现无限循环,持续检测按键状态,符合 FreeRTOS 任务 “永不返回” 的特性。

  • 按键检测与消抖
     

    if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)  //这是一个逻辑判断,结果为0或1
    {
        osDelay(20);  // 延时20ms消抖
        if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0 == GPIO_PIN_RESET))  // 再次检测确认
        {
            // 按键按下后的逻辑...
            while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET );  // 等待按键释放
        }
    }

if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)(先读引脚状态,再与 “低电平” 比较)。

消抖逻辑:第一次检测到按键按下后,延时 20ms(跳过机械抖动阶段),再次检测确认按下,避免误触发。

等待释放while循环等待按键松开,避免一次按下被多次识别。

  • 任务的创建与删除

    • taskLED01Handle == NULL(任务不存在)时:通过osThreadDef定义任务属性(名称、函数、优先级等),再用osThreadCreate创建任务,成功后更新句柄。
    • taskLED01Handle != NULL(任务已存在)时:用osThreadTerminate删除任务,并将句柄设为NULL(标记任务已删除)。
  • 延时调度循环末尾的osDelay(10)让任务让出 CPU,给其他任务运行机会,避免独占系统资源(FreeRTOS 的任务调度依赖延时函数触发)


/* USER CODE END Header_StartTaskKEY02 */
void StartTaskKEY02(void const * argument)
{
  /* USER CODE BEGIN StartTaskKEY02 */
	 static int flag = 0;//标志位
  /* Infinite loop */
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1 == GPIO_PIN_RESET))//当按键2呗按下
		{
			osDelay(20);//消抖
			if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1 == GPIO_PIN_RESET))
			{
				printf("KEY2按下!\r\n");
				if(flag == 0)
				{
					osThreadSuspend(taskLED02Handle);//挂起
					printf("任务2已暂停\r\n");
					flag = 1;
				}
				else
				{
					osThreadResume(taskLED02Handle);
					printf("任务2已恢复\r\n");
					flag = 0;
				}
			}
			while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == GPIO_PIN_RESET );
		}
    osDelay(10);
  }
  /* USER CODE END StartTaskKEY02 */
}

 FreeRTOS 的按键检测任务函数(StartTaskKEY02),核心功能是通过检测按键(连接在 GPIOA 的 PIN1 引脚)的状态,来切换另一个 LED 任务(taskLED02)的 “挂起” 与 “恢复” 状态
 

核心功能

循环检测按键(KEY02)是否被按下,通过一个标志位(flag)记录taskLED02的当前状态,每次按键按下时切换状态:

  • taskLED02正在运行,则将其挂起(暂停执行);
  • taskLED02已被挂起,则将其恢复(继续执行)。

代码细节分析

  1. 静态标志位flag
     

    static int flag = 0; // 标志位

  2. static修饰确保变量只在当前任务函数内可见,且任务运行期间不会被重置(保留上次的值)。
  3. 作用:记录taskLED02的状态 ——0表示任务正在运行,1表示任务已被挂起。
  4. 无限循环与按键检测
    for(;;)  // 无限循环,持续检测按键
    {
      if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1 )== GPIO_PIN_RESET)  // 检测按键是否按下
      {
        osDelay(20);  // 延时20ms消抖(跳过机械抖动阶段)
        if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1 == GPIO_PIN_RESET))  // 再次确认按键按下
        {
          // 按键按下后的逻辑...
          while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == GPIO_PIN_RESET );  // 等待按键释放
        }
      }
      osDelay(10);  // 让出CPU,给其他任务运行机会
    }

  • if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)(先读取引脚状态,再与 “低电平(按下)” 比较)。
  • 消抖逻辑:两次检测按键状态(间隔 20ms),避免按键机械抖动导致的误触发。
  • 等待释放while循环等待按键松开,确保一次按下只触发一次状态切换。

任务的挂起与恢复当按键确认按下后,根据flag的值执行操作:

if(flag == 0)  // 若标志位为0(任务正在运行)
{
  osThreadSuspend(taskLED02Handle);  // 挂起taskLED02(暂停执行)
  printf("任务2已暂停\r\n");
  flag = 1;  // 更新标志位为“已挂起”
}
else  // 若标志位为1(任务已挂起)
{
  osThreadResume(taskLED02Handle);  // 恢复taskLED02(继续执行)
  printf("任务2已恢复\r\n");
  flag = 0;  // 更新标志位为“运行中”
}
    • osThreadSuspend:FreeRTOS 函数,暂停指定任务(任务进入 “挂起态”,不再参与调度)。
    • osThreadResume:FreeRTOS 函数,恢复被挂起的任务(任务重新进入 “就绪态”,等待调度执行)。
  1. 任务调度循环末尾的osDelay(10)是 FreeRTOS 的延时函数,作用是让当前任务(StartTaskKEY02)暂时让出 CPU,允许系统调度其他任务运行,避免当前任务独占资源。

五、队列

什么是队列?

队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断
和任务间传递信息。
为什么不使用全局变量?
如果使用全局变量,免子(任务1)修改了变量a,等待树獭(任务3)处理,但树獭处理速
度很慢,在处理数据的过程中,狐狸(任务2)有可能又修改了变量a,导致树獭有可能得
到的不是正确的数据。

在这种情况下,就可以使用队列。免子和狐狸产生的数据放在流水线上,树獭可以慢慢一个
个依次处理。
关于队列的几个名词:
队列项目:队列中的每一个数据;
队列长度:队列能够存储队列项目的最大数量;
创建队列时,需要指定队列长度及队列项目大小。

队列特点

1. 数据入队出队方式

通常采用先进先出(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取。
也可以配置为后进先出(LIFO)方式,但用得比较少。

2.数据传递方式

采用实际值传递,即将数据拷贝到队列中进行传递,也可以传递指针,在传递较大的数据的
时候采用指针传递。

3.多任务访问

队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息

4.出队、入队阻塞

当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队。
阻塞时间如果设置为:

  • 0:直接返回不会等待;
  • 0-port_MAX_DELAY:等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待;
  • port_MAX_DELAY:死等,一直等到可以入队为止。出队阻塞与入队阻塞类似;

队列相关API函数

1. 创建队列
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize);

参数:

  • uxQueueLength:队列可同时容纳的最大项目数。
  • uxltemSize:存储队列中的每个数据项所需的大小(以字节为单位)。

返回值:
如果队列创建成功,则返回所创建队列的句柄。如果创建队列所需的内存无法分配,则返回NULL。

2.写队列

写队列总共有以下几个函数:

BaseType_t xQueueSend(
    QueueHandle_t xQueue,
    const void * pvItemToQueue,
    TickType_t xTicksToWait
);

参数:

  • xQueue:队列的句柄,数据项将发送到此队列。
  • pvItemToQueue:待写入数据
  • xTicksToWait:阻塞超时时间

返回值:
如果成功写入数据,返回pdTRUE,否则返回errQUEUE_FULL。

3.读队列

读队列总共有以下几个函数:

BaseType_t xQueueReceive(
                        QueueHandle_t xQueue,
                        void *pvBuffer,
                        TickType_t xTicksToWait
                        );

参数:

  • xQueue:待读取的队列
  • pvItemToQueue:数据读取缓冲区
  • xTicksToWait:阻塞超时时间

返回值:
成功返回pdTRUE,否则返回pdFALSE。

实操

实验需求

创建一个队列,按下KEY1向队列发送数据,按下KEY2向队列读取数据。

cube MX配置


 

六、二值信号量

什么是信号量?

信号量(SeMaphore),是在多任务环境下使用的一种机制,是可以用来保证两个或多个关
键代码段不被并发调用。
信号量这个名字,我们可以把它拆分来看,信号可以起到通知信号的作用,然后我们的量还
可以用来表示资源的数量,当我们的量只有0和1的时候,它就可以被称作二值信号量,只有
两个状态,当我们的那个量没有限制的时候,它就可以被称作为计数型信号量。
信号量也是队列的一种。|


什么是二值信号量?

二值信号量其实就是一个长度为1,大小为零的队列,只有0和1两种状态,通常情况下,我
们用它来进行互斥访问或任务同步。
互斥访问:比如门钥匙,只有获取到钥匙才可以开门
任务同步:比如我录完视频你才可以看视频
 

二值信号量相关API函数

  1. 创建二值信号量
    SemaphoreHandle_t xSemaphoreCreateBinary( void)

    参数:

    返回值:
    成功,返回对应二值信号量的句柄;
    失败,返回NULL。
     

  2. 释放二值信号量
    BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore)

    参数:
    xSemaphore:要释放的信号量句柄
    返回值:
    成功,返回 pdPASS;
    失败,返回errQUEUE_FULL。
  3. 获取二值信号量
BaseType_t xSemaphoreTake(
                        SemaphoreHandle_t xSemaphore,
                        TickType_t xTicksToWait 
);

参数:
xSemaphore:要获取的信号量句柄
xTicksToWait:超时时间,0表示不超时,portMAX_DELAY表示卡死等待;
返回值:
成功,返回 pdPASS;
失败,返回errQUEUE_FULL。

七、计数型信号量

什么是计数型信号量?

计数型信号量相当于队列长度大于1的队列,因此计数型信号量能够容纳多个资源,这在计
数型信号量被创建的时候确定的。

计数型信号量相关API函数


计数型信号量的释放和获取与二值信号量完全相同!

SemaphoreHandle_t xSemaphoreCreateCounting(
                                        UBaseType_tuxMaxCount,
                                        UBaseType_tuxInitialCount
                                        );


参数:
uxMaxCount:可以达到的最大计数值
uxInitialCount:创建信号量时分配给信号量的计数值
返回值:
成功,返回对应计数型信号量的句柄;
失败,返回NULL。

八、互斥量

什么是互斥量?

在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同
步,而互斥型信号量用于资源保护。
互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转
现象。

什么是优先级翻转?


以上图为例,系统中有3个不同优先级的任务H/M/L,最高优先级任务H和最低优先级任务L
通过信号量机制,共享资源。目前任务L占有资源,锁定了信号量,TaskH运行后将被阻
塞,直到TaskL释放信号量后,TaskH才能够退出阻塞状态继续运行。但是TaskH在等待
TaskL释放信号量的过程中,中等优先级任务M抢占了任务L,从而延迟了信号量的释放时
间,导致TaskH阻塞了更长时间,这种现象称为优先级倒置或反转。
优先级继承:当一个互斥信号量正在被一个低优先级的任务持有时,如果此时有个高优先级
的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先
级的任务会将低优先级任务的优先级提升到与自己相同的优先级。

优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影
响。


互斥量相关API函数

互斥信号量不能用于中断服务函数中!

SemaphoreHandle_t xSemaphoreCreateMutex(void)


参数:

返回值:
成功,返回对应互斥量的句柄;
失败,返回NULL。

九、事件标志组

什么是事件标志组?

事件标志位:表明某个事件是否发生,联想:全局变量flag。通常按位表示,每一个位表示
一个事件(高8位不算)
事件标志组是一组事件标志位的集合,可以简单的理解事件标志组,就是一个整数。
事件标志组本质是一个16位或32位无符号的数据类型EventBits_t,由configUSE_16_BIT_TICKS决定。
虽然使用了32位无符号的数据类型变量来存储事件标志,但其中的高8位用作存储事件标志
组的控制信息,低24位用作存储事件标志,所以说一个事件组最多可以存储24个事件标志!

事件标志组相关API函数

  1. 创建事件标志位
    EventGroupHandle_t xEventGroupCreate(void );

    参数:

    返回值:
    成功,返回对应事件标志组的句柄;
    失败,返回NULL。

  2. 设置事件标志位
    EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
    const EventBits_tuxBitsToSet );

    参数:
    xEventGroup:对应事件组句柄。
    uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值。
    返回值:
    设置之后事件组中的事件标志位值。

  3. 清除事件标志位
    EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToClear

    参数:
    xEventGroup:对应事件组句柄。
    uxBitsToClear:指定要在事件组中清除的一个或多个位的按位值。
    返回值:
    清零之前事件组中事件标志位的值。

  4. 等待事件标志位
    EventBits_t xEventGroupwaitBits
    const EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToWaitFor,
    const BaseType_t xClearOnExit,
    const BaseType_t xWaitForAllBits,
    TickType_t xTicksToWait );

    参数:
    xEventGroup:对应的事件标志组句柄
    uxBitsToWaitFor:指定事件组中要等待的一个或多个事件位的按位值
    xClearOnExit:pdTRUE——清除对应事件位,pdFALSE——不清除
    xWaitForAlIBits:pdTRUE——所有等待事件位全为1(逻辑与),pdFALSE—等待的事件
    位有一个为1(逻辑或)
    xTicksToWait:超时
    返回值:
    等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位
    其他值:等待事件标志位失败,返回事件组中的事件标志位

十、任务通知

什么十任务通知?

FreeRTOS从版本V8.2.0开始提供任务通知这个功能,每个任务都有一个32位的通知值。
按照FreeRTOS官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快45%,并且更加省内存(无需创建队列)。
在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度
为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快、使用的RAM更
少!

任务通知值得更新方式

FreeRTOS提供以下几种方式发送通知给任务:

  • ·发送消息给任务,如果有通知未读,不覆盖通知值
  • ·发送消息给任务,直接覆盖通知值
  • ·发送消息给任务,设置通知值的一个或者多个位
  • ·发送消息给任务,递增通知值

通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等。

任务通知的优势和劣势
任务通知的优势
  1. 使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。
  2. 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的劣势
  1. 只有任务可以等待通知,中断服务函数中不可以,因为中断没有TCB(在任务中开辟一个空间)。
  2. 通知只能一对一,因为通知必须指定任务。
  3. 等待通知的任务可以被阻塞,但是发送消息的任务,任何情况下都不会被阻塞等待。
  4. 任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据。
任务通知相关API函数
  1. 发送通知
    BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction );

    参数:
    xTaskToNotify:需要接收通知的任务句柄;
    ulValue:用于更新接收任务通知值,具体如何更新由形参eAction决定;
    eAction:一个枚举,代表如何使用任务通知的值;

    返回值:
    如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回
    pdFALSE,而其他情况均返回pdPASS。
    BaseType_t xTaskNotifyAndQuery(TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction,
    uint32_t *pulPreviousNotifyValue );

    参数:
    xTaskToNotify:需要接收通知的任务句柄;
    ulValue:用于更新接收任务通知值,具体如何更新由形参eAction决定;
    eAction:一个枚举,代表如何使用任务通知的值;
    pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为NULL,则不需要回传,这个时候就等价于函数xTaskNotify( )。
    返回值:
    如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回
    pdFALSE,而其他情况均返回pdPASS。

    BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);

    参数:
    xTaskToNotify:接收通知的任务句柄,并让其首身的任务通知值加1。
    返回值:
    总是返回pdPASS。

  2. 等待通知
    等待通知API函数只能用在任务,不可应用于中断中!
    uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
    TickType_t xTicksToWait );

    参数:
    xClearCountOnExit:指定在成功接收通知后,将通知值清零或减1,pdTRUE:把通知值清
    零(二值信号量);pdFALSE:把通知值减一(计数型信号量);
    xTicksToWait:阻塞等待任务通知值的最大时间;
    返回值:
    0:接收失败
    非0:接收成功,返回任务通知的通知值
    BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
    uint32_t ulBitsToClearOnExit,
    uint32_t *pulNotificationValue,
    TickType_t xTicksToWait);
    ulBitsToClearOnEntry:函数执行前清零任务通知值那些位。
    ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,在清0前,接收到的任
    务通知值会先被保存到形参*pulNotificationValue中。
    pulNotificationValue:用于保存接收到的任务通知值。如果不需要使用,则设置为NULL
    即可。
    xTicksToWait:等待消息通知的最大等待时间。

十一、延时函数

什么是延时函数?

在 FreeRTOS 中,常用的延时函数主要有两个:vTaskDelay() 和 vTaskDelayUntil(),此外在使用 CMSIS-RTOS 封装层时还会用到 osDelay()(本质是对vTaskDelay()的封装)。这些函数的核心作用是让当前任务进入阻塞态(释放 CPU 资源,允许其他任务运行),等待指定时间后再进入就绪态。

延时函数分类

相对延时:vTaskDelay
绝对延时:vTaskDelayUntil

vTaskDelay与 HAL_Delay 的区别

TaskDelay作用是让任务阻塞,任务阻塞后,RTOS系统调用其它处于就绪状态的优先级最
高的任务来执行。
HAL_Delay一直不停的调用获取系统时间的函数,直到指定的时间流逝然后退出,故其占用
了全部CPU时间。

十二、软件定时器

什么是定时器?

简单可以理解为闹钟,到达指定一段时间后,就会响铃。
TSTM32芯片自带硬件定时器,精度较高,达到定时时间后会触发中断,也可以生成PWM、
输入捕获、输出比较,等等,功能强大,但是由于硬件的限制,个数有限。
软件定时器也可以实现定时功能,达到定时时间后可调用回调函数,可以在回调函数里处理
信息。


软件定时器优缺点

优点:
1.简单、成本低;
2.只要内存足够,可创建多个;
缺点:
精度较低,容易受中断影响。在大多数情况下够用,但对于精度要求比较高的场合不建议使
用。


软件定时器原理

定时器是一个可选的、不属于FreeRTOS内核的功能,它是由定时器服务任务来提供的。
在调用函数vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器
的任务,这个任务就叫做软件定时器服务任务。
1.负责软件定时器超时的逻辑判断
2.调用超时软件定时器的超时回调函数
3.处理软件定时器命令队列
FreeRTOS提供了很多定时器有关的API函数,这些API函数大多都使用FreeRTOS的队列发送
命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给FreeRTOS的软件定时器使用的,用户不能直接访问!

软件定时器相关配置

软件定时器有一个定时器服务任务和定时器命令队列,这两个东西肯定是要配置的,相关的
配置也是放到文件FreeRTOSConfig.h中的,涉及到的配置如下:
1、configUSE_TIMERS
如果要使用软件定时器的话宏configUSE_TIMERS一定要设置为1,当设置为1的话定时器服
务任务就会在启动FreeRTOS调度器的时候自动创建。
2、configTIMER_TASK_PRIORITY
设置软件定时器服务任务的任务优先级,可以为0~(configMAX_PRIORITIES-1)。优先级一定
要根据实际的应用要求来设置。如果定时器服务任务的优先级设置的高的话,定时器命令队
列中的命令和定时器回调函数就会及时的得到处理。
3、configTIMER_QUEUE_LENGTH
此宏用来设置定时器命令队列的队列长度。
4、configTIMER_TASK_STACK_DEPTH
此宏用来设置定时器服务任务的任务堆栈大小。

单次定时器和周期定时器

单次定时器:只超时一次,调用一次回调函数。可手动再开启定时器;

周期定时器:多次超时,多次调用回调函数。

软件定时器相关API函数

1.创建软件定时器
TimerHandle_t xTimerCreate
(const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );

参数:
pcTimerName:软件定时器名称
xTimerPeriodInTicks:定时超时时间,单位:系统时钟节拍。宏pdMS_TO_TICKSO可用于
将以毫秒为单位指定的时间转换为以tick为单位指定的时间。
uxAutoReload:定时器模式,pdTRUE:周期定时器,pdFALSE:单次定时器
pvTimerID:软件定时器ID,用于多个软件定时器公用一个超时回调函数
pxCallbackFunction:软件定时器超时回调函数
返回值:
成功:定时器句柄
失败:NULL

2.开启软件定时器
BaseType_t xTimerStart (TimerHandle_t xTimer,
TickType_t xBlockTime );

参数:
xTimer:待开启的软件定时器的句柄
xTickToWait:发送命令到软件定时器命令队列的最大等待时间
返回值:
pdPASS:开启成功
pdFAIL:开启失败

3.停止软件定时器
BaseType_t xTimerStop(TimerHandle_t xTimer,
TickType_t xBlockTime );

参数与返回值同上。

4.复位软件定时器
BaseType_t xTimerReset(TimerHandle_t xTimer,
TickType_t xBlockTime );

参数与返回值同上。
该功能将使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻
重新定时。

5.更改软件定时器定时时间
BaseType_t xTimerChangePeriod(TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime);

xNewPeriod:新的定时超时时间,单位:系统时钟节拍。
其余参数与返回值同上。

十三、中断管理

中断定义

请参考51及STM32中断相关课程。

中断优先级

任何中断的优先级都大于任务!
在我们的操作系统,中断同样是具有优先级的,并且我们也可以设置它的优先级,但是他的
优先级并不是从0~15,默认情况下它是从5~15,0~4这5个中断优先级不是FreeRTOS
控制的(5是取决于configMAX_SYSCALL_INTERRUPT_PRIORITY)。

相关注意

1.在中断中必需使用中断相关的函数;
2.中断服务函数运行时间越短越好

Logo

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

更多推荐