编辑推荐: |
本文主要介绍了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); |
|
} |
|
|
|
} |
|
|