编辑推荐: |
本文主要介绍了FreeRTOS实时系统
相关内容。 希望对您的学习有所帮助。
本文来自于博客园,由火龙果软件Linda编辑、推荐。
|
|
一.FreeRTOS介绍
1.什么是FreeRTOS?
free即免费,RTOS的全称是Real time operating system,即实时操作系统。
注意:RTOS不是指一个确定的系统,而是指一类的系统。比如:us/OS,FreeOS,RTX,RT-Thread等,这些都是RTOS类操作系统。
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间
管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少
数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系
统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植
到各种单片机上运行,其最新版本为10.4.4版。
(以上来自百度百科)
2.为什么选择 FreeRTOS ?
FreeRTOS 是免费的;
很多半导体厂商产品的SDK(Software Development Kit)软件开发工具包,就使用FreeRTOS作为其
操作系统,尤其是WIFI、蓝牙这些带有协议栈的芯片或模块。
简单,因为FreeRTOS的文件数量很少。
二、FreeRTOS移植到STM32芯片上
使用Cubemx快速移植,
创建一个模板项目
打开mcu

选中芯片

选中system core
配置SYS

配置RCC


配置串口,用于打印调试

选中FreeRTOS系统

项目生成配置


选择generate code,生成代码
选择open project

在usart.c中加入
| | | #include "stdio.h" | | int fputc(int ch, FILE *f) | | { | | unsigned char temp[1]={ch}; | | HAL_UART_Transmit(&huart1,temp,1,0xffff); | | return ch; | | } | | |
|
在main中测试是否打印到串口上
| #include "stdio.h" | | | | printf("hello world!\r\n"); | | |
|
一些常见问题:
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. 内核配置、函数使能的一些翻译



三、任务的创建与删除
1.什么是任务?
任务可以理解为进程或者是一个任务,会在内存开辟一个空间用于执行任务。
比如,在电脑打开记事本,微信等都是一个任务。
任务通常含有一个while(1)死循环
2.任务创建与删除相关函数

动态创建与静态创建的区别:
动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。
大多数情况使用动态创建任务。
xTaskCreate()函数原型

pvTaskCode:指向任务函数的指针,任务必须实现为永不返回(即连续循环);2. pcName:任务的名字,主要是用来调试,默认情况下最大长度是16;
pvParameters:指定的任务栈的大小;
uxPriority:任务优先级,数值越大,优先级越大;
pxCreatedTask:用于返回已创建任务的句柄可以被引用。

官方案例:
| | | void vTaskCode( void * pvParameters ) | | { | | | | | | | | | | | | | | } | | } | | | | void vOtherFunction( void ) | | { | | BaseType_t xReturned; | | TaskHandle_t xHandle = NULL; | | | | xReturned = xTaskCreate( | | vTaskCode, | | "NAME", | | STACK_SIZE, | | ( void * ) 1, | | tskIDLE_PRIORITY, | | &xHandle ); | | | | if( xReturned == pdPASS ) | | { | | | | vTaskDelete( xHandle ); | | } | | } |
|
vTaskDelete函数原型:
| void vTaskDelete(TaskHandle_t xTaskToDelete); |
|
只需将待删除的任务句柄传入该函数,即可将该任务删除。
当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。
3.实现  四、任务调度
什么是任务调度器?
调度器就是使用相关的调度算法来决定当前执行到哪个任务了
FreeRTOS中任务调度器的函数是vTaskStartScheduler(),在FreeRTOS被封装为osKernelStart()
调度器规则:
FreeRTOS是一个实时操作系统,需要遵循一些调度规则:
抢占式调度:高优先级的抢占低优先级的,永远都是高优先级的优先执行
时间片调度:同等优先级的,谁先抢占到CPU资源,谁先执行。
抢占式调度运行过程:  时间片调度运行过程:

任务的状态
FreeRTOS中有4种状态:
Running运行态:当前任务正在使用CPU的资源,任务正在执行。
Ready就绪态:当前任务等待CPU的调度,任务等待运行。
Blocked阻塞态:当前任务因为一些延时、或等待信号量、消息队列、事件标记组而处于一个阻塞的状态。
Suspended挂起态:类似暂停,通过调用vTaskSuspend()对指定的任务进行挂起。需用通过调用vTaskResume()才能恢复。

总结:
只有就绪态才能变为运行态
其他状态只有调用指定的函数,先成功就绪态才能变为运行态。
任务小实验:
需求:
建 4 个任务:taskLED1,taskLED2,taskKEY1,taskKEY2,任务要求如下:
taskLED1:间隔 500ms 闪烁 LED1;
taskLED2:间隔 1000ms 闪烁 LED2;
taskKEY1:如果 taskLED1 存在,则按下 KEY1 后删除 taskLED1 ,否则创建
taskLED1 ;
taskKEY2:如果 taskLED2 正常运行,则按下 KEY2 后挂起 taskLED2 ,否则恢复
taskLED2
cubeMX配置:


代码实现:
| | | | | | | | | | | | | | | void StartTaskLED1(void const * argument) | | { | | | | | | for(;;) | | { | | HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8); | | osDelay(500); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskLED2(void const * argument) | | { | | | | | | for(;;) | | { | | HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_9); | | osDelay(1000); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskKEY1(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | printf("KEY1按下了!\r\n"); | | if(taskLED1Handle == NULL) | | { | | printf("任务1不存在,开始创建\r\n"); | | osThreadDef(taskLED1, StartTaskLED1, osPriorityNormal, 0, 128); | | taskLED1Handle = osThreadCreate(osThread(taskLED1), NULL); | | if(taskLED1Handle != NULL) | | printf("任务1创建成功\r\n"); | | } | | else | | { | | osThreadTerminate(taskLED1Handle); | | taskLED1Handle = NULL; | | printf("任务1已删除\r\n"); | | } | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskKEY2(void const * argument) | | { | | | | | | static int flag = 0; | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | if(flag == 0) | | { | | osThreadSuspend(taskLED2Handle); | | printf("任务2已暂停\r\n"); | | flag = 1; | | } | | else | | { | | osThreadResume(taskLED2Handle); | | printf("任务2已恢复\r\n"); | | flag = 0; | | } | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
五、队列
什么队列?
队列又称消息队列,是任务通信的一种数据结构,队列可以在任务与任务之间中断和任务间的传递消息。
为啥不能使用全局变量呢?
全局的变量,在多任务修改的情况下,获取的不是最新的值,容易导致数据的错读。
队列的特点
1.入队出队方式
通常先进先出(FIFO),数据先进去的先被读取。
还有一种后进先出(LIFO),比较少用,一般有于栈。
2.数据传递方式
采用实际值传递,则将数据拷贝到队列进行传递,也可以使用指针,在数据比较大的时候通常采用指针传递。
3、多任务访问
队列不属于某个任务,任何任务都可以使用队列发送跟接收数据。
4、出队、入队阻塞
当向一个队列发送消息是,可以指定一个阻塞时间,假设次数当队列已满无法入队时,可以设置一个阻塞时间:
0:直接返回,不阻塞。
0-portMAX_DELAY:设置指定的时间,超过该时间还没进入队列直接返回。
portMAX_DELAY:死等,等到队列有空闲位置为止。
队列相关API
1.创建队列
| QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, | | UBaseType_t uxItemSize ); |
|
参数:
uxQueueLength:队列的可容纳的长度
uxItemSize:一个队列容纳的大小
返回值:
如何一个队列创建成功,会分配到内存,反之失败则为NULL
2.写队列API 
| BaseType_t xQueueSend( | | QueueHandle_t xQueue, | | const void * pvItemToQueue, | | TickType_t xTicksToWait | | ); |
|
参数:
xQueue:队列的句柄,数据项将发送到此队列。
pvItemToQueue:待写入数据
xTicksToWait:阻塞超时时间
返回值:
如果成功写入数据,返回 pdTRUE,否则返回 errQUEUE_FULL。
3、读队列API

| BaseType_t xQueueReceive( | | QueueHandle_t xQueue, | | void *pvBuffer, | | TickType_t xTicksToWait | | ); |
|
参数:
xQueue:待读取的队列
pvItemToQueue:数据读取缓冲区
xTicksToWait:阻塞超时时间
返回值:
成功返回 pdTRUE,否则返回 pdFALSE。
实验:
创建一个队列,按下 KEY1 向队列发送数据,按下 KEY2 向队列读取数据。
cubeMX配置  代码实现
| | | | | | | | | | | | | | | void StartTaskSend(void const * argument) | | { | | | | uint16_t buf = 100; | | BaseType_t status; | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET) | | { | | status = xQueueSend(myQueue01Handle,&buf,0); | | if(status == pdTRUE) | | printf("数据发送成功:%d \r\n",buf); | | else | | printf("数据发送失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskReceive(void const * argument) | | { | | | | uint16_t buf = 100; | | BaseType_t status; | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET) | | { | | status = xQueueReceive(myQueue01Handle,&buf,0); | | if(status == pdTRUE) | | printf("数据接收成功:%d \r\n",buf); | | else | | printf("数据接收失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
六、二值信号量
什么是信号量?
信号量(semaphore)实在多任务环境下使用的一种机制,是可以用来保证两个或者多个关键代码段不被并发调用。
信号量可以拆分为信号和量,信号起到了通知的作用,而量表示资源的数量,当我们的量只有0和1的时候,我们称为二值信号量,只有两种状态,当我们那个量没有限制时,称为计数型信号量。
什么是二值信号量?
二值信号量其实是一个长度为1,大小为1的队列,只有0和1两种状态,通常用它进行互斥访问或任务同步。  二值信号量API

创建二值信号量
| SemaphoreHandle_t xSemaphoreCreateBinary( void ) |
|
参数:
无
返回值:
成功,返回对应二值信号量的句柄;
失败,返回 NULL 。
释放二值信号量
| BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ) |
|
参数:
xSemaphore:要释放的信号量句柄
返回值:
成功,返回 pdPASS ;
失败,返回 errQUEUE_FULL 。
获取二值信号量
| BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, | | TickType_t xTicksToWait ); |
|
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
参数:
xSemaphore:要获取的信号量句柄
xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
返回值:
成功,返回 pdPASS ;
失败,返回 errQUEUE_FULL 。
实现:
创建一个二值信号量,按下 KEY1 则释放信号量,按下 KEY2 获取信号量。
cubeMX配置  代码实现
| osSemaphoreDef(myBinarySem); | | | | myBinarySemHandle = xSemaphoreCreateBinary(); | | | | | | | | | | | | | | | | | | | | | | void StartTaskSemaphoreGive(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | if(xSemaphoreGive(myBinarySemHandle) == pdTRUE) | | printf("二极信号放入成功\r\n"); | | else | | printf("二极信号放入失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskSemaphoreTake(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | if(xSemaphoreTake(myBinarySemHandle, 0) == pdTRUE) | | printf("二极信号获取成功\r\n"); | | else | | printf("二极信号获取失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
七、计数型信号量
计量型信号量相当于长度大于1的队列,能容纳更多资源,在创建时确定了长度大小。
应用场景:
一个停车场有固定的停车位,当一辆车开进来时,车位减一,一辆车开走时,车位加一,当车位满了,表示队列装满了,此时再进来只能等待或者是开走。
计数型相关API函数

计数型信号量的释放和获取与二值信号量完全相同 !
| SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, | | UBaseType_t uxInitialCount); |
|
参数:
uxMaxCount:可以达到的最大计数值
uxInitialCount:创建信号量时分配给信号量的计数值
返回值:
成功,返回对应计数型信号量的句柄;
失败,返回 NULL 。
实现案例:
创建一个计数型信号量,按下 KEY1 则释放信号量,按下 KEY2 获取信号量。
cubeMX配置
将USE_COUNTING_SEMAPHORES的值从Disabled改成Enabled


代码实现
| | | | | | | | | | | | | | | void StartTaskSemaphoreGive(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | if(xSemaphoreGive(myCountingSemaHandle) == pdTRUE) | | printf("计数信号量放入成功\r\n"); | | else | | printf("计数信号量放入失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskSemaphoreTake(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | if(xSemaphoreTake(myCountingSemaHandle, 0) == pdTRUE) | | printf("计数信号量获取成功\r\n"); | | else | | printf("计数信号量获取失败\r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
八、互斥量
什么是互斥量?
在多数情况下,互斥型信号量与二值型信号量非常相似,但功能上,二值型信号量用于同步,而互斥型信号量用于资源保护。
互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现象。
什么是优先级翻转?  如图上所示,当低优先级获得CPU资源时,还没执行完就会被高优先级的任务打断,如果执行重要的东西时,容易发送错误,所以互斥型信号量就起到作用了,低优先级
在获取到CPU资源时,不会被其他高优先级的任务打断,直到释放信号。
互斥量相关API
互斥信号量不能用于中断服务函数中!

| SemaphoreHandle_t xSemaphoreCreateMutex( void ) |
|
参数:
无
返回值:
成功,返回对应互斥量的句柄;
失败,返回 NULL 。
实现
演示优先级翻转
使用互斥量优化优先级翻转问题
cubeMX配置  代码实现
创建了三个任务StartTaskH、StartTaskM、StartTaskL
| | | | | | | | | | | | | | | void StartTaskH(void const * argument) | | { | | | | | | for(;;) | | { | | xSemaphoreTake(myMutexHandle,portMAX_DELAY); | | printf("TaskH:我在上厕所...\r\n"); | | HAL_Delay(1000); | | printf("TaskH:我出来了...\r\n"); | | xSemaphoreGive(myMutexHandle); | | osDelay(1000); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskM(void const * argument) | | { | | | | | | for(;;) | | { | | printf("TaskM:我占用CPU资源\r\n"); | | osDelay(1000); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskL(void const * argument) | | { | | | | | | for(;;) | | { | | xSemaphoreTake(myMutexHandle,portMAX_DELAY); | | printf("TaskL:我在上厕所...\r\n"); | | HAL_Delay(3000); | | printf("TaskL:我出来了...\r\n"); | | xSemaphoreGive(myMutexHandle); | | osDelay(1000); | | } | | | | } | | |
|
九、事件标志组
什么是事件标志组?
事件标志位:表示某件事是否发生,联想:全局变量flag,通常每个位表示一个事件(高8位不算)
事件标志组:是一组事件标志位的集合,也就是一个整数
事件标志组本质是一个 16 位或 32 位无符号的数据类型 EventBits_t ,由 configUSE_16_BIT_TICKS
决定。
虽然使用了 32 位无符号的数据类型变量来存储事件标志, 但其中的高8位用作存储事件标志组的控制信息,
低 24 位用作存储事件标志 ,所以说一个事件组最多可以存储 24 个事件标志!  事件标志组API

1.创建事件标志组
| EventGroupHandle_t xEventGroupCreate( void ); |
|
参数:
无
返回值:
成功,返回对应事件标志组的handle;
失败,返回 NULL 。
2.设置事件标志位
| EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, | | const EventBits_t uxBitsToSet ); |
|
参数:
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:对应的事件标志组handle
uxBitsToWaitFor:指定事件组中要等待的一个或多个事件位的按位值
xClearOnExit:pdTRUE——清除对应事件位,pdFALSE——不清除
xWaitForAllBits:pdTRUE——所有等待事件位全为1(逻辑与),pdFALSE——等待的事件位有一个为1
(逻辑或)
xTicksToWait:超时
返回值:
等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位
其他值:等待事件标志位失败,返回事件组中的事件标志位
实现:
创建一个事件标志组和两个任务( task1 和 task2),task1 检测按键,如果检测到 KEY1
和 KEY2 都按过,
则执行 task2 。
| | | EventGroupHandle_t eventgroup_handle; | | | | | | | | | | eventgroup_handle = xEventGroupCreate(); | | | | | | | | | | | | | | | | | | | | | | void StartTask1(void const * argument) | | { | | | | | | for(;;) | | { | | | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | xEventGroupSetBits(eventgroup_handle, 0x01); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | xEventGroupSetBits(eventgroup_handle, 0x02); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTask2(void const * argument) | | { | | | | EventBits_t result; | | | | for(;;) | | { | | | | result = xEventGroupWaitBits(eventgroup_handle, 0x01 | 0x02 , pdTRUE, pdFALSE , portMAX_DELAY); | | printf("同意请假了:%#x \r\n",result); | | osDelay(1); | | } | | | | } |
|
十、任务通知
什么是任务通知?
FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照
FreeRTOS
官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队
列)。
在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可
以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!
任务通知值的更新方式
FreeRTOS 提供以下几种方式发送通知给任务 :
发送消息给任务,如果有通知未读, 不覆盖通知值
发送消息给任务,直接覆盖通知值
发送消息给任务,设置通知值的一个或者多个位
发送消息给任务,递增通知值
通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等。
任务通知的优势和劣势
任务通知的优势
使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。
使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的劣势
只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB 。
通知只能一对一,因为通知必须指定任务。
等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。
任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数
据。
任务通知相关API函数
1.发送通知

| BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, | | uint32_t ulValue, | | eNotifyAction eAction ); |
|
参数:
xTaskToNotify:需要接收通知的任务handle;
ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;
eAction:一个枚举,代表如何使用任务通知的值;

返回值:
如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE, 而其他
情况均返回pdPASS。
| BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify, | | uint32_t ulValue, | | eNotifyAction eAction, | | uint32_t *pulPreviousNotifyValue ); |
|
参数:
xTaskToNotify:需要接收通知的任务handle;
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:等待消息通知的最大等待时间。
实现:
模拟二值信号量
| | | | | | | | | | | | | | | void StartTaskSend(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | xTaskNotifyGive(TaskReceiveHandle); | | printf("任务通知:二值信号量数据发送成功! \r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskReceive(void const * argument) | | { | | | | uint32_t rev = 0; | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | rev = ulTaskNotifyTake(pdTRUE,portMAX_DELAY); | | if(rev != 0) | | printf("任务通知:二值信号量数据接收成功!rev=%d \r\n",rev); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
模拟计数型信号量
| | | | | | | | | | | | | | | void StartTaskSend(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | xTaskNotifyGive(TaskReceiveHandle); | | printf("任务通知:计数信号量数据发送成功! \r\n"); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskReceive(void const * argument) | | { | | | | uint32_t rev = 0; | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | rev = ulTaskNotifyTake(pdFALSE,portMAX_DELAY); | | if(rev != 0) | | printf("任务通知:计数信号量数据接收成功!rev=%d \r\n",rev); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } |
|
模拟事件标志组
| | | | | | | | | | | | | | | void StartTaskSend(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | printf("按下了KEY1,发送0x01\r\n"); | | xTaskNotify(TaskReceiveHandle,0x01,eSetBits); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | printf("按下了KEY2,发送0x02\r\n"); | | xTaskNotify(TaskReceiveHandle,0x02,eSetBits); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskReceive(void const * argument) | | { | | | | uint32_t notify_val = 0, event_bit = 0; | | | | for(;;) | | { | | xTaskNotifyWait(0,0xFFFFFFFF,¬ify_val, portMAX_DELAY); | | if(notify_val & 0x01) | | event_bit |= 0x01; | | if(notify_val & 0x02) | | event_bit |= 0x02; | | | | if(event_bit == (0x01 | 0x02)) | | { | | event_bit = 0; | | printf("请假成功\r\n"); | | } | | | | osDelay(10); | | } | | | | } |
|
模拟邮箱
| | | | | | | | | | | | | | | void StartTaskSend(void const * argument) | | { | | | | | | for(;;) | | { | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET) | | { | | printf("按下了KEY1\r\n"); | | xTaskNotify(TaskReceiveHandle,1,eSetValueWithOverwrite); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET); | | } | | | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | osDelay(20); | | if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET) | | { | | printf("按下了KEY2\r\n"); | | xTaskNotify(TaskReceiveHandle,0x02,eSetValueWithOverwrite); | | } | | while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET); | | } | | osDelay(10); | | } | | | | } | | | | | | | | | | | | | | | | | | void StartTaskReceive(void const * argument) | | { | | | | uint32_t notify_val = 0; | | | | for(;;) | | { | | xTaskNotifyWait(0,0xFFFFFFFF,¬ify_val, portMAX_DELAY); | | printf("接收到的通知值为:%d \r\n",notify_val); | | osDelay(10); | | } | | | | } |
|
十一、延时函数
什么是延时函数?
延时函数是延迟执行
延时函数分类
相对延时:vtaskDelay
不能在延迟时间中发生中断
绝对延时:vTaskDelayUntil
延时时间能在中断中,直到延时结束
vtaskDelay与HAL_DELAY区别

vTaskDelay 作用是让任务阻塞,任务阻塞后,RTOS系统调用其它处于就绪状态的优先级最高的任务来执
行。
HAL_Delay 一直不停的调用获取系统时间的函数,直到指定的时间流逝然后退出,故其占用了全部CPU时
间。
十二、软件定时器
什么是定时器?
简单可以理解为闹钟,到达指定一段时间后,就会响铃。
STM32 芯片自带硬件定时器,精度较高,达到定时时间后会触发中断,也可以生成 PWM 、输入捕获、输出
比较,等等,功能强大,但是由于硬件的限制,个数有限。
软件定时器也可以实现定时功能,达到定时时间后可调用回调函数,可以在回调函数里处理信息。
软件定时器优缺点
优点:
简单、成本低;
只要内存足够,可创建多个;
缺点:
精度较低,容易受中断影响。在大多数情况下够用,但对于精度要求比较高的场合不建议使用。
软件定时器原理
定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务任务来提供的。
在调用函数 vTaskStartScheduler() 开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个
任务就叫做软件定时器服务任务。
负责软件定时器超时的逻辑判断
调用超时软件定时器的超时回调函数
处理软件定时器命令队列
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_TICKS()
可用于将以毫秒为单位
指定的时间转换为以 tick 为单位指定的时间。
uxAutoReload:定时器模式, pdTRUE:周期定时器, pdFALSE:单次定时器
pvTimerID:软件定时器 ID,用于多个软件定时器公用一个超时回调函数
返回值:
成功:定时器handle
失败: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. 更改软件定时器定时时间
xNewPeriod:新的定时超时时间,单位:系统时钟节拍。
其余参数与返回值同上。
实现:
创建两个定时器:
定时器1,周期定时器,每1秒打印一次 liangxu shuai
定时器2,单次定时器,启动后 2 秒打印一次 laochen shuai
cubeMX配置
software timer ope修改USE_TIMERS的值为Enable


代码实现:
| | | | | | | | | | | | | | | void StartDefaultTask(void const * argument) | | { | | | | osTimerStart(myTimer01Handle, 1000); | | | | osTimerStart(myTimer02Handle, 2000); | | | | for(;;) | | { | | osDelay(1); | | } | | | | } | | | | | | void Callback01(void const * argument) | | { | | | | printf("callback01\r\n"); | | | | } | | | | | | void Callback02(void const * argument) | | { | | | | printf("callback02\r\n"); | | | | } |
|
十三、中断管理
中断定义
中断(Interrupt)是一种由硬件或软件触发的信号,用于通知处理器需要立即处理某个事件。中断机制允许处理器暂停当前任务,转而去执行高优先级的任务(中断服务程序,ISR),处理完成后恢复原任务。
中断优先级
任何中断的优先级都大于任务!
在我们的操作系统,中断同样是具有优先级的,并且我们也可以设置它的优先级,但是他的优先级并不是从 0~15
,默认情况下它是从 5~15 ,0~4 这 5 个中断优先级不是 FreeRTOS 控制的(5是取决于
configMAX_SYSCALL_INTERRUPT_PRIORITY)。
相关注意
在中断中必需使用中断相关的函数;
中断服务函数运行时间越短越好。
实现
创建一个队列及一个任务,按下按键 KEY1 触发中断,在中断服务函数里向队列里发送数据,任务则阻塞接
收队列数据。
cubeMX配置


代码实现
| | | #include "cmsis_os.h" | | | | | | | | | | void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) | | { | | | | uint32_t send_data = 1; | | xQueueSendFromISR(myQueue01Handle,&send_data,NULL); | | } | | |
|
freetos.c
| | | | | | | | | | | | | | | void StartDefaultTask(void const * argument) | | { | | | | uint32_t rev; | | | | for(;;) | | { | | if(xQueueReceive(myQueue01Handle, &rev, portMAX_DELAY) == pdTRUE) | | printf("rev = %d \r\n",rev); | | osDelay(1); | | } | | | | } |
|
|