一、RTOS入门
1.RTOS介绍
RTOS全称:Real Time OS,实时操作系统。
特点:
- 分而治之:实现功能划分多个任务。
- 延时函数:不会空等待,高优先级延时的时候执行低优先级,会让出CPU的使用权给其他任务,即任务调度。
- 抢占式:高优先级任务抢占低优先级任务。
- 任务堆栈:每个任务都有自己的栈空间,用于保存局部变量以及任务的上下文信息。
注意:
-
中断可以打断任意任务。
-
多任务可同一优先级。
2.FreeRTOS介绍
一款免费开源的嵌入式实时操作系统。
- 优先级不限:任务优先级分配没有限制,多任务可同一优先级。
- 任务不限:可创建的实时任务数量没有软件限制(如果是硬件,就有显示,STM32为0~31)。
- 支持抢占、时间片、协程三种任务调度。
二、FreeRTOS基础知识
1.任务调度简介
调度器就是使用相关的调度算法来决定当前需要执行的任务。
- 抢占式调度:优先级高的任务可以抢占优先级低的任务。被抢占的进入就绪状态。(主要是针对优先级不同的任务)
- 时间片调度:优先级相同的任务,在 系统时钟节拍到的时候切换任务。时间片大小取决于滴答定时器(SySTick)中断频率(主要是针对优先级相同的任务)
- 协程式调度:高优先级的抢占之前,要等低优先级的执行完之后再执行。
时间片调度时,如果一个任务没执行完,切换成了另一个任务,那这个任务剩下的时间就丢弃
2.任务状态
FreeRTOS中任务存在四个状态:
- 运行态:正在执行的任务,STM32中同一时间只有一个。
- 就绪态:已经能够被执行,但还未执行。
- 阻塞态:延时或者等待信号量。
- 挂起态:类似暂停。
相互转换图如下:
只有就绪态能转变成运行态
除了运行态,其他三种任务状态都有对应的任务状态列表:
- 就绪列表:pxReadTasksList[x],其中x代表任务优先级数值。
- 阻塞列表:pxDelayedTaskList
- 挂起列表:xSuspendedTaskList
任务就绪时,优先级的对应就绪列表寄存器会变成1,调度器从优先级由高到低寻找就绪的任务进入运行态。
中断优先级一样的挂在同一个就绪列表上。
三、FreeRTOS系统配置
1.FreeRTOSCongfig.h文件
相关宏大致可分为三类:
“INCLUDE”:配置FreeRTOS中可选的API函数
“config”:完成FreeRTOS的功能配置和裁剪
其他配置项:Pen的SV宏定义、SVC宏定义
可以计算出滴答定时器定时时间为1ms
四、FreeRTOS任务相关API函数介绍
1.任务的创建和删除的API函数
任务的创建和删除本质是调用FreeRTOS的API函数
API函数 | 描述 |
---|---|
xTaskCreate() | 动态方式创建任务 |
xTaskCreateStatic() | 静态方式创建任务 |
xTaskDelete() | 删除任务 |
钩子函数:回调函数
x
表示函数返回值是long
v
表示函数返回值是void
-
动态创建任务:任务的任务控制块以及任务的栈空间所需的内存,均由FreeRTOS从FreeRTOS管理的堆中分配
-
静态创建任务:任务的任务控制块以及任务的栈空间所需的内存,需用户分配提供
1.1.动态任务创建函数xTaxkCreate()
BaseType_t xTaskCreate
(
TaskFunction_t pxTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务名字,最大长度 configMAX_TASK_NAME_LEN
const uint16_t usStackDepth, // 任务堆栈大小,注意 字 为单位
void * const pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级,范围:0~configMAX_PRIORITIES_1
TaskHandle_t * const pxCreatedTask // 任务句柄,就是任务的任务控制块
)
返回值 | 描述 |
---|---|
pdPASS | 任务创建成功 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | 任务创建失败 |
1.1.1.实现动态创建任务流程
1、将宏configSUPPORT_DYNAMIC_ALLOCATION配置为1
2、定义函数入口参数
3、编写任务函数
此函数创建的任务会立刻进入就绪态,由任务调度器调度运行
1.1.2.内部实现步骤
1、申请堆栈内存&任务控制块内存(任务控制块就是任务的身份证)
2、TCB结构体成员赋值
3、添加新任务到就绪列表中
1.1.3.任务控制块结构体成员介绍
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; // 任务栈栈顶,必须为TCB的第一个成员
ListItem_t xStateListItem; // 任务状态列表项
ListItem_t xEventListItem; // 任务时间列表项
UBaseType_t uxPriority; // 任务优先级,数值越大,优先级越大
StackType_t *pxStack; // 任务栈起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ]; // 任务名字
……
省略很多条件编译的成员
}tskTCB;
任务栈栈顶,在任务切换时的任务
每个任务都有属于自己的任务控制块,类似于身份证
1.1.4.任务创建和删除的使用(动态方法)
设计四个任务:start_task、task1、task2、task3
start_task:创建其他三个任务
task1:实现LED0每500ms闪烁一次
task2:实现LED1每500ms闪烁一次
task3:判断按键KEY0是否按下,按下则删掉task1
stm32中,栈是从高地址往低地址,堆是低地址往高地址
下面代码以创建start_task为例
任务句柄:指向控制块的指针
/* START_TASK任务配置,包括:堆栈大小,任务优先级,任务句柄 */
#define START_TASK_STACK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t start_task_handler;
void start_task( void * pvParameters );
void freertos_demo(void) // RTOS入口函数
{
xTaskCreate((TaskFunction_t ) start_task, // 指向任务函数的指针
(char * ) "start_task", // 任务名字,最大长度 configMAX_TASK_NAME_LEN
(uint16_t ) START_TASK_STACK_SIZE, // 任务堆栈大小,注意 字 为单位
(void * ) NULL, // 传递给任务函数的参数
(UBaseType_t ) START_TASK_PRIO, // 任务优先级,范围:0~configMAX_PRIORITIES_1
(TaskHandle_t * ) &start_task_handler ); // 任务句柄,就是任务的任务控制块
vTaskStartScheduler(); //开启任务调度器,这个只在入口函数写
}
/**
* @brief 开始任务:创建任务1,2,3
* @param pvParameters:所需数据的指针
* @retval 无
*/
void start_task( void * pvParameters )
{
}
1.2.静态任务创建函数xTaskCreateStatic()
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务函数名
const uint32_t ulStackDepth, // 任务堆栈大小 注意 字 为单位
void * const pvParameters, // 传递的任务函数参数
UBaseType_t uxPriority, // 任务优先级
StackType_t * const puxStackBuffer, // 任务堆栈,一般为数组,由用户分配
StaticTask_t * const pxTaskBuffer // 任务控制块指针,由用户分配
)
返回值 | 描述 |
---|---|
NULL | 用户没有提供相应的内存,任务创建失败 |
errCO其他值 | 任务句柄,任务创建成功 |
1.2.1.实现静态创建任务流程
1、需将宏configSUPPORT_STATIC_ALLOCATION配置为1
2、定义空闲任务&定时器任务的任务堆栈及TCB
3、实现两个接口函数
vApplicationGetIdleTaskMemory()
vApplicationGetTimerTaskMemory()
4、定义函数入口参数
5、编写任务函数
此函数创建的任务会立刻进入就绪态,由任务调度器调度运行
1.2.2.内部实现步骤
1、TCB结构体成员赋值
2、添加新任务到就绪列表
1.2.3.任务创建和删除的使用(静态方法)
- 两个接口函数的实现
/* 空闲任务配置 */
StaticTask_t idle_task_tcb;
StackType_t idle_task_stack[configMINIMAL_STACK_SIZE];
/* 软件定时器任务配置 */
StaticTask_t timer_task_tcb;
StackType_t timer_task_stack[configTIMER_TASK_STACK_DEPTH];
/**
* @brief 空闲任务内存分配
* @param ppxIdleTaskTCBBuffer:空闲任务 任务控制块
* @param ppxIdleTaskStackBuffer:空闲任务 任务堆栈
* @param pulIdleTaskStackSize:空闲任务堆栈大小
* @retval 无
*/
void vApplicationGetIdleTaskMemory(StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
uint32_t * pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer = &idle_task_tcb;
*ppxIdleTaskStackBuffer = idle_task_stack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
/**
* @brief 软件定时器任务内存分配
* @param ppxTimerTaskTCBBuffer:软件定时器任务控制块
* @param ppxTimerTaskStackBuffer:软件定时器任务堆栈
* @param pulTimerTaskStackSize
* @retval 无
*/
void vApplicationGetTimerTaskMemory(StaticTask_t ** ppxTimerTaskTCBBuffer,
StackType_t ** ppxTimerTaskStackBuffer,
uint32_t * pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer = &timer_task_tcb;
*ppxTimerTaskStackBuffer = timer_task_stack;
*pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}
- 任务创建,这里以入口函数创建start_task为例
/* START_TASK任务配置,包括:堆栈大小,任务优先级,任务句柄 */
#define START_TASK_STACK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t start_task_handler;
StackType_t start_task_stack[START_TASK_STACK_SIZE];
StaticTask_t start_task_tcb;
/**
* @brief FreeRTOS入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
start_task_handler = xTaskCreateStatic(
(TaskFunction_t) start_task, // 指向任务函数的指针
(char *) "start_task", // 任务函数名
(uint32_t) START_TASK_STACK_SIZE, // 任务堆栈大小 注意 字 为单位
(void *) NULL, // 传递的任务函数参数
(UBaseType_t) START_TASK_PRIO, // 任务优先级
(StackType_t *) start_task_stack, // 任务堆栈,一般为数组,由用户分配
(StaticTask_t *) &start_task_tcb); // 任务控制块指针,由用户分配
vTaskStartScheduler(); //开启任务调度器
}
1.3.任务删除函数
void vTaskDelete(TaskHandle_t xTaskToDelete);
形参 | 描述 |
---|---|
xTaskToDelete | 待删除任务的任务句柄 |
用于删除已被创建的任务
被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除
不管任务处于什么状态,都会被删除
当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)
空闲任务会负责释放被删除任务中由系统分配的内存(动态创建,删除自身空闲任务释放,删除别的本删除任务释放),但是由用户任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露(静态创建,手动分配手动释放)
1.3.1.删除任务流程
1、使用删除任务函数,需将宏INXLUDE_vTaskDelete配置为1
2、入口参数输入需要删除的任务句柄(NULL代表删除本身)
1.3.2.内部实现
1、获取所要删除任务的控制块
通过传入的任务句柄,判断所需要删除哪个任务,NULL代表删除自身
2、将被删除任务,移除所在列表
将该任务从所在列表中移除,包括:就绪、阻塞、挂起、事件等列表
3、判断所需要删除的任务
删除任务自身,需先添加到等待删除列表,内存释放将在空闲任务执行
删除其他任务,释放内存,任务数量–
4、更新下个任务的阻塞时间
更新下一个任务的阻塞超时时间,以防止被删除的任务就是下一个阻塞超时的任务
1.4.临界区保护
保护不想被打断的程序段,关闭FreeRTOS所管理的中断,滴答中断和PendSV中断无法进行不能实现任务调度
任务调度实在PendSV中执行
taskENTER_CRITICAL(); // 进入临界区,临界区内不中断,也就是不切换任务
taskEXIT_CRITICAL(); // 退出临界区
2.任务挂起和恢复的API函数
API函数 | 描述 |
---|---|
vTaskSuspend() | 挂起任务 |
vTaskResume() | 恢复被挂起的任务 |
vTaskResumeFromISR() | 在中断 |
挂起:挂起任务类似暂停,可恢复;删除任务,无法恢复;
恢复:恢复被挂起的任务
“FromISR”:带“FromISR”后缀是在中断函数中专用的API函数
2.1.任务挂起函数
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
形参 | 描述 |
---|---|
xTaskToSuspend | 待挂起任务的任务句柄 |
此函数用于挂起任务,使用时需要将宏
INCLUDE_vTaskSuspend
配置为1
无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复
当传入的参数为NULL,则代表挂起任务自身(当前正在运行的任务)
2.2.任务恢复函数(任务中恢复)
任务中恢复被挂起函数:
void vTaskResume(TaskHandle_t xTaskToResume)
形参 | 描述 |
---|---|
xTaskToResume | 待恢复任务的任务句柄 |
使用该函数注意宏:INXLUDE_vTaskSuspend
必须定义为1
任务无论被vTaskSuspend()挂起多少次,只需要在任务中调用vTaskResume()恢复一次,就可以继续运行。且被恢复的任务会进入就绪状态
2.3.任务恢复函数(中断中恢复)
中断中恢复被挂起的函数:
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
形参 | 描述 |
---|---|
xTaskToResume | 待回复任务的任务句柄 |
函数:xTaskResumeFromISR返回值描述如下:
返回值 | 描述 |
---|---|
pdTRUE | 任务恢复后需要进行任务切换 |
pdFALSE | 任务恢复后不需要进行任务切换 |
使用该函数注意宏:INCLUDE_vTaskSuspend
和INCLUDE_vTaskResumeFromISR
必须定义为1
该函数专用于中断服务函数中,用于解挂被挂起任务
中断服务函数中要调用freeRTOS的API函数,中断优先级不能高于FreeRTOS所管理的最高优先级
恢复任务的时候,恢复到等待就绪列表
2.4.任务挂起恢复代码实现
/**
* @brief 任务三:按下按键KEY0挂起任务1,按下按键KEY2恢复任务1
* @param pvParameters:所需数据的指针
* @retval 无
*/
void task3( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = KEY_Scan(0);
if(key == KEY0_PRESS)
{
printf("在任务中挂起task1\r\n");
vTaskSuspend(task1_handler);
}
if(key == KEY2_PRESS)
{
printf("在任务中恢复task1\r\n");
vTaskResume(task1_handler);
}
vTaskDelay(10);
}
}
/**
* @brief 中断函数:在中断中恢复任务1
* @param 无
* @retval 无
*/
void EXTI15_10_IRQHandler() // 中断函数的名字是固定的
{
BaseType_t xYieldRequired;
// 先进行中断标志位的判断
if(EXTI_GetITStatus(EXTI_Line14) == SET) // 判断EXTI_Line14的中断标志位是否为1
{
xYieldRequired = xTaskResumeFromISR(task1_handler); //获取返回值
if(xYieldRequired == pdTRUE) //判断是否需要任务切换
{
printf("在中断中恢复task1\r\n");
portYIELD_FROM_ISR(xYieldRequired);
}
EXTI_ClearFlag(EXTI_Line14); // 清除中断标志位,否则一直中断
}
}
/**
* @brief 外部中断初始化
* @param 无
* @retval 无
*/
void EXTI1_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // 打开GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO时钟
// EXTI(时钟一直打开)和NVIC(内核外设),不需要开启
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14); // 配置AFIO中断引脚选择
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14; // 选择要启用或者禁用的EXTI线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 选择EXTI线的模式,启用还是禁用
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式,还可以选择事件模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发 ,还可以是上升沿,上升沿和下降沿
EXTI_Init(&EXTI_InitStructure); // EXTI初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 配置NVIT分组,两位抢占两位响应
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5; // 设置所选通道的抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 设置所选通道的响应优先级
NVIC_Init(&NVIC_InitStructure);
}
这里为挂起、恢复以及中断代码,其他代码和创建删除一致
需要注意在中断中恢复任务时,其中断优先级不能高于FreeRTOS所管理的中断优先级
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
五、FreeRTOS中断管理
1.什么是中断
1.1.简介
让CPU打断正常运行的程序,转而去处理紧急的事件(程序),就叫中断。
1.2.中断执行机制
1.中断请求:外设产生中断请求(GPIO外部中断、定时器中断等)
2.响应中断:CPU停止执行当前程序,转而去处理中断处理程序(ISR)
3.退出中断:执行完毕,返回被打断的程序处,继续往下执行
2.中断优先级分组设置
2.1.中断优先级介绍
ARM Cortex-M使用了8位宽的寄存器来配置中断的优先等级,这个寄存器就是中断优先级配置寄存器,但STM32只用了中断优先级配置寄存器的高4位[7:4],所以STM32提供最大16级的中断优先等级
低4位0,只有4-7位
STM32的中断优先级可以分为抢占优先级和子优先级(响应优先级)
抢占优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断
子优先级(响应优先级):当同时发生具有相同抢占优先级的两个中断时,子优先级高的优先执行
注意:
中断优先级数字越小,优先级越高
任务优先级数字越大,优先级越高
2.2.中断优先级分组设置
一共有五种分组方式
NVIC_PriorityGroup_0、1、2、3、4,其中数字代表抢占优先级所占的bit,比如:3代表3bit用于抢占优先级,即(02×2×2-1级抢占优先级,01级子优先级)
特点:
1、低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断才允许调用FreeRTOS的API函数
2、建议将所有优先级位指定为抢占优先级位,方便FreeFTOS管理
3、中断优先级数字越小,优先级越高;任务优先级数字越大,优先级越高
3.中断相关寄存器
3.1.中断相关寄存器介绍
三个系统中断优先级寄存器,分别为SHPR1、SHPR2、SHPR3
SHPR1寄存器地址:0xE000ED18
SHPR2寄存器地址:0xE000ED1C
SHPR3寄存器地址:0xE000ED20
下表:系统异常优先级寄存器阵列 0xE000_ED18 - 0xE000_ED23
地址 | 名称 | 描述 |
---|---|---|
0xE000_ED18 | PRI_4 | 存储器管理fault的优先级 |
0xE000_ED19 | PRI_5 | 总线fault的优先级 |
0xE000_ED1A | PRI_6 | 用法fault的优先级 |
0xE000_ED1B | ||
0xE000_ED1C | ||
0xE000_ED1D | ||
0xE000_ED1E | ||
0xE000_ED1F | PRI_11 | SVC优先级 |
0xE000_ED20 | PRI_12 | 调试监视器的优先级 |
0xE000_ED21 | ||
0xE000_ED22 | PRI_14 | PendSV的优先级 |
0xE000_ED23 | PRI_15 | SysTick的优先级 |
一个地址有8位,4个地址组成一个32位的寄存器
比如要设置PendSV的寄存器,SHPR3寄存器地址左移16位
3.2.FreeRTOS中PendSV和Systick中断优先级的配置
1、configPRIO_BITS
设置为4,configLIBRARY_LOWEST_INTERRUPT_PRIORITY
左移四位,设置的优先级就到了高位(STM32的中断优先级配置寄存器高4位提供中断优先级),保存到configKERNEL_INTERRUPT_PRIORITY
2、configKERNEL_INTERRUPT_PRIORITY
,左移16位就是pendSV,左移24位就是SysTick
这样,PendSV和SysTick设置最低优先级,可以保证系统任务切换不会阻塞系统其他中断的响应(中断可以打断任务,任务不能打断中断)
3.3.三个中断屏蔽寄存器
名字 | 功能描述 |
---|---|
PRIMASK | 这个寄存器只有1位。置1:关掉所有可屏蔽的异常,只剩下NMI和硬fault可以响应。缺省值0:没有关中断 |
FAULTMASK | 这个寄存器只有1位。置1:关掉所有可屏蔽的异常,只剩下NMI可以响应,包括中断和fault。缺省值0:没有关异常 |
BASEPRI | 这个寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当他被设定成某个值后,优先级号大于此值的中断都被关。缺省值0:不关闭任何中断 |
FreeRTOS的中断管理就是利用BASEPRI寄存器
比如:BASEPRI设置为0x50,代表中断优先级在5~15内的均被屏蔽,
开关中断代码:将值直接赋给msr basepri
- 在中断服务函数中调度FreeRTOS的API函数需注意:
- 中断服务函数的优先级需要在FreeRTOS所管理的范围内
- 在中断服务函数里边需调用FreeRTOS的API函数,必须使用带"FromISR"后缀的函数
4.FreeRTOS中断管理实验
start_task:创建任务
task1:5秒为间隔开关中断
void task1( void * pvParameters )
{
uint32_t task1_num = 0;
while(1)
{
if(++task1_num == 5)
{
task1_num = 0;
printf("关中断!!\r\n");
portDISABLE_INTERRUPTS();
delay_xms(5000);
printf("开中断!!\r\n");
portENABLE_INTERRUPTS();
}
LED0_TOGGLE();
vTaskDelay(1000);
}
}
六、临界段代码保护及任务调度器挂起和恢复
1.临界区代码保护介绍
1.1.什么是临界段
临界段代码也叫作临界区,是指那些必须完整运行,不能被打断的代码段。
中断和任务调度会打断当前正在运行的程序。
1.2.适用场合
1.外设:需严格按照时序初始化的外设:IIC、SPI等等
2.系统自身需求
3.用户需求
2.临界段代码保护函数介绍
临界区是直接屏蔽了中断,系统任务调度靠中断,ISR也靠中断
FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断
函数 | 描述 |
---|---|
taskENTER_CRITICAL() | 任务级进去临界区 |
taskEXIT_CRITICAL() | 任务级退出临界区 |
taskENTER_CRITICAL_FROM_ISR() | 中断级进去临界区 |
taskEXIT_CRITICAL_FROM_ISR() | 中断级退出临界区 |
// 任务级临界区调用格式示例
taskENTER_CRITICAL() ;
{
… … /* 临界区 */
}
taskEXIT_CRITICAL() ;
// 中断级临界区调用格式示例(在中断服务函数中调用)
uint32_t save_status;
save_status = taskENTER_CRITICAL_FROM_ISR();
{
… … /* 临界区 */
}
taskEXIT_CRITICAL_FROM_ISR(save_status );
成对使用、支持嵌套、尽量保持临界段耗时短
3.任务调度器的挂起和恢复
挂起任务调度器, 调用此函数不需要关闭中断,不关闭中断也是与临界区的区别
函数 | 描述 |
---|---|
vTaskSuspendAll() | 挂起任务调度器 |
xTaskResumeAll() | 恢复任务调度器 |
1、与临界区不一样的是,挂起任务调度器,未关闭中断;
2、它仅仅是防止了任务之间的资源争夺,中断照样可以直接响应;
3、挂起调度器的方式,适用于临界区位于任务与任务之间;既不用去延时中断,又可以做到临界区的安全
vTaskSuspendAll() ;
{
… … /* 内容 */
}
xTaskResumeAll() ;
4.调度器挂起和恢复API函数
4.1.挂起任务调度器
挂起任务调度器:vTaskSuspendAll()
调用一次挂起调度器,变量uxSchedulerSuspended就加一
变量uxSchedulerSuspended的值,将会影响Systick触发PendSV中断,即影响任务调度
4.2.恢复任务调度器
恢复任务调度器:xTaskResumeAll()
调用一次恢复调度器,变量uxSchedulerSuspended就减一
如果等于0,则允许调度
1、当任务数量大于0时,恢复调度器才有意义,如果没有一个已创建的任务就无意义;
2、移除等待就绪列表中的列表项,恢复至就绪列表,知道xPendingReadyList列表为空;
3、如果恢复的任务优先级比当前正在执行任务优先级更高,则将xYieldPending复制为pdTRUE,表示需要进行一次任务切换
4、在调度器被挂起的器件,是否有丢失未处理的滴答数。xPendedCounts是丢失的滴答数,有则调用xTaskIncrementTick()补齐丢失的滴答数;
5、判断是否允许任务切换;
6、返回任务是否已经切换;已经切换返回pdTRUE;反之返回pdFALSE;
七、列表和列表项
1.列表和列表项的简介
1.1.简介
列表是FreeRTOS中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS中的任务。
列表项就是存放在列表中的项目。
列表相当于链表,列表项相当于节点,FreeRTOS中的列表就是双向环形链表。
列表的特点:列表项间的地址非连续的,是人为的连接到一起的。列表项的数目是由后期添加的个数决定的,随时可以改变。
数组的特点:数组成员地址是连续的,数组在最初确定了成员数量后期无法改变
在OS中任务的数量是不确定的,并且任务状态是会发生改变的,所以非常适用列表(链表)这种数据结构
1.2.列表结构
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */
volatile UBaseType_t uxNumberOfItems; /* 列表中的列表项数量 */
ListItem_t * configLIST_VOLATILE pxIndex /* 用于遍历列表项的指针 */
MiniListItem_t xListEnd /* 末尾列表项 */
listSECOND_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */
} List_t;
1、在该结构体中, 包含了两个宏,这两个宏是确定的已知常量, FreeRTOS通过检查这两个常量的值,来判断列表的数据在程序运行过程中,是否遭到破坏 ,该功能一般用于调试, 默认是不开启的
2、成员uxNumberOfItems,用于记录列表中列表项的个数(不包含 xListEnd)
3、成员 pxIndex 用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项
4、成员变量 xListEnd 是一个迷你列表项,排在最末尾
1.3.列表项结构
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测列表项的数据完整性 */
configLIST_VOLATILE TickType_t xItemValue /* 列表项的值 */
struct xLIST_ITEM * configLIST_VOLATILE pxNext /* 下一个列表项 */
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious /* 上一个列表项 */
void * pvOwner /* 列表项的拥有者 */
struct xLIST * configLIST_VOLATILE pxContainer; /* 列表项所在列表 */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用 于检测列表项的数据完整性 */
};
typedef struct xLIST_ITEM ListItem_t;
1、成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序
2、成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项
3、成员变量 pxOwner 用于指向包含列表项的对象(通常是任务控制块)
4、成员变量 pxContainer 用于指向列表项所在列表
1.4.迷你列表项
迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测数据完整性 */
configLIST_VOLATILE TickType_t xItemValue; /* 列表项的值 */
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* 上一个列表项 */
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 下一个列表项 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
1、成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序
2、成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项
3、迷你列表项只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量 pxOwner 和 pxContainer,以节省内存开销
迷你列表性总是在列表末尾,插入的列表项在迷你列表项前面
2.列表相关API函数介绍
函数 | 描述 |
---|---|
vListInitialise() | 初始化列表 |
vListInitialiseItem() | 初始化列表项 |
vListInsertEnd() | 列表末尾插入列表项 |
vListInsert() | 列表插入列表项 |
uxListRemove() | 列表移除列表项 |
2.1.初始化列表vListInitialise()
/**
* @brief 初始化列表
* @param pxList:待初始化列表
* @retval 无
*/
void vListInitialise(List_t * const pxList){
/* 初始化时,列表中只有 xListEnd,因此 pxIndex 指向 xListEnd */
pxList->pxIndex = (ListItem_t *) &( pxList->xListEnd);
/* xListEnd 的值初始化为最大值,用于列表项升序排序时,排在最后 */
pxList->xListEnd.xItemValue = portMAX_DELAY;
/* 初始化时,列表中只有 xListEnd,因此上一个和下一个列表项都为 xListEnd 本身 */
pxList->xListEnd.pxNext = (ListItem_t *) &( pxList->xListEnd);
pxList->xListEnd.pxPrevious = (ListItem_t *) &(pxList->xListEnd);
/* 初始化时,列表中的列表项数量为 0(不包含 xListEnd) */
pxList->uxNumberOfItems = (UBaseType_t) 0U;
/* 初始化用于检测列表数据完整性的校验值 */
listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList);
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList);
}
2.2.初始化列表项vListInitialiseItem()
/**
* @brief 初始化列表项
* @param pxItem:待初始化列表项
* @retval 无
*/
void vListInitialiseItem(ListItem_t * const pxItem){
/* 初始化时,列表项所在列表设为空 */
pxItem->pxContainer = NULL;
/* 初始化用于检测列表项数据完整性的校验值 */
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem);
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem);
}
2.3.列表插入列表项vListInsert()
此函数用于将待插入列表的列表项按照列表项值升序进行排序,有序地插入到列表中
/**
* @brief 列表插入列表项
* @param pxList:列表
* @param pxNewListItem:待插入列表项
* @retval 无
*/
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; /* 获取列表项的数值依据数值升序排列 */
listTEST_LIST_INTEGRITY( pxList ); /* 检查参数是否正确 */
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); /* 如果待插入列表项的值为最大值 */
if( xValueOfInsertion == portMAX_DELAY )
{
pxIterator = pxList->xListEnd.pxPrevious; /* 插入的位置为列表 xListEnd 前面 */
} else
{
for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); /*遍历列表中的列表项,找到插入的位置*/
pxIterator->pxNext->xItemValue <= xValueOfInsertion;
pxIterator = pxIterator->pxNext ) { }
}
pxNewListItem->pxNext = pxIterator->pxNext; /* 将待插入的列表项插入指定位置 */
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;
pxNewListItem->pxContainer = pxList; /* 更新待插入列表项所在列表 */
( pxList->uxNumberOfItems )++; /* 更新列表中列表项的数量 */
}
2.4.pxIndex前面插入列表项vListInsertEnd()
/**
* @brief 列表末尾插入列表项,pxIndex的前面
* @param pxList:列表
* @param pxNewListItem:待插入列表项
* @retval 无
*/
void vListInsertEnd (List_t * const pxList,ListItem_t * const pxNewListItem)
{
//省略部分非关键代码 … …
/* 获取列表 pxIndex 指向的列表项 */
ListItem_t * const pxIndex = pxList->pxIndex;
/* 更新待插入列表项的指针成员变量 */
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
/* 更新列表中原本列表项的指针成员变量 */
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
/* 更新待插入列表项的所在列表成员变量 */
pxNewListItem->pxContainer = pxList;
/* 更新列表中列表项的数量 */
(pxList->uxNumberOfItems)++;
}
此函数用于将待插入列表的列表项插入到列表 pxIndex 指针指向的列表项前面,是一种无序的插入方法
2.5.列表移除列表项uxListRemove()
此函数用于将列表项从列表项所在列表中移除
/**
* @brief 列表移除列表项
* @param pxItemToRemove:待移除的列表项
* @retval 待移除列表项移除后,所在列表剩余列表项的数量
*/
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
/* 获取所在列表 */
List_t * const pxList = pxItemToRemove->pxContainer;
/* 从列表中移除列表项 */
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
/*如果 pxIndex 正指向待移除的列表项 */
if( pxList->pxIndex == pxItemToRemove )
{
/*pxIndex 指向上一个列表项*/
pxList->pxIndex = pxItemToRemove->pxPrevious;
} else
{
mtCOVERAGE_TEST_MARKER();
}
/*将待移除的列表项的所在列表指针清空*/
pxItemToRemove->pxContainer = NULL;
/*更新列表中列表项的数量*/
(pxList->uxNumberOfItems)--;
/*返回移除后的列表中列表项的数量*/
return pxList->uxNumberOfItems;
}
八、任务调度
1.开启任务调度器
vTaskStartScheduler()
用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度
该函数内部实现,如下:
1、创建空闲任务
2、如果使能软件定时器,则创建定时器任务
3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
4、初始化全局变量,并将任务调度器的运行标志设置为已运行
5、初始化任务运行时间统计功能的时基定时器
6、调用函数 xPortStartScheduler()
xPortStartScheduler()
该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
该函数内部实现,如下:
1、检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
2、配置 PendSV 和 SysTick 的中断优先级为最低优先级
3、调用函数 vPortSetupTimerInterrupt()配置 SysTick
4、初始化临界区嵌套计数器为 0
5、调用函数 prvEnableVFP()使能 FPU
6、调用函数 prvStartFirstTask()启动第一个任务
2.启动第一个任务
2.1.开启第一个任务
prvStartFirstTask() /* 开启第一个任务,用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,并使能全局中断*/
假设我们要启动的第一个任务是任务A,那么就需要将任务A的寄存器值恢复到CPU寄存器
任务A的寄存器值,在一开始创建任务时就保存在任务堆栈里边!
1、中断产生时,硬件自动将xPSR,PC(R15),LR(R14),R12,R3-R0出/入栈; 而R4~R11需要手动出/入栈
2、进入中断后硬件会强制使用MSP指针 ,此时LR(R14)的值将会被自动被更新为特殊的EXC_RETURN
MSP指针:程序在运行过程中需要一定的栈空间来保存局部变量信息。当有信息保存到栈中时,MCU会自动更新SP指针,ARM Cortex-M内核提供了两个栈空间;
主堆栈指针(MSP):它由OS内核、异常服务例程以及所有需要特权访问的应用程序代码来使用
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)
在FreeRTOS中,中断使用MSP,中断以外使用PSP。
进入中断后要使用MSP,就需要找到MSP指针,而向量表的第一个是MSP指针,所以需要从位置寄存器VTOR(0xE000ED08)获取向量表的存储的地址,根据向量表存储的地址,来访问第一个元素,也就是MSP。
2.2.SVC中断服务函数
vPortSVCHandler () /* SVC中断服务函数 */
当使能了全局中断,并且手动触发SVC中断后,就会进入到SVC的中断服务函数中
-
通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务 。(FreeRTOS中是软件定时器)
-
通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候,已初始化,然后设置 PSP 指针 。
-
通过往 BASEPRI 寄存器中写 0,允许中断。
-
R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN
而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示:
描述 | 使用浮点单元 | 未使用浮点单元 |
---|---|---|
中断返回后进入Hamdler模式,并使用MSP | 0xFFFFFFE1 | 0xFFFFFFF1 |
中断返回后进入线程模式,并使用 MSP | 0xFFFFFFE9 | 0xFFFFFFF9 |
中断返回后进入线程模式,并使用 PSP | 0xFFFFFFED | 0xFFFFFFFD |
入栈出栈汇编指令详0xFFFFFFFD解:
1、出栈(恢复现场),方向:从下往上(低地址往高地址)。从栈到寄存器
2、压栈(保存现场),方向:从上往下(高地址往低地址)。从寄存器到栈
SVC中断只在启动第一次任务时会调用一次,以后均不调用,之后的任务切换有PendSV完成
3.任务切换
3.1.任务切换过程
任务切换的本质:CPU寄存器的切换。把对应任务寄存器的值加载到CPU寄存器中。
假设当由任务A切换到任务B时,主要分为两步:
第一步:需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场;
第二步:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换
任务切换的过程R0R3寄存器、R12寄存器、LR寄存器、Task_a,xPSR寄存器由硬件自动保存和恢复,R4R11需要手动保存和恢复。
任务切换在PendSV中断服务函数里边完成
3.2.PendSV中断如何触发
1、滴答定时器中断调用
2、执行FreeRTOS提供的相关API函数:portYIELD()
内部主要是压栈和出栈,根据任务优先级获取任务控制块,通过SP指针进行出栈与入栈
3.3.前导置零指令
0 | 0 | 0 | 1 | 0 | …… | 1 | 1 |
---|---|---|---|---|---|---|---|
bit31 | bit30 | bit29 | bit28 | bit27 | …… | bit1 | bit0 |
_clz(uxReadyPriorities) = 3
31-3=28,第28位为最高优先级
前导置零指令,可以理解为计算32位数,头部0的个数
通过前导置零指令获得最高优先级
九、时间片调度实验
1.时间片调度简介
同等优先级任务轮流地享有相同的CPU时间(可设置),叫时间片,在FreeRTOS中,一个时间片就等于SysTick中断周期(可设置)
1、同等优先级任务,轮流执行,之间片流转
2、一个时间片大小,取决为滴答定时器中断频率
3、注意没有用完的时间片不会再使用,下次该任务得到的执行时间还是一个时间片大小
2.时间片调度实验
设计三个任务,start_task、task1、task2,task1和task2优先级相同均为2,为了使实验现象明显,将滴答定时器的中断频率设置为50ms一次
1.使用时间片调度需把宏 configUSE_TIME_SLICING
和 configUSE_PREEMPTION
置1
2.configTICK_RATE_HZ
设置为20即可设置周期为50ms
3.任务1任务2printf信息,观察实验结果
十、FreeRTOS任务相关API函数
1.FreeRTOS任务相关API函数介绍
函数 | 描述 |
---|---|
uxTaskPriorityGet() | 获取任务优先级 |
vTaskPrioritySet() | 设置任务优先级 |
uxTaskGetNumberOfTasks() | 获取系统中任务的数量 |
uxTaskGetSystemState() | 获取所有任务状态信息 |
vTaskGetInfo() | 获取指定单个的任务信息 |
xTaskGetCurrentTaskHandle() | 获取当前任务的任务句柄 |
xTaskGetHandle() | 根据任务名获取该任务的任务句柄 |
uxTaskGetStackHighWaterMark() | 获取任务的任务栈历史剩余最小值 |
eTaskGetState() | 获取任务状态 |
vTaskList() | 以“表格”形式获取所有任务的信息 |
vTaskGetRunTimeStats() | 获取任务的运行时间 |
2.获取任务优先级uxTaskPriorityGet( )
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )
使用该函数需将宏 INCLUDE_uxTaskPriorityGet 置 1
xTask:要查找的任务句柄,NULL代表任务自身
返回值:任务优先级数值(整数)
3.设置任务优先级vTaskPrioritySet()
void vTaskPrioritySet( TaskHandle_t xTask , UBaseType_t uxNewPriority )
使用该函数需将宏 INCLUDE_vTaskPrioritySet 为 1
xTask:任务句柄,NULL代表任务自身
uxNewPriority:需要设置的任务优先级
4.获取系统中任务数量uxTaskGetNumberOfTasks()
UBaseType_t uxTaskGetNumberOfTasks( void )
返回值:系统中任务的数量(整型)
5.获取系统中所有任务的任务状态信息uxTaskGetSystemState()
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime )
typedef struct xTASK_STATUS
{
TaskHandle_t xHandle; /* 任务句柄 */
const char * pcTaskName; /* 任务名 */
UBaseType_t xTaskNumber; /* 任务编号 */
eTaskState e CurrentState; /* 任务状态 */
UBaseType_t uxCurrentPriority; /* 任务优先级 */
UBaseType_t uxBasePriority; /* 任务原始优先级*/
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /* 任务运行时间*/
StackType_t * pxStackBase; /* 任务栈基地址 */
configSTACK_DEPTH_TYPE usStackHighWaterMark; /* 任务栈历史剩余最小值 */
} TaskStatus_t;
使用该函数需将宏 configUSE_TRACE_FACILITY 置 1
xTaskStatusArray:指向TaskStatus_t 结构体数组首地址
uxArraySize:接收信息的数组大小
pulTotalRunTime:系统总运行时间,为NULL 则省略总运行时间值
返回时:获取信息的任务数量(整型)
6.获取单个任务的状态信息vTaskGetInfo()
void vTaskGetInfo( TaskHandle_t xTask,
TaskStatus_t * pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState )
typedef enum
{
eRunning = 0, /* 运行态 */
eReady /* 就绪态 */
eBlocked, /* 阻塞态 */
eSuspended, /* 挂起态 */
eDeleted, /* 任务被删除 */
eInvalid /* 无效 */
} eTaskState;
使用该函数需将宏 configUSE_TRACE_FACILITY 置 1
xTask:任务状态,可直接赋值,如想获取代入“eInvalid”
pxTaskStatus: 任务栈历史剩余最小值,
当为“pdFALSE” 则跳过这个步骤,
当为“pdTRUE”则检查历史剩余最小堆栈
xGetFreeStackSpace:接收任务信息的变量
eState:任务状态,可直接赋值,如想获取代入“eInvalid”
7.获取当前任务的任务句柄xTaskGetCurrentTaskHandle()
TaskHandle_t xTaskGetCurrentTaskHandle( void )
使用该函数需将宏 INCLUDE_xTaskGetCurrentTaskHandle 置 1
返回值:当前任务的任务句柄
8.通过任务名获取任务句柄xTaskGetHandle()
TaskHandle_t xTaskGetHandle(const char * pcNameToQuery);
使用该函数需将宏 INCLUDE_xTaskGetHandle 置 1
pcNameToQuery:任务名
返回值:任务句柄
9.获取指定任务的任务栈历史最小剩余堆栈uxTaskGetStackHighWaterMark()
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
使用该函数需将宏 INCLUDE_uxTaskGetStackHighWaterMark 置 1
xTask:任务句柄
返回值:任务栈的历史剩余最小值
10.查询某个任务的运行状态eTaskGetState
eTaskState eTaskGetState(TaskHandle_t xTask)
typedef enum
{
eRunning = 0, /* 运行态 */
eReady /* 就绪态 */
eBlocked, /* 阻塞态 */
eSuspended, /* 挂起态 */
eDeleted, /* 任务被删除 */
eInvalid /* 无效 */
} eTaskState;
使用此函数需将宏 INCLUDE_eTaskGetState 置1
xTask:任务句柄
返回值:任务状态
11.以“表格”的形式获取系统中任务的信息vTaskList
void vTaskList(char * pcWriteBuffer)
使用此函数需将宏 configUSE_TRACE_FACILITY 和configUSE_STATS_FORMATTING_FUNCTIONS 置1
pcWriteBuffer:接收任务信息的缓存指针
- Name : 创建任务的时候给任务分配的名字。
- State : 任务的壮态信息, X是运行态,B 是阻塞态, R 是就绪态, S 是挂起态, D 是删除态
- Priority :任务优先级。
- Stack : 任务堆栈的“高水位线”,就是堆栈历史最小剩余大小。
- Num : 任务编号,这个编号是唯一的,当多个任务使用同一个任务名的时候可以通过此编号来做区分
12.统计任务的运行时间信息vTaskGetRunTimeStats()
void vTaskGetRunTimeStats( char * pcWriteBuffer )
使用此函数需将宏 configGENERATE_RUN_TIME_STAT 、configUSE_STATS_FORMATTING_FUNCTIONS 置1
Task :任务名字
Abs Time :任务实际运行的总时间(绝对时间),单位不是秒,需要*软件定时器周期
Time:占总处理时间的百分比
使用流程:
1、将宏 configGENERATE_RUN_TIME_STATS
置1
2、将宏configUSE_STATS_FORMATTING_FUNCTIONS
置1
3、当将此宏 configGENERATE_RUN_TIME_STATS
置1之后,还需要实现2个宏定义:
①portCONFIGURE_TIMER_FOR_RUNTIME_STATE()
:用于初始化用于配置任务运行时间统计的时基定时器;注意:这个时基定时器的计时精度需高于系统时钟节拍精度的10至100倍!
②portGET_RUN_TIME_COUNTER_VALUE()
:用于获取该功能时基硬件定时器计数的计数值 。
十一、FreeRTOS时间管理
1.延时函数介绍
函数 | 描述 |
---|---|
vTaskDelay() | 相对延时 |
xTaskDelayUntil() | 绝对延时 |
相对延时:指每次延时都是从执行函数vTaskDelay()开始,直到延时指定的时间结束(只有延时函数才算延时)
绝对延时:指将整个任务的运行周期看做一个整体,适用于需要按照一定频率运行的任务(从任务开始都算作延时,切换到其他任务的时间也算)
十二、消息队列
1.队列简介
队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(消息传递)。
1.1.队列的优点
-
全局变量的弊端:数据无保护,导致数据不安全,当多个任务同时对该变量操作时,数据易受损。
-
写a++步骤:1.把a读到寄存器;2.寄存器的值+1;3.把寄存器的值赋给a;
-
如果有两个任务都修改了一个全局变量,任务一在执行2之后切换到了任务二,那么只要第二个任务失效,导致数据不安全。
所以使用队列能提高数据的安全性
1.2.FreeRTOS中队列的功能
FreeRTOS基于队列, 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量,因此很有必要深入了解 FreeRTOS 的队列 。
读写队列的时候做好保护,加入临界区保护,防止多任务同时访问冲突。
1.3.队列的组成:
-
在队列中可以存储数量有限、大小固定的数据。队列中的每一个数据叫做“队列项目”,队列项目有大小,队列能够存储“队列项目”的最大数量称为队列的长度。
-
在创建队列时,就要指定队列长度以及队列项目的大小!
1.4.FreeRTOS队列特点
1.数据入队出队方式:队列通常采用“先进先出”(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取,FreeRTOS中也可以配置为“后进先出”LIFO方式;
2.数据传递方式:FreeRTOS中队列采用实际值传递,即将数据拷贝到队列中进行传递, FreeRTOS采用拷贝数据传递,也可以传递指针,所以在传递较大的数据的时候采用指针传递
3.多任务访问:队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息
4.出队、入队阻塞:当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队
- ①若阻塞时间为0 :直接返回不会等待;
- ②若阻塞时间为0~port_MAX_DELAY :等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待;
- ③若阻塞时间为port_MAX_DELAY :死等,一直等到可以入队为止。出队阻塞与入队阻塞类似;
队列满时还有待入队消息时:①将该任务的状态列表项挂载在pxDelayedTaskList;(阻塞列表)
②将该任务的事件列表项挂载在xTasksWaitingToSend;(等待发送列表)
队列为空还要读取消息时:①将该任务的状态列表项挂载在pxDelayedTaskList;
②将该任务的事件列表项挂载在xTasksWaitingToReceive;
队列中有空位置后,需要有任务发送消息入队,这个时候根据就绪任务的优先级,优先级越高的先入队,如果优先级一样,就等待时间最长的先入队。
2.队列结构体介绍
typedef struct QueueDefinition
{
int8_t * pcHead /* 存储区域的起始地址 */
int8_t * pcWriteTo; /* 下一个写入的位置 */
union
{
QueuePointers_t xQueue;
SemaphoreData_t xSemaphore;
} u ;
List_t xTasksWaitingToSend; /* 等待发送列表 */
List_t xTasksWaitingToReceive; /* 等待接收列表 */
volatile UBaseType_t uxMessagesWaiting; /* 非空闲队列项目的数量 */
UBaseType_t uxLength; /* 队列长度 */
UBaseType_t uxItemSize; /* 队列项目的大小 */
volatile int8_t cRxLock; /* 读取上锁计数器 */
volatile int8_t cTxLock; /* 写入上锁计数器 */
/* 其他的一些条件编译 */
} xQUEUE;
当联合体用于队列使用时:
typedef struct QueuePointers
{
int8_t * pcTail; /* 存储区的结束地址 */
int8_t * pcReadFrom; /* 最后一个读取队列的地址 */
} QueuePointers_t;
当联合体用于互斥信号量和递归互斥信号量时 :
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; /* 互斥信号量持有者 */
UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量的获取计数器 */
} SemaphoreData_t;
队列结构体整体示意图
3.队列相关API函数介绍
使用队列主要流程:创建队列 --> 写队列 --> 读队列
3.1.创建队列
函数 | 描述 |
---|---|
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
区别:队列所需的内存空间由FreeRTOS从它管理的堆中分配,而静态创建需要用户自行分配
#define xQueueCreate(uxQueueLength,uxItemSize) xQueueGenericCreate((uxQueueLength),(uxItemSize),(queueQUEUE_TYPE_BASE)) // 动态创建
参数:
-
uxQueueLength:队列长度
-
uxItemSize:队列项目的大小
-
queueQUEUE_TYPE_BASE:队列类型
返回值:
- NULL则创建失败
- 其他则创建成功,返回队列句柄
// 队列类型可以有以下类型,在queue.h文件中定义
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) /* 队列 */
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) /* 队列集 */
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) /* 互斥信号量 */
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) /* 计数型信号量 */
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) /* 二值信号量 */
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) /* 递归互斥信号量
3.2.写队列
函数 | 描述 |
---|---|
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
#define xQueueSend (xQueue, pvItemToQueue, xTicksToWait)
xQueueGenericSend ((xQueue), (pvItemToQueue),(xTicksToWait), queueSEND_TO_BACK)
#define xQueueSendToBack (xQueue, pvItemToQueue, xTicksToWait)
xQueueGenericSend ((xQueue), (pvItemToQueue),(xTicksToWait), queueSEND_TO_BACK)
#define xQueueSendToFront (xQueue, pvItemToQueue, xTicksToWait)
xQueueGenericSend ((xQueue), (pvItemToQueue),(xTicksToWait), queueSEND_TO_FRONT)
#define xQueueOverwrite (xQueue, pvItemToQueue)
xQueueGenericSend ((xQueue), (pvItemToQueue),0, queueOVERWRITE)
// 可以看到这几个写入函数调用的是同一个函数xQueueGenericSend( ),只是指定了不同的写入位置
// 前两个参数都一样,第四个腹泻队列不用阻塞,所以阻塞等待时间为0,最后一位是插入位置
队列一共有 3 种写入位置 :
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部 */
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部 */
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列*/
注意:覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用
BaseType_t xQueueGenericSend(QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition );
参数:
-
xQueue:待写入的队列
-
pvItemToQueue:待写入消息
-
xTicksToWait:阻塞超时时间
-
xCopyPosition:写入的位置
返回值:
-
pdTRUE:队列写入成功
-
errQUEUE_FULL:队列写入失败
3.3.读队列
函数 | 描述 |
---|---|
xQueueReceive() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeek() | 在中断中从队列头部读取消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait) // 读取后删除
BaseType_t xQueuePeek (QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait) // 读取后不删除
形参:
- xQueue:待读取的队列
- pvBuffer:信息读取缓冲区
- xTicksToWait:阻塞超时时间
返回值:
- pdTRUE:读取成功
- pdFALSE:读取失败
3.4.实验
// 创建队列
QueueHandle_t key_queue; // 小数据队列句柄
QueueHandle_t big_date_queue; // 大数据队列句柄
char buff[100] = {"我是一个大数组,大大的数组 1241243 jdklsafjklsdajklsda"};
key_queue = xQueueCreate( 2,sizeof(uint8_t)); //创建小数据队列
if(key_queue != NULL)
{
printf("key_queue队列创建成功 \r\n");
}
else
{
printf("key_queue队列创建失败 \r\n");
}
big_date_queue = xQueueCreate( 1,sizeof(char *)); //创建大数据队列
if(big_date_queue != NULL)
{
printf("big_date_queue队列创建成功 \r\n");
}
else
{
printf("big_date_queue队列创建失败 \r\n");
}
// 写队列
uint8_t key = 0;
BaseType_t err = 0;
char * buf;
buf = buff; // 相当于buf = &buff[0]
while(1)
{
key = KEY_Scan(0);
if(key == KEY0_PRESS)
{
err = xQueueSend(key_queue,&key,portMAX_DELAY);
if(err != pdTRUE)
{
printf("key_queue队列发送失败 \r\n");
}
}
if(key == KEY2_PRESS)
{
err = xQueueSend(big_date_queue,&buf,portMAX_DELAY);
if(err != pdTRUE)
{
printf("big_date_queue队列发送失败 \r\n");
}
}
vTaskDelay(10);
}
// 读小数据队列
BaseType_t err = 0;
uint8_t key = 0;
while(1)
{
err = xQueueReceive( key_queue,&key,portMAX_DELAY);
if(err != pdTRUE)
{
printf("key_queue队列接收失败 \r\n");
}else
{
printf("数据读取成功,数据:%d \r\n",key);
}
}
// 读大数据队列
BaseType_t err = 0;
char * buf;
while(1)
{
err = xQueueReceive( big_date_queue,&buf,portMAX_DELAY);
if(err != pdTRUE)
{
printf("key_queue队列接收失败 \r\n");
}else
{
printf("数据读取成功,数据:%s \r\n",buf);
}
}
十三、信号量
1.信号量简介
-
队列是传递数据的,信号量是传递状态的
-
信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。
-
信号量的计数值(资源数)有限制:限定最大值
-
二值信号量:限定最大值等于1
-
计数型信号量:限定最大值不是1
队列和信号量的对比
队列 | 信号量 |
---|---|
可以容纳多个数据; | 仅存放计数值,无法存放其他数据; |
创建队列有两部分内存:队列结构体+队列项存储空间 | 创建信号量,只需分配信号量结构体 |
写入队列:当队列满时,可阻塞; | 释放信号量:不可阻塞,计数值++,当计数值为最大值时,返回失败 |
读取队列:当队列为空时,可阻塞 | 获取信号量:计数值–,当没有资源时,可阻塞 |
2.二值信号量
2.1二值信号量介绍
二值信号量的本质是一个队列长度为 1 的队列 ,该队列就只有空和满两种情况,这就是二值。
二值信号量通常用于互斥访问或任务同步, 与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题 ,所以二值信号量更适合用于同步!
2.2.二值信号量相关API函数
使用二值信号量的过程:创建二值信号量 --> 释放二值信号量 --> 获取二值信号量
函数 | 描述 |
---|---|
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
-
创建二值信号量函数:
SemaphoreHandle_t xSemaphoreCreateBinary( void ) // 和队列创建是同一个函数,只是这个固定一个队列项,队列项的大小是0 #define xSemaphoreCreateBinary( ) xQueueGenericCreate(1,semSEMAPHORE_QUEUE_ITEM_LENGTH,queueQUEUE_TYPE_BINARY_SEMAPHORE) #define semSEMAPHORE_QUEUE_ITEM_LENGTH ( ( uint8_t ) 0U )
返回值:
- NULL:创建失败
- 其他值:创建成功返回二值信号量的句柄
队列项的大小是0
为什么队列项大小是0还能释放和获取信号量:其实是对信号量非空闲队列项目(这里叫计数值)的数量+1或者-1
-
释放二值信号量函数:
BaseType_t xSemaphoreGive( xSemaphore ) #define xSemaphoreGive ( xSemaphore ) xQueueGenericSend((QueueHandle_t)(xSemaphore),NULL,semGIVE_BLOCK_TIME,queueSEND_TO_BACK) #define semGIVE_BLOCK_TIME ( ( TickType_t ) 0U
形参:
- xSemaphore:要释放的信号量句柄
返回值:
- pdPASS:释放信号量成功
- errQUEUE_FULL:释放信号量失败
-
获取二值信号量函数:
BaseType_t xSemaphoreTake( xSemaphore, xBlockTime )
形参:
- xSemaphore:要释放的信号量句柄
- xBlockTime:阻塞时间
返回值:
- pdTRUE:获取信号量成功
- pdFALSE:超时,获取信号量失败
3.计数型信号量
3.1.计数型信号量简介
计数型信号量相当于队列长度大于1的队列,因此计数型信号量能够容纳多个资源,这再计数型信号量被创建的时候确定的
计数型信号量适用场合:
- 事件计数:当每次事件发生后,在事件处理函数中释放计数型信号量(计数值+1),其他任务会获取计数型信号量(计数值-1) ,这种场合一般在创建时将初始计数值设置为 0
- 资源管理:信号量表示有效的资源数目。任务必须先获取信号量(信号量计数值-1 )才能获取资源控制权。当计数值减为零时表示没有的资源。当任务使用完资源后,必须释放信号量(信号量计数值+1)。信号量创建时计数值应等于最大资源数目
3.2.计数型信号量相关API函数
使用计数型信号量过程:创建计数型信号量 --> 释放信号量 --> 获取信号量
函数 | 描述 |
---|---|
xSemaphoreCreateCounting() | 使用动态方法创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方法创建计数型信号量 |
uxSemaphoreGetCount() | 获取信号量的计数值 |
计数型信号量的释放和获取的API函数与二值信号量完全一样
-
创建计数型信号量
#define xSemaphoreCreateCounting (uxMaxCount, uxInitialCount) xQueueCreateCountingSemaphore ((uxMaxCount), (uxInitialCount))
形参:
- uxMaxCount:计数值的最大限定值
- uxInitialCount:计数值的初始值
返回值:
- NULL:创建失败
- 其他值:创建成功并返回计数型信号量的句柄
-
获取信号量当前计数值大小
#define uxSemaphoreGetCount (xSemaphore) uxQueueMessagesWaiting((QueueHandle_t) (xSemaphore))
形参:
- xSemaphore:信号量句柄
返回值:
- 整数:当前信号量的计数值大小
4.优先级翻转
4.1.优先级翻转简介
优先级翻转:高优先级的任务慢执行,低优先级的任务先执行。
优先级翻转在抢占式内核中是非常常见的,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转会破坏任务的预期顺序,可能会导致未知的严重后果。
在使用二值信号量的时候,经常会遇到优先级翻转的问题,例如以下场景:
-
有三个任务,优先级分别而1,2,3,其中任务1和任务3需要获取二值信号量执行,任务2不需要,任务1的执行时间比任务3长
-
执行顺序如下:
- 任务3先执行,然后任务2执行,然后任务1执行
- 任务1获取信号量之后被任务2抢占,(由于任务1获取了二值信号量,所以任务3阻塞,无法抢占),任务2执行,任务1执行,任务3执行,这样任务的优先级就发生了翻转
5.互斥信号量
5.1.互斥信号领简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中二值信号量最合适。互斥信号量适用于那些需要互斥访问的应用中。
优先级继承:当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响
注意:互斥信号量不能用于中断服务函数中,原因如下
(1) 互斥信号量有任务优先级继承的机制, 但是中断不是任务,没有任务优先级, 所以互斥信号量只能用与任务中,不能用于中断服务函数。
(2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
5.2.互斥信号量相关API函数
使用互斥信号量:首先将宏configUSE_MUTEXES置一
使用流程:创建互斥信号量 -->(task)获取信号量 -->(give)释放信号量
创建互斥信号量函数(创建完成自动释放一次信号量):
函数 | 描述 |
---|---|
xSemaphoreCreateMutex() | 使用动态方法创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方法创建互斥信号量 |
互斥信号量的释放和获取函数与二值信号量相同 !只不过互斥信号量不支持中断中调用
十四、队列集
1.队列集简介
-
一个队列只允许任务间传递的消息为同一种数据类型,如果需要在任务间传递不同数据类型的消息时,那么就可以使用队列集 !
-
作用:用于对多个队列或信号量进行“监听”,其中不管哪一个消息到来,都可让任务退出阻塞状态
2.队列集相关API函数介绍
函数 | 描述 |
---|---|
xQueueCreateSet() | 创建队列集 |
xQueueAddToSet() | 队列添加到队列集中 |
xQueueRemoveFromSet() | 从队列集中移除队列 |
xQueueSelectFromSet() | 获取队列集中有有效消息的队列 |
xQueueSelectFromSetFromISR() | 在中断中获取队列集中有有效消息的队列 |
2.1.创建队列集
启用队列集功能需要将宏configUSE_QUEUE_SETS
配置为 1
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );
形参:
uxEventQueueLength:队列集可容纳的队列数量
返回值:
NULL:队列集创建失败
其他值:队列集创建成功,返回队列集句柄
2.2.队列集中添加队列
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,QueueSetHandle_t xQueueSet);
此函数用于往队列集中添加队列,要注意的时,队列在被添加到队列集之前,队列中不能有有效的消息
形参 | 描述 |
---|---|
xQueueOrSemaphore | 待添加的队列句柄 |
xQueueSet | 队列集 |
返回值 | 描述 |
---|---|
pdPASS | 队列集添加队列成功 |
pdFAIL | 队列集添加队列失败 |
2.3.队列集中移除队列
BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore,QueueSetHandle_t xQueueSet );
此函数用于从队列集中移除队列, 要注意的是,队列在从队列集移除之前,必须没有有效的消息
形参 | 描述 |
---|---|
xQueueOrSemaphore | 待移除的队列句柄 |
xQueueSet | 队列集 |
返回值 | 描述 |
---|---|
pdPASS | 队列集移除队列成功 |
pdFAIL | 队列集移除队列失败 |
2.4.队列集中获取有效消息
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,TickType_t const xTicksToWait)
此函数用于在任务中获取队列集中有有效消息的队列
形参 | 描述 |
---|---|
xQueueSet | 队列集 |
xTicksToWait | 阻塞超时时间 |
返回值 | 描述 |
---|---|
NULL | 获取消息失败 |
其他值 | 获取到消息的队列句柄 |
十五、事件标志组
1.事件标志组简介
事件标志位:用一个位,来表示事件是否发生
事件标记组:用一个整数,来表示一组事件标志位,这个整数的二进制每一位表示一个事件标志位(高8位存储事件标志组的控制信息,不是事件标志位)
1.1.事件标志组的数据类型EventBits_t
一个事件组就包含了一个 EventBits_t
数据类型的变量,变量类型 EventBits_t
的定义如下所示:
typedef TickType_t EventBits_t;
#if ( configUSE_16_BIT_TICKS = = 1 )
typedef uint16_t TickType_t;
#else
typedef uint32_t TickType_t;
#endif
#define configUSE_16_BIT_TICKS 0 // 这个标志位为0就是32位
// EventBits_t 实际上是一个 16 位或 32 位无符号的数据类型 ,一个事件组最多可以存储24个事件标志
1.2.事件标志组与队列、信号量的区别
功能 | 唤醒对象 | 事件清除 |
---|---|---|
队列、信号量 | 事件发生时,只会唤醒一个任务 | 是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了 |
事件标志组 | 事件发生时,会唤醒所有符合条件的任务,可以理解为“广播”的作用 | 被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件 |
2.事件标志组相关API函数介绍
函数 | 描述 |
---|---|
xEventGroupCreate() | 使用动态方式创建事件标志组 |
xEventGroupCreateStatic() | 使用静态方式创建事件标志组 |
xEventGroupClearBits() | 清零事件标志位 |
xEventGroupClearBitsFromISR() | 在中断中清零事件标志位 |
xEventGroupSetBits() | 设置事件标志位 |
xEventGroupSetBitsFromISR() | 在中断中设置事件标志位 |
xEventGroupWaitBits() | 等待事件标志位 |
xEventGroupSync() | 设置事件标志位,并等待事件标志位 |
2.1.动态创建
EventGroupHandle_t xEventGroupCreate ( void ) ;
返回值 | 描述 |
---|---|
NULL | 事件标志组创建失败 |
其他值 | 事件标志组创建成功,返回其句柄 |
2.2.清除事件标志位
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear )
形参 | 描述 |
---|---|
xEventGroup | 待操作的事件标志组句柄 |
uxBitsToSet | 待清零的事件标志位 |
返回值 | 描述 |
---|---|
整数 | 清零事件标志位之前事件组中事件标志位的值 |
2.3.设置事件标志位
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet)
形参 | 描述 |
---|---|
xEventGroup | 待操作的事件标志组句柄 |
uxBitsToSet | 待设置的事件标志位 |
返回值 | 描述 |
---|---|
整数 | 函数返回时,事件组中的事件标志位值 |
2.4.等待事件标志位
可以等待某一位、也可以等待多位,等到后,还可以清除某些位
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait )
形参 | 描述 |
---|---|
xEvenrGroup | 等待的事件标志组句柄 |
uxBitsToWaitFor | 等待的事件标志位,可以用逻辑或等待多个事件标志位 |
xClearOnExit | 成功等待到事件标志位后,清除事件组中对应的事件标志位, pdTRUE :清除uxBitsToWaitFor指定位; pdFALSE:不清除 |
xWaitForAllBits | 等待 uxBitsToWaitFor 中的所有事件标志位(逻辑与) pdTRUE:等待的位,全部为1 pdFALSE:等待的位,某个为1 |
xTicksToWait | 等待的阻塞时间 |
返回值 | 描述 |
---|---|
等待的事件标志位值 | 等待事件标志位成功,返回等待到的事件标志位 |
其他值 | 等待事件标志位失败,返回事件组中的事件标志位 |
2.5.同步函数
等待一些位全部成功后,设置另一个位,这一些位即同步
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait)
形参 | 描述 |
---|---|
xEventGroup | 等待事件标志所在事件组 |
uxBitsToSet | 达到同步点后,要设置的事件标志 |
uxBitsToWaitFor | 等待的事件标志 |
xTicksToWait | 等待的阻塞时间 |
返回值 | 描述 |
---|---|
等待的事件标志位值 | 等待事件标志位成功,返回等待到的事件标志位 |
其他值 | 等待事件标志位失败,返回事件组中的事件标志位 |
十六、任务通知
1.任务通知简介
任务通知:用来通知任务的,任务控制块中的结构体成员变量ulNotifiedValue
就是这个通知值
- 队列,信号量,任务标志组都需要另外创建结构体,任务通过中间的结构体进行通信
- 任务通知使用任务结构体TCB中的内部对象,直接接收其他任务的通知
1.2.任务通知值的更新方式
- 不覆盖接受任务的通知值(不覆盖)
- 覆盖接受任务的通知值(覆盖)
- 更新接受任务通知值的一个或多个bit(更新一个或者多个位)
- 增加接受任务的通知值(增加)
利用这些特点,可以在一些场合中替代队列、信号量、事件标志组
1.3.任务通知的优势以及劣势
-
优势
- 效率更高:发送事件或数据更快
- 使用内存小:不用创建结构体
-
劣势
- 无法发送给ISR:ISR没有任务结构体,所以无法接受,但是ISR可以给任务发送
- 无法广播给多个任务:任务通知只能被指定的一个任务接受并处理
- 无法缓存多个数据:任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据
- 发送受阻不支持阻塞:发送方无法进入阻塞状态等待
2.任务通知值和通知状态
任务都有一个结构体:任务控制块TCB,它里面有两个结构体成员变量:
typedef struct tskTaskControlBlock
{
… …
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue [ configTASK_NOTIFICATION_ARRAY_ENTRIES ];// 一个是 uint32_t 类型,用来表示通知值
volatile uint8_t ucNotifyState [ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; // 一个是 uint8_t 类型,用来表示通知状态
endif
… …
} tskTCB;
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 定义任务通知数组的大小, 默认: 1 */
一个是 uint32_t 类型,用来表示通知值
一个是 uint8_t 类型,用来表示通知状态
2.1.任务通知值
任务通知值的更新方式有多种类型:
- 计数值(数值累加,类似信号量)
- 相应的位置1(类似事件标志组)
- 任意数值(支持覆写和不覆写,类似队列)
2.2.任务通知状态
其中任务通知状态共有3种取值:
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 任务未等待通知 */
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) /* 任务在等待通知 */
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 ) /* 任务在等待接收 */
- 任务未等待通知 :任务通知默认的初始化状态
- 等待通知:接收方已经准备好了(调用了接收任务通知函数),等待发送方给个通知
- 等待接收:发送方已经发送出去(调用了发送任务通知函数),等待接收方接收
3.任务通知相关API函数介绍
任务通知API函数主要有两类:1.发送通知,2.接收通知
注意:发送通知可以用于任务和中断,而接收通知只能在任务中
3.1.发送通知相关API函数
函数 | 描述 |
---|---|
xTaskNotify() | 发送通知,带有通知值 |
xTaskNotifyAndQuery() | 发送通知,带有通知值并且保留接收任务的原通知值 |
xTaskNotifyGive() | 发送通知,不带通知值 |
xTaskNotifyFromISR() | 在中断中发送任务通知 |
xTaskNotifyAndQueryFromISR() | |
vTaskNotifyGiveFromISR() |
#define xTaskNotifyAndQuery( xTaskToNotify, ulValue , eAction , pulPreviousNotifyValue ) // 带通知值,保留接收任务原通知值
xTaskGenericNotify(( xTaskToNotify),
(tskDEFAULT_INDEX_TO_NOTIFY ),
(ulValue ),
(eAction ),
(pulPreviousNotifyValue ) )
#define xTaskNotify ( xTaskToNotify , ulValue , eAction ) // 带通知值
xTaskGenericNotify( ( xTaskToNotify ) , ( tskDEFAULT_INDEX_TO_NOTIFY ) , ( ulValue ) , ( eAction ) , NULL )
#define xTaskNotifyGive( xTaskToNotify ) // 不带通知值
xTaskGenericNotify( ( xTaskToNotify ) , ( tskDEFAULT_INDEX_TO_NOTIFY ) , ( 0 ) , eIncrement , NULL )
// 任务通知原函数
BaseType_t xTaskGenericNotify(TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue )
形参 | 描述 |
---|---|
xTaskToNotify | 接收任务通知的任务句柄 |
uxIndexToNotify | 任务的指定通知(任务通知相关数组成员) |
ulValue | 任务通知值 |
eAction | 通知方式(通知值更新方式) |
pulPreviousNotificationValue | 用于保存更新前的任务通知值(为NULL则不保存) |
任务通知方式共有一下几种:
typedef enum
{
eNoAction = 0, /* 无操作 */
eSetBits /* 更新指定bit */
eIncrement /* 通知值加一 */
eSetValueWithOverwrite /* 覆写的方式更新通知值 , 有没有值都写入*/
eSetValueWithoutOverwrite /* 不覆写通知值 , 有值不写入*/
} eNotifyAction;
3.2.接收通知相关API函数
函数 | 描述 |
---|---|
ulTaskNotifyTake() | 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。 当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量。 |
xTaskNotifyWait() | 获取任务通知,比 ulTaskNotifyTak()更为复杂,可获取通知值和清除通知值的指定位 |
- 当任务通知用作于信号量时,使用函数获取信号量:ulTaskNotifyTake()
- 当任务通知用作于事件标志组或队列时,使用此函数来获取: xTaskNotifyWait()
#define ulTaskNotifyTake( xClearCountOnExit , xTicksToWait )
ulTaskGenericNotifyTake(( tskDEFAULT_INDEX_TO_NOTIFY ),
( xClearCountOnExit ),
( xTicksToWait ) )
此函数用于接收任务通知值,可以设置在退出此函数的时候将任务通知值清零或者减一
形参 | 描述 |
---|---|
uxIndexToWaitOn | 任务的指定通知(任务通知相关数组成员) |
xClearCountOnExit | 指定在成功接收通知后,将通知值清零或减 1, pdTRUE:把通知值清零;pdFALSE:把通知值减一 |
xTicksToWait | 阻塞等待任务通知值的最大时间 |
返回值 | 描述 |
---|---|
0 | 接收失败 |
非 0 | 接收成功,返回任务通知的通知值 |
#define xTaskNotifyWait(ulBitsToClearOnEntry,
ulBitsToClearOnExit,
pulNotificationValue,
xTicksToWait)
xTaskGenericNotifyWait( tskDEFAULT_INDEX_TO_NOTIFY,
( ulBitsToClearOnEntry ),
( ulBitsToClearOnExit ),
( pulNotificationValue ),
( xTicksToWait ) )
此函数用于获取通知值和清除通知值的指定位值,适用于模拟队列和事件标志组,使用该函数来获取任务通知 。
BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWaitOn,
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait);
形参 | 描述 |
---|---|
uxIndexToWaitOn | 任务的指定通知(任务通知相关数组成员) |
ulBitesToClearOnEntry | 等待前清零指定任务通知值的比特位(旧值对应bit清0) |
ulBitesToClearOnExit | 成功等待后清零指定的任务通知值比特位(新值对应bit清0) |
pulNotificationValue | 用来取出通知值(如果不需要取出,可设为NULL) |
xTicksToWait | 阻塞等待任务通知值的最大时间 |
返回值 | 描述 |
---|---|
pdTRUE | 等待任务通知成功 |
pdFALSE | 等待任务通知失败 |
十七、软件定时器
1.软件定时器简介
1.1.软件定时器优缺点
优点:
- 硬件定时器数量有限,而软件定时器理论上只需有足够内存,就可以创建多个;
- 使用简单、成本低
缺点:
- 软件定时器相对硬件定时器来说,精度没有那么高(因为它以系统时钟为基准,系统时钟中断优先级又是最低,容易被打断)。 对于需要高精度要求的场合,不建议使用软件定时器。
特点:
- 可裁剪:软件定时器是可裁剪可配置的功能, 如果要使能软件定时器,需将
configUSE_TIMERS
配置项配置成 1 - 单词和周期:软件定时器可以设置成:单词定时器或周期定时器
作用:
- 负责软件定时器超时的逻辑判断
- 调用超时软件定时器的超时回调函数
- 处理软件定时器命令队列
1.2.软件定时器服务任务
开启任务调度器会创建软件定时器服务任务和空闲任务
软件定时器的超时回调函数由软件定时器服务任务调用,软件定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数。
软件定时器服务任务:在调用函数 vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务。
应用程序发送命令到软件定时器命令队列,软件定时器服务任务从命令队列接收到命令,并执行
软件定时器的超时回调函数是在软件定时器服务任务中被调用的,服务任务不是专为某个定时器服务的,它还要处理其他定时器。
FreeRTOS有许多软件定时器相关的 API 函数,这些 API 函数大多都是往定时器的队列中写入消息(发送命令),这个队列叫做软件定时器命令队列,是提供给 FreeRTOS 中的软件定时器使用的,用户是不能直接访问的。
1.3.软件定时器状态
- 休眠态:软件定时器可以通过其句柄被引用,但因为没有运行,所以其定时超时回调函数不会被执行
- 运行态:运行态的定时器,当指定时间到达之后,它的超时回调函数会被调用
新创建的软件定时器处于休眠状态 ,也就是未运行的,需要发送命令队列启用
1.4.单次定时器和周期定时器
- 单次定时器:单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,不会自动重新开启定时,不过可以被手动重新开启。
- 周期定时器:周期定时器的一旦启动以后就会在执行完回调函数以后自动的重新启动 ,从而周期地执行其软件定时器回调函数。
1.5.软件定时器结构体成员介绍
typedef struct
{
const char * pcTimerName /* 软件定时器名字 */
ListItem_t xTimerListItem /* 软件定时器列表项 */
TickType_t xTimerPeriodInTicks; /* 软件定时器的周期 */
void * pvTimerID /* 软件定时器的ID */
TimerCallbackFunction_t pxCallbackFunction; /* 软件定时器的回调函数 */
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber /* 软件定时器的编号,调试用 */
#endif
uint8_t ucStatus; /* 软件定时器的状态 */
} xTIMER;
2.软件定时器相关配置
-
当FreeRTOS 的配置项
configUSE_TIMERS
设置为1,在启动任务调度器时,会自动创建软件定时器的服务/守护任务prvTimerTask( ) ; -
软件定时器服务任务的优先级为
configTIMER_TASK_PRIORITY
= 31; -
定时器的命令队列长度为 configTIMER_QUEUE_LENGTH = 5
所以,定时器的回调函数不要影响其他软件定时器
回调函数要尽快实行,不能进入阻塞状态,即不能调用那些会阻塞任务的 API 函数,如:vTaskDelay()
访问队列或者信号量的非零阻塞时间的 API 函数也不能调用
软件定时器列表有:软件定时器列表,溢出软件定时器列表
3.软件定时器相关API函数
函数 | 描述 |
---|---|
xTimerCreate() | 动态方式创建软件定时器 |
xTimerCreateStatic() | 静态方式创建软件定时器 |
xTimerStart() | 开启软件定时器定时 |
xTimerStartFromISR() | 在中断中开启软件定时器定时 |
xTimerStop() | 停止软件定时器定时 |
xTimerStopFromISR() | 在中断中停止软件定时器定时 |
xTimerReset() | 复位软件定时器定时 |
xTimerResetFromISR() | 在中断中复位软件定时器定时 |
xTimerChangePeriod() | 更改软件定时器的定时超时时间 |
xTimerChangePeriodFromISR() | 在中断中更改定时超时时间 |
3.1.创建软件定时器API函数
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
形参 | 描述 |
---|---|
pcTimerName | 软件定时器名 |
xTimerPeriodInTicks | 定时超时时间,单位:系统时钟节拍 |
uxAutoReload | 定时器模式, pdTRUE:周期定时器, pdFALSE:单次定时器 |
pvTimerID | 软件定时器 ID,用于多个软件定时器公用一个超时回调函数 |
pxCallbackFunction | 软件定时器超时回调函数 |
返回值 | 描述 |
---|---|
NULL | 软件定时器创建失败 |
其他值 | 软件定时器创建成功,返回其句柄 |
TimerHandle_t time1_handle = 0;
time1_handle = xTimerCreate("timer1",1000,pdFALSE,(void *)1,timer1_callback);
3.2.开启软件定时器API函数
BaseType_t xTimerStart( TimerHandle_t xTimer, const TickType_t xTicksToWait );
形参 | 描述 |
---|---|
xTimer | 待开启的软件定时器的句柄 |
xTickToWait | 发送命令到软件定时器命令队列的最大等待时间 |
返回值 | 描述 |
---|---|
pdPASS | 软件定时器开启成功 |
pdFAIL | 软件定时器开启失败 |
3.3.停止软件定时器API函数
BaseType_t xTimerStop( TimerHandle_t xTimer,const TickType_t xTicksToWait);
形参 | 描述 |
---|---|
xTimer | 待停止的软件定时器的句柄 |
xTickToWait | 发送命令到软件定时器命令队列的最大等待时间 |
返回值 | 描述 |
---|---|
pdPASS | 软件定时器停止成功 |
pdFAIL | 软件定时器停止失败 |
3.4.复位软件定时器API函数
BaseType_t xTimerReset( TimerHandle_t xTimer,const TickType_t xTicksToWait);
该功能将使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时
形参 | 描述 |
---|---|
xTimer | 待复位的软件定时器的句柄 |
xTickToWait | 发送命令到软件定时器命令队列的最大等待时间 |
返回值 | 描述 |
---|---|
pdPASS | 软件定时器复位成功 |
pdFAIL | 软件定时器复位失败 |
3.5.更改软件定时器超时时间API函数
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
const TickType_t xNewPeriod,
const TickType_t xTicksToWait);
形参 | 描述 |
---|---|
xTimer | 待更新的软件定时器的句柄 |
xNewPeriod | 新的定时超时时间,单位:系统时钟节拍 |
xTickToWait | 发送命令到软件定时器命令队列的最大等待时间 |
返回值 | 描述 |
---|---|
pdPASS | 软件定时器定时超时时间更改成功 |
pdFAIL | 软件定时器定时超时时间更改失败 |
3.6.软件定时器回调函数
/**
* @brief timer1单次定时器回调函数
* @param 定时器句柄
* @retval 无
*/
void timer1_callback(TimerHandle_t pxTimer)
{
uint32_t timer = 0;
printf("time1的运行次数: %d \r\n",++timer);
}
十八、Tickless低功耗模式
1.Tickless低功耗模式简介
Tickless低功耗模式的本质是通过调用指令WFI实现睡眠模式,WFI进入睡眠模式可以通过任意中断唤醒
任务运行时间统计实验中,大部分时间是在执行空闲任务的
空闲任务:是在系统中的所有其它任务都阻塞或被挂起时才运行的
Tickless模式要解决的难点:(FreeRTOS 的低功耗 Tickless 模式机制已经处理好了这些难点)
1、进入低功耗之后,多久唤醒?也就是下一个要运行的任务如何被准确唤醒
2、任何中断均可唤醒MCU,若滴答定时器频繁中断则会影响低功耗的效果?
将滴答定时器的中断周期修改为低功耗运行时间
退出低功耗后,需补上系统时钟节拍数
2.Tickless模式相关配置项
configUSE_TICKLESS_IDLE // 此宏用于使能低功耗 Tickless 模式
configEXPECTED_IDLE_TIME_BEFORE_SLEEP // 此宏用于定义系统进入相应低功耗模式的最短时长 在FreeRTOS.h里面
configPRE_SLEEP_PROCESSING(x) // 此宏用于定义需要在系统进入低功耗模式前执行的事务,如:进入低功耗前关闭外设时钟,以达到降低功耗的目的
configPOSR_SLEEP_PROCESSING(x) // 此宏用于定义需要在系统退出低功耗模式后执行的事务,如:退出低功耗后开启之前关闭的外设时钟,以使系统能 够正常运行
configUSE_TICKLESS_IDLE 设置为1
configEXPECTED_IDLE_TIME_BEFORE_SLEEP 设置为2
//断言
#define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)
// 在FreeRTOSConfig.h里断言里面设置如下
#include "freertos_demo.h"
#define configPOSR_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
#define configPRE_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()
十九、内存管理
1.内存管理简介
1.1.内存管理方式
-
动态方法创建:自动地从 FreeRTOS 管理的内存堆中申请创建对象所需的内存,并且在对象删除后,可将这块内存释放回FreeRTOS管理的内存堆
-
静态方法创建:需用户提供各种内存空间,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间一般没有其他用途
动态方式管理内存相比与静态方式,更加灵活。
除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free()来实现动态地申请和释放内存 。但是标准C库的动态内存管理方法有以下不足:
- 占用大量的代码空间 不适合用在资源紧缺的嵌入式系统中
- 没有线程安全的相关机制
- 运行有不确定性,每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
因此,FreeRTOS 提供了多种动态内存管理的算法,可针对不同的嵌入式系统!
1.2.FreeRTOS内存管理算法
FreeRTOS提供了5种动态内存管理算法,分别为: heap_1、heap_2、heap_3、heap_4、heap_5
算法 | 优点 | 缺点 |
---|---|---|
heap_1 | 分配简单,时间确定 | 只允许申请内存,不允许释放内存 |
heap_2 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 |
heap_3 | 直接调用C库函数malloc()和 free() ,简单 | 速度慢、时间不定 |
heap_4 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 |
heap_5 | 能够管理多个非连续内存区域的 heap_4 | 时间不定 |
在我们FreeRTOS例程中,使用的均为heap_4内存管理算法
2.heap_1内存管理算法
简介:实现了pvPortMalloc
,没有实现vPortFree
,只能申请内存,无法释放内存
内存管理算法:heap_1管理的内存堆是一个数组,简单地从数组中分出合适大小的内存
内存堆数组的定义如下
/* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
特点:只能申请无法释放
缺点:不能释放
适用场景:创建好的任务、队列、信号量等都不需要被删除
3.heap_2内存管理算法
内存管理算法:最适应算法,选出大小最适应的空闲内存块
特点:能申请能释放
缺点:不能将相邻的空闲内存块合并成一个大的空闲内存块,所以会有内存碎片
使用场景:频繁的创建和删除任务,且所创建的任务堆栈都相同,这类场景下Heap_2没有碎片化的问题
4.heap_4内存管理算法
内存管理算法:首次适应算法,按顺序选出第一个满足需求的空闲内存块
特点:能申请能释放,将空闲且相邻的内存合并,减少内存碎片。
使用场景:频繁地分配、释放不同大小的内存
5.heap_5内存管理算法
简介:在heap_4的基础上实现,增加了 管理多个非连续内存区域的能力
heap_5 内存管理算法默认并没有定义内存堆 , 需要用户手动指定内存区域的信息,对其进行初始化,使用如下结构体
typedef struct HeapRegion
{
uint8_t * pucStartAddress; /* 内存区域的起始地址 */
size_t xSizeInBytes; /* 内存区域的大小,单位:字节 */
} HeapRegion_t;
指定多块且不连续的内存:
Const HeapRegion_t xHeapRegions[] =
{
{ (uint8_t *)0x80000000, 0x10000 }, /* 内存区域 1 */
{ (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域 2 */
{ NULL, 0 } /* 数组终止标志 */
};
vPortDefineHeapRegions(xHeapRegions);
6.FreeRTOS内存管理相关API函数介绍
函数 | 描述 |
---|---|
void * pvPortMalloc( size_t xWantedSize ); | 申请内存 |
void vPortFree( void * pv ); | 释放内存 |
size_t xPortGetFreeHeapSize( void ); | 获取当前空闲内存的大小 |
void * pvPortMalloc( size_t xWantedSize );
- xWantedSize:申请的内存大小,以字节为单位;
- 返回值:返回一个指针 ,指向已分配大小的内存。如果申请内存失败,则返回 NULL
void vPortFree( void * pv );
* pv:指针指向一个要释放内存的内存块
size_t xPortGetFreeHeapSize( void );
返回值:返回当前剩余的空闲内存大小
般没有其他用途
动态方式管理内存相比与静态方式,更加灵活。
除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free()来实现动态地申请和释放内存 。但是标准C库的动态内存管理方法有以下不足:
- 占用大量的代码空间 不适合用在资源紧缺的嵌入式系统中
- 没有线程安全的相关机制
- 运行有不确定性,每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
因此,FreeRTOS 提供了多种动态内存管理的算法,可针对不同的嵌入式系统!
1.2.FreeRTOS内存管理算法
FreeRTOS提供了5种动态内存管理算法,分别为: heap_1、heap_2、heap_3、heap_4、heap_5
算法 | 优点 | 缺点 |
---|---|---|
heap_1 | 分配简单,时间确定 | 只允许申请内存,不允许释放内存 |
heap_2 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 |
heap_3 | 直接调用C库函数malloc()和 free() ,简单 | 速度慢、时间不定 |
heap_4 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 |
heap_5 | 能够管理多个非连续内存区域的 heap_4 | 时间不定 |
在我们FreeRTOS例程中,使用的均为heap_4内存管理算法
2.heap_1内存管理算法
简介:实现了pvPortMalloc
,没有实现vPortFree
,只能申请内存,无法释放内存
内存管理算法:heap_1管理的内存堆是一个数组,简单地从数组中分出合适大小的内存
内存堆数组的定义如下
/* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
特点:只能申请无法释放
缺点:不能释放
适用场景:创建好的任务、队列、信号量等都不需要被删除
3.heap_2内存管理算法
内存管理算法:最适应算法,选出大小最适应的空闲内存块
特点:能申请能释放
缺点:不能将相邻的空闲内存块合并成一个大的空闲内存块,所以会有内存碎片
使用场景:频繁的创建和删除任务,且所创建的任务堆栈都相同,这类场景下Heap_2没有碎片化的问题
4.heap_4内存管理算法
内存管理算法:首次适应算法,按顺序选出第一个满足需求的空闲内存块
特点:能申请能释放,将空闲且相邻的内存合并,减少内存碎片。
使用场景:频繁地分配、释放不同大小的内存
5.heap_5内存管理算法
简介:在heap_4的基础上实现,增加了 管理多个非连续内存区域的能力
heap_5 内存管理算法默认并没有定义内存堆 , 需要用户手动指定内存区域的信息,对其进行初始化,使用如下结构体
typedef struct HeapRegion
{
uint8_t * pucStartAddress; /* 内存区域的起始地址 */
size_t xSizeInBytes; /* 内存区域的大小,单位:字节 */
} HeapRegion_t;
指定多块且不连续的内存:
Const HeapRegion_t xHeapRegions[] =
{
{ (uint8_t *)0x80000000, 0x10000 }, /* 内存区域 1 */
{ (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域 2 */
{ NULL, 0 } /* 数组终止标志 */
};
vPortDefineHeapRegions(xHeapRegions);
6.FreeRTOS内存管理相关API函数介绍
函数 | 描述 |
---|---|
void * pvPortMalloc( size_t xWantedSize ); | 申请内存 |
void vPortFree( void * pv ); | 释放内存 |
size_t xPortGetFreeHeapSize( void ); | 获取当前空闲内存的大小 |
void * pvPortMalloc( size_t xWantedSize );
- xWantedSize:申请的内存大小,以字节为单位;
- 返回值:返回一个指针 ,指向已分配大小的内存。如果申请内存失败,则返回 NULL
void vPortFree( void * pv );
* pv:指针指向一个要释放内存的内存块
size_t xPortGetFreeHeapSize( void );
返回值:返回当前剩余的空闲内存大小