FreeRTOS
初识FreeRTOS
什么是FreeRTOS
RTOS (实时操作系统)并不是指某一特定的操作系统,而是指一类操作系统,例如, µC/OS,FreeRTOS, RTX, RT-Thread 等这些都是 RTOS 类的操作系统。 因此,从 FreeRTOS 的名字中就能看出, FreeROTS 是一款免费的实时操作系统。
操作系统是允许多个任务“同时运行” 的, 然而实际上, 一个 CPU 核心在某一时刻只能运行一个任务,而操作系统中任务调度器的责任就是决定在某一时刻 CPU 究竟要运行哪一个任务,任务调度器使得 CPU 在各个任务之间来回切换并处理任务, 由于切换处理任务的速度非常快,因此就给人造成了一种同一时刻有多个任务同时运行
的错觉。
FreeRTOS 是众多 RTOS 类操作系统中的一种, FreeRTOS 十分的小巧,可以在资源有限的微控制器中运行,当然了, FreeRTOS 也不仅仅局限于在微控制器中使用。 就单从文件数量上来看 FreeRTOS 要比 µC/OS 少得多。
为什么选择FreeRTOS
- 免费
- 简单
- 使用广泛
- 资料齐全
- 可移植性强
FreeRTOS的特点
FreeRTOS移植
使用CubeMX快速移植
-
在 SYS 选项里,将 Debug 设为 Serial Wire ,并且将 Timebase Source 设为 TIM2 (其它定时器也行)。为何要如此配置?下文解说。
-
将 RCC 里的 HSE 设置为 Crystal/Ceramic Resonator
-
时钟按下图配置
-
选择 FREERTOS 选项,并将 Interface 改为 CMSIS_V1 。V1 和 V2 有啥区别?下文解释。
-
配置项目信息,并导出代码。
一些常见问题
-
Timebase Source 为什么不能设置为 SysTick ?裸机的时钟源默认是 SysTick,但是开启FreeRTOS 后,FreeRTOS会占用 SysTick (用来生成1ms 定时,用于任务调度),所以需要为其他总线提供另外的时钟源。
-
FreeRTOS 版本问题
V2 的内核版本更高,功能更多,在大多数情况下 V1 版本的内核完全够用。 -
FreeRTOS 各配置选项卡的解释
-
Events:事件相关的创建
Task and Queues: 任务与队列的创建
Timers and Semaphores: 定时器和信号量的创建
Mutexes: 互斥量的创建
FreeRTOS Heap Usage: 用于查看堆使用情况
config parameters: 内核参数设置,用户根据自己的实际应用来裁剪定制 FreeRTOS 内核 Include parameters: FreeRTOS 部分函数的使能
User Constants: 相关宏的定义,可以自建一些常量在工程中使用
Advanced settings:高级设置
-
FreeRTOS系统配置
FreeRTOSConfig.h 文件
FreeRTOS 使用 FreeRTOSConfig.h 文件进行配置和裁剪。 FreeRTOSConfig.h 文件中有几十个配置项,这使得用户能够很好地配置和裁剪 FreeRTOS。
FreeRTOSConfig.h 文件中的配置项可分为三大类:“config”配置项、“INCLUDE”配置项和其他配置项。
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 头文件 */
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include <stdint.h>
extern uint32_t SystemCoreClock;
/* 基础配置项 */
#define configUSE_PREEMPTION 1 /* 1: 抢占式调度器, 0: 协程式调度器, 无默认需定义 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 /* 1: 使用硬件计算下一个要运行的任务, 0: 使用软件算法计算下一个要运行的任务, 默认: 0 */
#define configUSE_TICKLESS_IDLE 0 /* 1: 使能tickless低功耗模式, 默认: 0 */
#define configCPU_CLOCK_HZ SystemCoreClock /* 定义CPU主频, 单位: Hz, 无默认需定义 */
#define configSYSTICK_CLOCK_HZ (configCPU_CLOCK_HZ / 8)/* 定义SysTick时钟频率,当SysTick时钟频率与内核时钟频率不同时才可以定义, 单位: Hz, 默认: 不定义 */
#define configTICK_RATE_HZ 1000 /* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */
#define configMAX_PRIORITIES 32 /* 定义最大优先级数, 最大优先级=configMAX_PRIORITIES-1, 无默认需定义 */
#define configMINIMAL_STACK_SIZE 128 /* 定义空闲任务的栈空间大小, 单位: Word, 无默认需定义 */
#define configMAX_TASK_NAME_LEN 16 /* 定义任务名最大字符数, 默认: 16 */
#define configUSE_16_BIT_TICKS 0 /* 1: 定义系统时钟节拍计数器的数据类型为16位无符号数, 无默认需定义 */
#define configIDLE_SHOULD_YIELD 1 /* 1: 使能在抢占式调度下,同优先级的任务能抢占空闲任务, 默认: 1 */
#define configUSE_TASK_NOTIFICATIONS 1 /* 1: 使能任务间直接的消息传递,包括信号量、事件标志组和消息邮箱, 默认: 1 */
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 定义任务通知数组的大小, 默认: 1 */
#define configUSE_MUTEXES 1 /* 1: 使能互斥信号量, 默认: 0 */
#define configUSE_RECURSIVE_MUTEXES 1 /* 1: 使能递归互斥信号量, 默认: 0 */
#define configUSE_COUNTING_SEMAPHORES 1 /* 1: 使能计数信号量, 默认: 0 */
#define configUSE_ALTERNATIVE_API 0 /* 已弃用!!! */
#define configQUEUE_REGISTRY_SIZE 8 /* 定义可以注册的信号量和消息队列的个数, 默认: 0 */
#define configUSE_QUEUE_SETS 1 /* 1: 使能队列集, 默认: 0 */
#define configUSE_TIME_SLICING 1 /* 1: 使能时间片调度, 默认: 1 */
#define configUSE_NEWLIB_REENTRANT 0 /* 1: 任务创建时分配Newlib的重入结构体, 默认: 0 */
#define configENABLE_BACKWARD_COMPATIBILITY 0 /* 1: 使能兼容老版本, 默认: 1 */
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 0 /* 定义线程本地存储指针的个数, 默认: 0 */
#define configSTACK_DEPTH_TYPE uint16_t /* 定义任务堆栈深度的数据类型, 默认: uint16_t */
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t /* 定义消息缓冲区中消息长度的数据类型, 默认: size_t */
/* 内存分配相关定义 */
#define configSUPPORT_STATIC_ALLOCATION 0 /* 1: 支持静态申请内存, 默认: 0 */
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 1: 支持动态申请内存, 默认: 1 */
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) /* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义 */
#define configAPPLICATION_ALLOCATED_HEAP 0 /* 1: 用户手动分配FreeRTOS内存堆(ucHeap), 默认: 0 */
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0 /* 1: 用户自行实现任务创建时使用的内存申请与释放函数, 默认: 0 */
/* 钩子函数相关定义 */
#define configUSE_IDLE_HOOK 0 /* 1: 使能空闲任务钩子函数, 无默认需定义 */
#define configUSE_TICK_HOOK 0 /* 1: 使能系统时钟节拍中断钩子函数, 无默认需定义 */
#define configCHECK_FOR_STACK_OVERFLOW 0 /* 1: 使能栈溢出检测方法1, 2: 使能栈溢出检测方法2, 默认: 0 */
#define configUSE_MALLOC_FAILED_HOOK 0 /* 1: 使能动态内存申请失败钩子函数, 默认: 0 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 /* 1: 使能定时器服务任务首次执行前的钩子函数, 默认: 0 */
/* 运行时间和任务状态统计相关定义 */
#define configGENERATE_RUN_TIME_STATS 0 /* 1: 使能任务运行时间统计功能, 默认: 0 */
#if configGENERATE_RUN_TIME_STATS
#include "./BSP/TIMER/btim.h"
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
extern uint32_t FreeRTOSRunTimeTicks;
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks
#endif
#define configUSE_TRACE_FACILITY 1 /* 1: 使能可视化跟踪调试, 默认: 0 */
#define configUSE_STATS_FORMATTING_FUNCTIONS 1 /* 1: configUSE_TRACE_FACILITY为1时,会编译vTaskList()和vTaskGetRunTimeStats()函数, 默认: 0 */
/* 协程相关定义 */
#define configUSE_CO_ROUTINES 0 /* 1: 启用协程, 默认: 0 */
#define configMAX_CO_ROUTINE_PRIORITIES 2 /* 定义协程的最大优先级, 最大优先级=configMAX_CO_ROUTINE_PRIORITIES-1, 无默认configUSE_CO_ROUTINES为1时需定义 */
/* 软件定时器相关定义 */
#define configUSE_TIMERS 1 /* 1: 使能软件定时器, 默认: 0 */
#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) /* 定义软件定时器任务的优先级, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_QUEUE_LENGTH 5 /* 定义软件定时器命令队列的长度, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小, 无默认configUSE_TIMERS为1时需定义 */
/* 可选函数, 1: 使能 */
#define INCLUDE_vTaskPrioritySet 1 /* 设置任务优先级 */
#define INCLUDE_uxTaskPriorityGet 1 /* 获取任务优先级 */
#define INCLUDE_vTaskDelete 1 /* 删除任务 */
#define INCLUDE_vTaskSuspend 1 /* 挂起任务 */
#define INCLUDE_xResumeFromISR 1 /* 恢复在中断中挂起的任务 */
#define INCLUDE_vTaskDelayUntil 1 /* 任务绝对延时 */
#define INCLUDE_vTaskDelay 1 /* 任务延时 */
#define INCLUDE_xTaskGetSchedulerState 1 /* 获取任务调度器状态 */
#define INCLUDE_xTaskGetCurrentTaskHandle 1 /* 获取当前任务的任务句柄 */
#define INCLUDE_uxTaskGetStackHighWaterMark 1 /* 获取任务堆栈历史剩余最小值 */
#define INCLUDE_xTaskGetIdleTaskHandle 1 /* 获取空闲任务的任务句柄 */
#define INCLUDE_eTaskGetState 1 /* 获取任务状态 */
#define INCLUDE_xEventGroupSetBitFromISR 1 /* 在中断中设置事件标志位 */
#define INCLUDE_xTimerPendFunctionCall 1 /* 将函数的执行挂到定时器服务任务 */
#define INCLUDE_xTaskAbortDelay 1 /* 中断任务延时 */
#define INCLUDE_xTaskGetHandle 1 /* 通过任务名获取任务句柄 */
#define INCLUDE_xTaskResumeFromISR 1 /* 恢复在中断中挂起的任务 */
/* 中断嵌套行为配置 */
#ifdef __NVIC_PRIO_BITS
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 4
#endif
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* FreeRTOS可管理的最高中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_API_CALL_INTERRUPT_PRIORITY configMAX_SYSCALL_INTERRUPT_PRIORITY
/* FreeRTOS中断服务函数相关定义 */
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
/* 断言 */
#define vAssertCalled(char, int) printf("Error: %s, %d\r\n", char, int)
#define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )
/* FreeRTOS MPU 特殊定义 */
//#define configINCLUDE_APPLICATION_DEFINED_PRIVILEGED_FUNCTIONS 0
//#define configTOTAL_MPU_REGIONS 8
//#define configTEX_S_C_B_FLASH 0x07UL
//#define configTEX_S_C_B_SRAM 0x07UL
//#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY 1
//#define configALLOW_UNPRIVILEGED_CRITICAL_SECTIONS 1
/* ARMv8-M 安全侧端口相关定义。 */
//#define secureconfigMAX_SECURE_CONTEXTS 5
#endif /* FREERTOS_CONFIG_H */
任务的创建与删除
什么是任务?
任务可以理解为进程/线程,创建一个任务,就会在内存开辟一个空间。
任务可以被认为是一组函数,它们在运行时相互协作以完成特定的目标。
在 FreeROTS 中,任务可以分配不同的优先级,并按照优先级进行调度。当一个任务没有工作可以做时,操作系统会将 CPU 时间分配给另一个优先级更高的任务,以确保系统的正常运行。
任务通常都含有 while(1) 死循环。
任务创建与删除相关函数
任务创建与删除相关函数有如下三个:
函数名称 | 函数作用 |
---|---|
xTaskCreate() | 动态方式创建任务 |
xTaskCreateStatic() | 静态方式创建任务 |
vTaskDelete() | 删除任务 |
任务动态创建与静态创建的区别:
动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。 通常情况下使用动态方式创建任务。
xTaskCreate 函数原型
-
pxTaskCode:指向任务函数的指针,任务必须实现为永不返回(即连续循环);
-
pcName:任务的名字,主要是用来调试,默认情况下最大长度是16;
-
pvParameters:指定的任务栈的大小;
-
uxPriority:任务优先级,数值越大,优先级越大;
-
pxCreatedTask:用于返回已创建任务的句柄可以被引用
返回值 | 描述 |
---|---|
pdPASS | 任务创建成功 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | 任务创建失败 |
vTaskDelete 函数原型
void vTaskDelete(TaskHandle_t xTaskToDelete);
只需将待删除的任务句柄传入该函数,即可将该任务删除。
当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。
创建两个任务进行点灯实操
-
增加两个任务,一个用于点亮LED1,一个用于点亮LED2
-
查看原理图,设置两个LED灯的引脚为输出引脚
-
导出代码后加入下面代码,编译烧录32单片机
freertos.c文件
/*任务1函数*/ void StartTaskLED1(void const * argument) { for(;;) { HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8); osDelay(500); } } /*任务2函数*/ void StartTaskLED2(void const * argument) { for(;;) { HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_9); osDelay(1000); } }
任务调度
什么是任务调度?
调度器就是使用相关的调度算法来决定当前需要执行的哪个任务。
FreeRTOS中开启任务调度的函数是 vTaskStartScheduler() ,但在 CubeMX 中被封装为 osKernelStart()
FreeRTOS的任务调度规则是怎样的?
- 高优先级抢占低优先级任务,系统永远执行最高优先级的任务(即抢占式调度)
- 同等优先级的任务轮转调度(即时间片调度)
- 携程式调度:但官方已明确表示不更新,主要是用在小容量的芯片上,用得 也不多。
抢占式调度运行过程
前提:任务优先级不同
总结:
- 高优先级任务,优先执行;
- 高优先级任务不停止,低优先级任务无法执行;
- 被抢占的任务将会进入就绪态
时间片调度运行过程
前提:任务优先级相同
总结:
- 同等优先级任务,轮流执行,时间片流转;
- 一个时间片大小,取决为滴答定时器中断周期(默认1ms);
- 注意没有用完的时间片不会再使用,下次任务 Task3 得到执行,还是按照一个时间片的时钟 节拍运行
任务状态
FreeRTOS中任务共存在4种
状态:
Running 运行态
当任务处于实际运行状态称之为运行态,即CPU的使用权被这个任务占用(同一时间仅一个任务 处于运行态)。
Ready 就绪态
处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先级或更高优先级的任务正在运行。
Blocked 阻塞态
如果一个任务因延时,或等待信号量、消息队列、事件标志组等而处于的状态被称之为阻塞态。
Suspended 挂起态
类似暂停,通过调用函数 vTaskSuspend() 对指定任务进行挂起,挂起后这个任务将不被执行, 只有调用函数 xTaskResume() 才可以将这个任务从挂起态恢复。
![image-20240514213639865](../imgs/FreeRTOS/image-20240514213639865.png)
任务调度和任务的状态案例分析
实验需求
创建 4 个任务:taskLED1,taskLED2,taskKEY1,taskKEY2,任务要求如下:
taskLED1:间隔 500ms 闪烁 LED1;
taskLED2:间隔 1000ms 闪烁 LED2;
taskKEY1:如果 taskLED1 存在,则按下 KEY1 后删除 taskLED1 ,否则创建 taskLED1 ;
taskKEY2:如果 taskLED2 正常运行,则按下 KEY2 后挂起 taskLED2 ,否则恢复 taskLED2。
uart.c
#include "stdio.h"
int fputc(int ch,FILE *f)
{
unsigned char temp[1] = {ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
freertos.c
void StartTaskLED1(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
osDelay(500);
}
}
void StartTask02(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) == GPIO_PIN_RESET)
{
osDelay(20); // 延时消抖
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
printf("Key1 被按下\r\n");
if(TaskLED1Handle == NULL)
{
printf("任务1不存在,准备创建任务1\r\n");
osThreadDef(TaskLED1, StartTaskLED1, osPriorityNormal, 0, 128);
TaskLED1Handle = osThreadCreate(osThread(TaskLED1), NULL);
if(TaskLED1Handle != NULL)
{
printf("任务1创建成功\r\n");
}
}
else
{
printf("删除任务1\r\n");
osThreadTerminate(TaskLED1Handle);
TaskLED1Handle = NULL;
}
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
}
osDelay(10);
}
}
void StartTaskKey2(void const * argument)
{
static int flag = 0;
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
osDelay(20); // 延时消抖
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
printf("Key2 被按下\r\n");
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) == GPIO_PIN_RESET);
}
osDelay(10);
}
}
队列
什么是队列?
队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任 务间传递信息。
为什么不使用全局变量?
如果使用全局变量,任务1修改了变量 a ,等待任务3处理,但任务3处理速度很慢,在处理数据的过程中,任务2有可能又修改了变量 a ,导致任务3有可能得到的不是正确的数据。
在这种情况下,就可以使用队列。任务1和任务2产生的数据放在流水线上,任务3可以慢慢一个个依次处理。
关于队列的几个名词:创建队列时,需要指定队列长度及队列项目大小。
-
队列项目:队列中的每一个数据;
-
队列长度:队列能够存储队列项目的最大数量;
消息队列特点
-
数据入队出队方式
通常采用
先进先出(FIFO)
的数据存储缓冲机制,即先入队的数据会先从队列中被读取。也可以配置为后进先出(LIFO)方式,但用得比较少。 -
数据传递方式
采用实际值传递,即将数据拷贝到队列中进行传递,也可以传递指针,在传递较大的数据的时候 采用指针传递。 -
多任务访问
队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息 -
出队、入队阻塞
当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队。
阻塞时间如果设置为:
- 0:直接返回不会等待;
- 0~port_MAX_DELAY:等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待;
- port_MAX_DELAY:死等,一直等到可以入队为止。出队阻塞与入队阻塞类似;
消息队列相关 API 函数
创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,UBaseType_t uxItemSize );
参数:
- uxQueueLength:队列可同时容纳的最大项目数 。
- uxItemSize:存储队列中的每个数据项所需的大小(以字节为单位)。
返回值: 如果队列创建成功,则返回所创建队列的句柄 。 如果创建队列所需的内存无法分配 , 则返回 NULL。
写队列
函数 | 描述 |
---|---|
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
参数:
- xQueue:队列的句柄,数据项将发送到此队列。
- pvItemToQueue:待写入数据
- xTicksToWait:阻塞超时时间
返回值:
如果成功写入数据,返回 pdTRUE,否则返回 errQUEUE_FULL。
读队列
函数 | 描述 |
---|---|
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息,但是不删除消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);
参数:
- xQueue:待读取的队列
- pvItemToQueue:数据读取缓冲区
- xTicksToWait:阻塞超时时间
返回值:
- 成功返回 pdTRUE
- 否则返回 pdFALSE
消息队列实操
要求:创建一个队列,按下 KEY1 向队列发送数据,按下 KEY2 向队列读取数据。
-
然后创建两个任务和一个队列
-
设置按键引脚为输入,然后导出代码
freertos.c
/*用于监听Key1,Key1按下向队列发送消息*/ void StartSend(void const * argument) { /* USER CODE BEGIN StartSend */ /* Infinite loop */ uint16_t buf = 100; BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); //消抖 if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { status = xQueueSend(myQueue01Handle,&buf,0); if(status == pdTRUE) { printf("写入队列成功\r\n"); } else { printf("写入队列失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } osDelay(10); //10ms检测一次 } /* USER CODE END StartSend */ } /*用于监听key2,key2按下从队列中读出消息*/ void StartReceive(void const * argument) { /* USER CODE BEGIN StartReceive */ /* Infinite loop */ uint16_t buf = 100; BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); //消抖 if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { status = xQueueReceive(myQueue01Handle,&buf,0); if(status == pdTRUE) { printf("读出队列成功\r\n"); } else { printf("读出队列失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(10); } /* USER CODE END StartReceive */ }
二值信号量
什么是信号量?
信号量(Semaphore),是在多任务环境下使用的一种机制,是可以用来保证两个或多个关键代 码段不被并发调用。
信号量这个名字,我们可以把它拆分来看,信号可以起到通知信号的作用,然后我们的量还可以 用来表示资源的数量,当我们的量只有0和1的时候,它就可以被称作二值信号量,只有两个状 态,当我们的那个量没有限制的时候,它就可以被称作为计数型信号量。
信号量也是队列的一种。
什么是二值信号量?
二值信号量其实就是一个长度为1,大小为零的队列,只有0和1两种状态,通常情况下,我们用它来进行互斥访问或任务同步。
互斥访问:比如门跟钥匙,只有获取到钥匙才可以开门
任务同步:比如录完视频后才能看视频
二值信号量相关 API 函数
函数 | 描述 |
---|---|
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
创建二值信号量
SemaphoreHandle_t xSemaphoreCreateBinary( void )
参数:
- 无
返回值:
- 成功,返回对应二值信号量的句柄;
- 失败,返回 NULL 。
注意:
创建信号量时默认会释放
释放二值信号量
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
参数:
- xSemaphore:要释放的信号量句柄
返回值:
- 成功,返回 pdPASS ;
- 失败,返回 errQUEUE_FULL 。
获取二值信号量
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
参数:
- xSemaphore:要获取的信号量句柄
- xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
返回值:
- 成功,返回 pdPASS ;
- 失败,返回 errQUEUE_FULL 。
二值信号量实操
实验需求:创建一个二值信号量,按下 KEY1 则释放信号量,按下 KEY2 获取信号量。
-
打开CubeMX,创建两个任务用来放入和获取信号量
-
进行代码编写
freertos.c
/*检测Key1,Key1按下释放信号量*/ void StartKey01(void const * argument) { /* USER CODE BEGIN StartKey01 */ /* Infinite loop */ BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { status = xSemaphoreGive(myBinarySem01Handle); if(status == pdTRUE) { printf("释放二值信号量成功\r\n"); } else { printf("释放二值信号量失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } osDelay(1); } /* USER CODE END StartKey01 */ } /*检测Key2,Ke21按下获取信号量*/ void StartKey2(void const * argument) { /* USER CODE BEGIN StartKey2 */ /* Infinite loop */ BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { status = xSemaphoreTake(myBinarySem01Handle,0); if(status == pdTRUE) { printf("获取二值信号量成功\r\n"); } else { printf("获取二值信号量失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(1); } /* USER CODE END StartKey2 */ }
-
串口调试:创建信号时默认会释放一次
计数型信号量
什么是计数型信号量?
计数型信号量相当于队列长度大于1 的队列,因此计数型信号量能够容纳多个资源,这在计数型信号量被创建的时候确定的。
使用计数型信号量可以解决多个任务之间的同步问题,例如控制对共享资源的访问和协调任务的执行顺序。
计数型信号量相关 API 函数
计数型信号量的释放和获取与二值信号量完全相同 !
函数 | 描述 |
---|---|
xSemaphoreCreateCounting() | 使用动态方法创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方法创建计数型信号量 |
uxSemaphoreGetCount() | 获取信号量的计数值 |
创建计数型信号量
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);
参数:
- uxMaxCount:可以达到的最大计数值
- uxInitialCount:创建信号量时分配给信号量的计数值
返回值:
-
成功,返回对应计数型信号量的句柄;
-
失败,返回 NULL 。
释放计数型信号量
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
参数:
- xSemaphore:要释放的信号量句柄
返回值:
-
成功,返回 pdPASS ;
-
失败,返回 errQUEUE_FULL 。
获取计数型信号量
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
参数:
- xSemaphore:要获取的信号量句柄
- xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
返回值:
- 成功,返回 pdPASS ;
- 失败,返回 errQUEUE_FULL 。
计数型信号量实操
-
打开CubeMX,创建两个任务和设置按键引脚为输入,通过按键来放入和获取信号量
-
使能计数信号量
-
创建一个计数信号量,设置最多存放3个信号量
-
进行代码编写
/*监听Key1,Key1按下释放计数型信号量*/ void StartKey01(void const * argument) { /* USER CODE BEGIN StartKey01 */ /* Infinite loop */ BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { status = xSemaphoreGive(myCountingSem01Handle); if(status == pdTRUE) { printf("释放计数型信号量成功\r\n"); } else { printf("释放计数型信号量失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } osDelay(1); } /* USER CODE END StartKey01 */ } /*监听Key2,Key2按下获取计数型信号量*/ void StartKey2(void const * argument) { /* USER CODE BEGIN StartKey2 */ /* Infinite loop */ BaseType_t status; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { status = xSemaphoreTake(myCountingSem01Handle,0); if(status == pdTRUE) { printf("获取计数型信号量成功\r\n"); } else { printf("获取计数型信号量失败\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(1); } /* USER CODE END StartKey2 */ }
-
串口调试
互斥量
什么是互斥量?
在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同步, 而互斥型信号量用于资源保护。
互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现 象。
什么是优先级反转(翻转)和优先级继承
以上图为例,系统中有3个不同优先级的任务H/M/L,最高优先级任务H和最低优先级任务L通过 信号量机制,共享资源。目前任务L占有资源,锁定了信号量,Task H运行后将被阻塞,直到Task L释放信号量后,Task H才能够退出阻塞状态继续运行。但是Task H在等待Task L释放信号量的过 程中,中等优先级任务M抢占了任务L,从而延迟了信号量的释放时间,导致Task H阻塞了更长时 间,这种现象称为优先级倒置或优先级反转(翻转)。
优先级继承:
当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任 务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务 会将低优先级任务的优先级提升到与自己相同的优先级。
优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。
互斥量相关 API 函数
互斥信号量不能用于中断服务函数中!
函数 | 描述 |
---|---|
xSemaphoreCreateMutex() | 使用动态方法创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方法创建互斥信号量 |
SemaphoreHandle_t xSemaphoreCreateMutex( void )
参数:
- 无
返回值:
-
成功,返回对应互斥量的句柄;
-
失败,返回 NULL 。
优先级反转(翻转)示例
如下图,低优先级工作后高优先级被阻塞,然后发生优先级反转,中优先级比高优先级先工作
-
打开CubeMX,增加三个任务,优先级分别从高到低
-
增加一个二值信号量
-
代码编写
void StartTaskH(void const * argument) { /* USER CODE BEGIN StartTaskH */ /* Infinite loop */ for(;;) { xSemaphoreTake(myBinarySem01Handle,portMAX_DELAY); //获取二值信号量 printf("高优先级获得二值信号量,开始工作\r\n"); HAL_Delay(1000); printf("工作完毕后,释放二值信号量\r\n"); xSemaphoreGive(myBinarySem01Handle); //释放二值信号量 osDelay(1000); } /* USER CODE END StartTaskH */ } void StartTaskM(void const * argument) { /* USER CODE BEGIN StartTaskM */ /* Infinite loop */ for(;;) { printf("占用cpu资源,我就是玩\r\n"); osDelay(1000); } /* USER CODE END StartTaskM */ } void StartTaskL(void const * argument) { /* USER CODE BEGIN StartTaskL */ /* Infinite loop */ for(;;) { xSemaphoreTake(myBinarySem01Handle,portMAX_DELAY); //获取二值信号量 printf("低优先级获得二值信号量,开始工作\r\n"); HAL_Delay(3000); printf("工作完毕后,释放二值信号量\r\n"); xSemaphoreGive(myBinarySem01Handle); //释放二值信号量 osDelay(1000); } /* USER CODE END StartTaskL */ }
-
串口调试
使用互斥量优化优先级反转(翻转)问题示例
-
使用CubeMX在优先级反转示例中增加互斥量,导出代码、
-
编写代码
void StartTaskH(void const * argument) { /* USER CODE BEGIN StartTaskH */ /* Infinite loop */ for(;;) { xSemaphoreTake(myMutex01Handle,portMAX_DELAY); //获取二值信号量 printf("高优先级获得二值信号量,开始工作\r\n"); HAL_Delay(1000); printf("工作完毕后,释放二值信号量\r\n"); xSemaphoreGive(myMutex01Handle); //释放二值信号量 osDelay(1000); } /* USER CODE END StartTaskH */ } void StartTaskM(void const * argument) { /* USER CODE BEGIN StartTaskM */ /* Infinite loop */ for(;;) { printf("占用cpu资源,我就是玩\r\n"); osDelay(1000); } /* USER CODE END StartTaskM */ } void StartTaskL(void const * argument) { /* USER CODE BEGIN StartTaskL */ /* Infinite loop */ for(;;) { xSemaphoreTake(myMutex01Handle,portMAX_DELAY); //获取二值信号量 printf("低优先级获得二值信号量,开始工作\r\n"); HAL_Delay(3000); printf("工作完毕后,释放二值信号量\r\n"); xSemaphoreGive(myMutex01Handle); //释放二值信号量 osDelay(1000); } /* USER CODE END StartTaskL */ }
-
串口调试
事件标志组
什么是事件标志组?
事件标志位
表明某个事件是否发生,联想:全局变量 flag。通常按位表示,每一个位表示一个事件(高8位不算)
事件标志组
是一组事件标志位的集合, 可以简单的理解事件标志组,就是一个整数。
事件标志组本质是一个 16 位或 32 位无符号的数据类型 EventBits_t ,由 configUSE_16_BIT_TICKS 决定。
虽然使用了 32 位无符号的数据类型变量来存储事件标志, 但其中的高8位用作存储事件标志组的 控制信息,低 24 位用作存储事件标志 ,所以说一个事件组最多可以存储 24 个事件标志!
事件标志组相关 API 函数
函数 | 描述 |
---|---|
xEventGroupCreate() | 使用动态方式创建事件标志组 |
xEventGroupCreateStatic() | 使用静态方式创建事件标志组 |
xEventGroupClearBits() | 清零事件标志位 |
xEventGroupClearBitsFromISR() | 在中断中清零事件标志位 |
xEventGroupSetBits() | 设置事件标志位 |
xEventGroupSetBitsFromISR() | 在中断中设置事件标志位 |
xEventGroupWaitBits() | 等待事件标志位 |
创建事件标志组
EventGroupHandle_t xEventGroupCreate( void );
参数:
- 无
返回值:
-
成功,返回对应事件标志组的句柄;
-
失败,返回 NULL
设置事件标志位
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
参数:
- xEventGroup:对应事件组句柄。
- uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值。
返回值:
- 设置之后事件组中的事件标志位值。
清除事件标志位
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );
参数:
- xEventGroup:对应事件组句柄。
- uxBitsToClear:指定要在事件组中清除的一个或多个位的按位 值。
返回值:
- 清零之前事件组中事件标志位的值。
等待事件标志位
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——不清除
-
xWaitForAllBits:pdTRUE——所有等待事件位全为1(逻辑与),pdFALSE——等待的事件位有一个为1(逻辑或)
-
xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待
返回值:
等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位
其他值:等待事件标志位失败,返回事件组中的事件标志位
事件标志组实操
创建一个事件标志组和两个任务( task1 和 task2),task1 检测按键,如果检测到 KEY1 和 KEY2 都按过,则执行 task2 。
-
打开CubeMX,增加两个任务,配置两个输入引脚给按键Key1和Key2
-
在代码中创建一个事件标志组
/*定义事件标志组句柄*/ EventGroupHandle_t eventGroupHandle; /*创建事件标志组*/ eventGroupHandle = xEventGroupCreate();
-
task1 检测按键,如果检测到 KEY1 和 KEY2 都按过,则执行 task2
/*检测Key1和Key2有没有按下,如果按下则设置相应的事件标志组位*/ void StartTask1(void const * argument) { /* USER CODE BEGIN StartTask1 */ /* Infinite loop */ for(;;) { //检测Key1有无被按下,按下后设置事件标志组最低位 if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { printf("按键Key1按下\r\n"); xEventGroupSetBits(eventGroupHandle,0x01); } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } //检测Key2有无被按下,按下后设置事件标志组第二低位 if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { printf("按键Key2按下\r\n"); xEventGroupSetBits(eventGroupHandle,0x02); } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(10); } /* USER CODE END StartTask1 */ } /*该任务用于等待事件标志组*/ void StartTask2(void const * argument) { EventBits_t event_bit; /* USER CODE BEGIN StartTask2 */ /* Infinite loop */ for(;;) { event_bit = xEventGroupWaitBits(eventGroupHandle,0x01|0x02,pdTRUE,pdTRUE,portMAX_DELAY); printf("返回值:%#x,按键都按下,任务2执行了\r\n",event_bit); // %#x是带格式输出, 效果为在输出前加0x osDelay(1); } /* USER CODE END StartTask2 */ }
-
打开串口助手看运行结果
任务通知
什么是任务通知?
FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加 省内存(无需创建队列)。
在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!
任务一给任务二发送通知,其实就是任务一操作任务二的TCB里面的ulNotifiedValue,给它写入相应的值
任务都有一个结构体:任务控制块TCB,它里边有两个结构体成员变量:
typedef struct tskTaskControlBlock
{
… …
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue [ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState [ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
endif
… …
} tskTCB;
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 定义任务通知数组的大小, 默认: 1 */
- 一个是 uint32_t 类型,用来表示通知值
- 一个是 uint8_t 类型,用来表示通知状态
发送通知给任务 :
通过对以下方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等。
-
发送消息给任务,如果有通知未读, 不覆盖通知值
-
发送消息给任务,直接覆盖通知值
-
发送消息给任务,设置通知值的一个或者多个位
-
发送消息给任务,递增通知值
任务通知的优势和劣势
任务通知的优势
- 使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。
- 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的劣势
- 只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB 。
- 通知只能一对一,因为通知必须指定任务。
- 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。
- 任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保 持一个数据。
任务通知相关 API 函数
发送通知
函数 | 描述 |
---|---|
xTaskNotify() | 发送通知,带有通知值 |
xTaskNotifyAndQuery() | 发送通知,带有通知值并且保留接收任务的原通知值 |
xTaskNotifyGive() | 发送通知,不带通知值 |
xTaskNotifyFromISR() | 在中断中发送任务通知 |
xTaskNotifyAndQueryFromISR() | 在中断中发送任务通知 |
vTaskNotifyGiveFromISR() | 在中断中发送任务通知 |
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );
参数:
-
xTaskToNotify:需要接收通知的任务句柄;
-
ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;
-
eAction:一个枚举,代表如何使用任务通知的值;
枚举值 描述 eNoAction 发送通知,但不更新值(参数ulValue未使用) eSetBits 被通知任务的通知值按位或ulValue。(某些场景下可代替事件组,效率更高) eIncrement 被通知任务的通知值增加1(参数ulValue未使用),相当于 xTaskNotifyGive eSetValueWithOverwrite 被通知任务的通知值设置为 ulValue。(某些场景下可代替 xQueueOverwrite ,效率更高) eSetValueWithoutOverwrite 如果被通知的任务当前没有通知,则被通知的任务的通知值设为ulValue。如果被通知任务没有取走上一个通知,又接收到了一个通 知,则这次通知值丢弃,在这种情况下视为调用失败并返回 pdFALSE,(某些场景下可代替 xQueueSend ,效率更高)
返回值:如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 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。
等待通知
等待通知API函数只能用在任务,不可应用于中断中!
函数 | 描述 |
---|---|
ulTaskNotifyTake() | 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减 一。当任务通知用作二值信号量或者计数信号量的时候,使用此函数来 获取信号量。 |
xTaskNotifyWait() | 获取任务通知,比 ulTaskNotifyTake()更为复杂,可获取通知值和清除通知值的指定位 |
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );
参数:
- xClearCountOnExit:指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零(二值信号量);pdFALSE:把通知值减一(计数型信号量);
- xTicksToWait:阻塞等待任务通知值的最大时间;超时时间,0 表示不超时,portMAX_DELAY表示卡死等待
返回值:0:接收失败;非0:接收成功,返回任务通知的通知值
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
参数:
- ulBitsToClearOnEntry:函数执行前清零任务通知值那些位 。
- ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,在清 0 前,接收到的任务通知值会先被保存到形参 *pulNotificationValue 中。
- pulNotificationValue:用于保存接收到的任务通知值。 如果不需要使用,则设置为 NULL 即可
- xTicksToWait:等待消息通知的最大等待时间。超时时间,0 表示不超时,portMAX_DELAY表示卡死等待
任务通知实操
模拟二值信号量
-
创建两个任务和设置按键引脚为输入
-
设置两个按键分别发送和接收二值信号量
用到函数
- xTaskNotifyGive()
- ulTaskNotifyTake()
void StartTaskSend(void const * argument) { for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { xTaskNotifyGive(taskReceiveHandle); printf("发送任务通知,模拟二值信号量释放\r\n"); } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } osDelay(10); } } void StartTaskReceive(void const * argument) { uint32_t rev = 0; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { rev = ulTaskNotifyTake(pdTRUE,portMAX_DELAY); if(rev != 0) { printf("接收任务通知:模拟二值信号量接收\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(10); } }
-
打开串口助手,查看结果
模拟计数型信号量
模拟计数型信号量跟模拟二值信号量基本相同:
将ulTaskNotifyTake()函数中第一个参数从pdTRUE改为pdFALSE
代码示例:
用到函数
-
xTaskNotifyGive()
-
ulTaskNotifyTake()
void StartTaskSend(void const * argument) { for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET) { xTaskNotifyGive(taskReceiveHandle); printf("发送任务通知,模拟计数型信号量释放\r\n"); } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET); } osDelay(10); } } void StartTaskReceive(void const * argument) { uint32_t rev = 0; for(;;) { if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { osDelay(20); if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET) { rev = ulTaskNotifyTake(pdFALSE,portMAX_DELAY); if(rev != 0) { printf("接收任务通知:模拟二值技术型信号量接收\r\n"); } } while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET); } osDelay(10); } }
模拟事件标志组
用到函数
- xTaskNotify()
- xTaskNotifyWait()
示例代码
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
{
printf("将bit0位置1\r\n");
xTaskNotify(taskReceiveHandle,0x01,eSetBits);
}
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET);
}
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET)
{
printf("将bit1位置1\r\n");
xTaskNotify(taskReceiveHandle,0x02,eSetBits);
}
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
/* Infinite loop */
uint32_t notify_rev = 0, event_bit = 0;
for(;;)
{
xTaskNotifyWait(0,0xFFFFFFFF,¬ify_rev,portMAX_DELAY);
if(notify_rev & 0x01)
{
event_bit |= 0x01;
}
if(notify_rev & 0x02)
{
event_bit |= 0x02;
}
if(event_bit == (0x01 | 0x02))
{
printf("任务通知模拟事件标志组接收成功\r\n");
event_bit = 0;
}
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}
模拟消息邮箱
模拟邮箱大概就是向任务发送数据,但是与队列不同,任务邮箱发送消息受到了很多限制。
- 只能发送一个32位的值。
- 消息邮箱的值被保存为一个任务的通知值,而且只能保存一个任务的值,相当于队列长度为1
用到函数
- xTaskNotify()
- xTaskNotifyWait()
示例代码
void StartTaskSend(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
{
printf("按键1按下\r\n");
xTaskNotify(taskReceiveHandle,1,eSetValueWithOverwrite);
}
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET);
}
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET)
{
printf("按键2按下\r\n");
xTaskNotify(taskReceiveHandle,2,eSetValueWithOverwrite);
}
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10) == GPIO_PIN_RESET);
}
osDelay(10);
}
}
void StartTaskReceive(void const * argument)
{
uint32_t notify_rev = 0;
for(;;)
{
xTaskNotifyWait(0,0xFFFFFFFF,¬ify_rev,portMAX_DELAY);
printf("接收到的通知值为:%d\r\n",notify_rev);
osDelay(10);
}
}
延时函数和软件定时器
什么是延时函数?
延时函数是一种编程语言中常用的函数类型,用于暂停一段时间后再执行后续的操作。在延时函数执行期间,程序会暂停执行,直到指定的时间结束后才会继续执行后续代码。常见的延时函数参数是毫秒或微秒,可以控制真实时间和计算机时间的关系。延时函数常用于需要等待或暂停执行的场景,如控制程序运行速度、动画效果等。
不过,过度使用延时函数可能会影响程序的性能和响应速度。
延时函数分类
相对延时:vTaskDelay
绝对延时:vTaskDelayUntil
![image-20240516122605665](../imgs/FreeRTOS/image-20240516122605665.png)
![image-20240516122700990](../imgs/FreeRTOS/image-20240516122700990.png)
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 函数
函数 | 描述 |
---|---|
xTimerCreate() | 动态方式创建软件定时器 |
xTimerCreateStatic() | 静态方式创建软件定时器 |
xTimerStart() | 开启软件定时器定时 |
xTimerStop() | 停止软件定时器定时 |
xTimerReset() | 复位软件定时器定时 |
xTimerChangePeriod() | 更改软件定时器的定时超时时间 |
xTimerStartFromISR() | 在中断中开启软件定时器定时 |
xTimerStopFromISR() | 在中断中停止软件定时器定时 |
xTimerResetFromISR() | 在中断中复位软件定时器定时 |
xTimerChangePeriodFromISR() | 在中断中更改定时超时时间 |
创建软件定时器
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,用于多个软件定时器公用一个超时回调函数
- pxCallbackFunction:软件定时器超时回调函数
返回值:
- 成功:定时器句柄
- 失败:NULL
开启软件定时器
BaseType_t xTimerStart( TimerHandle_t xTimer,
TickType_t xBlockTime );
参数:
- xTimer:待开启的软件定时器的句柄
- xTickToWait:发送命令到软件定时器命令队列的最大等待时间
返回值:
- pdPASS:开启成功
- pdFAIL:开启失败
停止软件定时器
BaseType_t xTimerStop( TimerHandle_t xTimer,
TickType_t xBlockTime );
参数:
- xTimer:待开启的软件定时器的句柄
- xTickToWait:发送命令到软件定时器命令队列的最大等待时间
返回值:
- pdPASS:开启成功
- pdFAIL:开启失败
复位软件定时器
BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xBlockTime );
参数:
- xTimer:待开启的软件定时器的句柄
- xTickToWait:发送命令到软件定时器命令队列的最大等待时间
返回值:
- pdPASS:开启成功
- pdFAIL:开启失败
该功能将使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新 定时。
更改软件定时器定时时间
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime );
xNewPeriod:新的定时超时时间,单位:系统时钟节拍。
其余参数与返回值同上。
延时函数和软件定时器实操
创建两个定时器:
定时器1,周期定时器,每1秒打印一次 ten
定时器2,单次定时器,启动后 2 秒打印一次 zzz
-
使能软件定时器
-
创建两个定时器
-
代码示例:
void StartTask1(void const * argument) { /* USER CODE BEGIN StartTask1 */ /* Infinite loop */ xTimerStart(myTimer01Handle,1000); //开启软件定时器1,定时时间为1s xTimerStart(myTimer02Handle,2000); //开启软件定时器2,定时时间为2s for(;;) { osDelay(1); } /* USER CODE END StartTask1 */ } /* Callback01 function */ void Callback01(void const * argument) { /* USER CODE BEGIN Callback01 */ printf("周期定时器:ten\r\n"); /* USER CODE END Callback01 */ } /* Callback02 function */ void Callback02(void const * argument) { /* USER CODE BEGIN Callback02 */ printf("单次定时器:zzz\r\n"); /* USER CODE END Callback02 */ }
中断管理
中断定义
中断是指在程序执行的过程中,突然发生了某种事件,需要立即停止当前正在执行的程序,并转而处理这个事件,处理完后再回到原来的程序执行点继续执行的过程。
中断可以是硬件中断(由硬件设备触发)或软件中断(由程序执行中断指令触发)。
中断可以提高计算机的响应速度和效率,使得计算机可以同时处理多个任务。
中断优先级
任何中断的优先级都大于任务!
在我们的操作系统,中断同样是具有优先级的,并且我们也可以设置它的优先级,但是他的优先 级并不是从 0~15 ,默认情况下它是从 5~15 ,0~4 这 5 个中断优先级不是 FreeRTOS 控制的(5是 取决于 configMAX_SYSCALL_INTERRUPT_PRIORITY)。
相关注意
-
在中断中必需使用中断相关的函数;
-
中断服务函数运行时间越短越好。
中断相关函数
队列
xQueueReceiveFromISR():在中断中从队列头部读取消息,并删除消息
xQueuePeekFromISR():在中断中从队列头部读取消息
信号量
xSemaphoreGiveFromISR():在中断中释放信号量
xSemaphoreTakeFromISR():在中断中获取信号量
事件标志组
xEventGroupClearBitsFromISR():在中断中清零事件标志位
xEventGroupSetBitsFromISR():在中断中设置事件标志位
任务通知
xTaskNotifyFromISR():在中断中发送任务通知
xTaskNotifyAndQueryFromISR():在中断中发送任务通知
vTaskNotifyGiveFromISR():在中断中发送任务通知
软件定时器
xTimerStartFromISR():在中断中开启软件定时器定时
xTimerStopFromISR():在中断中停止软件定时器定时
xTimerResetFromISR():在中断中复位软件定时器定时
xTimerChangePeriodFromISR():在中断中更改定时超时时间
中断管理实操
创建一个队列及一个任务,按下按键 KEY1 触发中断,在中断服务函数里向队列里发送数据,任 务则阻塞接收队列数据。
-
打开CubeMX,在NVIC中使能中断
-
中断代码示例:
用到的中断函数:xQueueSendFromISR()
stm32f1xx_it.c
#include "cmsis_os.h" // 增加CubeMX封装好的头文件 extern osMessageQId myQueue01Handle; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint32_t snd = 1; xQueueSendFromISR(myQueue01Handle, &snd, NULL); }
freertos.c
void StartTask1(void const * argument) { uint32_t rev; for(;;) { if (xQueueReceive(myQueue01Handle, &rev, portMAX_DELAY) == pdTRUE) { printf("rev = %d\r\n", rev); } osDelay(1); } }
-
打开串口助手,查看结果