第一章 初识 FreeRTOS
1.1 FreeRTOS简介
FreeRTOS 采用了 MIT 开源许可,这允许将 FreeRTOS 操作系统用于商业应用,并且不需要公开源代码。此外,FreeRTOS 还衍生出了另外两个操作系统:OpenRTOS 和 SafeRTOS,其中 OpenRTOS 使用了和 FreeRTOS 相同的代码,只是 OpenRTOS 受商业授权保护。
SafeRTOS 同样是 FreeRTOS 的衍生版本,SafeRTOS 符合工业、医疗、汽车和其他国际安全标准的严格要求,具有更高的安全性。
1.2 FreeRTOS源码初探
官网下载源码。
Demo里面是FreeRTOS的演示工程。
License里面是FreeRTOS的相关许可信息。
Source里面是源码。
就文件数量而言,FreeRTOS比UCOS少了很多。
可以看到 源码文件 source目录里的 portable目录内包含了 FreeRTOS的移植文件。这些移植文件是针对不同芯片架构的。
Keil 文件夹是在 MDK 中使用 ARMCC 编译器(AC5)时使用的,打开 Keil 文件夹后可以看到只中有一个txt文件,文件名为:“See-also-the-RVDS-directory.txt”,看文件名就知道要转到 RVDS 文件夹了。接下来打开 RVDS 文件夹,如下图所示:
ARMClang 文件夹是在 MDK 中使用 ARMClang 编译器(AC6)时使用的,打开 后可以看到,ARMClang 文件夹中只有一个文件,文件名为:“Use-the-GCC-ports.txt”,看文件名知道要转到 GCC 文件夹了。接下来打开 GCC 文件夹:
找到与 ARM_CMx 等内核芯片相关的移植文件。
第二章 FreeRTOS
2.1 FreeRTOS 移植
2.1.1 添加FreeRTOS源码
在 keil工程中新建一个名为 FreeRTOS的文件,把 FreeRTOS内核的源码文件(source文件夹)添加到该目录。
至于 protable中只需要留下 Keil、MemMang、RVDS(移植文件)。
空文件夹KEIL是使用 ARMClang 编译器(AC6)时用到。MemMang是内存管理接口。RVDS是不同芯片架构的移植文件。
2.1.2 向工程分组添加文件
新建 FreeRTOS_CORE 和 FreeRTOS_PORTABLE,然后向这两个分组中添加文件。
FreeRTOS_CORE文件夹用来存放 FreeRTOS源码文件夹下的协同程序、事件组、链表、队列、任务、定时器。
FreeRTOS_PORT文件夹用来存放 FreeRTOS源码文件夹下的 RVDS/ARM_CM4F/port.c,用来支持 Cortex-M4内核和FPU。以及 MemMang/heap_4.c,MemMang是跟内存管理相关的,heap_1.c~heap_5.c是五种不同的内存管理方法。
添加完成后把头文件添加到相应路径。
此外,还需要把 freertos的裁剪配置文件,FreeRTOSConfig.h 添加到FreeRTOS源码的include目录下。
2.1.3 编译错误处理
FreeRTOSConfig.h 中还定义了 SystemCoreClock 来标记MCU的频率。需要修改该变量的相关宏定义:
//原代码
#include<stdint.h>
extern uint32_t SystemCoreClock;
//修改为
#if define(__ICCARM_)||defined(__CC_ARM)||defined(__GNUC_)
#include<stdint.h>
extern uint32_t SystemCoreClock;
#endif
port.c 和 stm32f4xx_it.c 这俩文件有重复定义的函数:
PendSV_Handler()、 //挂起中断处理函数,中断处理期间负责上下文切换
SVC_Handler() 、 //系统调用中断,软中断,可手动触发
Systick_Handler() //滴答定时器中断
SVC这种中断机制允许用户程序在需要时,通过发出SVC指令来请求系统服务,而无需直接访问硬件或执行特权级操作。
在 FreeRTOS 中仅仅使用 SVC 异常来启动第一个任务,后面的程序中就再也用不到 SVC 了。
启动第一个任务的流程是根据VTOR获取中断向量表的偏移量,然后得到MSP指针,再触发SVC异常,在SVC处理函数中获取任务的任务栈SP,然后开中断,执行PC保存的任务函数。
把 stm32f4xx_it.c中的屏蔽掉。
再编译提示一些以 Hook结尾的函数未定义,这些都是钩子函数,在 FreeRTOSConfig.h中开启了这些函数却没有定义它们因此报错,关闭掉就好。
将钩子函数的宏:
configUSE_IDLE_HOOK、 //空闲钩子函数,没有其他任务执行时调用
configUSE_TICK_HOOK、 //系统节拍中断钩子函数(拍一下执行一次)
configUSE_MALLOC_FAILED_HOOK 、 //malloc失败钩子函数
configCHECK_FOR_STACK_OVERFLOW //堆栈溢出检测开关
定义为0。
2.1.4 修改SYSTEM文件
正点原子提供的工程的 SYSTEM文件夹里面几个工具源码,一开始是针对 UCOS编写的。
1. 修改 sys.h文件
#define SYSTEM_SUPPORT_OS 1 //定义系统文件夹是否支持OS
2.修改 usart.c文件
添加 FreeRTOS.h文件头。
#if SYSTEM_SUPPIRT_OS
#include "FreeRTOS.h"
#eddif
修改USART1的中断服务函数,在使用 UCOS的时候进出中断需要添加 OSIntEnter() 和 OSIntExit(),使用FreeRTOS的话就不需要了,删掉。
3、修改 delay.c 文件
delay.c 文件修改的就比较大了,因为涉及到 FreeRTOS 的系统时钟,delay.c 文件里面有 4个函数。
函数 SysTick_Handler(),此函数是滴答定时器的中断服务函数。
FreeRTOS 的心跳就是由滴答定时器产生的,根据 FreeRTOS 的系统时钟节拍设置好滴答定时器的周期,这样就会周期触发滴答定时器中断了。
在滴答定时器中断服务函数SysTick_Handler中调用OS 的 API 函数 xPortSysTickHandler()。
extern void xPortSysTickHandler(void);
//systick 中断服务函数,使用 OS 时用到
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//判断系统是否已经运行
{
xPortSysTickHandler();//系统时钟节拍函数,时钟节拍递增
}
}
delay_init() 是用来初始化滴答定时器和延时函数。
//初始化延迟函数
//SYSTICK 的时钟固定为 AHB 时钟,基础例程里面 SYSTICK 时钟频率为 AHB/8
//这里为了兼容 FreeRTOS,所以将 SYSTICK 的时钟频率改为 AHB 的频率!
//SYSCLK:系统时钟频率
void delay_init()
{
u32 reload;
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择外部时钟 HCLK
fac_us=SystemCoreClock/1000000; //不论是否使用 OS,fac_us 都需要使用
reload=SystemCoreClock/1000000; //每秒钟的计数次数 单位为 M
reload*=1000000/configTICK_RATE_HZ; //根据 configTICK_RATE_HZ 设定溢出
//时间 reload 为 24 位寄存器,最大值:
//16777216,在 72M 下,约合 0.233s 左右
fac_ms=1000/configTICK_RATE_HZ; //代表 OS 可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启 SYSTICK 中断
SysTick->LOAD=reload; //每 1/configTICK_RATE_HZ 秒中断
//一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK
}
前面我们说了 FreeRTOS 的系统时钟是由滴答定时器提供的,那么肯定要根据 FreeRTOS 的系统时钟节拍来初始化滴答定时器了,delay_init()就是来完成这个功能的。
FreeRTOS 的系统时钟节拍由宏 configTICK_RATE_HZ 来设置,我们要根据这个值来初始化滴答定时器,其实就是设置滴答定时器的中断周期。滴答定时器中断一次,系统时钟节拍加一。
在基础例程中滴答定时器的时钟频率设置的是 AHB 的 1/8,这里为了兼容 FreeRTOS 将滴答定时器的时钟频率改为了 AHB,也就是 72MHz!这一点一定要注意!
接下来的三个函数都是延时的,代码如下:
//微妙延时
//nus:要延时的 us 数.
//nus:0~204522252(最大值即 2^32/fac_us@fac_us=168)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD 的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
//这里注意一下 SYSTICK 是一个递减的计数器就可以了.
if(tnow<told)tcnt+=told-tnow;
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
//毫秒延时,会引起任务调度
//nms:要延时的 ms 数
//nms:0~65535
void delay_ms(u32 nms)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
if(nms>=fac_ms) //延时的时间大于 OS 的最少时间周期
{
vTaskDelay(nms/fac_ms); //FreeRTOS 延时
}
nms%=fac_ms; //OS 已经无法提供这么小的延时了,
//采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
}
//毫秒延时,不会引起任务调度
//nms:要延时的 ms 数
void delay_xms(u32 nms)
{
u32 i;
for(i=0;i<nms;i++) delay_us(1000);
}
delay_us()是 us 级延时函数,delay_ms() 和 delay_xms()都是 ms 级的延时函数。
delay_us()和 delay_xms()不会导致任务切换。
delay_ms()其实就是对 FreeRTOS 中的延时函数 vTaskDelay()的简单封装,所以在使用 delay_ms()的时候就会导致任务切换。
至此编译即完成移植。
第三章 FreeRTOS系统配置
FreeRTOS 的系统配置文件为 FreeRTOSConfig.h,在此配置文件中可以完成 FreeRTOS 的裁剪和配置。
3.1 FreeRTOSConfig.h 文件
FreeRTOS 的配置基本是通过在 FreeRTOSConfig.h 中使用“#define”这样的语句来定义宏定义实现的。在 FreeRTOS 的官方 demo 中,每个工程都有一个 FreeRTOSConfig.h 文件,我们可以参考,甚至直接复制粘贴使用。
3.1.1 “INCLUDE_”开始的宏
使用 “INCLUDE_”开头的宏用来表示使能或除能 FreeRTOS 中相应的 API 函数,作用就是用来配置 FreeRTOS 中的可选 API 函数的。
3.1.2 “config”开始的宏
堆栈、断言、协程、CPU频率、动态分配、时间统计空闲任务、中断配置、消息队列、信号量、调度器、跟踪调试等都有关。
第四章 FreeRTOS 中断配置和临界段
FreeRTOS 需要根据所使用的 MCU 来具体配置中断。
4.1 Cortex-M 中断
4.1.1 中断简介
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后 CPU 就会中断当前的流程转而去处理中断服务,Cortex-M 内核的 MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。
Cotex-M3 的 NVIC 最多支持 240 个 IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个Systick(滴答定时器)定时器中断和 11个系统异常。
4.1.2 中断管理简介
Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器,这些寄存器大多数都在 NVIC 和系统控制块(SCB)中,CMSIS(通用微控制器软件接口标准) 将这些寄存器定义为结构体。以 STM32F103 为例,打开core_cm3.h,有两个结构体,NVIC_Type 和 SCB_Type。
typedef struct
{
__IO uint32_t ISER[8]; /*!< Offset: 0x000 中断设置使能寄存器 */
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; /*!< Offset: 0x080 中断清除使能寄存器 */
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; /*!< Offset: 0x100 中断设置挂起寄存器 */
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; /*!< Offset: 0x180 中断清除挂起寄存器 */
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; /*!< Offset: 0x200 中断激活位寄存器 */
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; /*!< Offset: 0x300 中断优先级寄存器 (8Bit wide) */
uint32_t RESERVED5[644];
__O uint32_t STIR; /*!< Offset: 0xE00 软件触发寄存器 */
} NVIC_Type;
typedef struct
{
__I uint32_t CPUID; /*!< Offset: 0x00 CPU ID 基址寄存器 */
__IO uint32_t ICSR /*!< Offset: 0x04 中断控制状态寄存器 */
__IO uint32_t VTOR; /*!< Offset: 0x08 向量表偏移寄存器 */
__IO uint32_t AIRCR; /*!< Offset: 0x0C 应用程序中断/复位控制寄存器 */
__IO uint32_t SCR; /*!< Offset: 0x10 系统控制寄存器 */
__IO uint32_t CCR; /*!< Offset: 0x14 配置控制寄存器 */
__IO uint8_t SHP[12]; /*!< Offset: 0x18 系统处理优先级寄存器 (4-7, 8-11, 12-15)*/
__IO uint32_t SHCSR; /*!< Offset: 0x24 系统处理控制和状态寄存器 */
__IO uint32_t CFSR; /*!< Offset: 0x28 配置错误状态寄存器 */
__IO uint32_t HFSR; /*!< Offset: 0x2C Hard Fault Status Register */
__IO uint32_t DFSR; /*!< Offset: 0x30 Debug Fault Status Register */ __IO uint32_t MMFAR; /*!< Offset: 0x34 Mem Manage Address Register */
__IO uint32_t BFAR; /*!< Offset: 0x38 Bus Fault Address Register */
__IO uint32_t AFSR; /*!< Offset: 0x3C Auxiliary Fault Status Register */
__I uint32_t PFR[2]; /*!< Offset: 0x40 Processor Feature Register */
__I uint32_t DFR; /*!< Offset: 0x48 Debug Feature Register */
__I uint32_t ADR; /*!< Offset: 0x4C Auxiliary Feature Register */
__I uint32_t MMFR[4]; /*!< Offset: 0x50 Memory Model Feature Register */
__I uint32_t ISAR[5]; /*!< Offset: 0x60 ISA Feature Register */
} SCB_Type;
NVIC 和 SCB 都位于系统控制空间(SCS)内,SCS 的地址从 0XE000E000 开始,SCB 和 NVIC 的地址也在 core_cm3.h 中有定义。
#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE + 0x0100) /*!< NVIC Base Address */
#define SCB_BASE (SCS_BASE + 0x0D00) /*!< System Control Block Base Address */
#define SCB ((SCB_Type * ) SCB_BASE ) /*!< SCB configuration struct */
#define NVIC ((NVIC_Type* ) NVIC_BASE ) /*!< NVIC configuration struct *//
以上的中断控制寄存器我们在移植 FreeRTOS 的时候是不需要关心的,这里只是提一下,大家要是感兴趣的话可以参考 Cortex-M 的权威指南,我们重点关心的是是三个中断屏蔽寄存器:PRIMASK、FAULTMASK 和 BASEPRI,这三个寄存器后面会详细的讲解。
4.1.3 优先级分组定义
Cortex 内核将内核的中断向量表编号为 0~15 的称为内核异常 ,而 16 以上的则称为外部中断(这里的外,相对内核而言) 。
STM32 对这个表重新进行了编排,把编号从-3 至 6 的中断向量定义为系统异常, 编号为负 的内核异常为固定优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7 开始的为外部中断,这些中断的优先级都是可以自行设置的。
比如,Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级,最多 128 个抢占等级。
但是实际的优先级数量是由芯片厂商来决定的。比如 STM32 就只有 16 级优先级。
在设计芯片的时候会裁掉表达优先级的几个低端有效位,以减少优先级数,所以不管用多少位来表达优先级,都是 MSB对齐 对齐的。
Most Significant Bit,MSB,最高有效位对齐
NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器(AIRCR)”,AIRCR 寄存器里面有个“优先级分组”位段。NVIC的 IP寄存器由 240个 8bit中断优先级寄存器(PRI_x)组成,每个可屏蔽中断占 8bit,也就是总共能够设置 240个可屏蔽中断的优先级。STM32只用到了其中的 82个。
IP寄存器只用到了高 4位。抢占优先级和响应优先级各占几位由 AIRCR寄存器来控制。
PRIGROUP 就是优先级分组位段,占 3位,也就是总共 8种优先级分组。
STM32用到了 5个优先级分组。ASRCR寄存器的 PRIGROUP位段全 1是优先级分组0,代表 IP[0~81]寄存器的高四位都用于响应优先级。IP[0~81]也就是 PRI_0~PRI-81
这 5个分组在 msic.h中定义。
/*!< 0 bits for pre-emption priority 4 bits for subpriority */
#define NVIC_PriorityGroup_0 ((uint32_t)0x700)
/*!< 1 bits for pre-emption priority 3 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600)
/*!< 2 bits for pre-emption priority 2 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500)
/*!< 3 bits for pre-emption priority 1 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400)
/*!< 4 bits for pre-emption priority 0 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300)
而移植 FreeRTOS 的时候我们配置的就是组 4,全抢占优先级。
优先级寄存器都可以按字节访问,当然也可以按半字/字来访问,有意义的优先级寄存器数目由芯片厂商来实现。
4个相临的寄存器可以拼成一个32位的寄存器,因此地址 0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为 0xE000_ED20 的 32 位寄存器。
FreeRTOS 在设置 PendSV 和 SysTick 的中断优先级时是直接操作的 PRI_x寄存器地址。
4.1.4 用于中断屏蔽的特殊寄存器
1、PRIMASK 和 FAULTMASK 寄存器
PRIMASK 屏蔽除 NMI 和 HardFalut 外的所有异常和中断。
UCOS 中的临界区代码保护就是通过开关中断或禁止任务调度来实现的,而开关中断就是直接操作 PRIMASK寄存器的。
FAULTMASK 可以连 HardFault 都屏蔽掉,使用方法和 PRIMASK 类似,FAULTMASK 会在中断处理程序退出时自动清零。
2、BASEPRI 寄存器
BASEPRI 寄存器屏蔽优先级不高于某一个阈值的中断。
4.2 FreeRTOS 中断配置宏
4.2.1 优先级寄存器有效位数
configPRIO_BITS
此宏用来设置 MCU 的 IPR_x寄存器使用了几位,STM32 使用的是 4 位,因此此宏为 4!
4.2.2 最低优先级数
configLIBRARY_LOWEST_INTERRUPT_PRIORITY
此宏是用来设置最低优先级,STM32 优先级使用了 4 位,而且 STM32 配置的使用组 4,也就是 4 位都是抢占优先级。因此优先级数就是 16 个,最低优先级那就是 15。所以此宏就是 15。
4.2.3 内核中断(PendSV和SysTic)优先级
configKERNEL_INTERRUPT_PRIORITY
此宏用来设置内核中断优先级,此宏定义如下:
#define configKERNEL_INTERRUPT_PRIORITY
( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
之所以左移是因为STM32的 IPR_x使用高四位作为优先级有效位。
此宏还可以用来设置 PendSV 和滴答定时器的中断优先级,port.c 中有如下定义:
//pendSV的中断优先级
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
//Systic的中断优先级
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
这里的移位是因为 PendSV 和 SysTick 的中断优先级设置是操作 0xE000_ED20 地址的,这样一次写入的是个 32 位的数据, SysTick 和 PendSV 的优先级寄存器分别对应这个 32位数据的最高 8 位和次高 8 位,所以一个左移 16 位,一个左移 24 位了。
PendSV 和 SysTick 优先级在函数 xPortStartScheduler()中设置,此函数在文件 port.c 中。
BaseType_t xPortStartScheduler(void)
{
/* 断言配置的最大系统调用中断优先级和CPU ID */
configASSERT(configMAX_SYSCALL_INTERRUPT_PRIORITY); // 确保配置了系统调用的最大中断优先级
configASSERT(portCPUID != portCORTEX_M7_r0p1_ID); // 确保CPU ID不是不支持的Cortex-M7 r0p1版本
configASSERT(portCPUID != portCORTEX_M7_r0p0_ID); // 确保CPU ID不是不支持的Cortex-M7 r0p0版本
/* 如果定义了断言检查,则执行以下代码块 */
#if (configASSERT_DEFINED == 1)
{
/* 声明和初始化变量 */
volatile uint32_t ulOriginalPriority; // 用于保存原始的中断优先级寄存器值
volatile uint8_t *const pucFirstUserPriorityRegister = (uint8_t *)(portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER);
// 指向第一个用户中断优先级寄存器的指针
volatile uint8_t ucMaxPriorityValue; // 用于存储最大优先级值
/* 保存并修改第一个用户中断的优先级,以检测可用的最高优先级位 */
ulOriginalPriority = *pucFirstUserPriorityRegister;
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* 断言以确保内核中断优先级在可用范围内 */
configASSERT(ucMaxPriorityValue == (configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue));
/* 计算系统调用的最大中断优先级 */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* 计算并设置优先级分组值 */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while ((ucMaxPriorityValue & portTOP_BIT_OF_BYTE) == portTOP_BIT_OF_BYTE)
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= (uint8_t)0x01;
}
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
/* 恢复原始的中断优先级寄存器值 */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */
/* 设置 PendSV 和 SysTick 中断的优先级 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; // 设置PendSV中断的优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; // 设置SysTick中断的优先级
/* 设置定时器中断(这通常用于产生RTOS的滴答中断) */
vPortSetupTimerInterrupt();
/* 初始化临界区嵌套计数器 */
uxCriticalNesting = 0;
/* 启动FreeRTOS的第一个任务 */
prvStartFirstTask();
/* 正常情况下,这个函数不应该返回,因为一旦启动任务调度器,控制权就交给了FreeRTOS */
return 0; // 实际上,这一行代码在正常情况下不会被执行到
}
4.2.4 可管理的优先级
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
此宏用来设置 FreeRTOS 系统可管理的最大优先级,也就是BASEPRI 寄存器代表的阈值优先级,这个可以自由设置,优先级编号低于阈值的不归FreeRTOS管理。
4.2.5 可系统调用的优先级
configMAX_SYSCALL_INTERRUPT_PRIORITY
可以安全调用 FreeRTOS API 的最高中断优先级。高于此优先级的中断不能被 FreeRTOS 禁止,也不能调用 FreeRTOS 的 API 函数!
4.3 FreeRTOS 开关中断
FreeRTOS 开关中断函数为
portENABLE_INTERRUPTS () //向BaseIPR写0,开启全部中断
portDISABLE_INTERRUPTS(),//向BaseIPR写[系统调用优先级阈值],低的全部屏蔽
这两个函数其实是宏定义,在 portmacro.h 中有定义:
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)//向BASEPRI写0,不屏蔽任何中断
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()//向BASEPRI写[可系统调用优先级阈值]
// 定义一个内联函数,用于设置基本优先级寄存器(BASEPRI)的值。
// 这个函数是强制内联的,以减少函数调用的开销,特别是在中断处理或性能敏感的代码段中。
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
// 使用内联汇编代码直接设置BASEPRI寄存器的值。
// BASEPRI寄存器用于设置当前线程或任务的最低可屏蔽中断优先级阈值。
// 当一个中断的优先级低于或等于BASEPRI时,该中断将被屏蔽(不会被处理)。
__asm
{
msr basepri, ulBASEPRI // msr是ARM汇编指令,用于将立即数或寄存器的内容写入到指定的系统寄存器中。
// 在这里,它用于将ulBASEPRI的值写入到BASEPRI寄存器。
}
}
/*-----------------------------------------------------------*/
// 定义一个内联函数,用于提高(或设置)基本优先级寄存器(BASEPRI)的值,
// 以增加当前线程或任务的最低可屏蔽中断优先级阈值。
// 这通常用于保护关键代码段,防止低优先级中断的干扰。
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
// 定义一个局部变量,存储新的BASEPRI值。这里假设configMAX_SYSCALL_INTERRUPT_PRIORITY
// 是预定义的宏,表示系统调用的最高(或最低,取决于如何定义)中断优先级。
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
// 使用内联汇编代码设置BASEPRI寄存器的值,并确保操作完成。
// msr指令用于写入BASEPRI寄存器,dsb(数据同步屏障)和isb(指令同步屏障)指令
// 确保所有之前的指令都已执行完毕,并且缓存和流水线都已清空,
// 从而保证后续指令的执行不会受到之前操作的影响。
__asm
{
msr basepri, ulNewBASEPRI // 设置BASEPRI为新值
dsb // 数据同步屏障,确保之前的操作完成
isb // 指令同步屏障,清空指令流水线
}
}
函数 vPortSetBASEPRI() 通过向BASEPRI寄存器写入一个值来设置系统的可屏蔽中断优先级阈值。写0来开启全部中断。
函数 vPortRaiseBASEPRI() 通过向BASEPRI寄存器写入可系统调用优先级阈值,也就是优先级低于该阈值的中断都会被屏蔽,用于保护关键代码段,防止被低优先级中断打断。
4.4 临界段代码
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段。
FreeRTOS中与临界段代码保护相关的四个宏定义函数是:
taskENTER_CRITICAL()、 //进入任务临界区
taskEXIT_CRITICAL()、 //退出任务临界区
taskENTER_CRITICAL_FROM_ISR()、//进入中断临界区
taskEXIT_CRITICAL_FROM_ISR()。 //退出中断临界区
前两者保护任务代码,后两者保护中断代码。
4.4.1 任务级临界段代码保护
taskENTER_CRITICAL()和 taskEXIT_CRITICAL()是任务级的临界代码保护,进入临界段,退出临界段。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
portENTER_CRITICAL()和 portEXIT_CRITICAL()也是宏定义,在文件 portmacro.h 中定义。
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
vPortEnterCritical()和 vPortExitCritical()在文件 port.c 中。
/**
* 进入临界区,并增加[临界区嵌套计数器]。当嵌套计数器从0增加到1时,表示进入了一个新的临界区。
* 理论上在进入临界区之前应该没有中断在执行,
*/
void vPortEnterCritical( void )
{
// 禁用中断
portDISABLE_INTERRUPTS();
// 增加临界区嵌套计数器
uxCriticalNesting++;
// 如果这是第一次进入临界区(嵌套计数器从0增加到1)
if( uxCriticalNesting == 1 )
{
// 通过检查NVIC的[中断控制寄存器]中的[活动中断位]来判断是否有中断在执行
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
/**
* 退出临界区,此函数用于减少临界区嵌套计数器,并在计数器减到0时重新启用中断。这标志着临界区的结束。
* 在减少嵌套计数器之前,会通过一个断言来确保嵌套计数器不为0,以避免潜在的错误。
*/
void vPortExitCritical( void )
{
// 验证嵌套计数器不为0,确保确实在临界区内
configASSERT( uxCriticalNesting );
// 减少临界区嵌套计数器
uxCriticalNesting--;
// 如果嵌套计数器减到0,表示已经退出所有临界区
if( uxCriticalNesting == 0 )
{
// 重新启用中断
portENABLE_INTERRUPTS();
}
}
注意临界区的代码一定要精简,因为进入临界区会关闭中断,导致优先级低于 [可系统调用的优先级阈值] 的中断得不到及时的响应。
4.4.2 中断级临界段代码保护
函数 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()是中断级别临界段代码保护,中断的优先级一定要低于[可系统调用中断优先级阈值]。
//task.h
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() //进中断临界区
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )//退中断临界区
//portmacro.h
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) //设置BasePRI。给0代表开全部中断
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() //设置BasePRI为[可系统调用优先级阈值]
第五章 FreeRTOS 任务基础知识
5.1 什么是多任务系统?
相对于多任务系统而言,裸机跑的单任务系统也称作前后台系统,中断服务函数作为前台程序,大循环 while(1)作为后台程序。
前后台系统消耗资源少,但是存在排队问题,比如假设我一个while循环要监听800个通信口的数据,处理一个通信口一分钟,这样每个就是800分钟才能处理一次,排队时间太久。
多任务系统分而治之,通过任务调度去解决并发问题。
5.2 FreeRTOS 任务与协程
FreeRTOS 中应用既可以使用任务,也可以使用协程(Co-Routine),或者两者混合使用。
5.2.1 任务(Task)的特性
RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个自己堆栈空间。
5.2.2 协程(Co-routine)的特性
在概念上协程和任务是相似的,但是任务的协程使用任务的堆栈,共用一个。
5.3 任务状态
就绪,等待被调度
阻塞,条件不满足,被动释放CPU资源
挂起,主动释放CPU资源
运行,获得CPU资源
每个任务都可以分配一个从 0~(configMAX_PRIORITIES-1)的优先级。
最高优先级的可运行任务有两种选择方法,一种是
configUSE_PORT_OPTIMISED_TASK_SELECTION = 1,将开启[任务选择优化]。这种方法通常利用了架构特定的汇编指令,能够更快速地从一组任务中找出优先级最高的任务。
当此宏被定义为 0 或未定义时,FreeRTOS 将使用一种更通用的方法来选择任务,这种方法对所有架构都是相同的,但可能不如优化方法快。
如果开启了[任务选择优化],那么宏 configMAX_PRIORITIES 不能超过 32!也就是优先级不能超过 32 级。其他情况下宏 configMAX_PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗最好设置为一个满足应用的最小值。
任务优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为 0。
任务调度器确保处于就绪态或运行态的高优先级的任务获得 CPU 使用权。
5.5 任务实现
使用函数 xTaskCreate()或 xTaskCreateStatic()来创建任务。
创建任务时将任务函数 taskFunction_t 作为参数传递。
5.6 任务控制块
TCB_t。就是线程控制块的FreeRTOS版本。
任务控制块用结构体定义的,存放了栈顶、栈底、堆栈起始地址、状态列表项、事件列表项、任务优先级、临界区嵌套深度、任务拿到的信号量个数、任务通知值、运行时间等任务相关信息。
状态列表项保存就绪、运行、挂起状态。
事件列表项保存事件标志。比如入队、出队的时候队伍满了,事件就会记录下来。
typedef struct tskTaskControlBlock
{
StackType_t *pxTopOfStack; // 任务堆栈栈顶
ListItem_t xStateListItem; // 状态列表项
ListItem_t xEventListItem; // 事件列表项
UBaseType_t uxPriority; // 任务优先级
StackType_t *pxStack; // 任务堆栈起始地址
char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名字
// 简化的条件编译字段
#if (configUSE_MUTEXES == 1)
UBaseType_t uxBasePriority; // 任务基础优先级
UBaseType_t uxMutexesHeld; // 任务持有的互斥量数量
#endif
// 本地存储指针(如果配置启用)
#if(configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0)
void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS];
#endif
// 运行时统计(如果配置启用)
#if(configGENERATE_RUN_TIME_STATS == 1)
uint32_t ulRunTimeCounter; // 任务运行总时间
#endif
// 任务通知(如果配置启用)
#if(configUSE_TASK_NOTIFICATIONS == 1)
volatile uint32_t ulNotifiedValue; // 任务通知值
volatile uint8_t ucNotifyState; // 任务通知状态
#endif
// 静态/动态分配标记
#if(tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0)
uint8_t ucStaticallyAllocated; // 标记任务是静态还是动态创建的
#endif
// 延迟中断标记(如果配置启用)
#if(INCLUDE_xTaskAbortDelay == 1)
uint8_t ucDelayAborted;
#endif
// 可以根据需要添加更多核心或常用字段
} tskTCB;
5.7 任务堆栈
任务调度器进行任务切换的时候会将当前任务现场(CPU 寄存器值等)保存在任务的任务堆栈中,切换回这个任务的时候就用该任务的任务堆栈中保存的值来恢复现场。
创建任务的时候需要给任务指定堆栈,如果使用的函数 xTaskCreate()创建任务(动态方法)的话那么任务堆栈会由该函数自动创建。
使用函数 xTaskCreateStatic()创建任务(静态方法)的话就需要手动创建,然后堆栈首地址作为函数的参数传递给函数。
堆栈大小:
不管是使用函数 xTaskCreate()还是 xTaskCreateStatic()创建任务都需要指定任务堆栈大小。
任务堆栈的数据类型为 StackType_t,本质上是 uint32_t。也就是一个堆栈的数据元素占 4个字节。而 栈的深度 决定了这个栈能容纳多少个栈元素。栈的真实大小= 栈深度*栈数据类型大小。
第六章 FreeRTOS 任务相关 API 函数
6.1 任务创建和删除 API 函数
xTaskCreate() 使用动态方法创建一个任务。任务堆栈自动分配
xTaskCreateStatic() 使用静态方法创建一个任务。任务堆栈手动分配
xTaskCreateRestricted() 创建使用 MPU 进行限制的任务,内存使用动态内存分配。
vTaskDelete() 删除动态或静态创建的任务。手动建的任务堆栈需手动删除。
函数 xTaskCreate()来动态创建任务所需的 RAM 会自动的从 FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认我们使用 heap_4.c 这个内存管理文件,而且宏 configSUPPORT_DYNAMIC_ALLOCATION 必须为 1。
使用函数 xTaskCreateStatic()创建的话这些 RAM 就需要用户来提供了。需要将宏configSUPPORT_STATIC_ALLOCATION 定义为 1。
调用任务创建函数时会自动使用 prvInitialiseNewTask 进行任务初始化。
/*动态创建任务*/
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //任务函数
const char * const pcName, //任务名字
const uint16_t usStackDepth, //任务堆栈深度
void * const pvParameters, //任务函数参数
UBaseType_t uxPriority, //任务优先级
TaskHandle_t * const pxCreatedTask )//用来保存任务句柄。
//其实就是任务堆栈
/*静态创建任务*/
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 )//任务控制块
/*创建MPU限制的任务*/
BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition, //描述符了任务函数、堆栈大小、优先级等的结构体
TaskHandle_t * pxCreatedTask )//任务句柄
/*删除动态或静态创建的任务*/
vTaskDelete( TaskHandle_t xTaskToDelete )
6.2 任务挂起和恢复 API 函数
vTaskSuspend() 挂起任务。
vTaskResume() 恢复任务。
xTaskResumeFromISR() 中断服务函数中恢复一个任务的运行。
/*挂起任务*/
void vTaskSuspend( TaskHandle_t xTaskToSuspend)//要挂起任务的句柄
可以使用 xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。
/*恢复挂起的任务*/
void vTaskResume( TaskHandle_t xTaskToResume)
/*在中断中恢复一个任务*/
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)
第七章 FreeRTOS 列表和列表项
7.1 什么是列表和列表项?
7.1.1 列表
学习 任务控制块 TCB_t 的时候提到任务控制块中有状态列表项、事件列表项,
列表 List_t 由列表项 xList_ITEM 组成。
列表项 xList_ITEM包含列表项值、前列表项指针、后列表项指针、Owner_TCB、Owner_List。
列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪任务。与列表相关的全部东西都在文件 list.c 和 list.h 中。在 list.h 中定义了一个叫 List_t 的结构体。
typedef struct xLIST
{
configLIST_VOLATILE UBaseType_t uxNumberOfItems; //列表中列表项的数量
ListItem_t * configLIST_VOLATILE pxIndex; //当前列表项索引号
MiniListItem_t xListEnd; //列表中最后一个列表项
} List_t;
列表的数据结构体 list_t 中记录了列表项的数量、当前列表项索引号、最后一个列表项。
注意,列表是双向环状链表结构,列表的当前列表项索引号指向的位置被当作列表的头部。
其中最后一个列表项为 miniList,是列表初始化时就放在列表中的。计算列表中的列表项数目时不统计该列表项。
7.1.2 列表项
列表项就是列表中的元素。FreeRTOS 提供了两种列表项:列表项和迷你列表项。都在文件 list.h 中有定义。
/*列表项结构体*/
struct xLIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; //列表项值
struct xLIST_ITEM * configLIST_VOLATILE pxNext; //下一个列表项
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; //前一个列表项
void * pvOwner; //该列表项归的所属任务控制块
void * configLIST_VOLATILE pvContainer; //列表项的所属列表
};
typedef struct xLIST_ITEM ListItem_t;
学习 任务控制块 TCB_t 的时候提到任务控制块中有状态队列项、事件列表项,
列表 List_t 由列表项 xList_ITEM 组成。
列表项 xList_ITEM包含列表项值、前列表项指针、后列表项指针、Owner_TCB、Owner_List。
7.1.3 迷你列表项
struct xMINI_LIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; //列表项值
struct xLIST_ITEM * configLIST_VOLATILE pxNext; //下一个列表项
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;//前一个列表项
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
迷你列表项包含列表项值、下一个列表项指针、前一个列表项指针。
7.2 列表和列表项初始化
7.2.1 列表初始化
新定义的列表需要对其做初始化处理,初始化列表结构体List_t 中的各个成员变量。
列表的初始化通过使函数 vListInitialise()来完成。
void vListInitialise( List_t * const pxList )
{
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); //初始化列表的列表项索引号
//指向最后一个列表项
pxList->xListEnd.xItemValue = portMAX_DELAY; //最大阻塞时间 0xffffffffUL
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); //下一个列表项
//只有一个元素时指向自身
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); //上一个列表项
//只有一个元素时指向自身
pxList->uxNumberOfItems = ( UBaseType_t ) 0U; //列表项数目。不包括结尾列表项,因此为0
}
列表由列表项结构体组成,列表项结构体包括列表项数量、当前列表项索引号、最后一个列表项。
最后一个列表项初始化时就放在列表中,是 miniList 类型结构体,包括列表项值、前列表项指针、后列表项指针。
列表项值初始化时给的是最大阻塞时间。前、后列表项指针指向自身。
7.2.2 列表项初始化
列表项在使用的时候也需要初始化,列表项初始化由函数 vListInitialiseItem()来完成。
void vListInitialiseItem( ListItem_t * const pxItem )
{
pxItem->pvContainer = NULL; //初始化 pvContainer 为 NULL
}
列表项初始化比较简单,就是给 [列表项所属列表] 赋个NULL。其他成员在创建任务的时候,对 TCB中的 状态列表项和 事件列表项进行初始化时进行初始化。
7.3 列表项插入
列表项的插入操作通过函数 vListInsert()来完成。
void vListInsert( List_t * const pxList, //列表
ListItem_t * const pxNewListItem )//列表项
列表项的插入根据 xItemValue (列表项值,阻塞时间)的值按照升序排列!
插入列表项的逻辑是,由于末尾列表项固定为最大阻塞时间,因此会放在列表最后,如果插入的列表项的阻塞时间等于最大阻塞时间,就放末尾列表项前面,不然的话就要从头开始一个个找自己的位置,按照升序找插入点。插入过程就是双向链表的插入。然后把列表的成员数加一。
7.4 列表项末尾插入
列表末尾插入列表项的操作通过函数 vListInsertEnd()来完成。
列表末尾插入其实就是把列表项插入 pxIndex指向的前一个列表项。
void vListInsertEnd( List_t * const pxList, //列表
ListItem_t * const pxNewListItem ) //列表项
源码逻辑为:
1、对列表和列表项的完整性检查、
2、直接使用列表项的成员 xListEnd找到末尾列表项,然后把 pxIndex指针指向的当前列表项作为列表项的头部,则该指针的前一个就是末尾。
3、标记新的列表项属于这个列表。
4、记录列表中列表项的数目加一。
7.5 列表项的删除
列表项的删除通过函数 uxListRemove()来完成。
/*删除列表中特定的列表项,返回剩余的列表项数目*/
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
注意,列表项的删除只是将指定的列表项从列表中删除,如果这个列表项是动态分配内存的话,并不会将这个列表项的内存给释放掉!删除的列表项如果恰好是[当前列表项指针pxIndex]指向的列表,则pxIndex前移。
7.6 列表的遍历
列表 List_t 中的成员变量 pxIndex 是用来遍历列表的,FreeRTOS提供了一个函数来完成列表的遍历,这个函数是 listGET_OWNER_OF_NEXT_ENTRY()。
每调用一次列表的 pxIndex遍历就指向下一个列表项。
列表的遍历,用于从多个同优先级的就绪任务中查找下一个要运行的任务。
/*通过pxIndex当前指针遍历列表项,并返回列表项所属的任务块pxTCB*/
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
List_t * const pxConstList = ( pxList );
//遍历
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
//如果指向了列表的xListEnd成员变量,表示到了列表末尾
//就跳过尾节点,这样xIndex就又指向了头节点
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )
{
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
}
//饭hi列表项所属的任务快pxTCB
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}
第八章 FreeRTOS 调度器开启和任务相关函数详解
8.1 调度器开启过程分析
8.1.1 任务调度器开启函数分析
函数 vTaskStartScheduler()的功能就是开启任务调度器的,在文件 tasks.c 中定义。
(1)、创建空闲任务,空闲任务的优先级为最低。
(2)、创建定时器服务任务。
(3)、关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4)、设置调度器开始运行的标志位。
(5)、使能时间统计功能。
(6)、初始化跟调度器启动有关的硬件,滴答定时器、FPU 单元和 PendSV 中断等等。
8.1.2 内核相关硬件初始化函数分析
FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成。
BaseType_t xPortStartScheduler( void )
{
/******************************************************************/
/****************此处省略一大堆的条件编译代码**********************/
/*****************************************************************/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置PndSV中断优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//设置Systic中断优先级
vPortSetupTimerInterrupt(); //设置Systic定时器周期并使能
uxCriticalNesting = 0; //设置临界区嵌套计数器
prvStartFirstTask(); //开启第一个任务
//代码正常执行的话是不会到这里的!
return 0;
}
(1)、设置 PendSV 的中断优先级,为最低优先级。
(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并使能中断。
(4)、初始化临界区嵌套计数器。
(5)、调用函数 prvStartFirstTask()开启第一个任务。
8.1.3 启动第一个任务
函数 prvStartFirstTask()用于启动第一个任务,这是一个汇编函数。
__asm void prvStartFirstTask( void )
{
PRESERVE8
ldr r0, =0xE000ED08 ; //R0=0XE000ED08 VTOR寄存器地址,用来做向量表偏移
ldr r0, [r0] ; //取 R0 所保存的地址处的值赋给 R0
ldr r0, [r0] ; //获取 MSP 初始值
msr msp, r0 ; //复位 MSP
cpsie I ; //使能中断(清除 PRIMASK)
cpsie f ; //使能中断(清除 FAULTMASK)
dsb ; //数据同步屏障
isb ; //指令同步屏障
svc 0 ; //触发 SVC 中断(异常)
nop
nop
}
启动第一个任务的流程:
从 VTOR寄存器读取到向量表的偏移地址,一般是0x8000_0000,并通过向量表的偏移地址读取到向量表的第一个数据,也就是主栈指针MSP的值。
然后使能中断。开指令屏障、开数据屏障,保证前面的代码执行无误。
调用SVC指令触发SVC中断。
8.1.4 SVC 中断服务函数
用于启动第一个任务的函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的。
SVC中断服务函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的。
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB ; //R3=pxCurrentTCB 的地址,当前正在执行的任务的TCB的地址
ldr r1, [r3] ; //取 R3 所保存的地址处的值赋给 R1
ldr r0, [r1] ; //取 R1 所保存的地址处的值赋给 R0
ldmia r0!, {r4-r11, r14} ; //出栈 ,R4~R11 和 R14 (4)
msr psp, r0 ; //进程栈指针 PSP 设置为任务的堆栈
isb ; //指令同步屏障
mov r0, #0 ; // R0=0
msr basepri, r0 ; //寄存器 basepri=0,开启中断
orr r14, #0xd ; //R14 寄存器的值与 0X0D 进行或运算,
//得到的结果就是 R14 寄存器的新值。
//表示退出异常以后 CPU 进入线程模式并且使用进程栈!
bx r14
}
(1) 获取 pxCurrentTCB 指针的存储地址。当前正在执行的任务的任务控制块。pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。
(2) 通过任务控制块TCB拿到任务的栈顶指针 pxTopOfStack。
(3) 设置进程栈指针 PSP。
(4) 设置寄存器 BASEPRI 为 0。也就是打开所有中断。
(5) 退出 SVC异常中断服务函数后,CPU进入线程模式,并且使用线程栈。
执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。FreeRTOS 的任务调度器正式开始运行!
8.1.5 空闲任务
讲解任务调度器开启函数 vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。
空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!
空闲任务的功能如下:
1、在空闲任务中释放被删除任务的任务控制块和任务堆栈。
2、运行用户设置的空闲任务钩子函数。
3、判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理。
空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask()。
8.2 任务创建过程分析
8.2.1 任务创建函数分析
任务创建包括 动态创建、静态创建和使用MPU创建三种情况,这里以动态创建简单分析。
(1) 使用函数 pvPortMalloc()给任务的任务堆栈申请内存,并对内存做字节对齐处理。
(2) 使用函数 pvPortMalloc()给任务的任务控制块申请内存。
(3) 初始化内存控制块中的任务堆栈字段 pxStack。
(4) 标记任务控制块TCB中的堆栈内存分配方法,静态动态还是MPU。
(6) 使用函数 prvInitialiseNewTask()初始化任务,完成对任务控制块中字段的初始化工作。
(7) 使用函数 prvAddNewTaskToReadyList()将新创建的任务加入到就绪列表中。
8.2.2 任务初始化函数分析
函数 prvInitialiseNewTask()用于完成对任务的初始化。其实就是任务控制块的初始化。
包括任务块中的任务堆栈指针、任务优先级、信号量个数、状态和事件列表项的所属字段,最后得到任务控制块的句柄返回。
堆栈初始化:
给任务堆栈申请内存的时候获得任务堆栈栈顶。
任务名称保存:
保存任务名称到任务控制块。
优先级检查与设置:
如果任务优先级参数设置过高,则设为最高任务优先级-1。configMAX_PRIORITIES-1
初始化任务控制块中的优先级字段uxPriority。
互斥信号量初始化(如果使能):
初始化任务控制块中拿到的信号量个数。
列表项初始化:
任务控制块TCB中有状态列表项和事件列表项。初始化这俩的pvOwner为当前任务。
列表项优先级排序:
设置事件列表项的事件值为最大任务优先级-任务参数给的优先级, xEventListItem的xItemValue为configMAX_PRIORITIES - uxPriority,以便在优先级队列中正确排序。
线程本地存储初始化(如果使能):
初始化线程本地存储指针。
堆栈具体初始化:
调用pxPortInitialiseStack()函数具体初始化任务堆栈,包括设置初始的堆栈帧等。
生成任务句柄:
任务句柄实际上就是任务控制块的指针,将其返回给调用者。
8.2.3 任务堆栈初始化函数分析
任务初始化函数 prvInitialiseNewTask中会调用任务堆栈初始化函数 pxPortInitialiseStack()。
堆栈用来在进行上下文切换的时候保存现场。因为ARM的寄存器只用来保存当前执行的状态,因此未被执行任务的状态需要栈来保存上下文。
ARM处理器有 17 个可访问的寄存器,R0~R16。
R16 是程序状态寄存器CPSR。
R15 是程序计数器PC。
R14 是链接寄存器LR。
R13 是堆栈指针SP。
任务栈保存上下文的顺序:
设置程序状态寄存器(xPSR):将xPSR寄存器设置为0x01000000,这表示任务将在Thumb状态下执行,即使用Thumb指令集。程序状态寄存器PSR保存指令集状态。
设置程序计数器(PC):将PC寄存器为任务执行的入口点。
设置链接寄存器(LR):将LR寄存器初始化函数返回地址。
跳过未初始化的寄存器:跳过R12、R3、R2、R1这四个寄存器,不对它们进行初始化。这些寄存器在任务启动前保持未定义状态。
设置参数寄存器(R0):将参数寄存器R0初始化为函数参数。在ARM架构中,R0至R3常用于函数调用的参数传递,R0也常用于返回结果。
跳过其他未初始化的寄存器:跳过R11至R4这八个寄存器,不对它们进行初始化。这些寄存器同样在任务启动前保持未定义状态。
在STM32中,堆栈指针包括主堆栈指针(MSP)和进程堆栈指针(PSP):
主堆栈指针(MSP):在处理器复位后,SP默认指向MSP。MSP主要用于系统级的堆栈操作,如中断服务例程(ISR)中的堆栈操作。
进程堆栈指针(PSP):在RTOS环境下,每个任务通常都有自己的堆栈空间,PSP则用于指向当前任务的堆栈顶部。当任务切换时,PSP会相应地更新,以指向新任务的堆栈顶部。
上下文切换的时候空闲线程通过任务控制块获得任务栈的地址。
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack,
TaskFunction_t pxCode,
void *pvParameters)
{
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; //程序状态,ARM指令还是Thumb指令
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; //程序计数器指向任务函数
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; //链接寄存器LR指向返回地址
pxTopOfStack -= 5; //跳过4个寄存器
*pxTopOfStack = ( StackType_t ) pvParameters; // 寄存器R0保存函数参数
pxTopOfStack -= 8; //跳过7个寄存器
return pxTopOfStack;
}
8.2.4 添加任务到就绪列表
任务创建完成以后就会被添加到就绪列表中,FreeRTOS 使用不同的列表表示任务的不同状态,在文件 tasks.c 中就定义了多个列表来完成不同的功能。
//任务就绪列表
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
//延迟任务列表1 延迟任务链表上的任务将等待xTickCount自增到某一值后重新被唤醒
PRIVILEGED_DATA static List_t xDelayedTaskList1;
//延迟任务列表2
PRIVILEGED_DATA static List_t xDelayedTaskList2;
//延迟任务列表 volatile修饰
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
//若有xItemValue值发生溢出则插入。溢出链表上的任务永远不会被唤醒,直到xTickCount值发生溢出将两个指针互换:
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
//挂起任务列表
PRIVILEGED_DATA static List_t xPendingReadyList;
列表数组 pxReadyTasksLists[]就是任务就绪列表,数组大小为最大任务优先级数 configMAX_PRIORITIES,也就是说一个优先级一个列表,相同优先级的任务就使用一个列表。
有一个全局变量 uxCurrentNumberOfTasks来统计任务的数量。如果任务创建以后,这个值为1,就代表创建的是第一个任务,因此首先需要初始化相应的列表。
如果新创建的任务比正在运行的任务优先级高,就会修改 pxCurrentTCB 为新任务的任务控制块。然后将任务添加进就绪任务列表,触发一次任务切换,切换为优先级高的任务。
8.3 任务删除过程分析
任务删除函数 vTaskDelete()。
主要工作就是通过任务句柄获取任务控制块,
将任务从任务就绪列表移除。将任务从信号量、消息队列之类的等待条件队列移除。
如果删除的是当前任务,要先放到等待结束的任务队列中,由空闲任务来释放内存。
更新全局变量记录任务数,调用钩子函数。
重新任务调度。
获取任务控制块:
调用prvGetTCBFromHandle()函数,传入任务句柄以获取该任务的任务控制块(TCB)。
如果传入的是NULL,表示要删除自身。会获得 pxCurrentTC,当前任务控制块。
从就绪列表中移除:
将任务从任务就绪列表中移除,确保它不再参与调度。
检查并处理等待状态:
检查任务是否等待某个事件(如信号量、消息队列等)。
如果等待中,将其从相应的事件等待列表中移除。
处理当前运行的任务:
如果要删除的是当前正在运行的任务,则不能立即释放其TCB和堆栈内存。
将当前任务添加到xTasksWaitingTermination列表中,并设置一个标记。
空闲任务(Idle Task)会定期检查并释放这些任务的内存。
更新全局变量:
更新uxDeletedTasksWaitingCleanUp以记录需要清理的任务数。
调用钩子函数:
调用用户定义的删除任务钩子函数,允许用户执行额外的清理或记录操作。
更新任务计数:
如果删除的是其他任务,则减少uxCurrentNumberOfTasks(当前任务数)。
删除任务控制块:
对于非当前运行的任务,直接调用prvDeleteTCB()删除其任务控制块。
重新计算任务调度:
重新计算下一个任务的解锁时间,确保调度不受已删除任务的影响。
强制任务切换:
如果删除了当前运行的任务,需要强制进行一次任务切换,以确保系统能继续运行。
8.4 任务挂起过程分析
挂起任务使用函数 vTaskSuspend()。
挂起的过程就是:
通过句柄获取任务控制块。
将任务从就绪或延时列表中删除。从信号量、消息队列之类的等待条件队列移除。
将任务添加到挂起任务列表的尾部。
重新上下文切换、任务调度。
获取任务控制块(TCB):
通过prvGetTCBFromHandle()函数,使用任务句柄获取要挂起的任务的任务控制块(TCB)。
从就绪和延时列表中删除任务:
将任务从其当前所在的就绪列表或延时列表中移除。
处理等待中的事件:
检查任务是否在等待某个事件(如信号量、队列等),并从相应的事件等待列表中删除该任务。
添加到挂起列表:
将任务添加到挂起任务列表(xSuspendedTaskList)的末尾。
更新任务调度时间:
重新计算下一个任务的执行时间,以确保调度器考虑了最新的任务状态变化。
处理当前运行任务的挂起:
如果挂起的任务是当前正在运行的任务,则调用portYIELD_WITHIN_API()强制任务切换。
确定下一个运行任务:
如果当前任务被挂起,检查是否所有任务都被挂起(通常不会,因为有空闲任务)。
如果不是所有任务都被挂起,调用vTaskSwitchContext()获取并设置下一个要运行的任务为pxCurrentTCB。
如果所有任务都被挂起(理论上不应发生),则pxCurrentTCB可能设置为NULL(具体行为取决于RTOS实现,但通常会有空闲任务)。
8.5 任务恢复过程分析
任务恢复函数有两个 vTaskResume()和 xTaskResumeFromISR(),一个是用在任务中的,一个是用在中断中的,但是基本的处理过程都是一样的。
通过句柄获取任务控制块。
进临界区。
从挂起列表删除任务。
判断优先级准备任务调度。
退临界区。
获取任务控制块(TCB):
根据提供的参数直接获取要恢复的任务的任务控制块(TCB)。
验证TCB有效性:
确保TCB不为NULL且不是pxCurrentTCB(当前运行的任务的任务控制块)。
进入临界区:
调用taskENTER_CRITICAL()以保护后续操作免受中断干扰。
检查任务挂起状态:
使用prvTaskIsTaskSuspended()函数检查任务是否已被挂起。只有挂起的任务才需要恢复。
从挂起列表中删除任务:
如果任务已被挂起,则将其从挂起任务列表(xSuspendedTaskList)中移除。
添加到就绪列表:
将任务添加到就绪任务列表中,以便它能够参与调度。
判断优先级并准备切换:
如果恢复的任务优先级高于当前运行任务的优先级,则准备进行任务切换。
可能的任务切换:
调用taskYIELD_IF_USING_PREEMPTION()(如果启用了抢占式调度)来检查是否需要立即进行任务切换。
退出临界区:
调用taskEXIT_CRITICAL()以允许中断并恢复正常的任务调度。
第九章 FreeRTOS任务切换
9.1 PendSV 异常
PendSV(可挂起的系统调用)异常对 OS 操作非常重要,其优先级可以通过编程设置。可以通过将中断控制和状态寄存器 ICSR 的 bit28,来触发 PendSV 中断。
若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,这对于上下文切换非常有用,也是各种 OS 设计中的关键。
在 OS 中,任务调度器决定是否应该执行上下文切换,如上图任务切换都是由 SysTick中断中执行。若中断请求(IRQ)在 SysTick 异常前产生,则 SysTick 异常可能会抢占 IRQ 的处理,此时OS 不应该执行上下文切换,否则中断请求 IRQ 处理就会被延迟。
为了解决Systic切换上下文时抢占IRQ的问题,PendSV 上下文切换将请求延迟到所有其他 IRQ 处理都已经完成后,此时需要将 PendSV 设置为最低优先级。若 OS 需要执行上下文切换,他会设置 PendSV 的挂起壮态,并在 PendSV 异常内执行上下文切换。
9.2 FreeRTOS 任务切换场合
PendSV 中断上下文(任务)切换被触发的场合:
● 执行一个系统调用
● 系统滴答定时器(SysTick)中断。
9.2.1 执行系统调用
执行系统调用就是执行 FreeRTOS系统提供的相关API函数,比如任务切换函数 taskYIELD(),FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。函数 taskYIELD()其实就是个宏,在文件 task.h中有如下定义:
#define taskYIELD() portYIELD()
函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:
#define portYIELD()
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //向 ICSR寄存器的PENDSV位写1
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。
中断级的任务切换函数为 portYIELD_FROM_ISR(),最终也是通过调用函数 portYIELD()来完成任务切换的。
#define portEND_SWITCHING_ISR( xSwitchRequired )
if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
9.2.2 系统滴答定时器(SysTick)中断
滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
xPortSysTickHandler();
}
}
在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler()。
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI(); //关闭中断
{
if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器 xTickCount 的值
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //向ICSR的PENDSV写1挂起PENDSV
//启动PENDSV中断
}
}
vPortClearBASEPRIFromISR(); //打开中断
}
9.3 PendSV 中断服务函数
PendSV 中断服务函数本应该为 PendSV_Handler(),但是 FreeRTOS 使用#define 重定义了,如下:
#define xPortPendSVHandler PendSV_Handler
PendSV中断服务函数的逻辑:
保存上下文,
关中断,
通过新任务的TCB拿到栈顶指针,恢复新任务的各个寄存器的值。
最后利用LR寄存器跳转到新任务的执行点。
保存当前任务上下文:
读取并保存当前任务的进程栈指针(PSP)到R0。
获取当前任务的任务控制块(TCB)地址并保存到R2。
将R4~R11和R14(LR,链接寄存器)的值保存到当前任务的栈中。
更新当前任务的TCB,将新的栈顶指针(R0)保存到TCB中。
准备任务切换:
将TCB地址(R3)和LR(R14)临时压入主栈(MSP)中,以防在任务切换过程中被覆盖。
关闭中断,防止在任务切换过程中被打断。
执行任务切换:
调用vTaskSwitchContext函数,找到下一个要运行的任务,更新pxCurrentTCB。
恢复中断和任务上下文:
打开中断,允许新的中断发生。
从MSP中恢复之前保存的TCB地址(R3)和LR(R14)。
准备新任务执行:
通过新的TCB获取新任务的栈顶指针,并保存到R0。
从新任务的栈中恢复R4~R11和R14的值。
更新PSP为新任务的栈顶指针。
切换回新任务:
通过执行BX指令,利用LR(R14)中的值跳转回新任务的执行点(通常是任务函数的入口),此时硬件自动恢复其他必要的寄存器状态(如R0~R3, R12, PC, xPSR),并切换到进程模式使用PSP。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp // (1) 将PSP(程序栈指针)的值读入r0
isb
ldr r3, =pxCurrentTCB // (2) 将pxCurrentTCB的地址读入r3
ldr r2, [r3] // (3) 从r3指向的地址读取pxCurrentTCB的值(当前TCB的指针)到r2
stmdb r0!, {r4-r11, r14} // (4) 将r4-r11和r14的值压入由r0指向的栈中,并更新r0的值
str r0, [r2] // (5) 将新的栈顶地址(r0)保存到当前TCB的栈顶指针位置
stmdb sp!, {r3,r14} // (6) 将r3和r14的值压入主栈(MSP)中,以保存它们
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY // (7) 设置r0为系统调用的最大中断优先级
msr basepri, r0 // (8) 更新BASEPRI寄存器以屏蔽更低优先级的中断
dsb
isb
bl vTaskSwitchContext // (9) 调用vTaskSwitchContext函数进行任务切换
mov r0, #0 // (10) 清除r0
msr basepri, r0 // (11) 清除BASEPRI寄存器,允许所有中断
ldmia sp!, {r3,r14} // (12) 从主栈(MSP)中弹出r3和r14的值
ldr r1, [r3] // (13) 再次从r3指向的地址读取pxCurrentTCB的值(更新后的TCB指针)到r1
ldr r0, [r1] // (14) 从r1指向的TCB中读取新的栈顶指针到r0
ldmia r0!, {r4-r11} // (15) 从新的栈顶指针指向的栈中弹出r4-r11的值
msr psp, r0 // (16) 更新PSP为新的栈顶指针
isb
bx r14 // (17) 使用R14(LR,链接寄存器)中的值进行跳转,通常是返回到上一个函数
nop
}
9.4 查找下一个要运行的任务
在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务。
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) // 检查调度器是否被挂起,调度器被挂起则不能切换
{
xYieldPending = pdTRUE; // 如果调度器被挂起,则设置yield挂起标志
}
else
{
xYieldPending = pdFALSE; // 否则,清除yield挂起标志
traceTASK_SWITCHED_OUT(); // 跟踪当前任务被切换出去
taskCHECK_FOR_STACK_OVERFLOW(); // 检查当前任务的栈溢出
taskSELECT_HIGHEST_PRIORITY_TASK(); // 选择最高优先级的任务
traceTASK_SWITCHED_IN(); // 跟踪新任务被切换进来
}
}
查找下一个要运行的任务有两种方法:一个是通用方法,另外一个就是使用硬件方法,选择哪种方法通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。宏为 1 就使用硬件方法,否则使用通用方法。
通用方法
通用方法是所有的处理器都可以用的方法。
一个优先级一个列表,同优先级的就绪任务都挂到相对应的就绪列表中。
每次创建任务的时候都会判断新任务的优先级是否大于就绪列表的最高优先级,然后优先级从高到底判断哪个优先级列表不为空,找到列表项,赋给pxCurrentTCB。
通用方法完全基于C语言实现,因此所有平台都能运行。
硬件方法
硬件方法就是使用处理器自带的硬件指令来实现的,比如 Cortex-M 处理器就带有的计算前导 0 个数指令:CLZ。
FreeRTOS使用全局变量 uxTopReadyPriority来代表处于就绪态的最高优先级。
当使用硬件方法的时候 uxTopReadyPriority 就不代表处于就绪态的最高优先级了,而是使用每个 bit 代表一个优先级,bit0 代表优先级 0,bit31 就代表优先级 31,当某个优先级有就绪任务的话就将其对应的 bit 置 1。使用硬件方法的话最多只能有 32 个优先级。
__clz(uxReadyPriorities)就是计算 uxReadyPriorities 的前导零个数,前导零个数就是指从最高位开始(bit31)到第一个为 1 的 bit,其间 0 的个数
用 31 减去这个前导零个数得到的就是处于就绪态的最高优先级。
然后找到对应优先级的列表,拿到列表项,赋值pxCurrentTCB。下一个任务就确定了。
9.5 FreeRTOS 时间片调度
FreeRTOS 支持多个任务同时拥有一个优先级,一个任务运行一个时间片(一个时钟节拍,也就是滴答定时器的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行。
要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期,单位为微秒。
Systic中断执行期间,调度器会检查是否有任务需要被调度或切换。
PendSV中断才是执行任务切换。
第十章 FreeRTOS 系统内核控制函数
10.1 内核控制函数预览
内核控制函数就是 FreeRTOS 内核所使用的函数。
taskYIELD() //任务切换。
taskENTER_CRITICAL() //进入临界区,用于任务中。
taskEXIT_CRITICAL() //退出临界区,用于任务中。
taskENTER_CRITICAL_FROM_ISR() //进入临界区,用于中断服务函数中。
taskEXIT_CRITICAL_FROM_ISR() //退出临界区,用于中断服务函数中。
taskDISABLE_INTERRUPTS() //关闭中断。
taskENABLE_INTERRUPTS() //打开中断。
vTaskStartScheduler() //开启任务调度器。
vTaskEndScheduler() //关闭任务调度器。
vTaskSuspendAll() //挂起任务调度器。
xTaskResumeAll() //恢复任务调度器。
vTaskStepTick() //设置系统节拍值。
10.2 内核控制函数详解
10.2.1 任务切换 taskYIELD()
此函数用于进行任务切换,本质上是一个宏。
通过 中断控制和状态寄存器ICSR[28] 来写1触发 PendSV中断进行上下文切换。
#define taskYIELD() portYIELD()
#define portYIELD()
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; //向 ICSR寄存器的PENDSV位写1
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
10.2.2 任务进临界区 taskENTER_CRITICAL()
进临界区,用于任务函数中,本质上是一个宏。
禁中断,任务TCB的临界区嵌套计数器自增。
会通过NVIC的中断控制器CTRL判断是否有中断在执行,有的话进入失败。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
/**
* 进入临界区,并增加[临界区嵌套计数器]。当嵌套计数器从0增加到1时,表示进入了一个新的临界区。
* 理论上在进入临界区之前应该没有中断在执行,
*/
void vPortEnterCritical( void )
{
// 禁用中断
portDISABLE_INTERRUPTS();
// 增加临界区嵌套计数器
uxCriticalNesting++;
// 如果这是第一次进入临界区(嵌套计数器从0增加到1)
if( uxCriticalNesting == 1 )
{
// 通过检查NVIC的[中断控制寄存器]中的[活动中断位]来判断是否有中断在执行
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
10.2.3 任务出临界区 taskEXIT_CRITICAL()
出临界区,用于任务函数中,本质上是一个宏。
任务TCB的嵌套计数器自减。开中断。
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
/**
* 退出临界区,此函数用于减少临界区嵌套计数器,并在计数器减到0时重新启用中断。这标志着临界区的结束。
* 在减少嵌套计数器之前,会通过一个断言来确保嵌套计数器不为0,以避免潜在的错误。
*/
void vPortExitCritical( void )
{
// 验证嵌套计数器不为0,确保确实在临界区内
configASSERT( uxCriticalNesting );
// 减少临界区嵌套计数器
uxCriticalNesting--;
// 如果嵌套计数器减到0,表示已经退出所有临界区
if( uxCriticalNesting == 0 )
{
// 重新启用中断
portENABLE_INTERRUPTS();
}
}
10.2.4 中断出临界区 taskENTER_CRITICAL_FROM_ISR()
进临界区,用于中断服务函数中,此函数本质上是一个宏。
中断进、出临界区的逻辑和任务进、出临界区的逻辑一样,开关中断,临界区嵌套计数器自增、自减。
//task.h
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() //进中断临界区
//portmacro.h
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) //设置BasePRI。给0代表开全部中断
10.2.5 中断出临界区 taskEXIT_CRITICAL_FROM_ISR()
退临界区,用于中断服务函数中,此函数本质上是一个宏。
中断进、出临界区的逻辑和任务进、出临界区的逻辑一样,开关中断,临界区嵌套计数器自增、自减。
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )//退中断临界区
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() //设置BasePRI为[可系统调用优先级阈值]
10.2.6 关中断 taskDISABLE_INTERRUPTS()
关闭可屏蔽的中断,此函数本质上是一个宏。
向 BASEPRI写入[最大可系统调用的中断优先级]。
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
/*向BASEPRI写入最大可系统调用的优先级,关中断*/
static portFORCE_INLINE void vPortRaiseBASEPRI(void)
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI //向 BASEPRI写入[最大可系统调用的中断优先级]
dsb
isb
}
}
10.2.7 开中断 portENABLE_INTERRUPTS()
打开可屏蔽的中断,此函数本质上是一个宏。
向 BASEPRI写入[0]。也就是所有可屏蔽中断都由FreeRTOS管理。
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
/*向 BASEPRI写入0,开中断*/
static portFORCE_INLINE void vPortSetBASEPRI(uint32_t ulBASEPRI)
{
__asm
{
msr basepri, ulBASEPRI //向 BASEPRI写入0,开中断
}
}
10.2.8 开任务调度器 vTaskStartScheduler()
开启任务调度器其实就是创建空闲任务,初始化Systic中断和PendSV中断。
(1) 创建空闲任务,空闲任务的优先级为最低。
(2) 创建定时器服务任务。
(3) 关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4) 设置调度器开始运行的标志位。
(5) 使能时间统计功能。
(6) 初始化跟调度器启动有关的硬件,滴答定时器、FPU 单元和 PendSV 中断等等。
10.2.9 关任务调度器 vTaskEndScheduler()
停止实时内核运行。所有创建的任务将自动删除。
注意删除任务不会删除任务堆栈。因为任务调度器关闭会把空闲任务也关了。
void vTaskEndScheduler( void )
{
portDISABLE_INTERRUPTS(); //关闭中断
xSchedulerRunning = pdFALSE; //标记任务调度器停止运行
vPortEndScheduler(); //调用硬件层关闭中断的处理函数
}
函数 vPortEndScheduler()在 port.c 中有定义,这个函数在移植 FreeRTOS 的时候要根据实际使用的处理器来编写,此处没有实现这个函数,只是简单的加了一行断言,函数如下:
{
configASSERT( uxCriticalNesting == 1000UL );
}
10.2.10 挂起任务调度器 vTaskSuspendAll()
将任务调度器挂起,以便执行临界区代码。挂起计数器自增。
uxSchedulerSuspended
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
10.2.11 恢复任务调度器 xTaskResumeAll()
将任务调度器恢复,以便执行任务调度。
挂起计数器自减,在临界区内处理事件、状态、就绪等列表,并进行任务调度。
挂起计数器自减。
进临界区,遍历挂起任务列表xPendingReadyList,
将任务从事件列表移除,
将任务从状态列表移除,
将任务添加到就绪列表中,
判断任务优先级是否高于当前任务,进行任务切换
退临界区。
uxSchedulerSuspended
10.2.12 vTaskStepTick()
设置系统节拍值 vTaskStepTick()函数在FreeRTOS的低功耗Tickless模式下,用于在系统从空闲恢复后,根据空闲时长来更新系统的时间节拍计数,确保系统时间的准确。
/*从低功耗模式唤醒后根据空闲时长设置系统节拍值*/
void vTaskStepTick( const TickType_t xTicksToJump )
{
configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
xTickCount += xTicksToJump;
traceINCREASE_TICK_COUNT( xTicksToJump );
}
xTicksToJump 就是空闲的时间。
第十一章 FreeRTOS 其他任务 API 函数
第十二章 FreeRTOS 时间管理
12.1 FreeRTOS 延时函数
12.1.1 函数 vTaskDelay()
延时函数可以设置为三种模式:相对模式、周期模式和绝对模式。
相对模式时间从延时函数调用开始算起。
vTaskDelay()是相对模式(相对延时函数),//进入函数的时间+延时参数
vTaskDelayUntil()是绝对模式(绝对延时函数)。//上次唤醒的时间+延时参数
函数 vTaskDelay()在文件 tasks.c 中有定义,使用此函数宏 INCLUDE_vTaskDelay 须为 1。
/*相对延时。延时参数必须>0*/
void vTaskDelay( const TickType_t xTicksToDelay )
相对延时 vTaskDelay()需要传入节拍数作为延时时间,延时时间要大于 0。否则的话相当于直接调用函数 portYIELD()进行任务切换。
相对延时函数会调用 vTaskSuspendAll()挂起任务调度器。
将要延时的任务添加到延时列表或者延时溢出列表中 。
恢复任务调度器。开始任务调度。
12.1.2 函数 prvAddCurrentTaskToDelayedList()
函数 prvAddCurrentTaskToDelayedList()用于将当前任务添加到等待列表中。
延时时间 = 进入函数的时间+延时参数。
12.1.3 函数 vTaskDelayUntil()
函数 vTaskDelayUntil()会按照绝对时间阻塞任务,适合需要按照一定的频率运行的任务。
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, //上次任务延时被唤醒的时间点
const TickType_t xTimeIncrement ) //本次延时的节拍数
挂起任务调度器。
记录进入函数 vTaskDelayUntil()的时间点值,并保存在 xConstTickCount 中。
根据延时时间 xTimeIncrement 计算任务下一次唤醒的时间,保存在xTimeToWake 中。
将任务添加进延时列表。
恢复任务调度器。
下次唤醒时间 ,绝对延时为上次唤醒的时间+延时节拍参数。
12.2 FreeRTOS系统时间节拍
不管是什么系统,运行都需要有个系统时钟节拍,xTickCount 就是系统时钟节拍计数器。滴答定时器中断一次,节拍计数器加一。
xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的。
如果调度器没有被挂起,
节拍计数器加一。如果节拍计数器溢出就交换延时列表和溢出列表。
更新延时列表中列表项的阻塞时间。
将阻塞时间结束的任务进行唤醒。从延时队列移除,添加到就绪列表。
将新唤醒的任务与当前任务比较优先级,判断是否进行任务调度。
任务调度。
时间片钩子函数。
如果调度器被挂起,
不直接更新节拍计数器,而是使用uxPendedTicks来记录挂起期间的时钟节拍数。
检查调度器状态:
首先确认任务调度器是否处于运行状态,如果被挂起则不执行后续操作。
更新时钟节拍计数器:
每次时钟中断时,将全局的时钟节拍计数器xTickCount加一并保存至xConstTickCount,准备用于后续的比较和更新。
处理计数器溢出:
如果xConstTickCount为0(表示计数器溢出),则交换延时列表和溢出列表,并更新下一个要解除阻塞的任务的时间点xNextTaskUnblockTime。
检查任务阻塞时间:
遍历延时列表,检查是否有任务到达了其设定的解除阻塞时间。如果延时列表为空,则将xNextTaskUnblockTime设置为最大值,表示无任务即将解除阻塞。否则,检查每个任务的唤醒时间是否已到。
任务唤醒处理:
对于已到达唤醒时间的任务,将其从延时列表中移除。如果任务还在等待其他事件(如信号量、消息队列等),也将其从相应的事件等待列表中移除。将任务添加到就绪列表中,准备执行。
任务切换判断:
如果新唤醒的任务优先级高于当前运行任务,则标记需要进行任务切换(xSwitchRequired设为pdTRUE)。
时间片调度处理(如果启用):
根据当前配置,处理与时间片相关的逻辑,如更新任务的时间片计数、决定是否切换任务等。
执行时间片钩子函数(如果定义):
如果系统配置了时间片钩子函数,则在每个时钟节拍中断时调用此函数,允许用户添加自定义逻辑。
调度器挂起处理:
如果任务调度器被挂起,则不直接更新xTickCount,而是使用uxPendedTicks来记录挂起期间的时钟节拍数。
第十三章 队列
13.1 队列结构体 queue_t
队列结构体的主要成员有:
队列存储区的开始地址、末尾地址、下一个空闲区域地址、
入队阻塞任务列表、出队阻塞任务列表、
队列锁、
队列类型等
typedef struct QueueDefinition
{
int8_t *pcHead; // 指向队列存储区开始地址。
int8_t *pcTail; // 指向队列存储区最后一个字节。
int8_t *pcWriteTo; // 指向存储区中下一个空闲区域。
union
{
int8_t *pcReadFrom; // 当用作队列的时候指向最后一个出队的队列项首地址
UBaseType_t uxRecursiveCallCount; // 当用作递归互斥量的时候用来记录递归互斥量被调用的次数。
} u;
List_t xTasksWaitingToSend; // 等待发送任务列表,那些因为队列满导致入队失败而进入阻塞态的任务就会挂到此列表上。
List_t xTasksWaitingToReceive; // 等待接收任务列表,那些因为队列空导致出队失败而进入阻塞态的任务就会挂到此列表上。
volatile UBaseType_t uxMessagesWaiting; // 队列中当前队列项数量,也就是消息数
UBaseType_t uxLength; // 创建队列时指定的队列长度,也就是队列中最大允许的队列项(消息)数量
UBaseType_t uxItemSize; // 创建队列时指定的每个队列项(消息)最大长度,单位字节
//接收计数器
volatile int8_t cRxLock; // 当队列上锁以后用来统计从队列中接收到的队列项数量,也就是出队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
//发送计数器
volatile int8_t cTxLock; // 当队列上锁以后用来统计发送到队列中的队列项数量,也就是入队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; // 如果使用静态存储的话此字段设置为 pdTURE。
#endif
#if ( configUSE_QUEUE_SETS == 1 ) // 队列集相关宏
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) // 跟踪调试相关宏
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
13.2 队列创建和初始化
有两种创建队列的方法。
函数 xQueueCreateStatic(),静态创建。
函数 xQueueCreate()。动态创建。
共有六种队列类型:
普通消息队列
队列集
互斥信号量
计数型信号量
二值信号量
递归互斥信号量
真正完成队列创建的函数是 xQueueGenericCreateStatic()和xQueueGenericCreate(),这两个函数在文件 queue.c 中有定义。
注意静态创建队列是要手动传申请的内存地址,动态创建是函数自动申请内存地址。
静态创建和动态创建队列的原理都差不多,
参数指定队列消息个数和每个消息的大小。
调用pvPortMalloc()给队列分配内存。队列结构体的内存后面就是队列的内存。
调用 prvInitialiseNewQueue()初始化队列。
队列初始化就是将【队列结构体的成员变量pcHead】指向【队列存储区的首地址】。然后调用队列复位函数对其他成员变量,包括入对阻塞列表、出队阻塞列表等初始化。
/*动态创建队列*/
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,//队列元素个数
UBaseType_t uxItemSize) //每个元素大小
/*静态创建队列*/
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength, //队列元素个数
UBaseType_t uxItemSize, //元素大小
uint8_t * pucQueueStorageBuffer,//队列动态分配的地址
StaticQueue_t * pxQueueBuffer) //存储队列的控制块
/*动态创建队列的底层函数*/
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, //队列消息数
const UBaseType_t uxItemSize, //队列消息大小
const uint8_t ucQueueType ) //队列类型
/*
FreeRTOS 中的信号量等也是通过队列来实现的,
创建信号量的函数最终也是使用此函数的.
共有六种队列类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
函 数 xQueueCreate()创建队列的时候此参数默认选择的就是
queueQUEUE_TYPE_BASE。
*/
/*静态创建队列的底层函数*/
QueueHandle_t xQueueGenericCreateStatic(const UBaseType_t uxQueueLength,//队列消息长度
const UBaseType_t uxItemSize, //消息大小
uint8_t * pucQueueStorage, //消息队列地址
StaticQueue_t * pxStaticQueue, //用于存储队列的控制块
const uint8_t ucQueueType ) //队列类型
/*队列初始化函数*/
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
const UBaseType_t uxItemSize, //队列项目长度
uint8_t * pucQueueStorage, //队列项目存储区
const uint8_t ucQueueType, //队列类型
Queue_t * pxNewQueue ) //队列结构体
13.3 队列复位
队列初始化函数 prvInitialiseNewQueue()中调用了函数 xQueueGenericReset()来复位队列。
复位队列其实就是将队列结构体的成员变量初始化,包括入队阻塞任务列表、出队阻塞任务列表等。
至此一个队列真正初始化完成。
/*队列复位*/
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, // 队列句柄
BaseType_t xNewQueue )// pdTrue 要复位的队列是新创建的队列
// pdFalse 要复位的队列不是创建的队列
13.4 向队列发送消息
13.4.1 任务级入队和中断级入队
创建好队列以后就可以向队列发送消息了,FreeRTOS 提供了 8 个向队列发送消息的 API函数,任务级入队函数和中断级入队函数各 4个。
任务级入队函数
//发送消息到队尾(后向入队),这两个函数是一样的
xQueueSend()
xQueueSendToBack()
//发送消息到队头(前向入队)
xQueueSendToFront()
//前向入队,带覆写,满了自动覆盖
xQueueOverwrite()
中断级入队函数
//后向入队。这两个函数是一样的
xQueueSendFromISR()
xQueueSendToBackFromISR()
//前向入队
xQueueSendToFrontFromISR()
//前向入队,可覆写
xQueueOverwriteFromISR()
入队函数详解
任务级入队函数:
//后向入队
BaseType_t xQueueSend( QueueHandle_t xQueue, //队列句柄
const void * pvItemToQueue,//消息
TickType_t xTicksToWait);//阻塞时间
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void* pvItemToQueue,
TickType_t xTicksToWait);
//前向入队
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);
//前向入队,覆写
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, //队列句柄
const void * pvItemToQueue); //消息
后向入队和前向入队都是调用的同一个函数,xQueueGenericSend()。
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, //队列句柄
const void * const pvItemToQueue, //消息
TickType_t xTicksToWait, //阻塞时间
const BaseType_t xCopyPosition ) //入队方式
/*
入队方式有3种:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT:前向入队
queueOVERWRITE: 覆写入队。
*/
中断级入队函数:
//后向入队
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, //队列句柄
const void * pvItemToQueue, //消息
BaseType_t * pxHigherPriorityTaskWoken); //存储pdTYPE,
//标记退出任务后
//是否进行任务切换
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
//前向入队
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
//前向入队,覆写
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
中断内后向入队和前向入队最终都是调用的函数 xQueueGenericSendFromISR()。
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue, //队列句柄
const void* pvItemToQueue, //消息
BaseType_t* pxHigherPriorityTaskWoken,//标记执行完入队函数是否进行任务切换。由函数设定,用户只需提供一个变量来保存此值。
BaseType_t xCopyPosition);//入队方式
/*
三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT:前向入队
queueOVERWRITE: 覆写入队
*/
13.4.2 任务级通用入队函数
任务级,不管是后向入队 、 前向入队还是覆写入队 , 最终调用的都是通用入队函数xQueueGenericSend()。
入队时检查队列是否满,不满则拷贝消息入队,并检查是否有等待接收的任务,有则唤醒;若队列满且设置阻塞,则将任务加入等待发送列表并阻塞,xTxLock自增,直至队列有空或超时。
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, //队列句柄
const void * const pvItemToQueue, //消息
TickType_t xTicksToWait, //阻塞时间
const BaseType_t xCopyPosition ) //入队类型。后向、前向、覆写。
13.4.3 中断级通用入队函数
中断级,不管是后向入队 、 前向入队还是覆写入队 , 最终调用的都是通用入队函数 xQueueGenericSendFromISR()。
队列未满或采用覆写方式时,数据成功拷贝入队。
读取队列的成员变量发送计数器 xTxLock,判断是否上锁。
给队列发送消息时 xTxLock上锁了就不能操作事件列表。中断级入队不会阻塞。
入队后检查 [任务等待接收列表],有阻塞任务则唤醒并可能触发任务切换。
队列满时直接返回错误表示无法入队。
队列的队列锁成员 发送计数器 xTxLock 接收计数器 xRxLock
等待发送 xTxLock+1,每接收一条 xTxLock-1,
当处理完以后标记 cTxLock 为 queueUNLOCKED 解锁状态。UNMODIFIED是上锁状态。
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, // 队列句柄
const void * const pvItemToQueue, // 消息
BaseType_t * const pxHigherPriorityTaskWoken, // 标记执行完是否任务切换
const BaseType_t xCopyPosition ) // 入队类型 后、前、覆写
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
// 检查中断优先级是否有效
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
// 保存当前中断状态并关闭中断
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
// 检查队列是否有空间或是否允许覆盖
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||
( xCopyPosition == queueOVERWRITE ) )
{
const int8_t cTxLock = pxQueue->cTxLock; // 获取当前队列锁状态
traceQUEUE_SEND_FROM_ISR( pxQueue ); // 跟踪事件
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); // 复制数据到队列
// 如果队列未上锁,则处理等待接收的任务
if( cTxLock == queueUNLOCKED )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
// 如果成功移除了任务,并且需要标记任务切换
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE; // 标记有更高优先级的任务被唤醒
}
}
}
}
xReturn = pdPASS; // 设置函数返回值为成功
}
else
{
// 如果队列满且不允许覆盖,则增加发送锁计数器
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue ); // 跟踪事件
xReturn = errQUEUE_FULL; // 设置函数返回值为队列满
}
}
// 恢复之前的中断状态
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn; // 返回函数结果
}
13.5 队列上锁和解锁
在上面讲解任务级通用入队函数和中断级通用入队函数的时候都提到了队列的上锁和解锁,队列的上锁和解锁是两个 API 函数:prvLockQueue()和 prvUnlockQueue()。
13.5.1 队列上锁 prvLockQueue
队列上锁就是将队列中的成员变量 cRxLock 和 cTxLock 设置 queueLOCKED_UNMODIFIED 就行了。
/*队列上锁*/
#define prvLockQueue( pxQueue ) taskENTER_CRITICAL();
{
if( ( pxQueue )->cRxLock == queueUNLOCKED )
{
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED;
}
if( ( pxQueue )->cTxLock == queueUNLOCKED )
{
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED;
}
}
taskEXIT_CRITICAL()
13.5.2 队列解锁 prvUnlockQueue
队列关于锁的结构体成员有入队计数器 cTxLock和出队计数器 cRxLock。
消息入队时,如果队列满了,任务就会放进[等待发送事件列表],cTxLock自增。
同理,接收消息时,如果队列为空,任务就会放进[等待接收事件列表],cRxLock自增。
队列解锁会进入临界区,
先判断cTxLock的状态,遍历[等待发送事件列表],每移除一个cTxLock自减。
再判断cRxLock的状态,遍历[等待接收事件列表],每移除一个cRxLock自减。
移除完就根据优先级判断是否调度。
/*队列解锁*/
static void prvUnlockQueue( Queue_t * const pxQueue )
{
// 进入临界区,防止中断或任务切换影响队列状态
taskENTER_CRITICAL();
{
// 处理 cTxLock(发送锁)。
int8_t cTxLock = pxQueue->cTxLock;
while( cTxLock > queueLOCKED_UNMODIFIED ) // 当cTxLock大于未修改值时循环
{
// 省略掉与队列集相关代码(可能包括更新队列状态等)
// 如果有任务在等待接收
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
// 尝试从等待接收的任务列表中移除一个任务
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
// 如果成功移除且该任务优先级高于当前任务,则标记需要任务切换
vTaskMissedYield();
}
else
{
// 移除失败,可能是列表为空但检查未及时更新
mtCOVERAGE_TEST_MARKER(); // 覆盖率测试标记
}
// 减少cTxLock计数,这里可能是一个逻辑上的特殊设计
// 通常,发送锁不会在接收操作中被减少
--cTxLock; // (5) 注意这里的逻辑可能不符合常规队列操作
}
else
{
// 没有等待接收的任务,退出循环
break;
}
}
// 将cTxLock设置为未锁定状态
pxQueue->cTxLock = queueUNLOCKED; // (6)
}
taskEXIT_CRITICAL();
// 处理 cRxLock(接收锁),逻辑与cTxLock类似但处理等待发送的任务
taskENTER_CRITICAL();
{
int8_t cRxLock = pxQueue->cRxLock;
while( cRxLock > queueLOCKED_UNMODIFIED )
{
// 如果有任务在等待发送
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
// 尝试从等待发送的任务列表中移除一个任务
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
// 如果成功移除且该任务优先级高于当前任务,则标记需要任务切换
vTaskMissedYield();
}
else
{
// 移除失败,可能是列表为空但检查未及时更新
mtCOVERAGE_TEST_MARKER(); // 覆盖率测试标记
}
// 减少cRxLock计数
--cRxLock;
}
else
{
// 没有等待发送的任务,退出循环
break;
}
}
// 将cRxLock设置为未锁定状态
pxQueue->cRxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
}
13.6 从队列读取消息
13.6.1 出队函数简介
FreeRTOS 提供了 4个从队列接收消息的 API,任务级出队函数和中断级出队函数各 2个。
/*任务级出队函数 */
xQueueReceive() //读取消息,读取完以后不删除消息
/*任务级出队函数*/
xQueuePeek() //读取消息,读取完以后删除消息
/*中断级出队*/
xQueueReceiveFromISR()//读取消息,删消息
xQueuePeekFromISR ()//读取消息,不删消息
13.6.2 出队函数详细
/*任务级出队列*/
//出队列,删队列项
BaseType_t xQueueReceive(QueueHandle_t xQueue,//队列句柄
void * pvBuffer, //缓冲区
TickType_t xTicksToWait);//阻塞时间
//出队列,不删队列项
BaseType_t xQueuePeek(QueueHandle_t xQueue, //队列句柄
void * pvBuffer, //缓冲区
TickType_t xTicksToWait);//阻塞时间
/*中断级出队列*/
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue, //队列句柄
void* pvBuffer, //缓冲区
TickType_t xTicksToWait //阻塞时间
BaseType_t xJustPeek) //是否删队列项
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, //队列句柄
void* pvBuffer, //缓冲区
BaseType_t * pxTaskWoken);//标记函数结束后是否任务切换。
//用户不需要设置,只需给个变量存值
第十四章 信号量
FreeRTOS中的信号量分为
二值信号量、计数型信号量、互斥信号量和递归互斥信号量。
14.1 二值信号量
14.1.1 创建二值信号量
根据信号量内存是FreeRTOS内存管理部分动态分配还是手动静态分配,有动态创建和静态创建的区别。
/*动态创建二值信号量*/
xSemaphoreCreateBinary() //内存由FreeRTOS内存管理部分动态分配
/*静态创建二值信号量*/
xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer) //内存手动分配
信号量是用队列实现的。
创建二值信号量的API底层使用函数 xQueueGenericCreate()创建了一个队列,队列长度为 1,队列项长度为 0, 队列类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE,二值信号量。
创建的队列是个没有存储区的队列,使用队列是否为空来表示二值信号量。队列是否为空可以通过队列结构体的成员变量 uxMessagesWaiting 来判断。
14.1.2 释放信号量
释放信号量的函数有两个,任务级信号量释放和中断级信号量释放各一个。
xSemaphoreGive( ) //任务级信号量释放。二值、计数、互斥
xSemaphoreGiveFromISR( )//中断级信号量释放。二值、计数
任务级释放信号量就是向队列发送消息的过程。只是这里并没有发送具体的消息, 阻塞时间为 0(宏 semGIVE_BLOCK_TIME 为 0),入队方式采用的后向入队。
入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了。
/*任务级信号量释放 此函数用于释放二值信号量、计数型信号量或互斥信号量*/
BaseType_t xSemaphoreGive( xSemaphore ) //信号量句柄
/*中断级信号量释放 释放二值信号量和计数型信号量*/
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, //信号量句柄
BaseType_t * pxHigherPriorityTaskWoken)//存标志。执行完是否调度。
14.1.3 获取信号量
获取信号量的函数有两个,任务级获取和中断级获取。
xSemaphoreTake() //任务级获取信号量。二值、计数、互斥
xSemaphoreTakeFromISR() //中断级获取信号量。二值、计数
获取信号量其实就是读取队列,将队列结构体成员变量 uxMessagesWaiting 减一。
同时标记队列的拥有者。将pxQueue->pxMutexHolder标记为当前任务的任务控制块TCB。
/*任务级获取信号量*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,//信号量句柄
TickType_t xBlockTime) //阻塞时间
/*中断级获取信号量*/
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, //信号量句柄
BaseType_t * pxHigherPriorityTaskWoken)//用来存储标志。任务结束是否调度。
14.2 计数型信号量
二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。
14.2.1 创建计数型信号量
xSemaphoreCreateCounting() 使用动态方法创建计数型信号量。
xSemaphoreCreateCountingStatic() 使用静态方法创建计数型信号量
计数型信号量也是在队列的基础上实现的,所以需要调用函数 xQueueGenericCreate() 创建一个队列, 队列长度为信号量最大值uxMaxCount , 队列项长度为 0 ,队列类型计数型信号量。
队列结构体的成员变量 uxMessagesWaiting 用于计数信号量的计数,为计数信号量初始值。
/*动态方法创建计数信号量,创建成功返回信号量句柄*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, //计数信号量最大值,大于此值释放失败
UBaseType_t uxInitialCount ) //计数信号量初始值
/*静态方法创建计数信号量*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
UBaseType_t uxMaxCount, //最大值
UBaseType_t uxInitialCount, //初始值
StaticSemaphore_t * pxSemaphoreBuffer )//缓冲区
14.2.2 释放和获取计数信号量
释放信号量:
xSemaphoreGive( ) //任务级信号量释放。二值、计数、互斥
xSemaphoreGiveFromISR( )//中断级信号量释放。二值、计数
获取信号量:
xSemaphoreTake() //任务级获取信号量。二值、计数、互斥
xSemaphoreTakeFromISR() //中断级获取信号量。二值、计数
14.3 优先级翻转
使用二值和计数信号量的时候会遇到优先级翻转问题,破坏任务的预期顺序。
高优先级任务因为等待低优先级任务释放资源而被迫等待,甚至被中优先级任务抢先执行,从而延迟了其执行时间。
互斥信号量的优先级继承:
低任务持有互斥信号时,高任务获取不到互斥锁被挂起,低任务就将任务优先级提升到与高任务相同,这个过程就是优先级继承。
将低任务提升到高优先级,低优任务释放后马上就轮到高任务了,减少了中任务抢占对高任务的等待时间拖累。将优先级翻转问题的影响降低。
14.4 互斥信号量
14.8.1 互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量。
互斥信号量使用和二值信号量相同的 API 操作函数。
互斥信号量具有优先级继承的特性:
当一个互斥信号量正在被一个低优先级任务使用,此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。而且这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。
优先级继承能够尽可能降低优先级翻转带来的影响。
互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数,用为中断服务函数不能被阻塞。
14.8.2 创建互斥信号量
//使用动态方法创建互斥信号量
xSemaphoreCreateMutex()
//使用静态方法创建互斥信号量
xSemaphoreCreateMutexStatic(buffer) //buffer指定StaticSemaphore_t类型地址
本质是创建一个队列,队列长度为 1,队列项长度为 0,队列类型为互斥信号量。
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
14.8.3 初始化互斥信号量
调用函数 prvInitialiseMutex()初始化互斥信号量。
static void prvInitialiseMutex( Queue_t *pxNewQueue )
虽然创建队列的时候会初始化队列结构体的成员变量,但是此时创建的是互斥信号量,因此有些成员变量需要重新赋值,尤其是那些用于优先级继承的。
当 Queue_t 用于表示队列的时候 pcHead 和 pcTail 指向队列的存储区域,当 Queue_t 用于表 示互斥信号量的时候就不需要 pcHead 和 pcTail 了。
14.8.4 释放互斥信号量
释放互斥信号量的时候和二值、计数型信号量一样,都是用的函数 xSemaphoreGive()。
/*任务级信号量释放 此函数用于释放二值信号量、计数型信号量或互斥信号量*/
BaseType_t xSemaphoreGive( xSemaphore ) //信号量句柄
最重要的一步就是将 uxMessagesWaiting 加一。
释放互斥信号量以后,任务的优先级复位到最初的优先级。
14.8.5 获取互斥信号量
获取互斥信号量的函数同获取二值、计数型信号量的函数相同,都是 xSemaphoreTake()。
/*任务级获取信号量*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,//信号量句柄
TickType_t xBlockTime) //阻塞时间
获取互斥信号量成功,需要标记互斥信号量的所有者。将pxQueue->pxMutexHolder标记为当前任务的任务控制块TCB。
14.5 递归互斥信号量
14.5.1 递归互斥信号量简介
递归互斥信号量可以看作是一个特殊的互斥信号量,也就是特殊的可重复获取的具有优先级继承的二值信号量。
用xSemaphoreTakeRecursive()获取了多少次,
就得用xSemaphoreGiveRecursive()释放多少次。
要使用递归互斥信号量的话宏 configUSE_RECURSIVE_MUTEXES 必须为 1。
14.5.2 创建递归互斥信号量
静态方法和动态方法两个互斥信号量创建函数。
xSemaphoreCreateRecursiveMutex() 使用动态方法创建递归互斥信号量。 xSemaphoreCreateRecursiveMutexStatic() 使用静态方法创建递归互斥信号量。
递归互斥信号量也是队列,用队列创建函数 xQueueCreateMutex(),创建时候类型选择为递归互斥信号量。
14.5.3 释放递归互斥信号量
递归互斥信号量有专用的释放函数:xSemaphoreGiveRecursive()。
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
uxRecursiveCallCount 用来记录递归信号量被获取的次数。
由于递归互斥信号量可以被一个任务多次获取,因此在释放的时候也要多次释放,但是只有在最后一次释放的时候才会调用函数 xQueueGenericSend()完成释放过程,其他的时候只是简单将 uxRecursiveCallCount 减一即可。
14.5.4 获取递归互斥信号量
获取递归互斥信号量使用函数 xSemaphoreTakeRecursive()。
#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
xQueueTakeMutexRecursive( ( xMutex ), //句柄
( xBlockTime ) )//阻塞时间
将 uxRecursiveCallCount 加一。标记队列的拥有者。
第十五章 FreeRTOS 软件定时器
15.1 软件定时器简介
软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。
回调函数是在定时器服务任务中执行,一定不能在回调函数中调用会阻塞任务的 API 函数!
15.2 定时器服务/Daemon 任务
15.2.1 定时器服务任务与队列
定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务(或 Daemon)任务 来提供的。
FreeRTOS 提供了很多定时器有关的 API 函数,这些 API 函数大多都使用 FreeRTOS 的队列发送命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给 FreeRTOS 的软件定时器使用的,用户不能直接访问。
15.2.2 定时器相关配置
软件定时器有一个定时器服务任务和定时器命令队列,相关的配置是放到文件 FreeRTOSConfig.h 中的。
1、configUSE_TIMERS
开启定时器服务任务。启动OS调度器时自动创建定时器服务任务。。
2、configTIMER_TASK_PRIORITY
设置软件定时器服务任务的任务优先级,可以为 0~( configMAX_PRIORITIES-1)。
3、configTIMER_QUEUE_LENGTH
此宏用来设置定时器命令队列的队列长度。
4、configTIMER_TASK_STACK_DEPTH
此宏用来设置定时器服务任务的任务堆栈大小,单位为字,不是字节!,对于 STM32 来说一个字是 4 字节。定时器服务任务中会执行定时器的回调函数。
15.3 单次定时器和周期定时器
软件定时器分两种:单次定时器和周期定时器。运行一次和周期运行的区别。
15.4 复位软件定时器
就是复位计数值,重新计数。
两个 API 函数来完成软件定时器的复位,任务中一个,中断中一个。
xTimerReset() 复位软件定时器,用在任务中。
xTimerResetFromISR() 复位软件定时器,用在中断服务函数中
/*任务中复位软件定时器*/
BaseType_t xTimerReset( TimerHandle_t xTimer, //定时器句柄
TickType_t xTicksToWait ) //阻塞时间
调用函数 xTimerReset ()开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_RESET 命令,向队列发送消息所以涉及到入队阻塞时间的设置。
/*中断中复位定时器*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, //定时器句柄
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否调度。只存储。
15.5 创建软件定时器
使用软件定时器之前要先创建软件定时器。老样子,动态方法创建和静态方法创建。
/*动态方法创建软件定时器*/
xTimerCreate()
/*静态方法创建软件定时器*/
xTimerCreateStatic()
新创建的软件定时器处于休眠状态,也就是未运行的 。
/*创建定时器。创建成功返回定时器句柄。*/
TimerHandle_t xTimerCreate( const char * const pcTimerName, //定时器名
TickType_t xTimerPeriodInTicks, //定时周期,节拍数
UBaseType_t uxAutoReload, //定时器模式,是否自动重载,单次还是周期
void * pvTimerID, //定时器ID号,
//回调函数根据定时器ID号处理不同定时器
TimerCallbackFunction_t pxCallbackFunction )//回调函数
/*静态创建定时器,创建成功返回定时器句柄*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName, //定时器名
TickType_t xTimerPeriodInTicks, //定时器周期。节拍数
UBaseType_t uxAutoReload, //是否自动重载
void * pvTimerID, //定时器ID
TimerCallbackFunction_t pxCallbackFunction,//回调函数
StaticTimer_t * pxTimerBuffer ) //定时器地址buffer
15.6 开启软件定时器
两个 API 函数来完成软件定时器的开启,任务中一个,中断中一个。
xTimerStart() 开启软件定时器,用于任务中。
xTimerStartFromISR() 开启软件定时器,用于中断中。
/*任务中开启软件定时器*/
BaseType_t xTimerStart( TimerHandle_t xTimer, //定时器句柄
TickType_t xTicksToWait ) //阻塞时间
调用函数 xTimerStart()开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_START 命令,向队列发送消息涉及到入队阻塞时间的设置。
/*中断中开启定时器*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, //定时器句柄
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否调度
15.7 停止软件定时器
两个 API 函数来完成软件定时器的停止,任务中一个,中断中一个。
xTimerStop() 停止软件定时器,用于任务中。
xTimerStopFromISR() 停止软件定时器,用于中断服务函数中。
/*任务中停止定时器*/
BaseType_t xTimerStop ( TimerHandle_t xTimer, //定时器句柄
TickType_t xTicksToWait ) //执行完是否调度
/*中断中停止定时器*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, //定时器句柄
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否调度
第十六章 FreeRTOS 事件标志组
16.1 事件标志组简介
1、事件位(事件标志)
事件位用来表明某个事件是否发生,事件位通常用作事件标志。
2、事件组
一个事件组就是一组的事件位,事件组中的事件位通过位编号来访问。
3、事件标志组和事件位的数据类型
事件标志组的数据类型为 EventGroupHandle_t。
当宏 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位,
当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位。
事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中。
typedef TickType_t EventBits_t;//事件位
#if( configUSE_16_BIT_TICKS == 1 )
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
#define portTICK_TYPE_IS_ATOMIC 1
#endif
可以看出当 configUSE_16_BIT_TICKS 为 0 的时候 TickType_t 是个 32 位的数据类型,因此 EventBits_t 也是个 32 位的数据类型。EventBits_t 类型的变量可以存储 24 个事件位,另外的那高 8 位有其他用。
16.2 创建事件标志组
使用事件标志组之前要先创建事件标志组。动态方法创建和静态方法创建。
xEventGroupCreate() 使用动态方法创建事件标志组。
xEventGroupCreateStatic() 使用静态方法创建事件标志组
/*动态创建事件标志组*/
EventGroupHandle_t xEventGroupCreate(void)
/*静态创建事件标志组*/
EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer)//指定buffer
16.3 设置事件位
4 个函数用来设置事件标志组中事件位(标志),事件位(标志)的设置包括清零和置 1 两种操作,又分任务和中断两种情况。
xEventGroupClearBits() 将指定的事件位清零,用在任务中。
xEventGroupSetBits() 将指定的事件位置 1,用在任务中。
xEventGroupClearBitsFromISR() 将指定的事件位清零,用在中断服务函数中
xEventGroupSetBitsFromISR() 将指定的事件位置 1,用在中断服务函数中。
/*任务中清零事件标志组*/
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, //事件标志组
const EventBits_t uxBitsToClear );//要清零的标志位,支持16进制,下标从0开始
/*中断中清零事件标志组*/
BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
/*任务中清零事件标志组*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
/*任务中设置事件标志组*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, //事件标志组
const EventBits_t uxBitsToSet, //要清零的标志位
BaseType_t * pxHigherPriorityTaskWoken );//函数结束是否调度.
16.4 获取事件标志组值
两个函数来查询事件标志组值,任务中一个,中断中一个。
xEventGroupGetBits() 获取当前事件标志组的值,用在任务中。
xEventGroupGetBitsFromISR() 获取当前事件标志组的值,用在中断服务函数中。
/*任务中,获取事件标志组的值*/
EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup )//事件标志组句柄
/*中断中,获取事件标志组的值*/
EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup )//事件标志组句柄
16.5 等待指定的事件位
某个任务可能需要与多个事件进行同步,那么这个任务就需要等待并判断多个事件位,使用函数 xEventGroupWaitBits()可以完成这个功能。调用函数以后如果任务要等待的事件位还没有准备好(置 1 或清零)的话任务就会进入阻塞态。
注意阻塞,所以等待指定的事件位xEventGroupWaitBits()只适合用在任务中。
/*等待事件标志位*/
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, //事件组句柄
const EventBits_t uxBitsToWaitFor, //等待的事件标志位
const BaseType_t xClearOnExit, //pdTrue 则等待的标志位清零
//pdFalse 则等待的标志位不改变
const BaseType_t xWaitForAllBits, //事件组的标志位全置1才返回
const TickType_t xTicksToWait );//阻塞时间,节拍数
第十七章 FreeRTOS 任务通知
17.1 任务通知简介
任务通知在 FreeRTOS 中是一个可选功能,要使用任务通知的话就需要将宏
configUSE_TASK_NOTIFICATIONS 定义为 1。
FreeRTOS 的每个任务都有一个通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。
任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态。
使用任务通知值的方法在一些场合中替代队列、二值信号量、计数信号量和事件标志组。响应速度更快,占用内存更小。
17.2 发送任务通知
任务通知发送函数有 6 个,任务中和中断中各三个。
任务的TCB控制块,有一个参数 pxTCB->ulNotifiedValue,代表任务通知值。
还有一个参数,pxTCB->ucNotifyState,代表任务通知状态。
任务通知状态转换:
pxTCB->ucNotifyState 默认是
taskWAITING_NOTIFICATION 意为等待通知。
taskNOTIFICATION_RECEIVED 代表接收到通知,但还未处理完毕。
任务中发通知,会根据TCB的任务通知状态,判断是否需要解除阻塞。
中断中发通知,会额外根据调度器的开关状态,判断是将任务放到挂起任务列表(调度器关),还是就绪状态列表(调度器开)。
xTaskNotify() 发通知,带通知值,任务用。
xTaskNotifyGive() 发通知,不带通知值,接收方通知值加一,任务用。
xTaskNotifyAndQuery() 发通知,带通知值,保留原通知值,任务用。
xTaskNotifyFromISR() 发通知,带通知值,中断用。
vTaskNotifyGiveFromISR() 发通知,不带通知值,接收方通知值加一,中断用。
xTaskNotiryAndQueryFromISR() 发通知,带通知值,保留原通知值,中断用。
/*发任务通知,带通知值,任务用*/
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, //任务句柄
uint32_t ulValue, //通知值
eNotifyAction eAction ) //通知更新的方法
/*发任务通知,带通知值,中断用*/
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否切换任务
/*发任务通知,不带通知值,只通知值加一,任务用*/
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
/*发任务通知,不带通知值,只通知值加一,中断用*/
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否切换任务
/*发任务通知,保存通知值,任务用*/
BaseType_t xTaskNotifyAndQuery ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
uint32_t * pulPreviousNotificationValue);//用来保存更新前的任务通知值
/*发任务通知,保存通知值,中断用*/
BaseType_t xTaskNotifyAndQueryFromISR ( TaskHandle_t xTaskToNotify, //任务句柄
uint32_t ulValue, //通知值
eNotifyAction eAction, //通知更新的方法
uint32_t * pulPreviousNotificationValue //原通知值
BaseType_t * pxHigherPriorityTaskWoken );//执行完是否切换
/*通知更新的方法*/
typedef enum
{
eNoAction = 0, //无动作
eSetBits, //更新指定的 bit
eIncrement, //通知值加一
eSetValueWithOverwrite, //覆写的方式更新通知值
eSetValueWithoutOverwrite //不覆写通知值
//如果原先的通知值已经被处理了就更新为任务通知值
//如果原先的通知值没有被处理的话就返回pdFAIL。
} eNotifyAction;
17.3 获取任务通知
获取任务通知的函数有两个。
ulTaskNotifyTake()
获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减一。当任务通知用作二值信号量或者计数信号量的时候使用此函数来获取信号量。
xTaskNotifyWait()
等待任务通知,比 ulTaskNotifyTak()更为强大,全功能版任务通知获取函数
/*获取任务通知函数*/
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, //pdFALSE则退出时通知值-1
//pdTRUE则退出时通知值清零。可做二值信号量
TickType_t xTicksToWait );//阻塞时间
/*等待任务通知函数*/
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,//没有接收到任务通知的时候
//当此参数为 0xffffffff
//或者 ULONG_MAX 的时候
//就会将任务通知值清零。
uint32_t ulBitsToClearOnExit, //接收到了任务通知,
//当此参数为 0xffffffff
//或 ULONG_MAX 退出函数前
//将任务通知值清零。
uint32_t * pulNotificationValue,//保存原通知值
TickType_t xTicksToWait ); //阻塞时间
第十八章 FreeRTOS 低功耗 Tickless 模式
18.1 STM32 低功耗模式
STM32 本身就支持低功耗模式,以本教程使用的 STM32F429 为例,共有三种低功耗模式: ● 睡眠(Sleep)模式。 ● 停止(Stop)模式。 ● 待机(Standby)模式。
WFI:等待任意中断唤醒。
WFE:等待特定事件唤醒。
休眠模式:
内核停,外设可运行,功耗适中,无唤醒延时。
可通过任意一个中断或事件唤醒。
停止模式:
内核部分电源保留,外设停,SRAM、FLASH可选断电。
可通过任意一个外部中断唤醒。
待机模式:
内核和外设都断电。
可WKUP引脚上升沿、RTC闹钟事件、NRST引脚复位或IWDG独立看门狗复位唤醒。
18.1.1 睡眠(Sleep)模式
设置SCB_SCR寄存器中的SLEEPDEEP位为1,以启用深度睡眠模式(这是进入停止模式的前提)。
● 进入睡眠模式
Cortex-M内核支持通过WFI(等待中断)和WFE(等待事件)指令进入低功耗睡眠模式。
系统控制寄存器(SCR)中的SLEEPONEXIT位决定了睡眠模式的行为:
SLEEPONEXIT为0,那么当CPU被中断或事件唤醒后,它会返回正常的工作模式,而不是再次进入睡眠。
SLEEPONEXIT为1,并且SLEEPDEEP也被设置,那么CPU在中断或事件唤醒后,会根据系统配置(如是否有挂起的中断或事件)决定是否再次进入睡眠模式。。
CMSIS(Cortex微控制器软件接口标准)提供了__WFI()和__WFE()函数,分别对应WFI和WFE指令,允许开发者在代码中直接控制CPU进入相应的睡眠模式。
● 退出睡眠模式
使用WFI,MCU休眠等待任意中断唤醒;
WFE则等待特定事件唤醒。在STM32F103休眠时,Cortex-M3内核停止,外设根据配置可能保持运行或低功耗,等待唤醒。
18.1.2 停止(Stop)模式
停止模式下,Cortex-M3进入深度休眠,1.2V域时钟停止,PLL、HSI、HSE禁用,但SRAM数据保留。调压器可设正常或低功耗模式。可选将Flash置为掉电状态以进一步节能,但唤醒后启动延时增加。
18.1.3 待机(Standby)模式
相比于前面两种低功耗模式,待机模式的功耗最低。待机模式是基于 Cortex-M3 的深度睡眠模式的。其中调压器被禁止。1.2V 域断电,PLL、HSI 振荡器和 HSE 振荡器也被关闭。除了备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失。
退出待机模式的话会导致 STM32F1 重启,所以待机模式的唤醒延时也是最大的。
18.2 Tickless 模式详解
18.2.1 如何降低功耗?
一般的简单应用中处理器大量的时间都在处理空闲任务,所以可以考虑当处理器处理空闲任务的时候就进入低功耗模式,当需要处理应用层代码的时候就将处理器从低功耗模式唤醒。
一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。
中断可以将 STM32从睡眠模式中唤醒,周期性的滴答定时器中断就会导致 STM32 周期性的进入和退出睡眠模式。因此,如果滴答定时器中断频率太高的话会导致大量的能量和时间消耗在进出睡眠模式中。低功耗模式的作用被大大的削弱。
为此,FreeRTOS 特地提供了一个解决方法——Tickless 模式,当处理器进入空闲任务周期以后就关闭滴答定时器中断,只有当其他中断发生或者其他任务需要处理的时候处理器才会被从低功耗模式中唤醒。
但在空闲函数周期关闭了滴答定时器会导致两个问题:
关闭系统节拍中断会导致系统节拍计数器停止,系统时钟就会停止。
可以记录下系统节拍中断的关闭时间,系统节拍中断再次开启运行的时候补上这段时间。
这就需要另外一个定时器来记录这段该补上的时间,如果使用专用的低功耗处理器的话基本上都会有一个低功耗定时器,比如 STM32L4 系列(L 系列是 ST 的低功耗处理器)就有一个叫做 LPTIM(低功耗定时器)的定时器。STM32F103 没有这种定时器那么就接着使用滴答定时器来完成这个功能。
如何保证下一个要运行的任务能被准确的唤醒。
在进入低功耗模式前,通过设置一个定时器,其定时周期基于下一个任务的预计执行时间,定时器到时产生中断,从而唤醒处理器。
18.2.2 Tickless 具体实现
1、宏 configUSE_TICKLESS_IDLE
将 FreeRTOSConfig.h 中的宏 configUSE_TICKLESS_IDLE设置为 1,表示开启低功耗tickless模式。
2、宏 portSUPPRESS_TICKS_AND_SLEEP()
当空闲任务是唯一可运行的任务且预计进入低功耗模式时间超过 [进入休眠前预期空闲时间]时,Tickless模式启用,调用portSUPPRESS_TICKS_AND_SLEEP()处理低功耗工作。
configEXPECTED_IDLE_TIME_BEFORE_SLEEP(进入休眠前的预期空闲时间,默认2,节拍数)
portSUPPRESS_TICKS_AND_SLEEP() 的参数指定了处理器处于低功耗模式的时长(节拍数),确保系统在任务就绪前保持低功耗状态,一旦有任务就绪,处理器将退出低功耗模式。
portSUPPRESS_TICKS_AND_SLEEP()应该根据自己所选择的平台来编写,此宏会被空闲任务调用来完成具体的低功耗工作。但是使用 STM32 的话 FreeRTOS 已经帮我们做好了。
如果自己编写的话需要先将 configUSE_TICKLESS_IDLE 设置为 2。
portSUPPRESS_TICKS_AND_SLEEP() 的本质是函数
vPortSuppressTicksAndSleep()
/*进入tickless低功耗模式*/
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )//期望的睡眠时间
1、xExpectedIdleTime设定低功耗时长,受限于滴答定时器24位限制。
2、计算滴答重装值,补偿暂停期间程序执行时间,并检查就绪任务。
3、配置低功耗模式,通过PRIMASK寄存器屏蔽中断处理,使用WFI 进入睡眠。
4、退出低功耗后,恢复工作与补偿系统时钟。
5、调用vTaskStepTick()补偿系统时钟节拍。
给系统时钟节拍计数器 xTickCount 加上一个补偿值。
补偿值怎么得到的?进入tickless给了个参数,记得吗
3、宏 configPRE_SLEEP_PROCESSING ()和 configPOST_SLEEP_PROCESSING()
在低功耗设计中,需降频甚至关系统时钟,切换至低功耗时钟源如内部RC振荡器,关闭非必要外设时钟及电源,这些需在硬件设计时预留控制机制。
FreeRTOS 为我们提供了一个宏来完成这些进入低功耗前的时钟和外设关闭操作,它就是 configPRE_SLEEP_PROCESSING(),这个宏的具体实现内容需要用户去编写。
退出低功耗模式以后需要恢复处理器频率、重新打开外设时钟等,这个操作在宏
configPOST_SLEEP_PROCESSING()中完成,同样的这个宏的具体内容也需要用户去编写。
//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing
4、宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
configEXPECTED_IDLE_TIME_BEFORE_SLEEP宏限制进入低功耗模式的最小时间,避免过短(如1个时钟节拍)的无意义低功耗,确保节能效果。
默认情况下 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 为 2 个时钟节拍,并且最小不能小于 2 个时钟节拍。
第十九章 FreeRTOS 空闲任务
19.1 空闲任务详解
19.1.1 空闲任务简介
当 FreeRTOS 的调度器启动以后就会自动创建一个空闲任务,这样就可以确保至少有一个任务可以运行。但是这个空闲任务使用最低优先级,不会跟高优先级的任务抢占 CPU 资源。
如果某个任务要调用函数 vTaskDelete()删除自身,且自身为动态创建,那么这个任务的任务控制块 TCB 和任务堆栈等这些由 FreeRTOS 系统自动分配的内存需要在空闲任务中释放掉,如果删除的是别的动态创建的任务那么相应的内存就会被直接释放掉,不需要在空闲任务中释放。
用户可以创建与空闲任务优先级相同的应用任务,当宏 configIDLE_SHOULD_YIELD 为 1的话应用任务就可以使用空闲任务的时间片,空闲任务会让出时间片给同优先级的应用任务。
19.1.2 空闲任务的创建
当调用函数 vTaskStartScheduler()启动任务调度器的时候此函数就会自动创建空闲任务。
宏 configSUPPORT_STATIC_ALLOCATION 配置使用静态方法创建空闲任务还是使用动态方法创建空闲任务。
静态创建:
如果支持静态内存分配,则应用需要提供一个函数vApplicationGetIdleTaskMemory来分配TCB和堆栈的静态内存。
动态创建:
如果不支持静态内存分配,则使用xTaskCreate函数动态创建空闲任务。
使用动态方法创建空闲任务,空闲任务的任务函数为 prvIdleTask(),任务堆栈大小为,任务堆栈大小可以在 FreeRTOSConfig.h 中修改。任务优先级宏 tskIDLE_PRIORITY 为 0,说明空闲任务优先级最低,用户不能随意修改空闲任务的优先级!
void vTaskStartScheduler( void )
{
BaseType_t xReturn; // 用于存储xTaskCreate或xTaskCreateStatic的返回值
// 创建空闲任务,使用最低优先级
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) // (1) 如果配置支持静态内存分配
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL; // 指向TCB(任务控制块)的静态缓冲区
StackType_t *pxIdleTaskStackBuffer = NULL; // 指向任务堆栈的静态缓冲区
uint32_t ulIdleTaskStackSize; // 空闲任务堆栈大小
// 调用应用提供的函数来获取空闲任务的内存
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
// 使用静态内存创建空闲任务
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, // 空闲任务函数
"IDLE", // 任务名称
ulIdleTaskStackSize, // 堆栈大小
( void * ) NULL, // 任务参数
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), // 优先级和特权位
pxIdleTaskStackBuffer, // 堆栈缓冲区
pxIdleTaskTCBBuffer ); // TCB缓冲区
if( xIdleTaskHandle != NULL ) // 如果空闲任务创建成功
{
xReturn = pdPASS; // 设置返回值为成功
}
else
{
xReturn = pdFAIL; // 设置返回值为失败
}
}
#else // (2) 如果配置不支持静态内存分配
{
// 使用动态内存创建空闲任务
xReturn = xTaskCreate( prvIdleTask, // 空闲任务函数
"IDLE", // 任务名称
configMINIMAL_STACK_SIZE, // 堆栈大小
( void * ) NULL, // 任务参数
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), // 优先级和特权位
&xIdleTaskHandle ); // 指向任务句柄的指针
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
// ...(省略其他代码)
}
19.1.3 空闲任务函数
空闲任务的任务函数为 prvIdleTask(),它是通过宏定义实现的。
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
portTASK_FUNCTION()就是空闲任务的任务函数。
简述为:
1、空闲任务执行prvIdleTask,检查并释放已删除任务的内存。
2、根据宏配置,空闲任务可能让出CPU给同优先级就绪任务。
3、检查空闲优先级就绪列表,非空则尝试任务切换。
4、执行空闲任务钩子函数。
5、若启用Tickless模式,计算进入低功耗模式的时长,然后挂起调度器并进入低功耗模式,最后恢复调度器。
19.2 空闲任务钩子函数详解
19.2.1 钩子函数
钩子函数类似回调函数,某个功能(函数)执行的时候就会调用钩子函数。命名以xxxHOOK。
空闲任务钩子函数、
时间片钩子函数、
内存申请失败钩子函数、
守护任务钩子函数。 //守护任务就是定时器服务任务
19.2.2 空闲任务钩子函数
每个空闲任务运行周期结束时都会调用空闲任务钩子函数。
绝对不能在空闲任务钩子函数中调用任何可以阻塞空闲任务的函数。
要使用空闲任务钩子函数首先要在 FreeRTOSConfig.h 中将宏 configUSE_IDLE_HOOK 改
为 1,然后编写空闲任务钩子函数 vApplicationIdleHook()。
通常在空闲任务钩子函数中将处理器设置为低功耗模式来节省电能,为了与 FreeRTOS 自带的 Tickless 模式做区分,暂且将这种低功耗的实现方法称之为通用低功耗模式。
Tickless 模式中只有空闲任务要运行时间的超过某个最小阈值的时候才会进入低功耗模式。
如果使用通用低功耗模式的话每个滴答定时器中断都会将处理器从低功耗模式中唤醒,因为不具有tickless那样抑制滴答定时器中断和系统节拍补偿的能力。
第二十章 FreeRTOS 内存管理
20.1 FreeRTOS 内存管理简介
当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存,不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。函数 pvPortMalloc()、vPortFree()与函数 malloc()、free()的函数原型类似。
FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这 5 个文件在 FreeRTOS 源码中。
20.2 内存碎片
内存碎片是伴随着内存申请和释放而来的。
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃。
FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。
20.3 heap_1 内存分配方法
20.3.1 分配方法简介
动态内存分配需要一个内存堆,FreeRTOS 中的内存堆为 ucHeap[ ],大小为configTOTAL_HEAP_SIZE。
不管是哪种内存分配方法,它们的内存堆都为 ucHeap[ ],而且大小都是 configTOTAL_HEAP_SIZE。内存堆在文件 heap_x.c(x 为 1~5)中定义的,比如 heap_1.c 文件就有如下定义:
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif
当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。
heap_1 实现起来就是当需要 RAM 的时候就从内存堆数组中分一小块出来。而且分配了一般情况下不会删除,因此不会产生内存碎片。
数组(内存堆)的容量为 configTOTAL_HEAP_SIZE。
heap_1 特性简述如下:
适用于静态任务、信号量和队列分配的应用。
内存分配时间一致且不会产生碎片。
实现简单,从静态数组分配内存,适合无需动态内存分配的场景。
20.3.2 内存申请函数详解
heap_1 的内存申请函数 pvPortMalloc()源码逻辑:
确保申请的内存大小是8字节对齐的,未对齐则调整。
在内存分配前挂起任务调度器,防止中断。
确保内存堆ucHeap的起始地址也满足8字节对齐要求,必要时需调整。
检查可用内存是否够分配。
申请内存。
恢复任务调度器。
如果内存申请失败则调用钩子函数。
20.3.3 内存释放函数详解
heap_1 的内存释放函数为 pvFree()。
void vPortFree( void *pv )
{
( void ) pv;
configASSERT( pv == NULL );
}
可以看出 vPortFree()并没有具体释放内存的过程。因此如果使用 heap_1,一旦申请内存成功就不允许释放。
20.4 heap_2 内存分配方法
20.4.1 分配方法简介
heap_2支持内存释放但因为不合并释放的内存块,易产生碎片。
heap_2适用于可重复删除对象的场景,但随机大小内存分配易致碎片。
若任务堆栈或队列区域大小固定,heap_2适用;
否则,若大小变化,易生碎片,此时heap_4更佳。
同时需直接调用pvPortMalloc()和vPortFree()来申请和释放内存,而不是通过OS 的其他 API 函数来间接调用。
20.4.2 内存块详解
同 heap_1 一样,heap_2 整个内存堆为 ucHeap[ ],大小为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。
为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定。
为了管理内存块又引入了一个单向的空闲块链表结构,链表结构如下:
/*内存块链表结构*/
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; //指向链表中下一个空闲内存块
size_t xBlockSize; //当前空闲内存块大小
} BlockLink_t;
每个内存块前面都会有一个 BlockLink_t 结构体类型的变量来描述此内存块。
heapSTRUCT_SIZE 记录 BlockLink_t 结构体的大小。
比如我们现在申请了一个 16 个字节的内存块,那么此内存块结构就如图:
图中内存块的总大小是 24 字节,虽然我们只申请了 16 个字节,但是还需要另外 8 字节来保存 BlockLink_t 类型的结构体变量。
pxNextFreeBlock指向的是下一个可用内存块,xBlockSize 记录的是此内存块的大小。
可用的内存块被全部组织在一个链表内,
局部静态变量 xStart, xEnd 用来记录这个链表的头和尾。
需要注意的是, xStart 和 xEnd 并不是内存堆中的内存块,因此 xStart 和 xEnd 内存块并不包含可分配的内存。xStart 的 xBlockSize=0,xEnd 的 xBlockSize = 内存堆总大小。
static BlockLink_t xStart, xEnd;//记录可用内存块链表的头、尾
20.4.3 内存堆初始化函数详解
内存堆初始化函数为 prvHeapInit()。
(1)、确保内存堆的可用起始地址为 8 字节对齐。
(2)、初始化 链表头 xStart 变量。
(3)、初始化 链表尾 xEnd 变量。
(4)、每个内存块前面保存一个 BlockLink_t 类型的结构体变量,用来描述内存块的大小和下一个空闲内存块的地址。
堆的大小通常由 configTOTAL_HEAP_SIZE 宏定义指定,但是为了保证内存访问的效率和安全性,往往需要对内存进行字节对齐处理。
configADJUSTED_HEAP_SIZE 就是考虑了字节对齐之后,堆内存实际可用的大小。
20.4.4 内存块插入函数详解
heap_2允许内存释放,释放的内存要添加到空闲内存链表中,宏
prvInsertBlockIntoFreeList()用来完成内存块的插入操作。
插入的逻辑就是从空闲链表头的xStart开始,遍历空闲链表结点,找到空闲块>=要存储的块,然后链表插入。
#define prvInsertBlockIntoFreeList( pxBlockToInsert )
20.4.5 内存申请函数详解
pvPortMalloc()用来申请堆内存。
/*heap_2内存申请函数*/
void *pvPortMalloc(size_t xWantedSize) //申请的内存大小
挂起任务调度器。 vTaskSuspendAll()
第一次申请内存要对堆初始化。 prvHeapInit()
申请的内存大小需要加上BlockLink_t结构体的大小,并字节对齐。
遍历空闲链表,找到大小合适内存块。
首地址对齐,空闲链表更新。
如果分配后剩余的内存块足够大,则将其分割并将多的部分重新插入空闲链表。
内存分配失败则调用钩子函数。 vApplicationMallocFailedHook()
恢复任务调度器。 xTaskResumeAll()
xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小。
申请内存返回的地址是跳过 BlockLink_t 结构体的。
20.4.6 内存释放函数详解
内存释放函数 vPortFree() 会让内存块重新插入空闲链表。
void vPortFree( void *pv )
传入的内存地址减去块结构体大小 定位内存块,验证后添加到空闲链表,更新空闲字节数。
释放就是用传入的内存地址减去块结构体大小,来定位内存块,验证后添加到空闲链表,更新空闲字节数。因为申请内存的时候返回的是块中的可用内存地址,而块由块结构体和可用内存共同组成。
xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小。
20.5 heap_3 内存分配方法
pvPortMalloc 和 vPortFree分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 封装时通过关闭调度器对这两个函数做了线程保护。
需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数。
在 heap_3 中 configTOTAL_HEAP_SIZE 是没用的。使用 STM32的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的大小。
20.6 heap_4 内存分配方法
20.6.1 分配方法简介
在 heap_2 使用内存块和空闲链表的基础上,heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。
内存堆为 ucHeap[ ],大小同样为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。
heap_4 也使用单向空闲链表来管理空闲内存块,链表结构体与 heap_2 一样,但最大块大小稍有区别,而且空闲块链表是按照低地址到高地址组织的。
空闲链表节点 = 块结构体+块可用空间 = 块指针 + 块大小 + 块可用空间
块大小 xBlockSize 是size_t类型,32位,最高位用来标记块是否可用
heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾。
xStart 和 pxEnd 并不是内存堆中的内存块,因此不包含可分配的内存。
xStart 的 xBlockSize=0,xEnd 的 xBlockSize = 内存堆总大小。
20.6.2 内存堆初始化函数详解
内存初始化函数 prvHeapInit()。
static void prvHeapInit( void )
初始化内存堆,包括
对堆的起始地址对齐、
计算可用堆的总大小、
初始化 xStart,xStart 为可用内存块链表头、尾。
初始化 BlockLink_t描述块,
跟踪最小空闲块和总剩余大小,
并设置块分配标记位限制最大块大小。
pucAlignedHeap 为内存堆字节对齐以后的可用起始地址。
xMinimumEverFreeBytesRemaining 记录最小的空闲内存块大小。
初始化静态变量 xBlockAllocatedBit,初始化完成以后此变量值为 0X80000000,此变量是size_t类型的,其实就是将size_t类型变量的最高位置1,对于32位MCU来说就是0X80000000。
此变量会用来标记某个内存块是被使用,BlockLink_t 中的成员变量 xBlockSize 是用来描述内存块大小的,在 heap_4 中其最高位表示此内存块是否被使用,如果为 1 的话就表示被使用了,所以在 heap_4 中一个内存块最大只能为 0x7FFFFFFF。
20.6.3 内存块插入函数详解
内存块插入函数 prvInsertBlockIntoFreeList()用来将内存块插入到空闲内存块链表中。
/*将内存块插入到空闲列表*/
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )//要插入的内存块描述结构体
heap_4内存管理方案在把空闲块插入空闲链表的时候,会检查要插入的空闲块地址是否与插入点连续,地址连续的话就代表内存块可以合并,合并完以后等于内存块进行了扩张,不能合并就链接起来。
20.6.4 内存申请函数详解
pvPortMalloc()用来申请空闲内存。
void *pvPortMalloc( size_t xWantedSize )
堆未初始化时(pxEnd为NULL),调用prvHeapInit()初始化。
xBlockSize记录内存块大小,最高位标记使用状态。申请空间字节对齐。
从链表头开始查找足够大的空闲内存块,确保非链表尾。
保存找到的内存块首地址于pvReturn,并从空闲链表移除该块。
若申请后有多余空间,则分割并插入新空闲块至链表。
更新全局空闲字节统计变量。
将找到的内存块标记为已使用(xBlockSize最高位置1)。
20.6.5 内存释放函数详解
vPortFree()用来释放空闲内存。
void vPortFree( void *pv )
获取内存块的BlockLink_t 块描述结构体。
检查xBlockSize 块大小最高位判断内存块是否已被使用,确保只释放已使用块。
清除xBlockSize最高位,重新标记为空闲,并调整大小(忽略最高位)。
将内存块插入空闲链表。
注意:申请内存时确保最终大小不超过0x7FFFFFFF,以避免最高位冲突。
20.7 heap_5 内存分配方法
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。
使用 heap_4 的话你就只能在内部 RAM 、外部 SRAM 、 SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。
如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions()来对内存堆做初始化处理,未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数。
函数 vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体。
typedef struct HeapRegion
{
uint8_t *pucStartAddress; //内存块的起始地址
size_t xSizeInBytes; //内存段大小
} HeapRegion_t;
heap_5 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。
注意,数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。
在 heap_2 和 heap_4 中只有一个内存堆,初始化的时候只也只需要处理一个内存堆。 heap_5 有多个内存堆,这些内存堆通过数组管理。