FreeRTOS系统

本文详细介绍了FreeRTOS实时操作系统的核心组件、文件结构、移植步骤,包括内核、FreeRTOSConfig.h配置、中断配置、任务创建与调度等内容,为读者提供了一个清晰的FreeRTOS学习和应用指南。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录


泉水

1 介绍

RTOS Real Time operation system 实时操作系统。

1.1 官网

FreeRTOS网址
很庆幸,已经是个中文网址了。
但是相关文档依旧是英文的。文档

1.2 内核

在学习任务切换时可能需要了解Cortex-M内核相关知识。
参考书目英文名字:
在这里插入图片描述
中文翻译版:
在这里插入图片描述

1.3 下载安装

下载链接
在这里插入图片描述

官网下载。
下载FreeRTOSv9.0.0.exe
在FreeRTOSv9.0.0文件夹中有如下:
在这里插入图片描述

2 文件夹介绍

RreeRTOS和FreeRTOS-Plus就是FreeRTOS源码。
Plus就是比前者功能多一点,配置强大一点。

2.1 FreeRTOS文件夹

在这里插入图片描述

2.1.1 Demo文件夹

在这里插入图片描述FreeRTOS针对不同的MCU提供了不同的Demo。我们重点关注F407。

2.1.2 License文件夹

该文件夹是相关许可信息。

2.1.3 Source文件夹

FreeRTOS本尊。
在这里插入图片描述其中include文件夹是头文件,移植时需要。
下面.c就是FreeRTOS就是源文件,移植时也需要。
protable这个文件夹里面就是FreeRTOS系统和具体硬件之间的连接桥梁。
在这里插入图片描述MemMang文件夹里面内存管理相关的,移植是非常必要的。
Keil文件夹里面的东西也是必须的,打开后有一个See-also-the_RVDS_directory.txt。参考该文件夹的意思。
打开RVDS文件夹
在这里插入图片描述该文件夹针对不同的架构MCU做了分类,407参考ARM_CM4F。
打开ARM_CM4F:
在这里插入图片描述移植时需要。

2.2 FreeRTOS-Plus文件夹

在这里插入图片描述Demo里面就是一些例程。
Source如下
在这里插入图片描述可以看到plus的源码是在FreeRTOS系统上另外增加的一些功能代码。如果只是学习FreeRTOS这个系统的话,Plus就没必要看了。

3 移植

3.1 文件移植

跑马灯工程作为基础

  1. 添加一个文件夹名FreeRTOS
    在这里插入图片描述将FreeRTOS的Source文件夹放到这里
    在这里插入图片描述打开portable文件夹,只留下keil MemMang和RVDS三个文件夹
    在这里插入图片描述
  2. 打开KEIL,新建分组FreeRTOS_Core和FreeRTOS_PORTABLE。
    添加文件如下:
    在这里插入图片描述其中port.c是RVDS文件夹下ARM_CM4F中文件。
    heap_4.c是MemMang文件夹中(内存管理相关),里面有heap1-5,都是不同的内存管理办法。只是实现路径不同。我们选择4即可。
  3. 添加头文件
    在这里插入图片描述
  4. 添加FreeRTOSConfig.h
    该文件可以自己创建,但不建议。
    我们在官方给407的Demo里面可以找到。文件夹是CORTEX_M4F_STM32F407ZG-SK,如下:
    在这里插入图片描述拿过来随便放到哪里就好
    该文件,FreeRTOSConfig.h可了不得,是核心配置文件,一般的操作系统功能都是通过在该文件宏定义完成配置的。
  5. 修改FreeRTOSConfig.h文件
    我们还需要修改SystemCoreClock,位置如下:
    在这里插入图片描述这里有一个条件编译,只有定义了_ICCARM_后才能生效。
    我们将#if defined(ICCARM)改成#if defined(ICCARM)||defined(_CC_ARM)||defined(GNUC)
  6. 消除重复定义函数
    port.c和stm32f4xx_it.c重复定义了PenSV_Handler()、SVC_Handler()和Systick_Handler(),这里把it.c中的三个函数屏蔽掉就可以。
  7. 关闭钩子函数
    将FreeRTOSConfig.h中的configUSE_IDLE_HOOK、configUSE_TICK_HOOK、configUSE_MALLOC_FAILED_HOOK、configCHECK_FOR_STACK_OVERFLOW定义为0即可。

3.2 文件修改

3.2.1 sys.h

将# define SYSTM_SUPPORT_OS 打开为1

3.2.2 delay.c

涉及到FreeRTOS的系统时钟。该文件有四个函数

3.2.2.1 SysTick_Handler

滴答定时器的中断服务函数

extern void xPortSysTickHandler(void);
//systick中断服务函数,使用OS时用到
void SysTick_Handler(void)
{	
	if(xTaskGetSchedulerState()!=taskSCHEDULE_NOT_STARTED)					//OS开始跑了,才执行正常的调度处理
	{
    xPortSysTickHandler();
	}
}

根据系统节拍设置好滴答定时器的周期,就可以周期性触发滴答定时器中断。
在中断中调用xPortSysTickHandler(),该函数用于任务切换:

但此时port.c和delay.c中重复定义了SysTick_Handler()。
如果要保留delay.c中的中断就打开FreeRTOSConfig.h,将#define xPortSysTickHandler SysTick_Handler屏蔽。

3.2.2.4 xPortSysTickHandler
void xPortSysTickHandler( void )
{
    //执行SysTick_Handler中断函数时,为了保证在freertos中属于最低优先级的此中断能顺利执行,
    //故要关闭FreeRTOS的所有可管理中断,保证系统计数时不被打断。
    vPortRaiseBASEPRI();  //关中断
    {
        if( xTaskIncrementTick() != pdFALSE )  //判断返回值,如果为pdTURE就要进行一次上下文切换
        {
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();  //开中断
}
  1. 关闭中断
  2. 通过向中断控制和状态寄存器ICSR的bit28写入1挂起PendSV来启动PendSV中断。PendSV中断可用来进行任务切换。
  3. 打开中断
3.2.2.2 delay_init

FreeRTOS的心跳就是滴答定时器产生的。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,在 168M 下,约合 0.0998s 左右
    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 的系统时钟节拍由宏 configTICK_RATE_HZ 来设置,这个值我们可以自由设置,但是一旦设置好以后我们就要根据这个值来初始化滴答定时器,其实就是设置滴答定时器的中断周期。
在基础例程中滴答定时器的时钟频率设置的是 AHB 的 1/8,这里为了兼容 FreeRTOS 将滴答定时器的时钟频率改为了 AHB,也就是 168MHz!这一点一定要注意!

  • 滴答每1/SystemCoreClock加1,reload*1/SystemCoreClock = 1/configTICK_RATE_HZ。也就是每 1/configTICK_RATE_HZ 秒中断一次。1/1000秒也就是1ms中断一次。
  • fac_ms=1000/configTICK_RATE_HZ; 代表的是几个中断是1ms。
3.2.2.3 延时函数

delay_us()是 us 级延时函数, delay_ms 和 delay_xms()都是 ms 级的延时函数, delay_us()和delay_xms()不会导致任务切换。
delay_ms()其实就是对 FreeRTOS 中的延时函数 vTaskDelay()的简单封装,所以在使用 delay_ms()的时候就会导致任务切换。

//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,会引起任务调度
//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,不会引起任务调度
//nms:要延时的 ms 数

 - [ ] List item

void delay_xms(u32 nms)
{
    u32 i;
    for(i=0;i<nms;i++) delay_us(1000);
}

3.2.3 usart.c

  1. 添加RTOS头文件
#include "FreeRTOS.h" //os 使用
  1. 中断服务函数

在使用 UCOS 的时候进出中断的时候需要添加OSIntEnter()和 OSIntExit(),使用 FreeRTOS 的话就不需要了。删除即可。

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	u8 Res;
#if 0//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了0x0d
			{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
			}
			else //还没收到0X0D
			{	
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收	  
				}		 
			}
		}   		 
  } 
#if 0//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntExit();  											 
#endif
} 

3.3 创建任务

start_task():用来创建其他三个任务。
led0_task ():控制 LED0 的闪烁,提示系统正在运行。
led1_task ():控制 LED1 的闪烁。
float_task():简单的浮点测试任务,用于测试 STM32F4 的 FPU 是否工作正常。

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define LED0_TASK_PRIO 2 //任务优先级
#define LED0_STK_SIZE 50 //任务堆栈大小
TaskHandle_t LED0Task_Handler; //任务句柄
void led0_task(void *p_arg); //任务函数
#define LED1_TASK_PRIO 3 //任务优先级
#define LED1_STK_SIZE 50 //任务堆栈大小
TaskHandle_t LED1Task_Handler; //任务句柄
void led1_task(void *p_arg); //任务函数
//任务优先级
#define FLOAT_TASK_PRIO		4
//任务堆栈大小	
#define FLOAT_STK_SIZE 		128
//任务句柄
TaskHandle_t FLOATTask_Handler;
//任务函数
void float_task(void *pvParameters);
int main(void)
{ 
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
	delay_init(168);		//初始化延时函数
	uart_init(115200);     	//初始化串口
	LED_Init();		        //初始化LED端口
	
	//创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度
}
 
//开始任务任务函数,用来创建其他三个任务
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           //进入临界区
    //创建LED0任务
    xTaskCreate((TaskFunction_t )led0_task,     	
                (const char*    )"led0_task",   	
                (uint16_t       )LED0_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )LED0_TASK_PRIO,	
                (TaskHandle_t*  )&LED0Task_Handler);   
    //创建LED1任务
    xTaskCreate((TaskFunction_t )led1_task,     
                (const char*    )"led1_task",   
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )LED1_TASK_PRIO,
                (TaskHandle_t*  )&LED1Task_Handler);        
    //浮点测试任务
    xTaskCreate((TaskFunction_t )float_task,     
                (const char*    )"float_task",   
                (uint16_t       )FLOAT_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )FLOAT_TASK_PRIO,
                (TaskHandle_t*  )&FLOATTask_Handler);  
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区
}
 
//LED0任务函数,控制LED0闪烁,提示系统正在运行
void led0_task(void *pvParameters)
{
    while(1)
    {
        LED0=~LED0;
        vTaskDelay(500);
    }
}   
 
//LED1任务函数,控制LED1的闪烁
void led1_task(void *pvParameters)
{
    while(1)
    {
        LED1=0;
        vTaskDelay(200);
        LED1=1;
        vTaskDelay(800);
    }
}
 
//浮点测试任务,用于测试STM32F4的FPU是否工作正常
void float_task(void *pvParameters)
{
	static float float_num=0.00;
	while(1)
	{
		float_num+=0.01f;
		printf("float_num的值为: %.4f\r\n",float_num);
        vTaskDelay(1000);
	}
}

证明支持FPU浮点有两方面
1.float_num在不断增加
2.使用了浮点寄存器和浮点指令
打开汇编窗口 view disassembly
在这里插入图片描述设置断点
在这里插入图片描述使用了s16和s0的浮点寄存器。VLDR、VADD.F32是浮点指令,说明了系统支持FPU。

4 配置FreeRTOSConfig.h

4.1 INCLUDE宏

很多的协议栈、RTOS 系统和 GUI 库等都是使用条件编译的方法来完成配置和裁剪的。

  • 当宏 INCLUDE_vTaskPrioritySet 设置为 0 的时 候 表 示 不 能 使 用 函 数 vTaskPrioritySet() 。
  • 当 设 置 为 1 的 时 候 就 表 示 可 以 使 用 函 数vTaskPrioritySet()。

在这里插入图片描述
条件编译的好处就是节省空间。

4.1.3 INCLUDE_vTaskDelay

vTaskDelay()的开关

4.1.4 INCLUDE_vTaskDelete

vTaskDelete()的开关

4.2 config宏

4.2.1 configAPPLICATION_ALLOCATED_HEAP

默 认 情 况 下 FreeRTOS 的 堆 内 存 是 由 编 译 器 来 分 配 的 。
宏configAPPLICATION_ALLOCATED_HEAP 定义为 1 的话堆内存可以由用户自行设置。
堆内存在 heap_1.c、heap_2.c、heap_3.c、heap_4.c 和 heap_5.c 中都有定义。

  • 如果我们的例程选择了 heap_4.c,那么在 heap_4.c 中就有:
    在这里插入图片描述出当宏 configAPPLICATION_ALLOCATED_HEAP 定义为 1 的话需要用户自行堆内存 ucHeap,否则的话就是编译器来分配。

4.2.2 configASSERT断言

类似 C 标准库中的 assert()函数,调试代码的时候可以检查传入的参数是否合理。

  • 使用断言的话会导致开销加大,一般在调试阶段使用。
  • 调用 configASSERT(x),当 x 为 0 的时候说明有错误发生

configASSERT()需要在 FreeRTOSConfig.h 中定义:

#define configASSERT((x)) if((x)==0) vAssertCalled(__FILE_,__LINE__)

vAssertCalled()函数需要用户自行去定义,可以是显示到 LCD 上的函数,也可以是通过串口打印出来的函数

#define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)

当调试完成以后尽量去掉断言,防止增加开销!

4.2.3 configCHECK_FOR_STACK_OVERFLOW堆栈溢出

设置堆栈溢出检测。
每个任务都有一个任务堆栈。

  • 使用函数 xTaskCreate()创建一个任务,堆栈是自动从 FreeRTOS 的堆(ucHeap)中分配的。
    • 堆栈的大小是由函数xTaskCreate()的参数 usStackDepth 来决定
  • 使用函数 xTaskCreateStatic()创建任务的话任务堆栈是由用户设置的。
    • 参数 pxStackBuffer 为任务堆栈,一般是一个数组

堆栈溢出是导致应用程序不稳定的主要因素,FreeRTOS 提供了两种可选的机制来帮助检测和调试堆栈溢出。

  • 使能了堆栈检测功能的话用户必须提供一个钩子函数(回调函数),当内核检测到堆栈溢出以后就会调用这个钩子函数。

钩子函数原型:

void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );

xTask 是任务句柄,pcTaskName 是任务名字。

注意:堆栈溢出太严重的话可能会损毁这两个参数,如果发生这种情况的话可以直接查看变量 pxCurrentTCB 来确定哪个任务发生了堆栈溢出。

  • 有些处理器可能在堆栈溢出的时候生成一个 fault 中断来提示这种错误
  • 堆栈溢出检测会增加上下文切换的开销,建议在调试的时候使用。

上下文切换的时候需要保存现场,现场是保存在堆栈中。这个时候任务堆栈使用率很可能达到最大值。

  • 一:不断的检测任务堆栈指针是否指向有效空间,如果指向了无效空间的话就会调用钩子函数。优点就是快!但是缺点就是不能检测所有的堆栈溢出。

    • configCHECK_FOR_STACK_OVERFLOW==1
  • 二: 创建任务的时候向任务堆栈填充一个已知的标记值,一直检测堆栈后面的几个 bytes(标记值)是否被改写,如果被改写的话就会调用堆栈溢出钩子函数。

    • 这种方法能检测到几乎所有的堆栈溢出,但是也有一些情况检测不到,比如溢出值和标记值相同的时候。
    • 方法二也会使用方法一中的机制,但方法二比方法一要慢一些。
    • configCHECK_FOR_STACK_OVERFLOW==2

4.2.4 configCPU_CLOCK_HZ

CPU的频率

4.2.5 configSUPPORT_DYNAMIC_ALLOCATION

定义为 1 的话在创建 FreeRTOS 的内核对象的时候所需要的 RAM 就会从 FreeRTOS 的堆中动态的获取内存。

  • 默认情况下宏configSUPPORT_DYNAMIC_ALLOCATION 为 1

定义为 0 的话所需的 RAM 就需要用户自行提供。

4.2.6 configENABLE_BACKWARD_COMPATIBILITY

开启了一堆宏
在这里插入图片描述在 V8.0.0 之前的 FreeRTOS 中会使用到这些数据类型。这些宏保证了你的代码从 V8.0.0 之前 的 版 本 升 级 到 最 新 版 本 的 时 候 不 需 要 做 出 修 改。
默 认 情 况 下 宏configENABLE_BACKWARD_COMPATIBILITY 为 1。

4.2.7 configGENERATE_RUN_TIME_STATS

设置为 1 开启时间统计功能。为 0 时关闭时间统计功能。
宏 configGENERATE_RUN_TIME_STATS 为 1 的话还需要定义:
在这里插入图片描述

4.2.8 configIDLE_SHOULD_YIELD

定义了空闲任务处于同等优先级的其它用户任务的行为。
当0时空闲任务不会为其它同优先级任务让出CPU使用权。
当1时空闲任务就会让出CPU使用权。
一般建议关闭,毕竟空闲任务用不了多少时间。

4.2.9 configMAX_CO_ROUTINE_PRIORITIES

设置可以分配给协程的最大优先级,也就是协程的优先级数。
设好以后协程的优先级可以 从 0 到 configMAX_CO_ROUTINE_PRIORITIES-1

  • 0 是 最 低 的 优 先 级
  • configMAX_CO_ROUTINE_PRIORITIES-1 为最高的优先级。

4.2.10 configMAX_PRIORITIES

设置任务的优先级数量。
置好以后任务就可以使用从 0 到 configMAX_PRIORITIES-1 的优先级。

  • 0 是最低优先级
  • configMAX_PRIORITIES-1 是最高优先级

4.2.11 configMAX_TASK_NAME_LEN

设置任务名的最大长度

4.2.12 configMINIMAL_STACK_SIZE

空闲任务的最小任务堆栈大小,以字为单位。如果在32上设置为100的话,意味着堆栈大小为100*4=400字节。

4.2.13 configQUEUE_REGISTRY_SIZE

设置可以注册的队列和信号量的最大数量。
使用内核调试器查看信号量和队列的时候需要设置此宏。
要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会再内核调试器中看到。

4.2.15 configSUPPORT_STATIC_ALLOCATION

宏定义为1,在创建一些内核对象的时候需要用户指定RAM
当定为0时,自动使用heap.c中的动态内存管理函数来自动申请RAM。

4.2.16 configTICK_RATE_HZ

设置FreeRTOS的系统时钟节拍频率,单位HZ,滴答定时器的中断频率,1000意味着1ms。

4.2.17 configTIMER_QUEUE_LENGTH

配置 FreeRTOS 软件定时器的。
FreeRTOS 的软件定时器 API 函数会通过命令队列向软件定时器任务发送消息,此宏用来设置这个软件定时器的命令队列长度。

4.2.18 configTIMER_TASK_PRIORITY

设置软件定时器任务的任务优先级。

4.2.19 configTIMER_TASK_STACK_DEPTH

设置定时器服务任务的任务堆栈大小。

4.2.20 configTOTAL_HEAP_SIZE

设置堆大小。
如果使用了动态内存管理的话,FreeRTOS 在创建任务、信号量、队列等的时候 就 会 使 用 heap_x.c(x 为 1~5) 中 的 内 存 申 请 函 数 来 申 请 内 存 。
这 些 内 存 就 是 从 堆ucHeap[configTOTAL_HEAP_SIZE]中申请的,堆的大小由 configTOTAL_HEAP_SIZE 来定义。

4.2.21 configUSE_16_BIT_TICKS

设 置 系 统 节 拍 计 数 器 变 量 数 据 类 型。

  • 当configUSE_16_BIT_TICKS 为 1 的时候 TickType_t 就是 16 位的
  • 当 configUSE_16_BIT_TICKS为 0 的话 TickType_t 就是 32 位的。

系 统 节 拍 计 数 器 变 量 类 型 为 TickType_t 。

4.2.22 configUSE_APPLICATION_TASK_TAG

编译函 数 configUSE_APPLICATION_TASK_TAGF() 和xTaskCallApplicationTaskHook()。

4.2.23 configUSE_CO_ROUTINES

宏为 1 的时候启用协程,协程可以节省开销,但是功能有限。

  • 现在的 MCU 性能已经非常强大了,建议关闭协程。

4.2.24 configUSE_COUNTING_SEMAPHORES

设置为 1 的时候启用计数型信号量

4.2.25 configUSE_DAEMON_TASK_STARTUP_HOOK

宏 configUSE_TIMERS 和 configUSE_DAEMON_TASK_STARTUP_HOOK 都为 1 的时需要定义函数 vApplicationDaemonTaskStartupHook():

4.2.26 configUSE_IDLE_HOOK

为 1 时使用空闲任务钩子函数,用户需要实现空闲任务钩子函数vApplicationIdleHook。

4.2.27 configUSE_MALLOC_FAILED_HOOK

为 1 时使用内存分配失败钩子函数,用户需要实现内存分配失败钩子函数vApplicationMallocFailedHook。

4.2.28 configUSE_MUTEXES

为 1 时使用互斥信号量

4.2.29 configUSE_PORT_OPTIMISED_TASK_SELECTION

FreeRTOS 有两种方法来选择下一个要运行的任务,一个是通用的方法,一个是是硬件方法。
通用方法:

  • 宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为 0。
  • 不限制最大优先级数目的时候。

硬件方法:

  • 宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 为 1
  • 硬件拥有特殊的指令,比如计算前导零(CLZ)指令。
  • 比通用方法效率高。
  • 会限制优先级数目,一般是 32 个。

4.2.30 configUSE_PREEMPTION

为 1 时使用抢占式调度器,为 0 时使用协程。
使用抢占式调度器的话内核会在每个时钟节拍中断中进行任务切换
当使用协程的话会在如下地方进行任务切换

  • 一个任务调用了函数 taskYIELD()。
  • 一个任务调用了可以使任务进入阻塞态的 API 函数。
  • 应用程序明确定义了在中断中执行上下文切换。

4.2.31 configUSE_QUEUE_SETS

为 1 时启用队列集功能。

4.2.32 configUSE_RECURSIVE_MUTEXES

为 1 时使用递归互斥信号量

4.2.33 configUSE_STATS_FORMATTING_FUNCTIONS

宏 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 都为1 的时候函数 vTaskList()和 vTaskGetRunTimeStats()会被编译。

4.2.34 configUSE_TASK_NOTIFICATIONS

为 1 的时候使用任务通知功能.
开启了此功能的话每个任务会多消耗 8 个字节。

4.2.35 configUSE_TICK_HOOK

为 1 时使能时间片钩子函数

4.2.36 configUSE_TICKLESS_IDLE

为 1 时使能低功耗 tickless 模式。
为 1 的话,那 么 宏 configTIMER_TASK_PRIORITY 、 configTIMER_QUEUE_LENGTH 和configTIMER_TASK_STACK_DEPTH 必须定义。

4.2.37 configUSE_TIMERS

为 1 时使用软件定时器。

4.2.38 configUSE_TIME_SLICING

默认情况下configUSE_TIME_SLICING 为 1,FreeRTOS 使用抢占式调度器。此时,调度器永远都在执行已经就绪了的最高优先级任务,优先级相同的任务在时钟节拍中断中进行切换。
configUSE_TIME_SLICING为 0 的 时 候 不 会 在 时 钟 节 拍 中 断 中 执 行 相 同 优 先 级 任 务 的 任 务 切 换。

4.2.39 onfigUSE_TRACE_FACILITY

为 1 启用可视化跟踪调试,会增加一些结构体成员和 API 函数。

5 FreeRTOS中断配置和临界段

5.1 Cortex_M中断

5.1.1 中断

中断由硬件产生,当中断产生以后 CPU 就会中断当前的流程转而去处理中断服务。

Cortex-M 内核的 MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。

Cotex-M3 和 M4 的 NVIC 支持:

  • 240 个 IRQ(中断请求)
  • 1 个不可屏蔽中断(NMI)
  • 1 个Systick(滴答定时器)定时器中断
  • 多个系统异常。

5.1.2 中断管理

Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器。
CMSIS 将这些寄存器定义为结构体。
打开core_cm4.h,有两个结构体,NVIC_Type 和 SCB_Type:

NVIC_Type:

typedef struct
{
__IOM uint32_t ISER[8U]; /*!< Offset: 0x000 (R/W) Interrupt Set Enable Register */
uint32_t RESERVED0[24U];
__IOM uint32_t ICER[8U]; /*!< Offset: 0x080 (R/W) Interrupt Clear Enable Register */
uint32_t RSERVED1[24U];
__IOM uint32_t ISPR[8U]; /*!< Offset: 0x100 (R/W) Interrupt Set Pending Register */
uint32_t RESERVED2[24U];
__IOM uint32_t ICPR[8U]; /*!< Offset: 0x180 (R/W) Interrupt Clear Pending Register */
uint32_t RESERVED3[24U];
__IOM uint32_t IABR[8U]; /*!< Offset: 0x200 (R/W) Interrupt Active bit Register */
uint32_t RESERVED4[56U];
__IOM uint8_t IP[240U]; /*!< Offset: 0x300 (R/W) Interrupt Priority Register (8Bit wide) */
uint32_t RESERVED5[644U];
__OM uint32_t STIR; /*!< Offset: 0xE00 ( /W) Software Trigger Interrupt Register */
} NVIC_Type

SCB_Type:

typedef struct
{
__IM uint32_t CPUID; /*!< Offset: 0x000 (R/ ) CPUID Base Register */
__IOM uint32_t ICSR; /*!< Offset: 0x004 (R/W) Interrupt Control and State Register */
__IOM uint32_t VTOR; /*!< Offset: 0x008 (R/W) Vector Table Offset Register */
__IOM uint32_t AIRCR; /*!< Offset: 0x00C (R/W) Application Interrupt and Reset
Control Register */
__IOM uint32_t SCR; /*!< Offset: 0x010 (R/W) System Control Register */
__IOM uint32_t CCR; /*!< Offset: 0x014 (R/W) Configuration Control Register */
__IOM uint8_t SHP[12U]; /*!< Offset: 0x018 (R/W) System Handlers Priority
Registers (4-7, 8-11, 12-15) */
__IOM uint32_t SHCSR; /*!< Offset: 0x024 (R/W) System Handler Control and
State Register */
__IOM uint32_t CFSR; /*!< Offset: 0x028 (R/W) Configurable Fault Status Register */
__IOM uint32_t HFSR; /*!< Offset: 0x02C (R/W) HardFault Status Register */
__IOM uint32_t DFSR; /*!< Offset: 0x030 (R/W) Debug Fault Status Register */
__IOM uint32_t MMFAR; /*!< Offset: 0x034 (R/W) MemManage Fault Address Register */
__IOM uint32_t BFAR; /*!< Offset: 0x038 (R/W) BusFault Address Register */
__IOM uint32_t AFSR; /*!< Offset: 0x03C (R/W) Auxiliary Fault Status Register */
__IM uint32_t PFR[2U]; /*!< Offset: 0x040 (R/ ) Processor Feature Register */
__IM uint32_t DFR; /*!< Offset: 0x048 (R/ ) Debug Feature Register */
__IM uint32_t ADR; /*!< Offset: 0x04C (R/ ) Auxiliary Feature Register */
__IM uint32_t MMFR[4U]; /*!< Offset: 0x050 (R/ ) Memory Model Feature Register */
__IM uint32_t ISAR[5U]; /*!< Offset: 0x060 (R/ ) Instruction Set Attributes Register */
uint32_t RESERVED0[5U];
__IOM uint32_t CPACR; /*!< Offset: 0x088 (R/W) Coprocessor Access Control Register */
} SCB_Type

二者都位于系统控制空间(SCS)内,SCS 的地址从 0XE000E000 开始,SCB 和 NVIC的地址也在 core_cm4.h

#define SCS_BASE (0xE000E000UL) /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE + 0x0100UL) /*!< NVIC Base Address */
#define SCB_BASE (SCS_BASE + 0x0D00UL) /*!< System Control Block Base Address
#define SCnSCB ((SCnSCB_Type*) SCS_BASE) /*!< System control Register not in SCB */
#define SCB ((SCB_Type *) SCB_BASE) /*!< SCB configuration struct */
#define NVIC ((NVIC_Type *) NVIC_BASE ) /*!< NVIC configuration struct *//

5.1.3 优先级分组定义

高优先级的中断**(优先级编号小)**首先得到响应

  • 中断嵌套:高优先级的中断可以抢占低优先级的中断

固定优先级中断:中断的优先级都是负数,优先级也是最高

  • 复位、NMI、HardFault

Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级,最多有 128 个抢占等级。

实际的优先级数量是由芯片厂商来决定。

  • 不管用多少位来表达优先级,都是 MSB 对齐,因为一般裁掉表达优先级的几个低端有效位。

STM32 就只有 16 级优先级

  • 选择了高 4 位作为优先级
    在这里插入图片描述优先级配置寄存器,八位位宽。
5.1.3.1 应用程序中断及复位控制寄存器(AIRCR)

在这里插入图片描述
PRIGROUP 就是优先级分组,它把优先级分为两个位段:

  • MSB 所在的位段(左边的)对应抢占优先级
  • LSB 所在的位段(右边的)对应亚优先级

在这里插入图片描述STM32 使用了 4 位,因此最多有 5 组优先级分组设置:

#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
  • 分组 0 对应的值是 7,全都是子优先级
  • 分组 4,4 位优先级就都全是抢占优先级,没有亚优先级,那么就有 0~15 共 16 个优先级

移植 FreeRTOS 的时候我们配置的就是组 4
FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组 4
在这里插入图片描述

5.1.4 优先级寄存器

每个外部中断都有一个对应的优先级寄存器

  • 每个寄存器占 8 位
  • 4 个相临的优先级寄存器拼成一个 32 位寄存器

优先级寄存器都可以按字节或者半字节访问
中断优先级寄存器地址:
在这里插入图片描述异常优先级寄存器地址:
在这里插入图片描述4 个 相 临 的 寄 存 器 可 以 拼 成 一 个 32 位 的 寄 存 器

  • 地 址0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为 0xE000_ED20 的 32 位寄存器。
  • FreeRTOS 在设置 PendSV 和 SysTick 的中断优先级的时候都是直接操作的地址 0xE000_ED20。

5.1.5 中断屏蔽寄存器

5.1.5.1 PRIMASK 和 FAULTMASK 寄存器

PRIMASK
用于禁止除 NMI 和 HardFalut 外的所有异常和中断
汇编编程的时候可以使用 CPS(修改处理器状态)指令修改 PRIMASK 寄存器的数值

CPSIE I; //清除 PRIMASK(使能中断)
CPSID I; //设置 PRIMASK(禁止中断)

UCOS 中的临界区代码代码保护就是通过开关中断实现的,关中断就是直接操作 PRIMASK寄存器。

  • 在 UCOS 中关闭中断的时候时关闭了除复位、NMI 和 HardFault 以外的所有中断

FAULTMASK
可以连 HardFault 都屏蔽掉,使用方法和 PRIMASK 类似。

5.1.5.2 BASEPRI 寄存器

有些场合需要对中断屏蔽进行更细腻的控制,比如只屏蔽优先级低于某一个阈值的中断。
屏蔽优先级不高于 0X60 的中断:

MOV R0, #0X60
MSR BASEPRI, R0

向 BASEPRI 写 0 的话就会停止屏蔽中断
FreeRTOS 的开关中断就是操作 BASEPRI 寄存器来实现的!

5.2 FreeRTOS 中断配置宏

5.2.1 configPRIO_BITS

此宏用来设置 MCU 使用几位优先级

  • STM32 使用的是 4 位,因此此宏为 4

5.2.2 configLIBRARY_LOWEST_INTERRUPT_PRIORITY

此宏是用来设置最低优先级

  • STM32 优先级使用了 4 位,优先级数就是 16 个,最低优先级那就是 15。此宏就是 15。

5.2.3 configKERNEL_INTERRUPT_PRIORITY

此宏用来设置内核中断优先级

#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移 8-configPRIO_BITS 位

  • STM32 使用了 4 位作为优先级,而这 4 位是高 4 位,因 此 要 左 移 4 位 才 是 真 正 的 优 先 级
  • 也可以直 接 将 宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 定义为 0XF0

configKERNEL_INTERRUPT_PRIORITY 用来设置 PendSV 和滴答定时器的中断优先级
port.c 中有如下定义:

#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) <<16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) <<24UL )

PendSV 和 SysTcik 的中断优先级设置是操作 0xE000_ED20 地址,一次写入的是个 32 位的数据。
SysTick 和 PendSV 的优先级寄存器分别对应这个 32位数据的最高 8 位和次高 8 位,也就是一个左移 16 位,一个左移 24 位。

xPortStartScheduler函数

在文件 port.c 中,设置PendSV 和 SysTick 优先级

portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置 PendSV 中断优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置 SysTick 中断优先级

PendSV 和 SysTick 的中断优先级都是最低的
portNVIC_SYSPRI2_REG 是个宏,在文件 port.c 中由定义,如下:

#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )

5.2.4 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

用来设置 FreeRTOS 系统可管理的最大优先级。
如果设置为 5。也就是高于 5 的优先级(优先级数小于 5)不归 FreeRTOS 管理!

5.2.5 configMAX_SYSCALL_INTERRUPT_PRIORITY

FreeRTOS 系统可管理的最大优先级左移 4 位而来
低于此优先级的中断可以安全的调用 FreeRTOS 的 API 函数
高于此优先级的中断 FreeRTOS 是不能禁止的,中断服务函数不能调用 FreeRTOS 的 API 函数。
在这里插入图片描述高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的优先级不会被 FreeRTOS 内核屏蔽

  • 那些对实时性要求严格的任务就可以使用这些优先级

5.3 FreeRTOS 开关中断

在 portmacro.h 中有定义 portENABLE_INTERRUPTS ()和 portDISABLE_INTERRUPTS()函数。

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)

5.3.1 vPortSetBASEPRI()

向寄存器 BASEPRI 写入一个值,此值作为参数 ulBASEPRI 传递进来

portENABLE_INTERRUPTS() 传递了个 0 给 vPortSetBASEPRI(),根据 BASEPRI 寄存器可知,结果就是开中断。

5.3.2 vPortRaiseBASEPRI()

向 寄 存 器 BASEPRI 写 入 宏configMAX_SYSCALL_INTERRUPT_PRIORITY。
优 先 级 低 于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽。

5.4 临界段代码

叫做临界区,是指那些必须完整运行,不能被打断的代码段。

  • 比如有的外设的初始化需要严格的时序,初始化过程中不能被打断

FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。

相关 的 函 数 有 4 个:taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、 taskENTER_CRITICAL_FROM_ISR() 、askEXIT_CRITICAL_FROM_ISR()。在 task.h 文件中有定义。

  • 前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。

5.4.1 任务级临界段代码保护

两个函数是成对使用,一个是进入临界段,一个是退出临界段。

#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 中
vPortEnterCritical
函数 vPortEnterCritical()先关闭中断
给变量 uxCriticalNesting加一

  • uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}

vPortExitCritical()
退出临界段调用的,函数每次将 uxCriticalNesting 减一
当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。

保证有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护。

任务级临界代码保护使用方法:

void taskcritical_test(void)
{
	while(1)
	{
		taskENTER_CRITICAL(); (1)
		total_num+=0.01f;
		printf("total_num 的值为: %.4f\r\n",total_num);
		taskEXIT_CRITICAL(); (2)
		vTaskDelay(1000);
	}
}

中间的代码就是临界区代码

  • 临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应。

5.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 )

portSET_INTERRUPT_MASK_FROM_ISR() 和portCLEAR_INTERRUPT_MASK_FROM_ISR(),这两个在文件 portmacro.h 中有定义:

#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
  • vPortSetBASEPRI()前面已经讲解了,就是给 BASEPRI 寄存器中写入一个值

函数 ulPortRaiseBASEPRI()在文件 portmacro.h 中定义的:

static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
	__asm
	{
		mrs ulReturn, basepri (1)
		msr basepri, ulNewBASEPRI (2)
		dsb
		isb
	}
return ulReturn; (3)
}

将 configMAX_SYSCALL_INTERRUPT_PRIORITY 写入到寄存器 BASEPRI 中。

5.4.3 中断级临界代码保护使用方法

void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
	{
		status_value=taskENTER_CRITICAL_FROM_ISR(); (1)
		total_num+=1;
		printf("float_num 的值为: %d\r\n",total_num);
		taskEXIT_CRITICAL_FROM_ISR(status_value); (2)
	}
	TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}

进入临界区+退出临界区

5.3.5 中断测试实验

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4
	delay_init(168); //初始化延时函数
	uart_init(115200); //初始化串口
	LED_Init(); //初始化 LED 端口
	TIM3_Int_Init(10000-1,8400-1); //初始化定时器 3,定时器周期 1S
	TIM5_Int_Init(10000-1,8400-1); //初始化定时器 5,定时器周期 1S
	//创建开始任务
	xTaskCreate((TaskFunction_t )start_task, //任务函数
					(const char* )"start_task", //任务名称
					(uint16_t )START_STK_SIZE, //任务堆栈大小
					(void* )NULL, //传递给任务函数的参数
					(UBaseType_t )START_TASK_PRIO, //任务优先级
					(TaskHandle_t* )&StartTask_Handler); //任务句柄
	vTaskStartScheduler(); //开启任务调度
}

//中断测试任务函数
void interrupt_task(void *pvParameters)
{
static u32 total_num=0;
	while(1)
	{
		total_num+=1;
		if(total_num==5) (2)
		{
			printf("关闭中断.............\r\n");
			portDISABLE_INTERRUPTS(); //关闭中断 (3)
			delay_xms(5000); //延时 5s (4)
			printf("打开中断.............\r\n"); //打开中断
			portENABLE_INTERRUPTS(); (5)
		}
		LED0=~LED0;
		vTaskDelay(1000);
	}
}

//通用定时器 3 中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器 3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //使能 TIM3 时钟
	TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值
	TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频
	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
	TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure); //初始化 TIM3
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器 3 更新中断
	TIM_Cmd(TIM3,ENABLE); //使能定时器 3
	NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器 3 中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x04; //抢占优先级 4 (1)
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00; //子优先级 0
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

//定时器 3 中断服务函数
void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
	{
		printf("TIM3 输出.......\r\n"); (3)
	}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}

在 main 函 数 中

  • 一 开 始 肯 定 是 初 始 化 各 种 硬 件 外 设
  • 初 始 化 完 外 设 以 后 调 用 函 数xTaskCreate()创建一个开始任务
  • 创建开始任务是在调用函数 vTaskStartScheduler()开启任务调度器之前,这样当后面开启任务调度器以后就会直接运行开始任务了。
  • 开始任务的职责就是创建其他应用任务和信号量、队列等这些内核对象的,所以它只需要执行一次,当这些东西创建完成以后就可以删除掉开始任务了
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建中断测试任务
xTaskCreate((TaskFunction_t )interrupt_task, //任务函数 (1)
			(const char* )"interrupt_task", //任务名称
			(uint16_t )INTERRUPT_STK_SIZE, //任务堆栈大小
			(void* )NULL, //传递给任务函数的参数
			(UBaseType_t )INTERRUPT_TASK_PRIO, //任务优先级
			(TaskHandle_t* )&INTERRUPTTask_Handler); //任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
}

//task1 任务函数

6 FreeRTOS任务基础知识

6.1 多任务系统

前后台系统,也就是裸机,中断服务程序作为前台程序,大循环while作为后台程序。
在这里插入图片描述
前后台系统的实时性差.
前后台系统各个任务都是排队等候,相当于所有任务优先级都是一样的。
多任务系统是并发处理的,不是每个时刻同时执行很多任务,而是每个任务的执行时间都很短,就像同一时刻执行了很多任务一样。
多任务有一个问题,谁先执行任务,这就要看任务调度器。
FreeRTOS的任务调度器是一个抢占式的实时多任务系统。
在这里插入图片描述高优先级的任务可以打断低优先级任务的运行而获得CPU的使用权,这样可以保证紧急任务的运行。

6.2 任务与协程

FreeRTOS 中应用既可以使用任务,也可以使用协程(Co-Routine)。

  • 协程是为那些资源很少的 MCU 准备的,其开销很小
  • 但是 FreeRTOS 官方已经不打算再更新协程了,所以本教程只讲解任务

6.2.1 任务特性

每个应用都可以作为一个独立的任务

  • 不依赖于其它的任务或者RTOS任务调度器。

任何一个时间点只能有一个任务运行,具体哪个任务执行要看RTOS调度器,RTOS调度器会重复的开启、关闭每个任务。

每个任务都有一个堆栈,当任务切换时,将现场保存在堆栈中。

任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。
特点
1、简单。
2、没有使用限制。
3、支持抢占
4、支持优先级
5、每个任务都拥有堆栈导致了 RAM 使用量增大。
6、如果使用抢占的话的必须仔细的考虑重入的问题。

6.2.2 协程(Co-routine)特性

协程是为那些资源很少的 MCU 而做的。
随着MCU 性能越来越强大,现在协程几乎很少用到。
协程为了降低对 RAM 的消耗做了很多的限制。
协程与任务有如下的不同:

  • 堆栈使用:所有的协程使用同一个堆栈,这样就比使用任务消耗更少的 RAM。
  • 调度器和优先级:协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程。
  • 宏实现:协程是通过宏定义来实现的。

6.3 任务状态

任务的状态有运行、就绪、阻塞、挂起

  • 运行
    如果是一个单核处理器的话,不管在任何时刻都永远只有一个任务处于运行状态。

  • 就绪
    就绪就是已经准备运行的任务,之所以没有运行,因为有一个同优先级 或者优先级更高的任务正在运行。

  • 阻塞

    当任务等待某个外部事件的时候就处于阻塞态,比如说调用了vTaskDelay的话就会进入阻塞。
    任务在等待队列、信号量、事件组、通知或者互斥信号量的时候也会进入阻塞态。
    任务超过超时时间就会退出阻塞态。

  • 挂起态
    任务挂起后也不能被调度器调用进入运行状态,就相当于这哥们中场休息了。挂起是没有超时时间的。任务进入和退出挂起用vTaskSuspend和xTaskResume

在这里插入图片描述

6.4 任务优先级

优先级数字越低优先级越低。0最小(空闲任务)。

configMAC_PRIORITIES-1的优先级最高。

调度器为了确保处于就绪态或运行态的高优先级的任务获得CPU。

当宏configUSE_TIME_SLICING定义为1的时候,多个任务可以使用一个优先级,数量不限。

  • 默认为情况下,该宏就是1
  • 此时处于就绪的优先级相同的任务就会使用时间片轮询调度器获取运行时间。

6.5 任务实现

函数 xTaskCreate()或 xTaskCreateStatic()来创建任务。
第一个参数 pxTaskCode,就是这个任务的任务函数。
FreeRTOS 官方给出的任务函数模板:

void vATaskFunction(void *pvParameters) (1)
{
	for( ; ; ) (2)
	{
	--任务应用程序-- (3)
		vTaskDelay(); (4)
	}
/* 不 能 从 任 务 函 数 中 返 回 或 者 退 出 , 从 任 务 函 数 中 返 回 或 退 出 的 话 就 会 调 用
configASSERT(),前提是你定义了 configASSERT()。如果一定要从任务函数中退出的话那一定
要调用函数 vTaskDelete(NULL)来删除此任务。*/
vTaskDelete(NULL); (5)
}

注意:

  • 任务函数的返回类型一定要为 void 类型,也就是无返回值
  • 任务的参数也是 void 指针类型的!
  • 任务函数名可以根据实际情况定义。

for(; ; )就代表一个循环,作用和 while(1)一样。

能让 FreeRTOS 发生任务切换的 API 函数:

  • 延时函数、请求信号量、队列等,甚至直接调用任务调度器

任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数 vTaskDelete(NULL)删除此任务。

其他 RTOS的任务函数基本也是这种方式。

6.6 任务控制

每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t。

在使用函数 xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。

结构体在文件 tasks.c 中有定义:

typedef struct tskTaskControlBlock
{
	volatile StackType_t *pxTopOfStack; //任务堆栈栈顶
	#if ( portUSING_MPU_WRAPPERS == 1 )
	xMPU_SETTINGS xMPUSettings; //MPU 相关设置
	#endif
	ListItem_t xStateListItem; //状态列表项
	ListItem_t xEventListItem; //事件列表项
	UBaseType_t uxPriority; //任务优先级
	StackType_t *pxStack; //任务堆栈起始地址
	char pcTaskName[ configMAX_TASK_NAME_LEN ];//任务名字
	#if ( portSTACK_GROWTH > 0 )
	StackType_t *pxEndOfStack; //任务堆栈栈底
	#endif
	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
	UBaseType_t uxCriticalNesting; //临界区嵌套深度
	#endif
	#if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到 debug 的时候用到
	UBaseType_t uxTCBNumber;
	UBaseType_t uxTaskNumber;
	#endif
	#if ( configUSE_MUTEXES == 1 )
	UBaseType_t uxBasePriority; //任务基础优先级,优先级反转的时候用到
	UBaseType_t uxMutexesHeld; //任务获取到的互斥信号量个数
	#endif
	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
	TaskHookFunction_t pxTaskTag;
	#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_NEWLIB_REENTRANT == 1 )
	struct _reent xNewLib_reent; //定义一个 newlib 结构体变量
	#endif
	#if( configUSE_TASK_NOTIFICATIONS == 1 ) //任务通知相关变量
	volatile uint32_t ulNotifiedValue; //任务通知值
	volatile uint8_t ucNotifyState; //任务通知状态
	#endif
	#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
	//用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为 pdTURE,
	//如果是动态创建的就为 pdFALSE
	uint8_t ucStaticallyAllocated;
	#endif
	#if( INCLUDE_xTaskAbortDelay == 1 )
	uint8_t ucDelayAborted;
	#endif
} tskTCB;
//新版本的 FreeRTOS 任务控制块重命名为 TCB_t,但是本质上还是 tskTCB,主要是为了兼容
//旧版本的应用。
typedef tskTCB TCB_t;

当不使用某些功能的时候与其相关的变量就不参与编译,任务控制块大小就会进一步的减小。

6.7 任务堆栈

FreeRTOS通过任务堆栈恢复一个任务的运行。

  • 任务调度器在任务切换时会将当前现场保存在此任务的堆栈汇中。
  • 等待下次恢复现场后继续从中断的地方开始执行。

创建任务的时候需要指定堆栈

  • 如果使用xTaskCreate()创建任务,则任务堆栈由xTaskCreate()自动创建。
  • 如果使用xTaskCreateStatic()创建任务,就需要程序员自行定义任务堆栈,然后堆栈首地址作为函数的参数puxStackBuffer传递给函数。

如下:
在这里插入图片描述
堆栈大小:
任务堆栈的数据类型为 StackType_t,StackType_t 本质上是 uint32_t,在 portmacro.h 中有定义:

#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;

StackType_t 类型的变量为 4 个字节,那么任务的实际堆栈大小就应该是我们所定义的 4 倍。

7 FreeRTOS任务相关函数

7.1 任务创建与删除

在这里插入图片描述

7.1.1 xTaskCreate()

使用动态方法创建一个任务。
创建任务的时候自动从FreeRTOS的堆中分配RAM,因此必须提供内存管理文件。
我们默认使用heap_4.c这个内存管理文件。
此时,宏configSUPPORT_DYNAMIC_ALLOCATION必须为1。
新建的任务默认就绪态,如果没有比它更高的优先级任务运行的话,任务会立刻进入运行。
不管在任务调度器启动前还是启动后,都可以创建任务。
在这里插入图片描述

  • pxTaskCode: 任务函数

  • pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过configMAX_TASK_NAME_LEN。不用的话可以设为Null。

  • usStackDepth 每个任务有自己的栈空间,这里根据任务占用需求设置栈空间的大小,单位是字(Word)。

    • 实际申请到的堆栈是 usStackDepth 的 4 倍。
    • 空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
  • pvParameters 用于传递给任务的参数,不用的话可以设为Null

  • uxPriority 设置任务的优先级,范围由0到(configMAX_PRIORITIES – 1)。数值越大,等级越高

  • pxCreatedTask 任务的句柄(handle),通句柄可以对任务进行设置,比如改变任务优先级等,不用可以设为Null

    • 任务创建成功以后会返回此任务的任务句柄
    • 这个句柄其实就是任务的任务堆栈
  • 函数的返回值有两个pdPass和pdFail

    • pdPass表示任务创建成功
    • 相反pdFail表示创建失败,创建失败的原因大多是因为系统没有足够的堆空间来保存任务。

7.1.2 xTaskCreateStatic()

和 xTaskCreate()的功能相同,也是用来创建任务的。
如 果 要 使 用 此 函 数 的 话 需 要 将 configSUPPORT_STATIC_ALLOCATION 定义为 1。
同时需 要 用 户 实 现 两 个 函 数 vApplicationGetIdleTaskMemory() 和vApplicationGetTimerTaskMemory()。

  • 这两个函数来给空闲任务和定时器任务的任务堆栈和任务控制块分配内存

两个函数我们在 mainc.c 中定义:

//空闲任务任务堆栈
static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
//空闲任务控制块
static StaticTask_t IdleTaskTCB;
//定时器服务任务堆栈
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];
//定时器服务任务控制块
static StaticTask_t TimerTaskTCB;
//获取空闲任务地任务堆栈和任务控制块内存,因为本例程使用的
//静态内存,因此空闲任务的任务堆栈和任务控制块的内存就应该
//有用户来提供,FreeRTOS 提供了接口函数 vApplicationGetIdleTaskMemory()
//实现此函数即可。
//ppxIdleTaskTCBBuffer:任务控制块内存
//ppxIdleTaskStackBuffer:任务堆栈内存
//pulIdleTaskStackSize:任务堆栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
				StackType_t **ppxIdleTaskStackBuffer,
				uint32_t *pulIdleTaskStackSize)
{
	*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
	*ppxIdleTaskStackBuffer=IdleTaskStack;
	*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
//获取定时器服务任务的任务堆栈和任务控制块内存
//ppxTimerTaskTCBBuffer:任务控制块内存
//ppxTimerTaskStackBuffer:任务堆栈内存
//pulTimerTaskStackSize:任务堆栈大小
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
				StackType_t **ppxTimerTaskStackBuffer,
				uint32_t *pulTimerTaskStackSize)
{
	*ppxTimerTaskTCBBuffer=&TimerTaskTCB;
	*ppxTimerTaskStackBuffer=TimerTaskStack;
	*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;
}

用户定义静态的任务堆栈和任务控制块内存,然后将这些内存 传 递 给 函 数 参 数 。
最 后 创 建 空 闲 任 务 和 定 时 器 服 务 任 务 的 API 函 数 会 调 用vApplicationGetIdleTaskMemory()和 vApplicationGetTimerTaskMemory()来获取这些内存。

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 )


#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
StackType_t StartTaskStack[START_STK_SIZE]; //任务堆栈 (1)
StaticTask_t StartTaskTCB; //任务控制块 (2)
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数

定义一个数组作为任务堆栈,堆栈数组为StackType_t 类型。
定义任务控制块,注意任务控制块类型要用 StaticTask_t。

参数:

  • usStackDepth: 任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出
    • 一般是个数组,此参数就是这个数组的大小。
  • puxStackBuffer: 任务堆栈,一般为数组,数组类型要为 StackType_t 类型。
  • pxTaskBuffer: 任务控制块。

返回值:

  • NULL: 任务创建失败,puxStackBuffer 或 pxTaskBuffer 为 NULL 的时候会导致这个错误的发生。
  • 其他值: 任务创建成功,返回任务的任务句柄。

其它同xTaskCreate。

7.1.3 xTaskCreateRestricted()

也是用来创建任务的
此函数要求所使用的 MCU 有 MPU(内存保护单元),用此函数创建的任务会受到 MPU 的保护。

BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition,
TaskHandle_t * pxCreatedTask 

参数:

  • pxTaskDefinition: 指向一个结构体 TaskParameters_t,这个结构体描述了任务的任务函数、堆栈大小、优先级等。此结构体在文件 task.h 中有定义。
  • pxCreatedTask: 任务句柄。

返回值:

  • pdPASS: 任务创建成功。
  • 其他值: 任务未创建成功,很有可能是因为 FreeRTOS 的堆太小了。

7.1.4 vTaskDelete()

删除一个用函数 xTaskCreate()或者 xTaskCreateStatic()创建的任务。
任务被删除以后就不能再使用此任务的句柄!

  • 使用 xTaskCreate()创建的任务,被删除以后此任务之前申请的堆栈和控制块内存会在空闲任务中被释放掉。
  • 当调用函数 vTaskDelete()删除任务以后必须给空闲任务一定的运行时间。
  • 户分配给任务的内存需要用户自行释放掉
    • 某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内存
    • 那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否则会导致内存泄露
vTaskDelete( TaskHandle_t xTaskToDelete )

参数:
xTaskToDelete: 要删除的任务的任务句柄。

7.2 任务挂起和恢复

在这里插入图片描述

7.2.1 vTaskSuspend()

将某个任务设置为挂起态

  • 进入挂起态的任务永远都不会进入运行态

退出挂起态的唯一方法就是调用任务恢复函数 vTaskResume()或 xTaskResumeFromISR()。

void vTaskSuspend( TaskHandle_t xTaskToSuspend)

xTaskToSuspend: 要挂起的任务的任务句柄

  • 如 果 使 用 函 数 xTaskCreate() 创 建 任 务 的 话 那 么 函 数 的 参 数pxCreatedTask 就是此任务的任务句柄
  • 如果使用函数 xTaskCreateStatic()创建任务的话那么函数的返回值就是此任务的任务句柄。
  • 如果参数为 NULL 的话表示挂起任务自己。

可以通过函数 xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。

7.2.2 vTaskResume()

将一个任务从挂起态恢复到就绪态。
函数原型如下:

void vTaskResume( TaskHandle_t xTaskToResume)

参数:
xTaskToResume: 要恢复的任务的任务句柄。

7.2.3 xTaskResumeFromISR()

vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务。

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)

参数:
xTaskToResume: 要恢复的任务的任务句柄。
返回值:

  • pdTRUE: 恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换。
  • pdFALSE: 恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数的以后不需要进行上下文切换。

8 FreeRTOS 列表和列表项

8.1 列表和列表项

8.1.1 列表

列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似。
列表被用来跟踪 FreeRTOS中的任务。
相关文件 list.c 和 list.h 中。

typedef struct xLIST
{
	listFIRST_LIST_INTEGRITY_CHECK_VALUE (1)
	configLIST_VOLATILE UBaseType_t uxNumberOfItems; (2)
	ListItem_t * configLIST_VOLATILE pxIndex; (3)
	MiniListItem_t xListEnd; (4)
	listSECOND_LIST_INTEGRITY_CHECK_VALUE (5)
} List_t;
  • (1) 和 (5) :用 来 检 查 列 表 完 整 性 的,默认不开启这个功能。
    • 将 宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1
    • 开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2
    • 初始化列表的时候会在这两个变量中写入一个特殊的值
  • uxNumberOfItems 用来记录列表中列表项的数量。
  • pxIndex 用来记录当前列表项索引号,用于遍历列表。
  • xListEnd最后一个列表项,用来表示列表结束,变量类型为 MiniListItem_t。

列表结构示意图:
在这里插入图片描述

8.1.2 列表项

列表项就是存放在列表中的项目
FreeRTOS 提供了两种列表项:列表项和迷你列表项
两个都在文件 list.h 中有定义
列表项定义:

struct xLIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
	configLIST_VOLATILE TickType_t xItemValue; (2)
	struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4
	void * pvOwner; (5)
	void * configLIST_VOLATILE pvContainer; (6)
	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE (7)
};
typedef struct xLIST_ITEM ListItem_t;
  • (1)和(7):用法和列表一样,用来检查列表项完整性。
  • xItemValue 为列表项值。
  • pxNext 指向下一个列表项。
  • pxPrevious 指向前一个列表项,和 pxNext 配合起来实现类似双向链表的功能。
  • pvOwner 记录此链表项归谁拥有,通常是任务控制块。
  • pvContainer 用来记录此列表项归哪个列表。

在 TCB_t 中有两个变量 xStateListItem 和 xEventListItem,这两个变量的类型就是 ListItem_t,也就是说这两个成员变量都是列表项。

  • 当创建一个任务以后 xStateListItem 的 pvOwner 变量就指向这个任务的任务控制块,表示 xSateListItem属于此任务。
  • 当任务就绪态以后 xStateListItem 的变量 pvContainer 就指向就绪列表,表明此列表项在就绪列表中。
    在这里插入图片描述

8.1.3 迷你列表项

迷你列表项在文件 list.h 中有定义

struct xMINI_LIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
	configLIST_VOLATILE TickType_t xItemValue; (2)
	struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

(1)、用于检查迷你列表项的完整性。
(2)、xItemValue 记录列表列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向上一个列表项。

迷你列表项只是比列表项少了几个成员变量.
用于某些情况下不需要列表项这么全的功能,只需要其中的某几个成员变量。防止造成内存浪费。

8.2 列表和列表项初始化

8.2.1 列表初始化

列表的初始化通过使函数 vListInitialise()来完成,此函数在 list.c 中有定义:

void vListInitialise( List_t * const pxList )
{
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); (1)
	pxList->xListEnd.xItemValue = portMAX_DELAY; (2)
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); (3)
	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); (4)
	pxList->uxNumberOfItems = ( UBaseType_t ) 0U; (5)
	listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); (6)
	listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); (7)
}

xListEnd 用来表示列表的末尾,而 pxIndex 表示列表项的索引号,此时列表只有一个列表项,那就是 xListEnd,所以 pxIndex 指向 xListEnd。

xListEnd 的列表项值初始化为 portMAX_DELAY

  • portMAX_DELAY 是个宏,在文件portmacro.h 中有定义
  • 根据所使用的 MCU 的不同,portMAX_DELAY 值也不相同,本教程中为 0xffffffffUL

列表初始化完以后如图
在这里插入图片描述

8.2.2 列表项初始化

列表项在使用的时候也需要初始化,列表项初始化由函数vListInitialiseItem()来完成

void vListInitialiseItem( ListItem_t * const pxItem )
{
	pxItem->pvContainer = NULL; //初始化 pvContainer 为 NULL
	//初始化用于完整性检查的变量,如果开启了这个功能的话。
	listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
	listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

只是将列表项成员变量 pvContainer 初始化为 NULL,并且给用于完整性检查的变量赋值。

  • 列表项要根据实际使用情况来初始化
  • 比如任务创建函数 xTaskCreate()就会对任务堆栈中的 xStateListItem 和 xEventListItem 这两个列表项中的其他成员变量在做初始化

8.3 列表项插入

8.3.1 列表项插入函数vListInsert

列表项的插入操作通过函数 vListInsert()来完成

void vListInsert( List_t * const pxList,
		ListItem_t * const pxNewListItem )

参数:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。

参数 pxList 决定了列表项要插入到哪个列表中
pxNewListItem 决定了要插入的列表项
要插入的位置由列表项中成员变量xItemValue 来决定

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t *pxIterator;
	const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; (1)
	listTEST_LIST_INTEGRITY( pxList ); (2)
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
	if( xValueOfInsertion == portMAX_DELAY ) (3)
	{
		pxIterator = pxList->xListEnd.pxPrevious; (4)
	}
	else
	{
		for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->\ (5)
		pxNext->xItemValue <=xValueOfInsertion; pxIterator = pxIterator->pxNext )
		{
		//空循环,什么也不做!
		}
	}
	pxNewListItem->pxNext = pxIterator->pxNext; (6)
	pxNewListItem->pxNext->pxPrevious = pxNewListItem;
	pxNewListItem->pxPrevious = pxIterator;
	pxIterator->pxNext = pxNewListItem;
	pxNewListItem->pvContainer = ( void * ) pxList; (7)
	( pxList->uxNumberOfItems )++; (8)
}
  1. 获取要插入的列表项值,即列表项成员变量 xItemValue 的值

  2. 检查列表和列表项的完整性的。检查列表和列表项中用于完整性检查的变量值是否被改变。

  3. 获取要插入点

    • 如果要插入的列表项的值等于 portMAX_DELAY,插入的列表项会被放到 xListEnd 前面。
    • 否则需要在列表中一个一个的找自己的位置,查找过程是按照升序的方式查找列表项插入点的。
  4. 将列表项插入到列表中,插入过程和数据结构中双向链表的插入类似。

  5. 成员变量 pvContainer 记录此列表项属于哪个列表。

  6. 列表的成员变量 uxNumberOfItems 加一,表示又添加了一个列表项。

8.3.2 插入列表项

在一个空的列表 List 中插入一个列表值为 40 的列表项 ListItem1
在这里插入图片描述

  • 插入后列表 List 中的 uxNumberOfItems 变为了 1。表示现在列表中有一个列表项。
  • 列表项 ListItem1 中的pvContainer 变成了 List,表示此列表项属于列表 List。

再插入一个值为 60 的列表项 ListItem2
在这里插入图片描述
列表项是按照升序的方式插入的
ListItem2肯定是插入到 ListItem1 的后面、xListEnd 的前面。
uxNumberOfItems 再次加一变为 2 了,说明此时列表中有两个列表项。

8.4 列表项末尾插入

8.4.1 列表项末尾插入函数vListInsertEnd

void vListInsertEnd( List_t * const pxList,
			ListItem_t * const pxNewListItem )

pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。

vListInsertEnd()源码如下:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t * const pxIndex = pxList->pxIndex;
	listTEST_LIST_INTEGRITY( pxList ); (1)
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
	pxNewListItem->pxNext = pxIndex; (2)
	pxNewListItem->pxPrevious = pxIndex->pxPrevious;
	mtCOVERAGE_TEST_DELAY();
	pxIndex->pxPrevious->pxNext = pxNewListItem;
	pxIndex->pxPrevious = pxNewListItem;
	pxNewListItem->pvContainer = ( void * ) pxList; (3)
	( pxList->uxNumberOfItems )++; (4)
}
  1. 完成对列表和列表项的完整性检查。
  2. 将要插入的列表项插入到列表末尾。
  3. 列表项的位置是通过列表项的值 xItemValue 来确定.
  4. 标记新的列表项 pxNewListItem 属于列表 pxList。
  5. 录列表中的列表项数目的变量加一,更新列表项数目。

8.4.2 列表项末尾插入图示

在插入列表项之前我们先准备一个默认列表
在这里插入图片描述
列表的 pxIndex 所指向的列表项,这里为 ListItem1,不再是 xListEnd 了。

用vListInsertEnd插入值为 50 的列表项ListItem3
在这里插入图片描述
列表 List 的 pxIndex 指向列表项 ListItem1,调用函数 vListInsertEnd()插入 ListItem3 的话就会在 ListItem1 的前面插入。

8.5 列表项的删除uxListRemove

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
	List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; (1)
	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; (2)
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
	mtCOVERAGE_TEST_DELAY();
	if( pxList->pxIndex == pxItemToRemove )
	{
		pxList->pxIndex = pxItemToRemove->pxPrevious; (3)
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
	pxItemToRemove->pvContainer = NULL; (4)
	( pxList->uxNumberOfItems )--;
	return pxList->uxNumberOfItems; (5)
}
  1. 读取列表项中的成员变量 pvContainer ,得到此列表项处于哪个列表中。
  2. 完成列表项的删除,其实就是将要删除的列表项的前后两个列表项“连接”在一起。
  3. 如果列表的 pxIndex 正好指向要删除的列表项,删除列表项以后重新给pxIndex指向被删除的列表项的前一个列表项。
  4. 被删除列表项的成员变量 pvContainer 清零。
  5. 回新列表的当前列表项数目。

8.6 列表的遍历

列表 List_t 中的成员变量 pxIndex 是用来遍历列表的。
FreeRTOS提供了一个函数来完成列表的遍历,这个函数是 listGET_OWNER_OF_NEXT_ENTRY()。
每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项。并且返回这个列表项的 pxOwner
变量值。
这个宏在文件 list.h 中:

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ (1)
{ \
	List_t * const pxConstList = ( pxList ); \
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (2)
	if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )\ (3)
	{ \
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (4)
	} \
	( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; 
}
  1. pxTCB 用来保存 pxIndex 所指向的列表项的 pvOwner 变量值,也就是这个列表项归属。
  2. 如果 pxIndex 指向了列表的 xListEnd 成员变量,表示到了列表末尾。
  3. 如果到了列表末尾的话就跳过 xListEnd,pxIndex 再一次重新指向处于列表头的列表项,这样就完成了一次对列表的遍历。
  4. 将 pxIndex 所指向的新列表项的 pvOwner 赋值给 pxTCB。

此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。

9. 调度器的开启和任务相关函数

9.1 调度器开启过程分析

9.1.1 vTaskStartScheduler

函数的功能就是开启任务调度器

  • 如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的
  • 变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
  • 调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等

9.1.2 内核相关硬件初始化函数xPortStartScheduler

BaseType_t xPortStartScheduler( void )
{
/******************************************************************/
/****************此处省略一大堆的条件编译代码**********************/
/*****************************************************************/
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2)
	vPortSetupTimerInterrupt(); (3)
	uxCriticalNesting = 0; (4)
	prvEnableVFP(); (5)
	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; (6)
	prvStartFirstTask(); (7)
	//代码正常执行的话是不会到这里的!
	return 0;
}
  • 设置 PendSV 的中断优先级,为最低优先级。
  • 设置滴答定时器的中断优先级,为最低优先级。
  • 调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断
  • 初始化临界区嵌套计数器。
  • 调用函数 prvEnableVFP()使能 FPU。
  • 设置寄存器 FPCCR 的 bit31 和 bit30 都为 1,这样 S0~S15 和 FPSCR 寄存器在异常入口和退出时的壮态自动保存和恢复。

9.1.3 使能 FPU 函数

在函数 xPortStartScheduler()中会通过调用 prvEnableVFP()来使能 FPU。
在文件 port.c 中有定义

__asm void prvEnableVFP( void )
{
	PRESERVE8
	ldr.w r0, =0xE000ED88 ;R0=0XE000ED88 (1)
	ldr r1, [r0] ;从 R0 的地址读取数据赋给 R1 (2)
	orr r1, r1, #( 0xf << 20 ) ;R1=R1|(0xf<<20) (3)
	str r1, [r0] ;R1 中的值写入 R0 保存的地址中 (4)
	bx r14 (5)
	nop
}

利用寄存器 CPACR 可以使能或禁止 FPU

  • 此寄存器的地址为 0XE000ED88
  • 具体参考《权威指南》“第 13 章 浮点运算”13.2.3 章节

9.1.4 启动第一个任务

函数 prvStartFirstTask()用于启动第一个任务。汇编语言。

__asm void prvStartFirstTask( void )
{
	PRESERVE8
	ldr r0, =0xE000ED08 ;R0=0XE000ED08 (1)
	ldr r0, [r0] ;取 R0 所保存的地址处的值赋给 R0 (2)
	ldr r0, [r0] ;获取 MSP 初始值 (3)
	msr msp, r0 ;复位 MSP (4)
	cpsie I ;使能中断(清除 PRIMASK) (5)
	cpsie f ;使能中断(清除 FAULTMASK) (6)
	dsb ;数据同步屏障 (7)
	isb ;指令同步屏障 (8)
	svc 0 ;触发 SVC 中断(异常) (9)
	nop
	nop
}
  1. 将 0XE000ED08 保存在寄存器 R0 中。
    • 一般来说向量表应该是从起始地址(0X00000000),不过,有些应用可能需要在运行时修改或重定义向量表。
    • Cortex-M 处理器提供了一个叫做向量表重定位的特性,名为向量表偏移寄存器(VTOR),地址0XE000ED08,通过这个寄存器可以重新定义向量表。
    • ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器
    • 向量表的起始地址存储的就是 MSP 初始值(主栈指针)
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00
  • 如上就将向量表开始地址重新定义到了 0X08000000。
  • 详情参见《权威指南》的“第 7 章异常和中断”的 7.5 小节。
  1. 读取寄存器 VTOR中的值,并将其保存在 R0 寄存器中。
  2. 读取地址 0X08000000处存储的数据,并将其保存在 R0 寄存器中。
  3. 复位 MSP,将初始值赋值给 MSP 。
  4. 使能中断,参见权威指南》的“第 4 章 架构”第 4.2.3 小节。
  5. 同5
  6. 数据同步和指令同步屏障,参见权威指南》的“第 5章 指令集”的 5.6.13 小节
  7. 同7
  8. 调用 SVC 指令触发 SVC 中断
    • SVC 也叫做请求管理调用,参考《权威指南》的“第 10 章 OS 支持特性”的 10.3 小节。
    • SVC 和 PendSV 异常对于OS 的设计来说非常重要。
    • 在 FreeRTOS 中仅使用 SVC 异常来启动第一个任务,后面用不到了。

9.1.5 SVC中断服务函数SVC_Handler

FreeRTOSConfig.h 中通过#define 的方式重新定义SVC_Handler为了 xPortPendSVHandler()

#define xPortPendSVHandler PendSV_Handler

vPortSVCHandler()在文件 port.c 中定义:汇编

__asm void vPortSVCHandler( void )
{
	PRESERVE8
	ldr r3, =pxCurrentTCB ;R3=pxCurrentTCB 的地址 (1)
	ldr r1, [r3] ;取 R3 所保存的地址处的值赋给 R1 (2)
	ldr r0, [r1] ;取 R1 所保存的地址处的值赋给 R0 (3)
	ldmia r0!, {r4-r11, r14} ;出栈 ,R4~R11 和 R14 (4)
	msr psp, r0 ;进程栈指针 PSP 设置为任务的堆栈 (5)
	isb ;指令同步屏障
	mov r0, #0 ;R0=0 (6)
	msr basepri, r0 ;寄存器 basepri=0,开启中断 (7)
	bx r14 (8)
}
  1. pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。

  2. 取 R3 所保存的地址处的值赋给 R1。获取到了当前任务的任务控制块的存储地址。

  3. 取 R3 所保存的地址处的值赋给 R0,读取任务控制块所在的首地址(0X20000E90)就是栈顶指针所指向的地址

    • 任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置
  4. R4~R11,R14 这些寄存器出栈。

    • 使用了指令 LDMIA,LDMIA 指令是多加载/存储指令
LDMIA Rn! , {reg list}
  • 从 Rn 指定的存储器位置读取多个字 地址在每次读取后增加(IA),Rn 在传输完成以后写回。
LDR R0, =0X800
LDMIA R0!, {R2~R4}

上面两行代码就是将

  • 0X800 地址的数据赋值给寄存器 R2
  • 0X804 地址的数据赋值给寄存器 R3
  • 0X8008 地址的数据赋值给 R4 寄存器
  • 此时 R0 为 800A

此时堆栈的栈顶指针:
在这里插入图片描述
恢复 R4~R11 和 R14 以后堆栈的栈顶指针应该指向地址 0X20000E60。
退出中断服务函数以后进程栈指针 PSP 应该从这个地址开始恢复其他的寄存器值。

  1. 设置进程栈指针 PSP,PSP=R0=0X20000E60。
  2. 设置寄存器 R0 为 0。
  3. 设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
  4. 执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值。
    • 堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。
    • FreeRTOS 的任务调度器正式开始。

9.1.6 空闲任务

任务函数为 prvIdleTask()。
函数 vTaskStartScheduler()会创建一个名为“IDLE”的任务,这就是空闲任务。
空闲任务就是空闲的时候运行的任务。
任务调度器启动以后就必须有一个任务运行。
空闲任务中:

  1. 判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。
  2. 运行用户设置的空闲任务钩子函数。
  3. 判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理

空闲任务的创建:当调用函数 vTaskStartScheduler()启动任务调度器的时候此函数就会自动创建空闲任务。

9.1.7 空闲任务函数prvIdleTask()

实际上是找不到这个函数的,因为它是通过宏定义来实现的

在文件 portmacro.h 中有如下宏定义:

#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )

其中 portTASK_FUNCTION()在文件 tasks.c 中有定义

static portTASK_FUNCTION( prvIdleTask, pvParameters ) (1)
{
( void ) pvParameters; //防止报错
//本函数为 FreeRTOS 的空闲任务任务函数,当任务调度器启动以后空闲任务会自动
//创建
for( ;; )
{

太多了 省略

不管什么时候都要保证系统中至少有一个任务可以运行
绝对不能在空闲任务钩子函数中调用任何可以阻塞空闲任务的 API 函数,比如 vTaskDelay()

9.1.8 空闲任务的钩子函数

FreeRTOS 中有多个钩子函数,钩子函数类似回调函数。
当某个功能(函数)执行的时候就会调用钩子函数,钩子函数的具体用户来编写。
钩子函数是一个可选功能,可以通过宏定义来选择使用哪个钩子函数

在这里插入图片描述

  1. 要使用空闲任务钩子函数首先要在 FreeRTOSConfig.h 中将宏 configUSE_IDLE_HOOK 改为 1
  2. 编写空闲任务钩子函数 vApplicationIdleHook()
  3. 通常在空闲任务钩子函数中将处理器设置为低功耗模式来节省电能

空闲任务钩子函数主要目的就是调用 WFI 指令使 STM32F407 进入睡眠模式

//空闲任务钩子函数
void vApplicationIdleHook(void)
{
	__disable_irq();
	__dsb(portSY_FULL_READ_WRITE );
	__isb(portSY_FULL_READ_WRITE );
	BeforeEnterSleep(); //进入睡眠模式之前需要处理的事情
	__wfi(); //进入睡眠模式
	AfterExitSleep(); //退出睡眠模式之后需要处理的事情
	__dsb(portSY_FULL_READ_WRITE );
	__isb(portSY_FULL_READ_WRITE );
	__enable_irq();
}

9.2 任务创建过程

9.2.1 xTaskCreate()

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/********************************************************************/
/***************使用条件编译的向上增长堆栈相关代码省略***************/
/********************************************************************/
StackType_t *pxStack;
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) *\ (1)
sizeof( StackType_t ) ) );
if( pxStack != NULL )
{
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); (2)
if( pxNewTCB != NULL )
{
pxNewTCB->pxStack = pxStack; (3)
}
else
{
vPortFree( pxStack ); (4)
}
}
else
{
pxNewTCB = NULL;
}
if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
pxNewTCB->ucStaticallyAllocated =\ (5)
tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, \ (6)
pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB ); (7)
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
  1. 使用函数 pvPortMalloc()给任务的任务堆栈申请内存
  2. 接 着使用函数 pvPortMalloc() 给 任 务 控 制 块 申 请 内 存
  3. 初始化内存控制块中的任务堆栈字段 pxStack
  4. 任务控制块内存申请失败的话就释放前面已经申请成功的任务堆栈的内存。
  5. 标记任务堆栈和任务控制块是使用动态内存分配方法得到的。
  6. 使用函数 prvInitialiseNewTask()初始化任务,这个函数完成对任务控制块中各个字段的初始化工作!
  7. 使用函数 prvAddNewTaskToReadyList()将新创建的任务加入到就绪列表中。

9.2.2 任务初始化函数prvInitialiseNewTask

用于完成对任务的初始化

static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,
		const char * const pcName,
		const uint32_t ulStackDepth,
		void * const pvParameters,
		UBaseType_t uxPriority,
		TaskHandle_t * const pxCreatedTask,
		TCB_t * pxNewTCB,
		const MemoryRegion_t * const xRegions )
{
StackType_t *pxTopOfStack;
UBaseType_t x;
#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( configUSE_TRACE_FACILITY ==\
1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) )
{
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE,\ (1)
( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 ); (2)
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) &\
( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ]; (3)
if( pcName[ x ] == 0x00 )
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0'; (4)
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ) (5)
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority; (6)
#if ( configUSE_MUTEXES == 1 ) (7)
{
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); (8)
vListInitialiseItem( &( pxNewTCB->xEventListItem ) ); (9)
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); (10)
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), \ (11)
( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB ); (12)
#if ( portCRITICAL_NESTING_IN_TCB == 1 ) //使能临界区嵌套
{
pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */
#if ( configUSE_APPLICATION_TASK_TAG == 1 ) //使能任务标签功能
{
pxNewTCB->pxTaskTag = NULL;
}
#endif /* configUSE_APPLICATION_TASK_TAG */
#if ( configGENERATE_RUN_TIME_STATS == 1 ) //使能时间统计功能
{
pxNewTCB->ulRunTimeCounter = 0UL;
}
#endif /* configGENERATE_RUN_TIME_STATS */
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )
{
for( x = 0; x < ( UBaseType_t ) configNUM_THREAD_LOCAL_STORAGE_POINTERS;\
x++ )
{
pxNewTCB->pvThreadLocalStoragePointers[ x ] = NULL; (12)
}
}
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 ) //使能任务通知功能
{
pxNewTCB->ulNotifiedValue = 0;
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_REENT_INIT_PTR( ( &( pxNewTCB->xNewLib_reent ) ) );
}
#endif
#if( INCLUDE_xTaskAbortDelay == 1 ) //使能函数 xTaskAbortDelay()
{
pxNewTCB->ucDelayAborted = pdFALSE;
}
#endif
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode,\ (13)
pvParameters );
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB; (14)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
  1. 使 能 了 堆 栈 溢 出 检 测 功 能 或 者 追 踪 功 能 的处理。
  2. 计算堆栈栈顶 pxTopOfStack,后面初始化堆栈的时候需要用到。
  3. 保存任务的任务名。
  4. 任务名数组添加字符串结束符’\0’。
  5. 判断任务优先级是否合法
  6. 初始化任务控制块的优先级字段 uxPriority。
  7. 使能互斥信号量功能,需要初始化相应的字段。
  8. 初始化列表项 xStateListItem 和 xEventListItem
  9. 同8
  10. 设置列表项 xStateListItem 和 xEventListItem 属于当前任务的任务控制块
  11. 设置列表项 xEventListItem 的字段 xItemValue 为 configMAX_PRIORITIES- uxPriority
  12. 是否初始化线程本地存储指针
  13. 调用函数 pxPortInitialiseStack()初始化任务堆栈。
  14. 生成任务句柄,返回给参数 pxCreatedTask,从这里可以看出任务句柄其实就是任务控制块。

9.2.3 任务堆栈初始化函数pxPortInitialiseStack

StackType_t *pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_XPSR; (1)
	pxTopOfStack--;
	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; (2)
	pxTopOfStack--;
	*pxTopOfStack = ( StackType_t ) prvTaskExitError; (3)
	pxTopOfStack -= 5; (4)
	*pxTopOfStack = ( StackType_t ) pvParameters; (5)
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_EXEC_RETURN; (6)
	pxTopOfStack -= 8; (7)
	return pxTopOfStack;
}

堆栈是用来在进行上下文切换的时候保存现场的。
初始化就是对 Cortex-M 内核的某些寄存器赋初值。
初值就保存在任务堆栈中,保存的顺序按照:xPSR、R15(PC)、R14(LR)、R12、R3-R0、R11~R14。

  • 寄存器 xPSR 值为 portINITIAL_XPSR,其值为 0x01000000。
    • xPSR 是 Cortex-M4 的个内核寄存器,叫做程序状态寄存器。
    • 寄存器的 bit24 为 1 表示处于 Thumb状态
  • 寄存器 PC 初始化为任务函数 pxCode。
  • 寄存器 LR 初始化为函数 prvTaskExitError。
  • 跳过 4 个寄存器,R12,R3,R2,R1,这四个寄存器不初始化。
  • 寄存器 R0 初始化为 pvParameters
  • 保存 EXC_RETURN 值,用于退出 SVC 或 PendSV 中断的时候处理器应该处于什么状态 。
  • 跳过 8 个寄存器,R11、R10、R8、R7、R6、R5、R4。

初始化后的“:
在这里插入图片描述

8.2.4 添加任务到就绪列表

FreeRTOS在文件 tasks.c 中就定义了多个列表来完成不同的功能

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
PRIVILEGED_DATA static List_t xPendingReadyList;

pxReadyTasksLists[]就是任务就绪列表,数组大小为 configMAX_PRIORITIES。

  • 相同优先级的任务就使用一个列表。

函数 prvAddNewTaskToReadyList()来完成列表添加。
prvAddNewTaskToReadyList最终调用函数 prvAddTaskToReadyList()将任务添加到就绪列表中。

#define prvAddTaskToReadyList( pxTCB ) \
	traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), \
	&( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

宏 portRECORD_READY_PRIORITY()用来记录处于就绪态的任务

  • 通过全局变量 uxTopReadyPriority 来实现,该变量用来查找处于就绪态的优先级最高任务。

vListInsertEnd()将任务添加到就绪列表末尾。

9.3 任务删除过程vTaskDelete

void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
//如果参数为 NULL 的话那么说明调用函数 vTaskDelete()的任务要删除自身。
pxTCB = prvGetTCBFromHandle( xTaskToDelete ); (1)
//将任务从就绪列表中删除。
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (2)
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//任务是否在等待某个事件?
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) (3)
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
uxTaskNumber++;
if( pxTCB == pxCurrentTCB ) (4)
{
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->\ (5)
xStateListItem ) );
++uxDeletedTasksWaitingCleanUp; (6)
portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); (7)
}
else
{
--uxCurrentNumberOfTasks; (8)
prvDeleteTCB( pxTCB ); (9)
prvResetNextTaskUnblockTime(); (10)
}
traceTASK_DELETE( pxTCB );
}
taskEXIT_CRITICAL();
//如果删除的是正在运行的任务那么就需要强制进行一次任务切换。
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API(); (11)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
  1. 调用函数 prvGetTCBFromHandle()获取要删除任务的任务控制块,参数为任务句柄。
    • 如果参数为当前正在执行的任务句柄那么返回值就为 NULL。
  2. 将任务从任务就绪列表中删除。
  3. 查看任务是否正在等待某个事件(如信号量、队列等)
  • 如果任务等待某个事件的话这个任务会被放到相应的列表中
  • 需要将其从相应的列表中删除掉。
  1. 删除的是当前正在运行的任务,处理。
  2. 释放任务的任务控制块和任务堆栈所占用的内存。
  3. 、uxDeletedTasksWaitingCleanUp 是一个全局变量,用来记录有多少个任务需要释放内存。
  4. 调用任务删除钩子函数,钩子函数的具体内容需要用户自行实现。
  5. 删除的是别的任务,变量 uxCurrentNumberOfTasks 减一,也就是当前任务数减一。
  6. 删除别的任务,可以直接调用函数 prvDeleteTCB()删除任务控制块。
  7. 重新计算一下下一个任务的解锁时间,防止有任务的解锁时间参考了刚刚被删除的那个任务。
  8. 如果删除的是正在运行的任务那么删除完以后需要强制进行一次任务切换。

9.5 任务调度器的挂起和恢复vTaskSuspend

挂起任务调度器,不需要关闭中断,这是跟临界区的区别。
在这里插入图片描述使用格式

vTaskSuspendAll();
{
... 内容
}
xTaskResumeAll();

特点:

  • 与临界区相比,没有关闭中断。
  • 内容不允许别其它任务打断(防止任务与任务的资源抢夺)。中断可以打断。
  • 适用于任务与任务之间的临界区。这样不会造成延时中断(因为没关闭中断),防止中断长时间未能响应。

vTaskDelay就会用到调度器的挂起与恢复。成对出现。

  • 恢复是有返回值的。

任务切换是通过PendSV实现的。

10 FreeRTOS任务切换

10.0 上下文切换

上下文切换被触发的场合可以是:

  • 执行一个系统调用
  • 系统滴答定时器(SysTick)中断。

在 OS 中,任务调度器决定是否应该执行上下文切换。
任务切换都是由 SysTick中断中执行。

若中断请求(IRQ)在 SysTick 异常前产生

  • 如果上下文切换, SysTick 异常可能会抢占 IRQ 的处理
  • 这种情况下,OS 不应该执行上下文切换,否则中断请求 IRQ 处理就会被延迟

对于 Cortex-M3 和 Cortex-M4 处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式。

  • 如果存在活跃中断服务,且 OS 试图返回到线程模式,则将触发用法 fault

在这里插入图片描述

  1. 要解决这个问题,可以在运行中断服务时不执行上下文切换

    • 通过检查栈帧中的压栈 xPSR 或 NVIC 中的中断活跃状态寄存器
    • 但是当中断源在 SysTick 中断前后持续产生请求时,这样上下文切换可能就没有执行的机会了。
  2. 为了解决这个问题,PendSV 异常将上下文切换请求延迟到所有其他 IRQ 处理都已经完成后

    • 此时需要将 PendSV 设置为最低优先级
    • 若 OS 需要执行上下文切换,他会设置 PendSV 的挂起状态,并在 PendSV 异常内执行上下文切换

在这里插入图片描述
FreeRTOS 系统的任务切换最终都是在 PendSV 中断服务函数中完成的,UCOS 也是在 PendSV 中断中完成任务切换的。

10.0.1 执行系统调用

执行 FreeRTOS 系统提供的相关 API 函数,比如任务切换函数taskYIELD()。
FreeRTOS 有些 API 函数也会调用函数 taskYIELD()。
相关 API 函数和任务切换函数 taskYIELD()都统称为系统调用。

在文件 task.h 中有如下定义:

#define taskYIELD() portYIELD()

函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:

#define portYIELD() \
{ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ (1)
\
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}

中断级的任务切换函数为 portYIELD_FROM_ISR()

#define portEND_SWITCHING_ISR( xSwitchRequired ) \
if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

portYIELD_FROM_ISR()最终也是通过调用函数 portYIELD()来完成任务切换的。

10.0.2 滴答定时器中断

FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换。
详见10.5

10.1 任务调度器

调度器(scheduler)简单来说是一个决定哪个任务应该执行的算法代码。在FreeRTOS中采用了round-robin的调度算法,包含抢占式(preemptive)和合作式(cooperative)两种模式。模式的选择在FreeRTOSConfig.h头文件中由configUSE_PREEMPTION这个参数决定,为1时是抢占式模式,为0时是合作式模式。

抢占式模式下,在每次调度器运行时,高优先级的任务会被切换优先执行,当前处于运行状态的低优先级的任务则会立刻进入就绪状态等待运行,如下图所示,高优先级的Task2抢占了Task1。如果几个任务的优先级一样的话,它们就会轮流执行共享CPU资源。
在这里插入图片描述在合作式模式下,高优先级任务不会抢占当前正在运行状态的低优先级任务,直到低优先级任务完成进入阻塞状态(比如调用osDelay()函数)或就绪状态(比如调用osThreadYield()函数)或者被系统置于挂起状态后才会切换任务,如下图所示。
在这里插入图片描述

10.2 任务的调度

FreeRTOS对任务的调度采用基于时间片(time slicing)的方式。时间片,顾名思义,把一段时间等分成了很多个时间段,在每一个时间段保证优先级最高的任务能执行

  • 同时如果几个任务拥有相等的优先级,则它们会轮流使用每个时间段占用CPU资源。调度器会在每个时间片结束的时候通过周期中断(tick interrupt)执行一次,调度器根据设置的抢占式还是合作式模式选择哪个任务在下一个时间片会运行。

时间片的大小由configTICK_RATE_HZ这个参数设置。如果configTICK_RATE_HZ设置为10HZ,则时间片的大小为100ms。configTICK_RATE_HZ的值由应用需求决定,通常设为100HZ(时间片大小相应为10ms)。
在这里插入图片描述在上图任务调度的演示中,Kernel表示系统内核即调度程序,Task1和Task2是两个优先级相同的任务。t1到t2是一个时间片,t2到t3是另一个时间片。在每一个时间片快结束的时候,调度程序通过周期中断(tick interrupt)被调用并选择在下一个时间片要执行的任务(红色部分代表调度程序Kernel在运行)。此时因为两个任务的优先级相同,调度程序会让两个任务轮流占用时间片进行运行(蓝色部分代表Task1在运行,绿色部分代表Task2在运行)。

可以把FreeRTOS当成一个时间离散的系统(时间并不是连续的),时间的最小单位是一个节拍(tick),延时函数 vTaskDelayUntil和vTaskDelay的参数需要的是延时的节拍数,不能直接设置延时时间,因此使用 pdMS_TO_TICKS 函数将时间转换为节拍数。通过pdMS_TO_TICKS这个函数可以把时间转换成节拍数(一个节拍代表一个时间片),并且调用这个函数可以保证即使configTICK_RATE_HZ的值不同时时间是一致的。

在 FreeRTOS 中允许一个任务运行一个时间片后(还没执行完)让出 CPU 的使用权,让拥有同优先级的下一个任务运行。

在这里插入图片描述
1、任务 3 正在运行。
2、任务 3 的时间片用完,但是任务 3 还没有执行完。
3、FreeRTOS 将任务切换到任务 1。
4、任务 1 连续运行至时间片用完。
5、任务 3 再次获取到 CPU 使用权,接着运行。
6、任务 3 运行完成,调用任务切换函数 portYIELD()强行进行任务切换放弃剩余的时间片,从而使优先级 N 下的下一个就绪的任务运行。
7、FreeRTOS 切换到任务 1。
8、任务 1 执行完其时间片。

使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。
时间片的长度由宏 configTICK_RATE_HZ 来确定。

10.3 空闲任务

空闲任务(Idle Task)是调度器在vTaskStartScheduler函数调用启动后自动创建的一个任务。

  • 空闲任务具有最低的优先级0
  • 当用户创建的任务都在阻塞状态或挂起状态时,空闲任务便得以执行
  • 最低的优先级确保空闲任务不会抢占用户任务。
  • 同时空闲任务负责清理内核的资源,所以当有任务被删除后,应该保证空闲任务能运行清理和回收内核的资源。

10.4 PendSV中断服务函数

PendSV称为可悬起的系统调用,这与SVC不同,SVC异常是必须被立即响应的,如若不然,就会产生硬件错误。
PendSV异常是可以像普通中断一样根据优先级从而判断是否执行。
此外,PendSV的异常实现也很简单,只要像地址为0xe000ed04的中断控制和状态寄存器(ICSR)的bit28位置1悬起PendSV,从而引起PendSV异常。

  • PendSV是不精确的,因此它的挂起壮态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
  • 若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,这对于上下文切换非常有用,也是各种 OS 设计中的关键.

PendSV 中断服务函数本应该为 PendSV_Handler()
FreeRTOS 使用#define 重定义了

#define xPortPendSVHandler PendSV_Handler

函数 xPortPendSVHandler()源码如下:

__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp (1)
isb
ldr r3, =pxCurrentTCB (2)
ldr r2, [r3] (3)
tst r14, #0x10 (4)
it eq (5)
vstmdbeq r0!, {s16-s31} (6)
stmdb r0!, {r4-r11, r14} (7)
str r0, [r2] (8)
stmdb sp!, {r3} (9)
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (10)
msr basepri, r0 (11)
dsb
isb
bl vTaskSwitchContext (12)
mov r0, #0 (13)
msr basepri, r0 (14)
ldmia sp!, {r3} (15)
ldr r1, [r3] (16)
ldr r0, [r1] (17)
ldmia r0!, {r4-r11, r14} (18)
tst r14, #0x10 (19)
it eq (20)
vldmiaeq r0!, {s16-s31} (21)
msr psp, r0 (22)
isb
bx r14 (23)
}

执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。

这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!

10.4.1 查找下一个要运行的任务

在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务.

  • 已经就绪了的优先级最高的任务
void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) (1)
	{
	xYieldPending = pdTRUE;
	}
	else
	{
	xYieldPending = pdFALSE;
	traceTASK_SWITCHED_OUT();
	taskCHECK_FOR_STACK_OVERFLOW();
	taskSELECT_HIGHEST_PRIORITY_TASK(); (2)
	traceTASK_SWITCHED_IN();
	}
}
  • 如果调度器挂起那就不能进行任务切换。
  • 调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。(在 tasks.c 中有定义)

FreeRTOS 中查找下一个要运行的任务有两种方法,通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定。之前有介绍过。

通用方法,所有的处理器都可以用的方法。

#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
	UBaseType_t uxTopPriority = uxTopReadyPriority; \
	while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \ (1)
	{ \
	configASSERT( uxTopPriority ); \
	--uxTopPriority; \
	} \
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, \ (2)
	&( pxReadyTasksLists[ uxTopPriority ] ) ); \
	uxTopReadyPriority = uxTopPriority; \
}

pxReadyTasksLists[]为就绪任务列表数组,一个优先级一个列表。

uxTopReadyPriority 代表处于就绪态的最高优先级值。

  • 创建任务的时候都会判断新任务的优先级是否大于 uxTopReadyPriority。
  • 大于的话就将这个新任务的优先级赋值给变量 uxTopReadyPriority。

从最高优先级开始判断,看看哪个列表不为空就说明 哪 个 优 先 级 有 就 绪 的 任 务 。

  • 函 数 listLIST_IS_EMPTY() 用 于 判 断 某 个 列 表 是 否 为 空。

找到了有就绪任务的优先级了,就是从对应的列表中找出下一个要运行的任务。

  • 查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项。
  • 获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB。

通用方法是完全通过 C 语言来实现的,适用于不同的芯片和平台。

硬件方法

硬件方法就是使用处理器自带的硬件指令来实现的。

函数如下:

	#define taskSELECT_HIGHEST_PRIORITY_TASK() \
	{ \
	UBaseType_t uxTopPriority; \
	portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \ (1)
	configASSERT( listCURRENT_LIST_LENGTH( & \
	( pxReadyTasksLists[ uxTopPriority ] ) )> 0 ); \
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, \ (2)
	&( pxReadyTasksLists[ uxTopPriority ] ) ); \
}

通 过 函 数 portGET_HIGHEST_PRIORITY() 获 取 处 于 就 绪 态 的 最 高 优 先 级 ,portGET_HIGHEST_PRIORITY 本质上是个宏。

定义如下:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL\
	- ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

使用硬件方法的时候 uxTopReadyPriority 就不代表处于就绪态的最高优先级了,而是使用每个 bit 代表一个优先级,bit0 代表优先级 0,bit31 就代表优先级 31。当某个优先级有就绪任务的话就将其对应的 bit 置 1。

  • 可以看出,如果使用硬件方法的话最多只能有 32 个优先级。
  • __clz(uxReadyPriorities)就是计算 uxReadyPriorities 的前导零个数
    • 前导零个数就是指从最高位开始(bit31)到第一个为 1 的 bit,其间 0 的个数

得到 uxTopReadyPriority 的前导零个数以后在用 31 减去这个前导零个数得到的就是处于就绪态的最高优先级了。

找到了处于就绪态的最高优先级了,接下来就是从对应的列表中找出下一个要运行的任务。

  • 查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项。
  • 将获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB。

硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级,但是会限制任务的优先级数。

10.5 系统滴答定时器(SysTick)中断

除了PendSV异常进行任务切换外,FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:

void xPortSysTickHandler( void )
{
    //执行SysTick_Handler中断函数时,为了保证在freertos中属于最低优先级的此中断能顺利执行,
    //故要关闭FreeRTOS的所有可管理中断,保证系统计数时不被打断。
    vPortRaiseBASEPRI();  //关中断
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )  //判断返回值,如果为pdTURE就要进行一次上下文切换
        {
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR();  //开中断
}
  1. 关闭中断
  2. 通过向中断控制和状态寄存器ICSR的bit28写入1挂起PendSV来启动PendSV中断。PendSV中断可用来进行任务切换。
  3. 打开中断

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 触发PendSV中断。
根据portNVIC_INT_CTRL_REG可以看到其地址,在M3权威指南中搜索地址可以得到如下:
在这里插入图片描述我们的xTaskIncrementTick()通过一个变量的值来进行返回是否为pDfalse的。
当我们直接改变那个值的时候任务切换中断就无法触发。

10.6 功耗模式

在这里插入图片描述

10.6.1 通用低功耗模式

每个滴答定时器中断都会将处理器从低功耗模式中唤醒

  • 以(1)为例,再 T2 时刻处理器从低功耗模式中唤醒,但是接下来由于没有就绪的其他任务所以处理器又再一次进入低功耗模式。
  • T2、T3 和 T4 这三个时刻都一样,反复的进入低功耗、退出低功耗
  • 意义不大,因为进出低功耗也是需要时间的。

10.6.2 低功耗 Tickless 模式

在(1)中的 T1 时刻处理器进入低功耗模式,在 T5 时刻退出低功耗模式。

  • 少了 3 次进出低功耗模式的操作。

Tickless 模式中只有空闲任务要运行时间的超过某个最小阈值的时候才会进入低功耗模式

  • 此阈值通过 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 来设置

11 FreeRTOS 系统内核控制函数

只供系统内核使用,用户应用程序一般不允许使用

11.1 内核控制函数

在这里插入图片描述

11.1.1 函数 vTaskEndScheduler()

关闭任务调度器
在这里插入图片描述此函数仅用于 X86 架构的处理器
调用此函数以后所有系统时钟就会停止运行,所有创建的任务都会自动的删除掉。
可以调用函 数vTaskStartScheduler()来重新开启任务调度器

void vTaskEndScheduler( void )
{
	portDISABLE_INTERRUPTS(); //关闭中断
	xSchedulerRunning = pdFALSE; //标记任务调度器停止运行
	vPortEndScheduler(); //调用硬件层关闭中断的处理函数
}

此函数在文件 tasks.c 中有如下定义。

函数 vPortEndScheduler()在 port.c 中有定义,这个函数在移植 FreeRTOS 的时候要根据实际使用的处理器来编写。

void vPortEndScheduler( void )
{
	configASSERT( uxCriticalNesting == 1000UL );
}

11.1.2 函数 vTaskSuspendAll()

挂起任务调度器,调用此函数不需要关闭可屏蔽中断即可挂起任务调度器,函数在文件
tasks.c 中有如下定义:

void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}

此函数只是简单的将变量 uxSchedulerSuspended 加一,uxSchedulerSuspended 是挂起嵌套计数器。

使用函数 xTaskResumeAll()可以恢复任务调度器,调用了几次 vTaskSuspendAll()挂起调度器,同样的也得调用几次 xTaskResumeAll()才会最终恢复任务调度器。

11.1.3 函数 vTaskStepTick()

此 函 数 在 使 用 FreeRTOS 的 低 功 耗 tickless 模 式 的 时 候 会 用 到。
宏configUSE_TICKLESS_IDLE 为 1。
当使能低功耗 tickless 模式以后在执行空闲任务的时候系统时钟节拍中断就会停止运行。
系统时钟中断停止运行的这段时间必须得补上,这个工作就是由函数 vTaskStepTick()来完成的。
此函数在文件 tasks.c 中有如下定义:

void vTaskStepTick( const TickType_t xTicksToJump )
{
	configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
	xTickCount += xTicksToJump; (1)
	traceINCREASE_TICK_COUNT( xTicksToJump );
}

12 FreeRTOS 其他任务 API 函数

12.1 任务相关 API 函数预览

在这里插入图片描述

12.2 任务相关 API 函数

12.2.1 函数 uxTaskPriorityGet()

此函数用来获取指定任务的优先级
要使用此函数的话宏 INCLUDE_uxTaskPriorityGet 应该定义为 1。
函数原型如下:

UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )

参数:

  • xTask: 要查找的任务的任务句柄。

返回值: 获取到的对应的任务的优先级

12.2.2 函数 vTaskPrioritySet()

用 于 改 变 某 一 个 任 务 的 任 务 优 先 级
此时宏INCLUDE_vTaskPrioritySet 应该定义为 1

void vTaskPrioritySet( TaskHandle_t xTask,
	UBaseType_t uxNewPriority )

参数:

  • xTask: 要查找的任务的任务句柄。
  • uxNewPriority: 任务要使用的新的优先级,可以是 0~configMAX_PRIORITIES – 1。

12.2.3 uxTaskGetSystemState()

此函数用于获取系统中所有任务的任务壮态
每个任务的壮态信息保存在一个 TaskStatus_t类型的结构体里面
要使用此函数的话宏 configUSE_TRACE_FACILITY 应该定义为 1

UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
	const UBaseType_t uxArraySize,
	uint32_t * const pulTotalRunTime )

参数:

  • pxTaskStatusArray: 指向 TaskStatus_t 结构体类型的数组首地址
    • 每个任务至少需要一个TaskStatus_t 结 构 体
typedef struct xTASK_STATUS
{
	TaskHandle_t xHandle; //任务句柄
	const char * pcTaskName; //任务名字
	UBaseType_t xTaskNumber; //任务编号
	eTaskState eCurrentState; //当前任务壮态,eTaskState 是一个枚举类型
	UBaseType_t uxCurrentPriority; //任务当前的优先级
	UBaseType_t uxBasePriority; //任务基础优先级
	uint32_t ulRunTimeCounter;//任务运行的总时间
	StackType_t * pxStackBase; //堆栈基地址
	uint16_t usStackHighWaterMark; //从任务创建以来任务堆栈剩余的最小大小,此
	//值如果太小的话说明堆栈有溢出的风险。
} TaskStatus_t;
  • uxArraySize: 保存任务壮态数组的数组的大小。
  • pulTotalRunTime: 如果 configGENERATE_RUN_TIME_STATS 为 1 的话此参数用来保存系统总的运行时间。

返回值: 统计到的任务壮态的个数,也就是填写到数组 pxTaskStatusArray 中的个数。

  • 此值应该等于函数 uxTaskGetNumberOfTasks()的返回值。
  • 如果参数uxArraySize 太小的话返回值可能为 0。

12.2.4 函数 vTaskGetInfo()

获取指定的单个任务的壮态
任务的壮态信息填充到参数 pxTaskStatus 中
要使用 此 函数的话 宏configUSE_TRACE_FACILITY 要定义为 1

void vTaskGetInfo( TaskHandle_t xTask,
	TaskStatus_t * pxTaskStatus,
	BaseType_t xGetFreeStackSpace,
	eTaskState eState )

参数:

  • xTask: 要查找的任务的任务句柄。
  • pxTaskStatus: 指向类型为 TaskStatus_t 的结构体变量。
  • xGetFreeStackSpace:
    • 在结构体 TaskStatus_t 中有个字段usStackHighWaterMark 来保存自任务运行以来任务堆栈剩余的历史最小大小,这个值越小说明越接近堆栈溢出 , 但 是 计 算 这 个 值 需 要 花 费 一 点 时 间
    • 所 以 我 们 可 以 通 过 将xGetFreeStackSpace 设置为 pdFALSE 来跳过这个步骤
    • 当设置为 pdTRUE的时候就会检查堆栈的历史剩余最小值。
  • eState:
    • 结构体 TaskStatus_t 中有个字段 eCurrentState 用来保存任务运行壮态
    • 这个字段是 eTaskState 类型的,这是个枚举类型
    • 获取任务运行壮态会耗费不少时间
    • 为了加快函数 vTaskGetInfo()的执行速度结构体 TaskStatus_t 中的字段 eCurrentState 就可以由用户直接赋值,参数 eState 就是要赋的值。
    • 如果不在乎这点时间,那么可以将 eState 设置为eInvalid,这样任务的壮态信息就由函数 vTaskGetInfo()去想办法获取。
typedef enum
{
	eRunning = 0, //运行壮态
	eReady, //就绪态
	eBlocked, //阻塞态
	eSuspended, //挂起态
	eDeleted, //任务被删除
	eInvalid //无效
} eTaskState;

12.2.5 函数 xTaskGetApplicationTaskTag()

此函数用于获取任务的 Tag(标签)值
任务控制块中有个成员变量 pxTaskTag 来保存任务的标签值。
要使用此函数的话宏 configUSE_APPLICATION_TASK_TAG 必须为1

函数原型如下:

TaskHookFunction_t xTaskGetApplicationTaskTag( TaskHandle_t xTask )

参数:

  • xTask: 要获取标签值的任务对应的任务句柄,如果为 NULL 的话就获取当前正在运行的任务标签值

返回值: 任务的标签值。

12.2.6 函数 xTaskGetCurrentTaskHandle()

此函数用于获取当前任务的任务句柄,获取到的就是任务控制块。
如 果 要 使 用 此 函 数 的 话 宏INCLUDE_xTaskGetCurrentTaskHandle 应该为 1。

TaskHandle_t xTaskGetCurrentTaskHandle( void )

参数: 无
返回值: 当前任务的任务句柄。

12.2.7 函数 xTaskGetHandle()

此函数根据任务名字获取任务的任务句柄
要使用此函数的话宏 INCLUDE_xTaskGetHandle 应该设置为 1

TaskHandle_t xTaskGetHandle( const char * pcNameToQuery )

参数:

  • pcNameToQuery: 任务名,C 语言字符串。

返回值:

  • NULL: 没有任务名 pcNameToQuery 所对应的任务。
  • 其他值: 任务名 pcNameToQuery 所对应的任务句柄

12.2.8 函数 xTaskGetIdleTaskHandle()

此 函 数 用 于 返 回 空 闲 任 务 的 任 务 句 柄
要 使 用 此 函 数 的 话 宏INCLUDE_xTaskGetIdleTaskHandle 必须为 1

TaskHandle_t xTaskGetIdleTaskHandle( void )

参数: 无
返回值: 空闲任务的任务句柄。

12.2.9 函数 uxTaskGetStackHighWaterMark()

每个任务都有自己的堆栈
此函数用于检查任务从创建好到现在的历史剩余最小值
这个值越小说明任务堆栈溢出的可能性就越大
FreeRTOS 把这个历史剩余最小值叫做“高水位线”。

此函数相对来说会多耗费一点时间,所以在 代 码 调 试 阶 段 可 以 使 用 , 产 品 发 布 的 时 候 最 好 不 要 使 用 。

要 使 用 此 函 数 的 话 宏INCLUDE_uxTaskGetStackHighWaterMark 必须为 1

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )

参数:

  • xTask: 要查询的任务的任务句柄,当这个参数为 NULL 的话说明查询自身任务

返回值: 任务堆栈的“高水位线”值,也就是堆栈的历史剩余最小值。

12.2.10 函数 eTaskGetState()

此函数用于查询某个任务的运行壮态,比如:运行态、阻塞态、挂起态、就绪态等。
要使用此函数的话宏 INCLUDE_eTaskGetState 必须为 1

eTaskState eTaskGetState( TaskHandle_t xTask )

参数:xTask: 要查询的任务的任务句柄。
返回值: 返回值为 eTaskState 类型

12.2.11 函数 pcTaskGetName()

根据某个任务的任务句柄来查询这个任务对应的任务名

char *pcTaskGetName( TaskHandle_t xTaskToQuery )

参数:

  • xTaskToQuery: 要查询的任务的任务句柄,此参数为 NULL 的话表示查询自身任务

返回值: 返回任务所对应的任务名

12.2.12 函数 xTaskGetTickCount()

此函数用于查询任务调度器从启动到现在时间计数器 xTickCount 的值。
什么时候溢出取决于宏 configUSE_16_BIT_TICKS,当此宏为 1 的时候 xTixkCount 就是个 16 位的变量,当为 0 的时候就是个 32 位的变量。

TickType_t xTaskGetTickCount( void )

参数: 无。
返回值: 时间计数器 xTickCount 的值

12.2.13 函数 xTaskGetTickCountFromISR()

此函数是 xTaskGetTickCount()的中断级版本,用于在中断服务函数中获取时间计数器xTickCount 的值。

TickType_t xTaskGetTickCountFromISR( void )

参数: 无。
返回值: 时间计数器 xTickCount 的值。

12.2.14 函数 xTaskGetSchedulerState()

此函数用于获取 FreeRTOS 的任务调度器运行情况:运行?关闭?还是挂起!
要使用此函数的话宏 INCLUDE_xTaskGetSchedulerState 必须为 1

BaseType_t xTaskGetSchedulerState( void )

参数: 无。
返回值:

  • taskSCHEDULER_NOT_STARTED: 调 度 器 未 启 动
  • taskSCHEDULER_RUNNING: 调度器正在运行。
  • taskSCHEDULER_SUSPENDED: 调度器挂起。

12.2.15 函数 uxTaskGetNumberOfTasks()

此函数用于查询系统当前存在的任务数量,函数原型如下:

UBaseType_t uxTaskGetNumberOfTasks( void )

参数: 无。
返回值: 当前系统中存在的任务数量

12.2.16 函数 vTaskList()

此函数会创建一个表格来描述每个任务的详细信息
在这里插入图片描述

  • Name: 创建任务的时候给任务分配的名字。
  • State: 任务的壮态信息,B 是阻塞态,R 是就绪态,S 是挂起态,D 是删除态。
  • Priority:任务优先级。
  • Stack: 任务堆栈的“高水位线”,就是堆栈历史最小剩余大小。
  • Num: 任务编号

函数原型如下:

void vTaskList( char * pcWriteBuffer )

参数:
pcWriteBuffer: 保存任务壮态信息表的存储区。

12.2.17 函数 vTaskGetRunTimeStats()

FreeRTOS 可以通过相关的配置来统计任务的运行时间信息
任务的运行时间信息提供了每个任务获取到 CPU 使用权总的时间。
函数 vTaskGetRunTimeStats()会将统计到的信息填充到一个表里面,表里面提供了每个任务的运行时间和其所占总时间的百分比。

在这里插入图片描述
要 使 用 此 函 数 的 话 宏configGENERATE_RUN_TIME_STATS 和 configUSE_STATS_FORMATTING_FUNCTIONS 必须都为 1。

如果宏 configGENERATE_RUN_TIME_STATS 为 1 的话还需要实现一下几个宏定义:

  • portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),此宏用来初始化一个外设来提供时间统计功能所需的时基。
  • portGET_RUN_TIME_COUNTER_VALUE()或者portALT_GET_RUN_TIME_COUNTER_VALUE(Time),这两个宏实现其中一个就行了,这两个宏用于提供当前的时基的时间值。
void vTaskGetRunTimeStats( char *pcWriteBuffer )

参数:
pcWriteBuffer: 保存任务时间信息的存储区。

12.2.18 函数 vTaskSetApplicationTaskTag()

此函数是为高级用户准备的
用于设置某个任务的标签值
如果要使用此函数的话宏configUSE_APPLICATION_TASK_TAG 必须为 1

void vTaskSetApplicationTaskTag( TaskHandle_t xTask,
	TaskHookFunction_t pxHookFunction )

参数:

  • xTask: 要设置标签值的任务,此值为 NULL 的话表示设置自身任务的标签值。
  • pxHookFunction: 要设置的标签值,这是一个 TaskHookFunction_t 类型的函数指针

12.2.19 函数 SetThreadLocalStoragePointer()

此函数用于设置线程本地存储指针的值
线 程 本 地 存 储 指 针 数 组 的 大 小 由 宏configNUM_THREAD_LOCAL_STORAGE_POINTERS 来决定。
如果要使用此函数的话宏configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为 0,宏的具体值是本地存储指针数组的大小。

void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet,
		BaseType_t xIndex,
		void * pvValue )

参数:

  • xTaskToSet: 要设置线程本地存储指针的任务的任务句柄,如果是 NULL 的话表示设置任务自身的线程本地存储指针。
  • xIndex: 要设置的线程本地存储指针数组的索引。
  • pvValue: 要存储的值。

返回值: 无

12.2.20 函数 GetThreadLocalStoragePointer()

此 函 数 用 于 获 取 线 程 本 地 存 储 指 针 的 值
如 果 要 使 用 此 函 数 的 话 宏configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为 0.

void *pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery,
	BaseType_t xIndex )

参数:

  • xTaskToSet: 要获取的线程本地存储指针的任务句柄,如果是 NULL 的话表示获取任务自身的线程本地存储指针。
  • xIndex: 要获取的线程本地存储指针数组的索引。

返回值: 获取到的线程本地存储指针的值。

13 FreeRTOS时间管理

13.1 FreeRTOS 延时函数

13.1.1 函数 vTaskDelay()

函数 vTaskDelay()是相对模式(相对延时函数。要使用此函数的话宏 INCLUDE_vTaskDelay 必须为 1。
函数 vTaskDelayUntil()是绝对模式(绝对延时函数)。

vTaskDelay()在文件 tasks.c 中有定义:

void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
//延时时间要大于 0。
	if( xTicksToDelay > ( TickType_t ) 0U ) (1)
	{
		configASSERT( uxSchedulerSuspended == 0 );
		vTaskSuspendAll(); (2)
		{
			traceTASK_DELAY();
			prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); (3)
		}
		xAlreadyYielded = xTaskResumeAll(); (4)
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
	if( xAlreadyYielded == pdFALSE ) (5)
	{
		portYIELD_WITHIN_API(); (6)
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}
  1. 延时时间由参数 xTicksToDelay 来确定,为要延时的时间节拍数,延时时间肯定要大于 0。否则的话相当于直接调用函数 portYIELD()进行任务切换。

  2. 调用函数 vTaskSuspendAll()挂起任务调度器。

  3. 调 用 函 数 prvAddCurrentTaskToDelayedList() 将 要 延 时 的 任 务 添 加 到 延 时 列 表pxDelayedTaskList 或 者 pxOverflowDelayedTaskList() 中 。

  4. 调用函数 xTaskResumeAll()恢复任务调度器。

  5. 如果函数 xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度

  6. 调用函数 portYIELD_WITHIN_API()进行一次任务调度。

13.1.2 函数 prvAddCurrentTaskToDelayedList()

用于将当前任务添加到等待列表中

13.1.3 函数 vTaskDelayUntil()

vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间
需要按照一定的频率运行的任务可以使用函数 vTaskDelayUntil()。
函数再文件 tasks.c 中有定义:

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); (1)
{
const TickType_t xConstTickCount = xTickCount; (2)
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement; (3)
if( xConstTickCount < *pxPreviousWakeTime ) (4)
{
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake >\ (5)
xConstTickCount ) )
{
xShouldDelay = pdTRUE; (6)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > \ (7)
xConstTickCount ) )
{
xShouldDelay = pdTRUE; (8)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
*pxPreviousWakeTime = xTimeToWake; (9)
if( xShouldDelay != pdFALSE ) (10)
{
traceTASK_DELAY_UNTIL( xTimeToWake );
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );(11)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll(); (12)
if( xAlreadyYielded == pdFALSE )
{
ortYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

参数:
pxPreviousWakeTime: 上一次任务延时结束被唤醒的时间点

  • 任务中第一次调用函数vTaskDelayUntil 的话需要将 pxPreviousWakeTime 初始化进入任务的 while()循环体的时间点值。
  • 在以后的运行中函数 vTaskDelayUntil()会自动更新 pxPreviousWakeTime。

xTimeIncrement:任务需要延时的时间节拍数(相对于 pxPreviousWakeTime 本次延时的节拍数)。

  1. 挂起任务调度器。
  2. 记录进入函数 vTaskDelayUntil()的时间点值,并保存在 xConstTickCount 中。
  3. 根 据 延 时 时 间 xTimeIncrement 来 计 算 任 务 下 一 次 要 唤 醒 的 时 间 点, 存 在xTimeToWake 中。
  4. 这个延时时间是相对于 pxPreviousWakeTime也就是上一次任务被唤醒的时间点
  5. 。。。省略一大堆,有兴趣再说。
  6. 调用函数 xTaskResumeAll()恢复任务调度器。

vTaskDelayUntil()的使用方法如下:

void TestTask( void * pvParameters )
{
	TickType_t PreviousWakeTime;
	//延时 50ms,但是函数 vTaskDelayUntil()的参数需要设置的是延时的节拍数,不能直接
	//设置延时时间,因此使用函数 pdMS_TO_TICKS 将时间转换为节拍数。
	const TickType_t TimeIncrement = pdMS_TO_TICKS( 50 );
	PreviousWakeTime = xTaskGetTickCount(); //获取当前的系统节拍值
	for( ;; )
	{
	/******************************************************************/
	/*************************任务主体*********************************/
	/******************************************************************/
	//调用函数 vTaskDelayUntil 进行延时
	vTaskDelayUntil( &PreviousWakeTime, TimeIncrement);
	}
}

vTaskDelayUntil()只能保证你按照一定的周期取消阻塞,进入就绪态。
不 一 定 就 能 周 期 性 的 运 行 。
如果有更高优先级或者中断的话你还是得等待。
绝对延时只是相对于 vTaskDelay()这个简单的延时函数而言的。

13.2 系统时钟节拍

xTickCount 就是FreeRTOS 的系统时钟节拍计数器
每个滴答定时器中断中 xTickCount 就会加一
具体操作过程是在函数 xTaskIncrementTick()中进行
此函数在文件 tasks.c 中:

BaseType_t xTaskIncrementTick( void )
{
	TCB_t * pxTCB;
	TickType_t xItemValue;
	BaseType_t xSwitchRequired = pdFALSE;
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
	{
	/***************************************************************************/
	/***************************此处省去一大堆代码******************************/
	/***************************************************************************/
	#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) (1)
	{
	if( listCURRENT_LIST_LENGTH( &( \
	pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) (2)
	{
	xSwitchRequired = pdTRUE; (3)
	}
	else
	{
	mtCOVERAGE_TEST_MARKER();
	}
	}
	#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
	}
	return xSwitchRequired;
}
  • 当宏 configUSE_PREEMPTION 和宏 configUSE_PREEMPTION 都为 1 的时候下面的代码才会编译。
  • 如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE。
  • 于 函 数 返 回 值 为 pdTURE 因 此 函 数xPortSysTickHandler()就会进行一次任务切换

如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不不会更新 xTickCount 了。

  • 取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍数。

14 FreeRTOS队列

在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式。
如果在使用操作系统的应用中用全局变量来传递消息就会涉及到“资源管理”的问题。
FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递。

14.1 简介

队列是为了任务与任务、任务与中断之间的通信而准备的
队列中可以存储有限的、大小固定的数据项目。

  • 列所能保存的最大数据项目数量叫做队列的长度
  • 创建队列的时候会指定数据项目的大小和队列的长度

由于队列用来传递消息的,所以也称为消息队列。

14.1.1数据存储

队列一般采用先进先出(FIFO)的存储缓冲机制。
也可以使用 LIFO 的存储缓冲,也就是后进先出。

数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中。
使用拷贝方式的做叫值传递。

  • 采用值传递的话虽然会导致数据拷贝,会浪费一点时间
  • 但一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。
  • 但直接往队列中发送指向这个消息的地址指针,就变相实现了引用传递。

UCOS 的消息队列采用的是引用传递,传递的是消息指针。

  • 采用引用传递会节省时间

14.1.2 多任务访问

队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。

14.1.3 出队阻塞

当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间。
这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。
出队阻塞是针对从队列中读取消息的任务而言的。

如果阻塞时间为 0~ portMAX_DELAY

  • 当任务没有从队列中获取到消息的话就进入阻塞态
  • 阻塞时间指定了任务进入阻塞态的时间
  • 当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码
  • 如果在阻塞时间内接收到了数据就立即返回。

14.1.4 入队阻塞

入队说的是向队列中发送消息,将消息加入到队列中。
当一个任务向队列发送消息的话也可以设置阻塞时间。
比如任务 B 向消息队列 Q 发送消息,但是此时队列 Q 是满的,那肯定是发送失败的。

14.1.5 队列操作过程

创建队列
在这里插入图片描述首先创建一个队列,并且指定队列的长度和每条消息的长度。
要传递的是x 值,而 x 是个 int 类型的变量,所以每条消息的长度就是 int 类型的长度。

向队列发送第一个消息
在这里插入图片描述任务 A 的变量 x 值为 10,先将这个值发送到消息队列中。
此时队列剩余长度就是3 。

14.1.6 向队列发送第二个消息

在这里插入图片描述任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。此时队列剩余长度为 2。

14.1.7 从队列中读取消息

在这里插入图片描述任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10了。
任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。

  • 当选择清除这个消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成 3。
  • 如果不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。

14.2 队列结构体

有一个结构体,叫做 Queue_t,在文件 queue.c 中定义:

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;

14.3 队列创建

有两种创建队列的方法

  • 一种是静态的,使用函数xQueueCreateStatic()
  • 一个是动态的,使用函数 xQueueCreate()

两个函数本质上都是宏,真正完成队列创建的函数是 xQueueGenericCreate()和 xQueueGenericCreateStatic()。

在文件 queue.c 中有定义。

14.3.1函数 xQueueCreate()

QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
	UBaseType_t uxItemSize)

本质上是一个宏,用来动态创建队列,此宏最终调用的是函数 xQueueGenericCreate()。

参数:

  • uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
  • uxItemSize: 队列中每个项目(消息)的长度,单位为字节

返回值:

  • 其他值: 队列创捷成功以后返回的队列句柄!
  • NULL: 队列创建失败。

14.3.2 函数 xQueueCreateStatic()

此函数也是用于创建队列的,但是使用的静态方法创建队列。

  • 队列所需要的内存由用户自行分配
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
		UBaseType_t uxItemSize,
		uint8_t * pucQueueStorageBuffer,
		StaticQueue_t * pxQueueBuffer)
  • 此函数本质上也是一个宏,此宏最终调用的是函数 xQueueGenericCreateStatic()

参数:

  • uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
  • uxItemSize: 队列中每个项目(消息)的长度,单位为字节
  • pucQueueStorage: 指向队列项目的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节。
  • pxQueueBuffer: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。

返回值:

  • 其他值: 队列创捷成功以后的队列句柄!
  • NULL: 队列创建失败。

14.3.3 函数xQueueGenericCreate()

函 数 xQueueGenericCreate() 用 于 动 态 创 建 队 列 , 创 建 队 列 过 程 中 需 要 的 内 存 均 通 过FreeRTOS 中的动态内存管理函数 pvPortMalloc()分配。

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
	const UBaseType_t uxItemSize,
	const uint8_t ucQueueType )

参数:

  • uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
  • uxItemSize: 队列中每个项目(消息)的长度,单位为字节。
  • ucQueueType: 队列类型,在创建的时候需要指定此队列的用途。

队列类型,一共有六种类型:

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。

返回值:

  • 其他值: 队列创捷成功以后的队列句柄!
  • NULL: 队列创建失败。

14.3.4 函数 xQueueGenericCreateStatic()

此函数用于动态创建队列,创建队列过程中需要的内存需要由用户自行分配好。

QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
	const UBaseType_t uxItemSize,
	uint8_t * pucQueueStorage,
	StaticQueue_t * pxStaticQueue,
	const uint8_t ucQueueType )

参数:

  • uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
  • uxItemSize: 队列中每个项目(消息)的长度,单位为字节
  • pucQueueStorage: 指向队列项目的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节。
  • pxStaticQueue: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
  • ucQueueType: 队列类型。

返回值:

  • 其他值: 队列创捷成功以后队列句柄!
  • NULL: 队列创建失败。

14.3.5 队列初始化函数prvInitialiseNewQueue

函数在文件 queue.c 中,函数代码如下:

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
	const UBaseType_t uxItemSize, //队列项目长度
	uint8_t * pucQueueStorage, //队列项目存储区
	const uint8_t ucQueueType, //队列类型
	Queue_t * pxNewQueue ) //队列结构体
{
	//防止编译器报错
	( void ) ucQueueType;
	if( uxItemSize == ( UBaseType_t ) 0 )
	{
	//队列项(消息)长度为 0,说明没有队列存储区,这里将 pcHead 指向队列开始地址
	pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
	}
	else
	{
	//设置 pcHead 指向队列项存储区首地址
	pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; (1)
	}
	//初始化队列结构体相关成员变量
	pxNewQueue->uxLength = uxQueueLength; (2)
	pxNewQueue->uxItemSize = uxItemSize;
	( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3)
	#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关字段初始化
	{
	pxNewQueue->ucQueueType = ucQueueType;
	}
	#endif /* configUSE_TRACE_FACILITY */
	#if( configUSE_QUEUE_SETS == 1 ) //队列集相关字段初始化
	{
	pxNewQueue->pxQueueSetContainer = NULL;
	}
	#endif /* configUSE_QUEUE_SETS */
	traceQUEUE_CREATE( pxNewQueue );
}
  • 队列结构体中的成员变量 pcHead 指向队列存储区中首地址。
  • 初始化队列结构体中的成员变量 uxQueueLength 和 uxItemSize,这两个成员变量保存队列的最大队列项目和每个队列项大小。
  • 调用函数 xQueueGenericReset()复位队列。

14.3.6 队列复位函数xQueueGenericReset

用来复位队列:

BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )

在这里插入图片描述

14.4 向队列发送消息

在这里插入图片描述

14.4.1 函数 xQueueSend()、xQueueSendToBack()和 xQueueSendToFront()

这三个函数本质都是宏

  • 函数 xQueueSend()和 xQueueSendToBack()是一样的,都是后向入队,即将新的消息插入到队列的后面。
  • 函数xQueueSendToToFront()是前向入队,即将新消息插入到队列的前面。

这三个函数最后都是调用的同一个函数:xQueueGenericSend()。
这三个函数只能用于任务函数中,不能用于中断服务函数,中断服务函数有专用的函数,它们以“FromISR”结尾

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);

参数:

  • xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
  • pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
  • xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。
    • 如果为 0 的话当队列满的时候就立即返回;
    • 当为 portMAX_DELAY 的话 就 会 一 直 等 待 , 直 到 队 列 有 空 闲 的 队 列 项

返回值:

  • pdPASS: 向队列发送消息成功!
  • errQUEUE_FULL: 队列已经满了,消息发送失败。

14.4.2 函数 xQueueOverwrite()

此函数也是用于向队列发送数据的,当队列满了以后会覆写掉旧的数据。
不管这个旧数据有没有被其他任务或中断取走。
此函数也是一个宏,最终调用的也是函数 xQueueGenericSend()

BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
		const void * pvItemToQueue);

参数:

  • xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
  • pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。

返回值:

  • pdPASS: 向队列发送消息成功,此函数也只会返回 pdPASS!因为此函数执行过程中不在乎队列满不满,满了的话我就覆写掉旧的数据,总之肯定能成功。

14.4.3 函数 xQueueGenericSend()任务级通用入队函数

此函数才是真正干活的。
上面讲的所有的任务级入队函数最终都是调用的此函数。

BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
		const void * const pvItemToQueue,
		TickType_t xTicksToWait,
		const BaseType_t xCopyPosition )

参数:

  • xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
  • pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
  • xTicksToWait: 阻塞时间。
  • xCopyPosition: 入队方式,有三种入队方式:
  • queueSEND_TO_BACK: 后向入队
  • queueSEND_TO_FRONT: 前向入队
  • queueOVERWRITE: 覆写入队。

返回值:

  • pdTRUE: 向队列发送消息成功!
  • errQUEUE_FULL: 队列已经满了,消息发送失败。

14.4.4 函数 xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()

也是向队列中发送消息的,这三个函数用于中断服务函数中。

  • 本质也是宏,其中函数 xQueueSendFromISR ()和 xQueueSendToBackFromISR ()是一样的都是后向入队。
  • 函数 xQueueSendToFrontFromISR ()是前向入队,即将新消息插入到队列的前面。

这三个函数同样调用同一个函数 xQueueGenericSendFromISR ()。

BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
	const void * pvItemToQueue,
	BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
	const void * pvItemToQueue,
	BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
	const void * pvItemToQueue,
	BaseType_t * pxHigherPriorityTaskWoken);

参数:

  • xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
  • pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换
    • 用户只需要提供一个变量来保存这个值就行了。
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdTRUE: 向队列中发送消息成功!
  • errQUEUE_FULL: 队列已经满了,消息发送失败。

14.4.5 函数 xQueueOverwriteFromISR()

此函数是 xQueueOverwrite()的中断级版本
在队列满的时候自动覆写掉旧的数据
此函数也是一个宏,实际调用的也是函数 xQueueGenericSendFromISR()

BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
	const void * pvItemToQueue,
	BaseType_t * pxHigherPriorityTaskWoken);

此函数的参数和返回值同上面三个函数相同。

14.4.6 函数 xQueueGenericSendFromISR()中断级通用入队函数

4 个中断级入队函数最终都是调用的函数 xQueueGenericSendFromISR()

BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,
		const void* pvItemToQueue,
		BaseType_t* pxHigherPriorityTaskWoken,
		BaseType_t xCopyPosition);

参数:

  • xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
  • pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换。
    • 用户只需要提供一个变量来保存这个值就行了。
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
  • xCopyPosition: 入队方式,有三种入队方式:
    • queueSEND_TO_BACK: 后向入队
    • queueSEND_TO_FRONT: 前向入队
    • queueOVERWRITE: 覆写入队。
      返回值:
  • pdTRUE: 向队列发送消息成功!
  • errQUEUE_FULL: 队列已经满了,消息发送失败。

14.5 队列上锁和解锁

prvLockQueue()和 prvUnlockQueue()

14.5.1 prvLockQueue

#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
	if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
	{ \
	( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
	} \
	if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
	{ \
	( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
	} \
} \
taskEXIT_CRITICAL()

prvLockQueue()函数很简单,就是将队列中的成员变量 cRxLock 和 cTxLock 设置为queueLOCKED_UNMODIFIED 。

14.5.2 prvUnlockQueue

static void prvUnlockQueue( Queue_t * const pxQueue )
{
	//上锁计数器(cTxLock 和 cRxLock)记录了在队列上锁期间,入队或出队的数量,当队列
	--- 
taskEXIT_CRITICAL();
}

14.6 从队列读取消息

有入队就有出队,出队就是从队列中获取队列项(消息):
在这里插入图片描述

14.6.1 函数 xQueueReceive()

此函数用于在任务中从队列中读取一条(请求)消息
读取成功以后就会将队列中的这条数据删除
在读取消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据
此函数的本质是一个宏,真正执行的函数是 xQueueGenericReceive()

函数原型如下:

BaseType_t xQueueReceive(QueueHandle_t xQueue,
		void * pvBuffer,
		TickType_t xTicksToWait);

参数:

  • xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
  • pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
  • xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。
    • 如果为 0 的话当队列空的时候就立即返回
    • 当为 portMAX_DELAY的 话 就 会 一 直 等 待

注意:宏INCLUDE_vTaskSuspend 必须为 1。

返回值:

  • pdTRUE: 从队列中读取数据成功。
  • pdFALSE: 从队列中读取数据失败。

14.6.2 函数 xQueuePeek()

此函数用于从队列读取一条(请求)消息
此函数在读取成功以后不会将消息删除,此函数是一个宏,真正执行的函数是 xQueueGenericReceive()。
此函数在读取消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据。

函数原型如下:

BaseType_t xQueuePeek(QueueHandle_t xQueue,
			void * pvBuffer,
			TickType_t xTicksToWait);

参数:

  • xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
  • pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
  • xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。
    • 如果为 0 的话当队列空的时候就立即返回
    • 当为 portMAX_DELAY的 话 就 会 一 直 等 待

注意:宏INCLUDE_vTaskSuspend 必须为 1。

返回值:

  • pdTRUE: 从队列中读取数据成功。
  • pdFALSE: 从队列中读取数据失败。

14.6.3 函数 xQueueGenericReceive()

函 数 xQueueReceive() 还 是 xQueuePeek() , 最 终 都 是 调 用 的 函 数xQueueGenericReceive(),此函数是真正干事的。

函数原型如下:

BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,
		void* pvBuffer,
		TickType_t xTicksToWait
		BaseType_t xJustPeek)

参数:

  • xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
  • pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。
  • xTicksToWait: 阻塞时间。同上。
  • xJustPeek: 标记当读取成功以后是否删除掉队列项,当为 pdTRUE 的时候就不用删除。

返回值:

  • pdTRUE: 从队列中读取数据成功。
  • pdFALSE: 从队列中读取数据失败。

14.6.4 函数 xQueueReceiveFromISR()

xQueueReceive()的中断版本
用于在中断服务函数中从队列中读取(请求)一条消息,读取成功以后就会将队列中的这条数据删除。

BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
		void* pvBuffer,
		BaseType_t * pxTaskWoken);

参数参见之前。

  • pxTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的
    • 用户只需要提供一个变量来保存这个值就行了。
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdTRUE: 从队列中读取数据成功。
  • pdFALSE: 从队列中读取数据失败。

14.6.5 函数 xQueuePeekFromISR()

此函数是 xQueuePeek()的中断版本
此函数在读取成功以后不会将消息删除

BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,
		void * pvBuffer)

参数:

  • xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
  • pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中。

返回值:

  • pdTRUE: 从队列中读取数据成功。
  • pdFALSE: 从队列中读取数据失败。

14.7 队列使用

//查询 Message_Queue 队列中的总队列数量和剩余队列数量
void check_msg_queue(void)
{
	u8 *p;
	u8 msgq_remain_size; //消息队列剩余大小
	u8 msgq_total_size; //消息队列总大小
	taskENTER_CRITICAL(); //进入临界区
	msgq_remain_size=uxQueueSpacesAvailable(Message_Queue);//得到队列剩余大小 (1)
	msgq_total_size=uxQueueMessagesWaiting(Message_Queue)+\ (2)
	uxQueueSpacesAvailable(Message_Queue);//得到队列总大小,总大小=使用+剩余的。
	p=mymalloc(SRAMIN,20); //申请内存
	sprintf((char*)p,"Total Size:%d",msgq_total_size); //显示 DATA_Msg 消息队列总的大小
	LCD_ShowString(10,150,100,16,16,p);
	sprintf((char*)p,"Remain Size:%d",msgq_remain_size); //显示 DATA_Msg 剩余大小
	LCD_ShowString(10,190,100,16,16,p);
	myfree(SRAMIN,p); //释放内存
	taskEXIT_CRITICAL(); //退出临界区
}

main()函数

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4
	delay_init(168); //初始化延时函数
	uart_init(115200); //初始化串口
	LED_Init(); //初始化 LED 端口
	KEY_Init(); //初始化按键
	BEEP_Init(); //初始化蜂鸣器
	LCD_Init(); //初始化 LCD
	TIM9_Int_Init(5000,16800-1); //初始化定时器 9,周期 500ms
	my_mem_init(SRAMIN); //初始化内部内存池
	freertos_load_main_ui(); //加载主 UI
	//创建开始任务
	xTaskCreate((TaskFunction_t )start_task, //任务函数
	(const char* )"start_task", //任务名称
	(uint16_t )START_STK_SIZE, //任务堆栈大小
	(void* )NULL, //传递给任务函数的参数
	(UBaseType_t )START_TASK_PRIO, //任务优先级
	(TaskHandle_t* )&StartTask_Handler); //任务句柄
	vTaskStartScheduler(); //开启任务调度
}

任务函数

//按键消息队列的数量
#define KEYMSG_Q_NUM 1 //按键消息队列的数量 (1)
#define MESSAGE_Q_NUM 4 //发送数据的消息队列的数量 (2)
QueueHandle_t Key_Queue; //按键值消息队列句柄
QueueHandle_t Message_Queue; //信息队列句柄
//开始任务任务函数
void start_task(void *pvParameters)
{
	taskENTER_CRITICAL(); //进入临界区
	//创建消息 Key_Queue
	Key_Queue=xQueueCreate(KEYMSG_Q_NUM,sizeof(u8)); (1)
	//创建消息 Message_Queue,队列项长度是串口接收缓冲区长度
	Message_Queue=xQueueCreate(MESSAGE_Q_NUM,USART_REC_LEN); (2)
	//创建 TASK1 任务
	xTaskCreate((TaskFunction_t )task1_task,
			(const char* )"task1_task",
			(uint16_t )TASK1_STK_SIZE,
			(void* )NULL,
			(UBaseType_t )TASK1_TASK_PRIO,
			(TaskHandle_t* )&Task1Task_Handler);
	//创建 TASK2 任务
	xTaskCreate((TaskFunction_t )Keyprocess_task,
			(const char* )"keyprocess_task",
			(uint16_t )KEYPROCESS_STK_SIZE,
			(void* )NULL,
			(UBaseType_t )KEYPROCESS_TASK_PRIO,
			(TaskHandle_t* )&Keyprocess_Handler);
	vTaskDelete(StartTask_Handler); //删除开始任务
	taskEXIT_CRITICAL(); //退出临界区
}

15 信号量

一般用来进行资源管理和任务同步。
信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。

15.1 信号量简介

信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙才能够执行。
信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。

  • 在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了
  • 当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行
  • 当中断发生的时候就只释放信号量,如果任务获取到信号量说明中断发生了,那么就开始完成相应的处理,好处就是中断执行时间非常短。

15.2 二值信号量

15.2.1 简介

通常用于互斥访问或同步。

  • 二值信号量和互斥信号量非常类似
  • 但互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。

二值信号更适合用于同步(任务与任务或任务与中断的同步)。
互斥信号量适合用于简单的互斥访问。

信号量 API 函数允许设置一个阻塞时间

  • 阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。
  • 如果多个任务同时阻塞在同一一个信号量上的话那么优先级最高的哪个任务优先获得信号量。

其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的。

如果使用一个任务去轮询的查询很浪费CPU 资源的,而且也阻止了其他任务的运行。

最理想的方法就是当没有网络数据的时候网络任务就进入阻塞态,把 CPU 让给其他的任务,当有数据的时候网络任务才去执行。

网络任务只是在一直的获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。

这样做相当于推迟了中断处理过程。

二值信号量的工作过程:

  • 二值信号量无效
    • 任务 Task 通过函数 xSemaphoreTake()获取信号量,任务 Task 进入阻塞态。
      在这里插入图片描述
  • 中断释放信号量
    • 在中断服务函数中通过函数 xSemaphoreGiveFromISR()释放信号量,因此信号量变为有效。

在这里插入图片描述

  • 任务获取信号量成功
    • 信号量已经有效了,所以任务 Task 获取信号量成功,任务从阻塞态解除,开始执行相关的处理过程。
      在这里插入图片描述
  • 信号量再次无效
    • 任务做完相关的处理以后就会再次调用函数xSemaphoreTake()获取信号量。
    • 执行完第三步以后二值信号量就已经变为无效的了,所以任务将再次进入阻塞态

15.2.2 创建二值信号量

在这里插入图片描述函数 vSemaphoreCreateBinary ()
此函数是老版本 FreeRTOS 中的创建二值信号量函数,新版本已经不再使用了。在文件 semphr.h 中有定义。

void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
  • xSemaphore:保存创建成功的二值信号量句柄。
  • 此 函 数 是 个 宏 , 具 体 创 建 过 程 是 由 函 数xQueueGenericCreate()来完成的

返回值:

  • NULL: 二值信号量创建失败。
  • 其他值: 二值信号量创建成功。

函数 xSemaphoreCreateBinary()
此函数是 vSemaphoreCreateBinary()的新版本
所需要的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的。
创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake()是获取不到的。

emaphoreHandle_t xSemaphoreCreateBinary( void )
  • 返回值同上
  • 此函数也是 个宏, 具体 创建过程是 由函数xQueueGenericCreate()来完成

函数 xSemaphoreCreateBinaryStatic()
此函数也是创建二值信号量的
此函数创建二值信号量的话信号量所需要的RAM 需要由用户来分配。

emaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )

参数:

  • pxSemaphoreBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:

  • NULL: 二值信号量创建失败。
  • 其他值: 创建成功的二值信号量句柄。

15.2.3 释放信号量

在这里插入图片描述释放信号量也分为任务级和中断级。
不管是二值信号量、计数型信号量还是互斥信号量,它们都使用表 14.2.4.1 中的函数释放信号量。
递归互斥信号量有专用的释放函数。

函数 xSemaphoreGive()
此函数用于释放二值信号量、计数型信号量或互斥信号量的,函数原型如下:
BaseType_t xSemaphoreGive( xSemaphore )

  • 此函数是一个宏,真正释放信号量的过程是由函数 xQueueGenericSend()来完成

参数:

  • xSemaphore:要释放的信号量句柄。

返回值:

  • pdPASS: 释放信号量成功。
  • errQUEUE_FULL: 释放信号量失败。

函数 xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量
只能用来释放二值信号量和计数型信号量
绝对不 能 用 来 在 中 断 服 务 函 数 中 释 放 互 斥 信 号 量

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
		BaseType_t * pxHigherPriorityTaskWoken)
  • 此 函 数 是 一 个 宏 , 真 正 执 行 的 是 函 数xQueueGiveFromISR()

参数:

  • xSemaphore: 要释放的信号量句柄。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdPASS: 释放信号量成功。
  • errQUEUE_FULL: 释放信号量失败。

15.2.4 获取信号量

在这里插入图片描述不管是二值信号量、计数型信号量还是互斥信号量,它们都使用表 14.2.5.1 中的函数获取信号量

函数 xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
		TickType_t xBlockTime)

-此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive ()来完成的。

  • 函数 xQueueGenericReceive()如果队列为空并且阻塞时间为 0 的话就立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务添加到延时列表中。

参数:

  • xSemaphore:要获取的信号量句柄。
  • xBlockTime: 阻塞时间。

返回值:

  • pdTRUE: 获取信号量成功。
  • pdFALSE: 超时,获取信号量失败。

此函数在文件 semphr.h 中有定义。

函数 xSemaphoreTakeFromISR ()
此函数用于在中断服务函数中获取信号量
此函数用于获取二值信号量和计数型信号量
绝 对 不 能 使 用 此 函 数 来 获 取 互 斥 信 号 量

BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)
  • 此 函 数 是 一 个 宏 , 真 正 执 行 的 是 函 数xQueueReceiveFromISR ()

参数:

  • xSemaphore: 要获取的信号量句柄。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdPASS: 获取信号量成功。
  • pdFALSE: 获取信号量失败。

15.2.5 代码

/将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{
	u8 i;
	for(i=0;i<len;i++)
	{
		if((96<str[i])&&(str[i]<123)) //小写字母
		str[i]=str[i]-32; //转换为大写
	}
}

开始任务任务函数

void start_task(void *pvParameters)
{
	taskENTER_CRITICAL(); //进入临界区
	//创建二值信号量
	BinarySemaphore=xSemaphoreCreateBinary();
	//创建 TASK1 任务
	xTaskCreate((TaskFunction_t )task1_task,
			(const char* )"task1_task",
			(uint16_t )TASK1_STK_SIZE,
			(void* )NULL,
			(UBaseType_t )TASK1_TASK_PRIO,
			(TaskHandle_t* )&Task1Task_Handler);
	//创建 TASK2 任务
	xTaskCreate((TaskFunction_t )DataProcess_task,
			(const char* )"keyprocess_task",
			(uint16_t )DATAPROCESS_STK_SIZE,
			(void* )NULL,
			(UBaseType_t )DATAPROCESS_TASK_PRIO,
			(TaskHandle_t* )&DataProcess_Handler);
	vTaskDelete(StartTask_Handler); //删除开始任务
	taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函

task1 任务函数

void task1_task(void *pvParameters)
{
while(1)
{
	LED0=!LED0;
	vTaskDelay(500); //延时 500ms,也就是 500 个时钟节拍
}
}
//DataProcess_task 函数
void DataProcess_task(void *pvParameters)
{
u8 len=0;
u8 CommandValue=COMMANDERR;
BaseType_t err=pdFALSE;
u8 *CommandStr;
POINT_COLOR=BLUE;
while(1)
{
if(BinarySemaphore!=NULL)
{
	err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY);//获取信号量 (1)
	if(err==pdTRUE) //获取信号量成功
	{
		len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度
		CommandStr=mymalloc(SRAMIN,len+1); //申请内存
		sprintf((char*)CommandStr,"%s",USART_RX_BUF);
		CommandStr[len]='\0'; //加上字符串结尾符号
		LowerToCap(CommandStr,len); //将字符串转换为大写 (2)
		CommandValue=CommandProcess(CommandStr); //命令解析 (3)
		if(CommandValue!=COMMANDERR) //接收到正确的命令
		{
		LCD_Fill(10,90,210,110,WHITE); //清除显示区域
		LCD_ShowString(10,90,200,16,16,CommandStr);//在 LCD 上显示命令
		printf("命令为:%s\r\n",CommandStr);
		switch(CommandValue) //处理命令 (4)
		{
		case LED1ON:
		LED1=0;
		break;
		case LED1OFF:
		LED1=1;
		break;
		case BEEPON:
		BEEP=1;
		break;
		case BEEPOFF:
		BEEP=0;
		break;
		}
		}
		else
		{
		printf("无效的命令,请重新输入!!\r\n");
		}
		USART_RX_STA=0;
		memset(USART_RX_BUF,0,USART_REC_LEN);//串口接收缓冲区清零
		myfree(SRAMIN,CommandStr); //释放内存
		}
	}
	else if(err==pdFALSE)
	{
	vTaskDelay(10); //延时 10ms,也就是 10 个时钟节拍
	}
}
}

中断服务函数如下:

extern SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
void USART1_IRQHandler(void) //串口 1 中断服务程序
{
u8 Res;
BaseType_t xHigherPriorityTaskWoken;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
	if((USART_RX_STA&0x8000)==0)//接收未完成
	{
	if(USART_RX_STA&0x4000)//接收到了 0x0d
	{
	if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
	else USART_RX_STA|=0x8000; //接收完成了
	}
	else //还没收到 0X0D
	{
	if(Res==0x0d)USART_RX_STA|=0x4000;
	else
	{
	USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
	USART_RX_STA++;
	if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
	}
	}
	}
	}
	//释放二值信号量
	if((USART_RX_STA&0x8000)&&(BinarySemaphore!=NULL))
	{
	//释放二值信号量
		xSemaphoreGiveFromISR(BinarySemaphore,&xHigherPriorityTaskWoken); (1)
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//如果需要的话进行一次任务切换
	}
}

15.2 计数型信号量

15.2.1 计数型信号量简介

也叫做数值信号量
二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。
同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可
应用场景:

  1. 事件计数,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值 ) , 其 他 任 务 会 获 取 信 号 量来处理事件(信 号 量 计 数 值 减 一)。
  2. 资源管理: 信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。
    • 一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。
    • 当信号量值为 0 的时候说明没有资源了。
    • 当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。

15.2.2 创建计数型信号量

在这里插入图片描述函数 xSemaphoreCreateCounting()
此函数用于创建一个计数型信号量
所需要的内存通过动态内存管理方法分配。

SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )
  • 本质是一个宏,真正完成信号量创建的是函数 xQueueCreateCountingSemaphore()

参数:

  • uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
  • uxInitialCount: 计数信号量初始值。

返回值:

  • NULL: 计数型信号量创建失败。
  • 其他值: 计数型信号量创建成功,返回计数型信号量句柄。
函数 xSemaphoreCreateCountingStatic()

此函数也是用来创建计数型信号量的,使用此函数创建计数型信号量的时候所需要的内存
需要由用户分配。

SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
	UBaseType_t uxInitialCount,
	StaticSemaphore_t * pxSemaphoreBuffer )
  • 此函数也是一个宏,真正执行的是函数 xQueueCreateCountingSemaphoreStatic()

参数:

  • uxMaxCount: 计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
  • uxInitialCount: 计数信号量初始值。
  • pxSemaphoreBuffer:指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:

  • NULL: 计数型信号量创建失败。
  • 其他值: 计数型号量创建成功,返回计数型信号量句柄。

15.2.3 释放和获取计数信号量

计数型信号量的释放和获取与二值信号量相同

15.3 优先级翻转

在使用二值信号量的时候会遇到很常见的一个问题.
先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象
这样会破坏任务的预期顺序,可能会导致严重的后果

在这里插入图片描述
(1) 任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。
(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。
(3) 任务 L 获得信号量并开始使用该共享资源。
(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。
(5) 任务 H 开始运行。
(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。
(7) 任务 L 继续运行。
(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务L 的 CPU 使用权。
(9) 任务 M 处理该处理的事。
(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L。
(11) 任务 L 继续运行。
(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换。
(13) 任务 H 得到该信号量并接着运行。
在这种情况下,任务 H 的优先级实际上降到了任务 L 的优先级水平。因为任务 H 要一直等待直到任务 L 释放其占用的那个共享资源。
由于任务 M 剥夺了任务 L 的 CPU 使用权,使得任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。

当一个低优先级任务和一个高优先级任务同时使用同一个信号量,而系统中还有其他中等优先级任务时。如果低优先级任务获得了信号量,那么高优先级的任务就会处于等待状态,但是,中等优先级的任务可以打断低优先级任务而先于高优先级任务运行(此时高优先级的任务在等待信号量 ,所以不能运行),这是就出现了优先级翻转的现象。

解决优先级反转——互斥信号量!

15.4 互斥信号量

15.4.1 简介

互斥信号量其实就是一个拥有优先级继承的二值信号量

互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙

互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。

当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。

此时高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。

降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。并不能完全的消除优先级翻转。

互斥信号量不能用于中断服务函数中

  • 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
  • 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

15.4.2 创建互斥信号量

在这里插入图片描述函数 xSemaphoreCreateMutex()
此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。

SemaphoreHandle_t xSemaphoreCreateMutex( void )
  • 本质是一个宏,真正完成信号量创建的是函数 xQueueCreateMutex()

参数:

  • 无。

返回值:

  • NULL: 互斥信号量创建失败。
  • 其他值: 创建成功的互斥信号量的句柄。

函数 xSemaphoreCreateMutexStatic()
此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的RAM 需要由用户来分配。

SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t     *pxMutexBuffer )
  • 此函数是个宏,具体创建过程是通过函数 xQueueCreateMutexStatic ()

参数:

  • pxMutexBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:

  • NULL: 互斥信号量创建失败。
  • 其他值: 创建成功的互斥信号量的句柄。

15.4.3 释放互斥信号量

释 放 互 斥 信 号 量 的 时 候 和 二 值 信 号 量 、 计 数 型 信 号 量 一 样 , 都 是 用 的 函 数xSemaphoreGive()

  • 实际上完成信号量释放的是函数 xQueueGenericSend()
  • 不过由于互斥信号量涉及到优先级继承的问题,所以具体处理过程会有点区别。

15.4.4 获取互斥信号量

获 取 互 斥 信 号 量 的 函 数 同 获 取 二 值 信 号 量 和 计 数 型 信 号 量 的 函 数 相 同 , 都 是xSemaphoreTake()

  • 实际执行信号量获取的函数是 xQueueGenericReceive()
  • 获取互斥信号量的过程也需要处理优先级继承的问题

15.5递归互斥信号量

15.5.1 简介

一个特殊的互斥信号量

  • 正常已经获取了互斥信号量的任务就不能再次获取这个互斥信号量
  • 但已经获取了递归互斥信号量的任务可以再次获取这个递归互斥信号量,而且次数不限!
  • 但一个任务使用函数 xSemaphoreTakeRecursive()成功的获取了多少次递归互斥信号量就得使用函数 xSemaphoreGiveRecursive()释放多少次

递归互斥信号量也有优先级继承的机制,所以当任务使用完递归互斥信号量以后一定要记得释放。

要使用递归互斥信号量的话宏 configUSE_RECURSIVE_MUTEXES 必须为 1!

同互斥信号量一样,递归互斥信号量不能用在中断服务函数中。

15.5.2 创建互斥信号量

在这里插入图片描述

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )
SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic( StaticSemaphore_t *pxMutexBuffer )

与前文相同,不再赘述

15.5.3 释放递归互斥信号量

递归互斥信号量有专用的释放函数:xSemaphoreGiveRecursive()

#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )

真 正 的 释 放 是 由 函 数xQueueGiveMutexRecursive()来完成的

15.5.4 获取递归互斥信号量

递归互斥信号量的获取使用函数 xSemaphoreTakeRecursive(),此函数是个宏

#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
	xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )

第一个参数是要获取的递归互斥信号量句柄,第二个参数是阻塞时间。
真正的获取过程是由函数 xQueueTakeMutexRecursive()来完成的

16 FreeRTOS 软件定时器

FreeRTOS 也提供了定时器功能,不过是软件定时器,软件定时器的精度肯定没有硬件定时器那么高。
当 MCU 的硬件定时器不够的时候就可以考虑使用 FreeRTOS 的软件定时器。

16.1 软件定时器简介

软件定时器允许设置一段时间,当设置的时间到达之后就执行指定的功能函数,被定时器调用的这个功能函数叫做定时器的回调函数。

当定时器的定时周期到了以后就会执行回调函数。

16.2 定时器服务/Daemon 任务

定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务(或 Daemon)任务来提供的。

FreeRTOS 提供了很多定时器有关的 API 函数。
这些 API 函数大多都使用 FreeRTOS的队列发送命令给定时器服务任务。

  • 这个队列叫做定时器命令队列。用户不能直接访问

在这里插入图片描述

  • 左侧部分属于用户应用程序的一部分,并且会在某个用户创建的用户任务中调用。
  • 图中右侧部分是定时器服务任务的任务函数
  • 定时器命令队列将用户应用任务和定时器服务任务连接在一起。

16.3 定时器相关配置

软件定时器有一个定时器服务任务和定时器命令队列。相关的配置也是放到文件 FreeRTOSConfig.h 中。
涉及到的配置如下:

16.3.1 configUSE_TIMERS

如果要使用软件定时器的话宏 configUSE_TIMERS 一定要设置为 1,当设置为 1 的话定时器服务任务就会在启动 FreeRTOS 调度器的时候自动创建。

16.3.2 configTIMER_TASK_PRIORITY

设置软件定时器服务任务的任务优先级,可以为 0~( configMAX_PRIORITIES-1)。

16.3.3 configTIMER_QUEUE_LENGTH

此宏用来设置定时器命令队列的队列长度。

16.3.4 configTIMER_TASK_STACK_DEPTH

此宏用来设置定时器服务任务的任务堆栈大小,单位为字,不是字节!

16.3.5 单次定时器和周期定时器

软件定时器分两种:单次定时器和周期定时器
单次定时器

  • 单次定时器的话定时器回调函数就执行一次,然后定时器就会停止运行。
  • 对于单次定时器我们可以再次手动重新启动

周期定时器一旦启动以后就会在执行完回调函数以后自动的重新启动

在这里插入图片描述

16.4 复位软件定时器

有时候我们可能会在定时器正在运行的时候需要复位软件定时器
复位软件定时器的话会重新计算定时周期到达的时间点
这个新的时间点是相对于复位定时器的那个时刻计算的

在这里插入图片描述
提供了两个函数用于复位软件定时器:
在这里插入图片描述

16.4.1 函数 xTimerReset()

此函数只能用在任务中,不能用于中断服务函数
此函数是一个宏,真正执行的是函数 xTimerGenericCommand()

BaseType_t xTimerReset( TimerHandle_t xTimer,
	TickType_t xTicksToWait )

参数

  • xTimer: 要复位的软件定时器的句柄。
  • xTicksToWait: 设置阻塞时间
    • 调用函数 xTimerReset ()开启软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_RESET 命令
    • 既然是向队列发送消息,那肯定会涉及到入队阻塞时间的设置。

返回值:

  • pdPASS: 软件定时器复位成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器复位失败,命令发送失败。

16.4.1 函数 xTimerResetFromISR()

xTimerReset()的中断版本,此函数用于中断服务函数中

BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
	BaseType_t * pxHigherPriorityTaskWoken );

此函数是一个宏,真正执行的是函数 xTimerGenericCommand()。

参数:

  • xTimer: 要复位的软件定时器的句柄。
  • pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换

返回值:

  • pdPASS: 软件定时器复位成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器复位失败,命令发送失败。

16.5 创建软件定时器

在这里插入图片描述

16.5.1 函数 xTiemrCreate()

此函数用于创建一个软件定时器,所需要的内存通过动态内存管理方法分配。
新创建的软件 定 时 器是 未 运 行 的 。

TimerHandle_t xTimerCreate( const char * const pcTimerName,
		TickType_t xTimerPeriodInTicks,
		UBaseType_t uxAutoReload,
		void * pvTimerID,
		TimerCallbackFunction_t pxCallbackFunction )

参数:

  • pcTimerName: 软件定时器名字,名字是一串字符串,用于调试使用。
  • xTimerPeriodInTicks : 软 件 定 时 器 的 定 时 器 周 期 , 单 位 是 时 钟 节 拍 数 。
  • uxAutoReload: 设置定时器模式,单次定时器还是周期定时器
  • pvTimerID: 定时器 ID 号,一般情况下每个定时器都有一个回调函数,当定时器定时周期到了以后就会执行这个回调函数。
  • pxCallbackFunction: 定时器回调函数

返回值:

  • NULL: 软件定时器创建失败。
  • 其他值: 创建成功的软件定时器句柄。

函 数 xTimerStart() 、 xTimerReset() 、xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和 xTimerChangePeriodFromISR()可以使新创建的定时器进入活动状态

16.5.2 函数 xTimerCreateStatic()

此函数用于创建一个软件定时器,所需要的内存需要用户自行分配。
新创建的软件定时器是未运行的。

TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
		TickType_t xTimerPeriodInTicks,
		UBaseType_t uxAutoReload,
		void * pvTimerID,
		TimerCallbackFunction_t pxCallbackFunction,
		StaticTimer_t * pxTimerBuffer )

函数 xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod()和 xTimerChangePeriodFromISR()可以使新创建的定时器进入活动状态。

16.6 开启软件定时器

在这里插入图片描述

16.6.1 函数 xTimerStart()

启动软件定时器

  • 如果软件定时器没有运行的话调用函数 xTimerStart()就会计算定时器到期时间。
  • 如果软件定时器正在运行的话调用函数 xTimerStart()的结果和 xTimerReset()一样。

此函数是个宏,真正执行的是函数 xTimerGenericCommand。
实质上是向定时器命令队列发送一条 tmrCOMMAND_START 命令。
函数原型如下:

BaseType_t xTimerStart( TimerHandle_t xTimer,TickType_t xTicksToWait )

参数:

  • xTimer: 要开启的软件定时器的句柄。
  • xTicksToWait: 设置阻塞时间

返回值:

  • pdPASS: 软件定时器开启成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器开启失败,命令发送失败。

16.6.2 函数 xTimerStartFromISR()

此函数是函数 xTimerStart()的中断版本
真正执行的是函数 xTimerGenericCommand()

BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,BaseType_t * pxHigherPriorityTaskWoken );

参数:

  • xTimer: 要开启的软件定时器的句柄。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换。这个变量的值是由函数来设置的
    • 用户只需要提供一个变量来保存这个值就行了。
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdPASS: 软件定时器开启成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器开启失败,命令发送失败。

16.7 停止软件定时器

在这里插入图片描述

16.7.1 函数 xTimerStop()

此函数用于停止一个软件定时器
此函数是一个宏,真正调用的是函数 xTimerGenericCommand()

BaseType_t xTimerStop ( TimerHandle_t xTimer,TickType_t xTicksToWait )

参数:

  • xTimer: 要停止的软件定时器的句柄。
  • xTicksToWait: 设置阻塞时间,调用函数 xTimerStop()停止软件定时器其实就是向定时器命令队列发送一条 tmrCOMMAND_STOP 命令。

返回值:

  • pdPASS: 软件定时器停止成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器停止失败,命令发送失败。

16.7.2 函数 xTimerStopFromISR()

此函数是 xTimerStop()的中断版本
此函数是一个宏,真正执行的是函数 xTimerGenericCommand(),函数原型如下:

BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,BaseType_t * pxHigherPriorityTaskWoken );

参数:

  • xTimer: 要停止的软件定时器句柄。
  • pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换
    • 用户只需要提供一个变量来保存这个值就行了。
    • 当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdPASS: 软件定时器停止成功,其实就是命令发送成功。
  • pdFAIL: 软件定时器停止失败,命令发送失败。

17 FreeRTOS 事件标志组

使用信号量来同步的话任务只能与单个的事件或任务进行同步。
有时候某个任务可能会需要与多个事件或任务进行同步,FreeRTOS 为此提供了一个可选的解决方法,那就是事件标志组。

17.2 事件标志组简介

17.2.1 事件位(事件标志)

事件位用来表明某个事件是否发生,事件位通常用作事件标志。

  • 当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置 1
  • 当队列中没有消息需要处理的时候就可以将这个位(标志)置 0。

17.2.2 事件组

一个事件组就是一组的事件位,事件组中的事件位通过位编号来访问。

  • 事件标志组的 bit0 表示队列中的消息是否处理掉。
  • 事件标志组的 bit1 表示是否有消息需要从网络中发送出去。

17.2.3 事件标志组和事件位的数据类型

事件标志组的数据类型为 EventGroupHandle_t

  • 当 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位。
  • 当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位。

事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中。
在 event_groups.h 中有如下定义:

typedef TickType_t EventBits_t;

数据类型 TickType_t 在文件 portmacro.h 中有如下定义:

#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 位的数据类型。ventBits_t 类型的变量可以存储 24 个事件位,另外的那高 8 位有其他用。

对于 STM32 来说一个事件标志组最多可以存储 24 个事件位

在这里插入图片描述

17.3 创建事件标志组

FreeRTOS 提供了两个用于创建事件标志组的函数

在这里插入图片描述

17.3.1 函数 xEventGroupCreate()

此函数用于创建一个事件标志组,所需要的内存通过动态内存管理方法分配。
事 件 标 志 组 可 用 的 bit 数 取 决 于 configUSE_16_BIT_TICKS。

EventBits_t 类型的变量用来存储事件标志组中的各个事件位,函数原型如下:

EventGroupHandle_t xEventGroupCreate( void )

参数:
无。
返回值:

  • NULL: 事件标志组创建失败。
  • 其他值: 创建成功的事件标志组句柄。

17.3.2 函数 xEventGroupCreateStatic()

此函数用于创建一个事件标志组定时器,所需要的内存需要用户自行分配。

EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer )

参数:

  • pxEventGroupBuffer: 参数指向一个 StaticEventGroup_t 类型的变量,用来保存事件标志组结构体。

返回值:
NULL: 事件标志组创建失败。
其他值: 创建成功的事件标志组句柄。

17.4 设置事件位

在这里插入图片描述在这里插入图片描述

17.4.1函数 xEventGroupClearBits()

将事件标志组中的指定事件位清零
只能用在任务中,不能用在中断服务函数中!

EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToClear );

参数:

  • xEventGroup: 要操作的事件标志组的句柄。
  • uxBitsToClear: 要清零的事件位,比如要清除 bit3 的话就设置为 0X08。

返回值:
任何值: 将指定事件位清零之前的事件组值。

17.4.2 函数 xEventGroupClearBitsFromISR()

此函数为函数 xEventGroupClearBits()的中断级版本
此函数原型如下:

BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToSet );

返回值:
pdPASS: 事件位清零成功。
pdFALSE: 事件位清零失败。

17.4.3 函数 xEventGroupSetBits()

设置指定的事件位为 1,此函数只能用在任务中,不能用于中断服务函数

不能用于中断服务函数,此函数原型如下:

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToSet );

参数:

  • xEventGroup: 要操作的事件标志组的句柄。
  • uxBitsToClear: 指定要置 1 的事件位,比如要将 bit3 值 1 的话就设置为 0X08。

返回值:
任何值: 在将指定事件位置 1 后的事件组值。

17.4.4 函数 xEventGroupSetBitsFromISR()

此函数也用于将指定的事件位置 1,此函数是 xEventGroupSetBits()的中断版本,用在中断
服务函数中。

BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
		const EventBits_t uxBitsToSet,
		BaseType_t * pxHigherPriorityTaskWoken );

参数:
xEventGroup: 要操作的事件标志组的句柄。
uxBitsToClear: 指定要置 1 的事件位,比如要将 bit3 值 1 的话就设置为 0X08。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换。

返回值:
pdPASS: 事件位置 1 成功。
pdFALSE: 事件位置 1 失败。

17.5 获取事件标志组值

在这里插入图片描述

17.5.1 函数 xEventGroupGetBits()

此函数用于获取当前事件标志组的值
此函数是个宏,真正执行的是函数 xEventGroupClearBits()

EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup )

参数:
xEventGroup: 要获取的事件标志组的句柄。

返回值:
任何值:当前事件标志组的值。

17.5.2 函数 xEventGroupGetBitsFromISR()

获取当前事件标志组的值,此函数是 xEventGroupGetBits()的中断版本

EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup )

参数:
xEventGroup: 要获取的事件标志组的句柄。
返回值:
任何值: 当前事件标志组的值。

17.6 等待指定的事件位

某个任务可能需要与多个事件进行同步,那么这个任务就需要等待并判断多个事件位。

使用函数 xEventGroupWaitBits()可以完成这个功能。

调用函数以后如果任务要等待的事件位还没有准备好(置 1 或清零)的话任务就会进入阻塞态,直到阻塞时间到达或者所等待的事件位准备好。

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
	const EventBits_t uxBitsToWaitFor,
	const BaseType_t xClearOnExit,
	const BaseType_t xWaitForAllBits,
	const TickType_t xTicksToWait );

参数:

  • xEventGroup: 指定要等待的事件标志组。
  • uxBitsToWaitFord:指定要等待的事件位,比如要等待 bit0 和(或)bit2 的时候此参数就是 0X05
  • xClearOnExit: 此参数要是为 pdTRUE 的话,那么在退出此函数之前由参数 uxBitsToWaitFor所设置的这些事件位就会清零。
  • xWaitForAllBits:
    • 此参数如果设置为 pdTRUE 的话,当 uxBitsToWaitFor 所设置的这些事件位都置 1。或者指定的阻塞时间到的时候函数xEventGroupWaitBits()才会返回。
    • 当此函数为 pdFALSE 的话,只要 uxBitsToWaitFor 所设置的这些事件 位 其 中 的 任 意 一 个 置 1 , 或 者 指 定 的 阻 塞 时 间 到 的 话 函 数xEventGroupWaitBits()就会返回。
  • xTicksToWait: 设置阻塞时间,单位为节拍数。

返回值:
任何值: 返回当所等待的事件位置 1 以后的事件标志组的值,或者阻塞时间到。

  • 如果函数因为阻塞时间到而返回,这个返回值就不代表任何的含义。

18 FreeRTOS 任务通知

从 v8.2.0 版本开始,可以使用任务通知来代替信号量、消息队列、事件标志组等这些东西。

  • 使用任务通知的话效率会更高。

要 使 用 任 务 通 知 的 话 就 需 要 将 宏configUSE_TASK_NOTIFICATIONS 定义为 1。

FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。

18. 1 任务通知简介

任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态。

任务通知可以通过如下方法更新接收任务的通知值:

  • 不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。
  • 覆盖接收任务的通知值。
  • 更新接收任务通知值的一个或多个 bit。
  • 增加接收任务的通知值。

使用任务通知来实现二值信号量功能的时候,解除任务阻塞的时间比直接使用二值信号量要快 45%。

  • 任务通知的发送使用函数 xTaskNotify()或者 xTaskNotifyGive()
  • 这 个 通 知 值 会 一 直 被 保 存 着 , 直 到 接 收 任 务 调 用 函 数 xTaskNotifyWait() 或 者ulTaskNotifyTake()来获取这个通知值
  • FreeRTOS 的任务通知只能有一个接收任务
  • 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。

18.2 发送任务通知

在这里插入图片描述

18.2.1 函数 xTaskNotify()

此函数发送任务通知的时候带有通知值

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
	uint32_t ulValue,
	eNotifyAction eAction )

此函数是个宏,真正执行的函数 xTaskGenericNotify()
参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • ulValue: 任务通知值。
  • eAction: 任务通知更新的方法,eNotifyAction 是个枚举类型,在文件 task.h 中有定义
typedef enum
{
	eNoAction = 0,
	eSetBits, //更新指定的 bit
	eIncrement, //通知值加一
	eSetValueWithOverwrite, //覆写的方式更新通知值
	eSetValueWithoutOverwrite //不覆写通知值
} eNotifyAction;

返回值:

  • pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
  • pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。

18.2.2 函数 xTaskNotifyFromISR()

此函数用于发送任务通知,是函数 xTaskNotify()的中断版本

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
	uint32_t ulValue,
	eNotifyAction eAction,
	BaseType_t * pxHigherPriorityTaskWoken );

参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • ulValue: 任务通知值。
  • eAction: 任务通知更新的方法。
  • pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
    • 用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

此函数是个宏,真正执行的是函数 xTaskGenericNotifyFromISR()

返回值:

  • pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
  • pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。

18.2.3 函数 xTaskNotifyGive()

发送任务通知,相对于函数 xTaskNotify() ,此函数发送任务通知的时候不带有通知值。

BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

此函数是个宏,真正执行的是函数 xTaskGenericNotify()

参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。

返回值:

  • pdPASS: 此函数只会返回 pdPASS。

18.2.4 函数 vTaskNotifyGiveFromISR()

此函数为 xTaskNotifyGive()的中断版本

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
	BaseType_t * pxHigherPriorityTaskWoken );

参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
    • 用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

18.2.5 函数 xTaskNotifyAndQuery()

此函数和 xTaskNotify()很类似,此函数比 xTaskNotify()多一个参数,此参数用来保存更新前的通知值。

BaseType_t xTaskNotifyAndQuery ( TaskHandle_t xTaskToNotify,
	uint32_t ulValue,
	eNotifyAction eAction
	uint32_t * pulPreviousNotificationValue);

此函数是个宏,真正执行的是函数 xTaskGenericNotify()
参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • ulValue: 任务通知值。
  • eAction: 任务通知更新的方法。
  • pulPreviousNotificationValue:用来保存更新前的任务通知值。

返回值:

  • pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
  • pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。

18.2.6 函数 xTaskNotifyAndQueryFromISR()

此函数为 xTaskNorityAndQuery()的中断版本

BaseType_t xTaskNotifyAndQueryFromISR ( TaskHandle_t xTaskToNotify,
	uint32_t ulValue,
	eNotifyAction eAction,
	uint32_t * pulPreviousNotificationValue
	BaseType_t * pxHigherPriorityTaskWoken );

此函数同样为宏,真正执行的是函数 xTaskGenericNotifyFromISR()
参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • ulValue: 任务通知值。
  • eAction: 任务通知更新的方法。
  • pulPreviousNotificationValue:用来保存更新前的任务通知值。
  • pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换
    • 用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。

返回值:

  • pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
  • pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。

18. 3 任务通知通用发送函数

18. 3.1 xTaskGenericNotify 函数

任务级通知通用发送函数

xTaskNotify()、xTaskNotifyGive()和 xTaskNotifyAndQuery(),这三个函数最终调用的都是函数 xTaskGenericNotify()。

此函数在文件 tasks.c 中有定义

18.3.2 xTaskGenericNotifyFromISR函数

中断级任务通知发送函数
函数 xTaskNotifyFromISR()和 xTaskNotifyAndQueryFromISR()最终调用的都是函数 xTaskGenericNotifyFromISR()。

BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify,
	uint32_t ulValue,
	eNotifyAction eAction,
	uint32_t * pulPreviousNotificationValue,
	BaseType_t * pxHigherPriorityTaskWoken )

参数:

  • xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
  • ulValue: 任务通知值。
  • eAction: 任务通知更新的方法。
  • pulPreviousNotificationValue:用来保存更新前的任务通知值。
  • pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换

返回值:

  • pdFAIL: 当参数 eAction 设置为 eSetValueWithoutOverwrite 的时候,如果任务通知值没有更新成功就返回 pdFAIL。
  • pdPASS: eAction 设置为其他选项的时候统一返回 pdPASS。

函数 xTaskGenericNotifyFromISR()在文件 tasks.c 中有定义。

18.4 获取任务通知

获取任务通知的函数有两个
在这里插入图片描述

18.4.1 函数 ulTaskNotifyTake()

此函数为获取任务通知函数
当任务通知用作二值信号量或者计数型信号量的时候可以使用此函数来获取信号量。

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
	TickType_t xTicksToWait );

参数:

  • xClearCountOnExit: 参数为 pdFALSE 的话在退出函数 ulTaskNotifyTake()的时候任务通知值
    减一,类似计数型信号量。

    • 当此参数为 pdTRUE 的话在退出函数的时候任务任务通知值清零,类似二值信号量。
  • xTickToWait: 阻塞时间。

返回值

  • 任何值 : 任务通知值减少或者清零之前的值。

此函数在文件 tasks.c 中有定义。

18.4.3 函数 xTaskNotifyWait()

此函数也是用来获取任务通知的,不过此函数比 ulTaskNotifyTake()更为强大

  • 不管任务通知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取任 务 通 知
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
	uint32_t ulBitsToClearOnExit,
	uint32_t * pulNotificationValue,
	TickType_t xTicksToWait );

参数:

  • ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算。
    • 当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
  • ulBitsToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与 此 参 数 的 取 反 值 进 行 按 位 与 运 算
    • 当 此 参 数 为 0xffffffff 或 者ULONG_MAX 的时候就会将任务通知值清零。
  • pulNotificationValue:此参数用来保存任务通知值。
  • xTickToWait: 阻塞时间。

返回值

  • pdTRUE: 获取到了任务通知。
  • pdFALSE: 任务通知获取失败。

此函数在文件 tasks.c 中有定义

19 FreeRTOS 低功耗 Tickless 模式

19.1 STM32F4 低功耗模式

STM32F407 共有三种低功耗模式:

  • 睡眠(Sleep)模式。
  • 停止(Stop)模式。
  • 待机(Standby)模式。

在这里插入图片描述

19.1.1 睡眠(Sleep)模式

进入睡眠模式
进入睡眠模式有两种指令:WFI(等待中断)和 WFE(等待事件)。

Cortex-M 内核的 SCR(系统控制)寄存器可以选择使用立即休眠还是退出时休眠。

  • 当 SCR 寄存器的 SLEEPONEXIT(bit1)位为 0 的时候使用立即休眠
  • 当为 1 的时候使用退出时休眠。
  • 参考《权威指南》“第 9 章 低功耗和系统控制特性”章节

CMSIS(Cortex 微控制器软件接口标准)提供了两个函数来操作指令 WFI 和 WFE,我们可以直接使用这两个函数:__WFI 和__WFE。

退出休眠模式
如果使用 WFI 指令进入休眠模式的话那么任意一个中断都会将 MCU 从休眠模式中唤醒
如果使用 WFE 指令进入休眠模式的话那么当有事件发生的话就会退出休眠模式

当 STM32F429 处于休眠模式的时候 Cortex-M4F 内核停止运行,但是其他外设运行正常,比如 NVIC、SRAM 等

19.1.2 停止(Stop)模式

停止模式基于 Cortex-M4F 的深度休眠模式与外设时钟门控

  • 此模式下 1.2V 域的所有时钟都会停止,PLL、HSI 和 HSE RC 振荡器会被禁止,但是内部 SRAM 的数据会被保留。
  • 调压器可以工作在正常模式,也可配置为低功耗模式。
  • 如果有必要的话可以通过将 PWR_CR 寄存器的 FPDS 位置 1 来使 Flash 在停止模式的时候进入掉电状态
  • 当 Flash 处于掉电状态的时候 MCU从停止模式唤醒以后需要更多的启动延时。

在这里插入图片描述

19.1.3 待机(Standby)模式

待机模式的功耗最低。
待机模式是基于 Cortex-M4F 的深度睡眠模式的

  • 调压器被禁止。
  • 1.2V 域断电
  • PLL、HSI 振荡器和 HSE 振荡器也被关闭。
  • 除了备份区域和待机电路相关的寄存器外,SRAM 和其他寄存器的内容都将丢失

在这里插入图片描述
退出待机模式的话会导致 STM32F4 重启,所以待机模式的唤醒延时也是最大的。

19.2 Tickless 模式

当处理器处理空闲任务的时候就进入低功耗模式,
当需要处理应用层代码的时候就将处理器从低功耗模式唤醒。
一般会在空闲任务的钩子函数中执行低功耗相关处理

  • 比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。

Tickless 模式
当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断)。
只有当其他中断发生或者其他任务需要处理的时候处理器才会被从低功耗模式中唤醒。

问题一:关闭系统节拍中断会导致系统节拍计数器停止,系统时钟就会停止。

  • FreeRTOS 的系统时钟是依赖于系统节拍中断(滴答定时器中断)的,如果关闭了系统节拍中断的话就会导致系统时钟停止运行,这是绝对不允许的!
  • 解决方法,记录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间。

问题二:如何保证下一个要运行的任务能被准确的唤醒?

  • 开一个定时器,进入低功耗模式之前能够获取到还有多长时间运行下一个任务,定时器的定时周期设置为这个时间值。

19.3 Tickless 具体实现

  1. 宏 configUSE_TICKLESS_IDLE
    要想使用 Tickless 模式,首先必须将 FreeRTOSConfig.h 中的宏configUSE_TICKLESS_IDLE设置为 1。
#define configUSE_TICKLESS_IDLE 1 //1 启用低功耗 tickless 模式
  1. 宏 portSUPPRESS_TICKS_AND_SLEEP()
    使 能 Tickless 模 式 以 后当 下 面 两 种 情 况 都 出 现 的 时 候 FreeRTOS 内 核 就 会 调 用 宏portSUPPRESS_TICKS_AND_SLEEP()来处理低功耗相关的工作。
  • 空闲任务是唯一可运行的任务,因为其他所有的任务都处于阻塞态或者挂起态。
  • 系统处于低功耗模式的时间至少大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP个时钟节拍
  • portSUPPRESS_TICKS_AND_SLEEP()有个参数,此参数用来指定还有多长时间将有任务进入就绪态,其实就是处理器进入低功耗模式的时长
  • portSUPPRESS_TICKS_AND_SLEEP()会被空闲任务调用来完成具体的低功耗工作。
    • 本来是由用户根据自己所选择的平台来编写的, FreeRTOS 已经帮我们做好了。

宏 portSUPPRESS_TICKS_AND_SLEEP 在文件 portmacro.h 中如下定义:

#ifndef portSUPPRESS_TICKS_AND_SLEEP
	extern void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime );
	#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )
		vPortSuppressTicksAndSleep( xExpectedIdleTime )
#endif

portSUPPRESS_TICKS_AND_SLEEP() 的 本 质 就 是 函 数vPortSuppressTicksAndSleep(),此函数在文件 port.c 中有定义。

__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
	uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements,\
	ulSysTickCTRL;
	TickType_t xModifiableIdleTime;
	//确保滴答定时器的 Reload(重装载)值不会溢出,也就是不能超过滴答定时器最大计数值。
	if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks ) (1)
	{
	xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
	}
	//停止滴答定时器。
	portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;

太多了 省略
  • 调用函数__enable_irq()重新打开中断,因为在(4)中我们调用函数__disable_irq()关闭了中断。

19.3.1 宏 configPRE_SLEEP_PROCESSING ()和 configPOST_SLEEP_PROCESSING()

在真正的低功耗设计中不仅仅是将处理器设置到低功耗模式就行了,还需要做一些其他的处理

  1. 将处理器降低到合适的频率,因为频率越低功耗越小
  2. 修改时钟源,晶振的功耗肯定比处理器内部的时钟源高,进入低功耗模式以后可以切换到内部时钟源
  3. 关闭其他外设时钟,比如 IO 口的时钟。
  4. 关闭板子上其他功能模块电源,这个需要在产品硬件设计的时候就要处理好

configPRE_SLEEP_PROCESSING()需要用户自己编写

  • 比如在进入低功耗模式之前我们降低了处理器频率、关闭了某些外设时钟等

宏configPOST_SLEEP_PROCESSING()

  • 退出低功耗模式以后就 需 要 恢 复 处 理 器 频 率 、 重 新 打 开 外 设 时 钟 等

这两个宏会被函数 vPortSuppressTicksAndSleep()调用,我们可以在 FreeRTOSConfig.h 定义这两个宏

/********************************************************************************/
/* FreeRTOS 与低功耗管理相关配置 */
/********************************************************************************/
extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING PreSleepProcessing
//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING PostSleepProcessing

函数 PreSleepProcessing()和 PostSleepProcessing()可以在任意一个 C 文件中编写

19.3.2 宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP

对 工 作 在 低 功 耗 模 式 的 时 间 做 个 限 制 , 不 能 太 短
此宏默认在文件FreeRTOS 中有定义,如下:

#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP
	#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif
#if configEXPECTED_IDLE_TIME_BEFORE_SLEEP < 2
	#error configEXPECTED_IDLE_TIME_BEFORE_SLEEP must not be less than 2
#endif

默认情况下 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 为 2 个时钟节拍

20 FreeRTOS 内存管理

20.1 FreeRTOS 内存管理简介

使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。

标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但在FreeRTOS中不行。

  • 在小型的嵌入式系统中效率不高。
  • 会占用很多的代码空间。
  • 它们不是线程安全的。
  • 具有不确定性,每次执行的时间不同。
  • 会导致内存碎片。
  • 使链接器的配置变得复杂。

一个内存分配算法可以作为系统的可选选项。

  • 当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存
  • 不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。

FreeRTOS 提供了 5 种内存分配方法
这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。

20.2 内存碎片

内存碎片是伴随着内存申请和释放而来
在这里插入图片描述
第三步:

  • 有些应用使用完内存,进行了释放
  • 如果此时有个应用需要 50B 的内存,那么它可以从两个地方来获取到
    • 一个是最前面的还没被分配过的剩余内存
    • 一个就是刚刚释放出来的 80B 的内存块。
    • 明显,刚刚释放出来的这个 10B 的内存块就没法用,除非此时有另外一个应用所需要的内存小
      于 10B。

第四步:

  • 经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!

内存碎片可能会导致最终应用程序因为分配不到合适的内存而崩溃。

FreeRTOS 的 heap_4.c 提供了的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

20.3 heap_1 内存分配

动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS 中 的 内 存 堆 为 ucHeap[]。

  • 大 小 为configTOTAL_HEAP_SIZE

不管是哪种内存分配方法数组都是一样的。

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 的时候需要用户自行定义内存堆,否则的话由编译器来决定。

heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来

  • 大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE
  • 使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。

heap_1 特性如下:

  1. 适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用
  2. 具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。

20.3.1 内存申请函数

pvPortMalloc()源码如下:

void *pvPortMalloc( size_t xWantedSize )

具体函数省略,太多了。

字节对齐
宏 portBYTE_ALIGNMENT 是需要对齐的字节数,默认为 8

  • 也就是说参数 xWantedSize 要为 8 的倍数,如果不是的话就需要调整为
    8 的倍数。
  • 当 不是 8 字节对齐的时候就需要调整为 8 字节对齐,调整方法就是找出大于它并且离它最近的那个 8 字节对齐的数,对于 13 来说就是 16。

内存堆的可用起始地址也得是 8 字节对齐的

  • 内存堆 ucHeap 的起始地址是由编译器分配的,ucHeap 的起始地址不一定是 8 字节对齐的。
  • 如果ucHeap 实际起始地址为 0x200006C4,这个地址不是 8 字节对齐的, 经 过 字 节 对 齐 以 后 可 以 使 用 的 开 始 地 址 是 0x200006C8 , 所 以pucAlignedHeap 就为 0x200006C8。

检查一下可用内存是否够分配,分配完成以后是否会产生越界
如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn
任务调度器
调用函数 vTaskSuspendAll()挂起任务调度器,因为申请内存过程中要做保护
申请到内存之后,调用函数 xTaskResumeAll()恢复任务调度器。

钩子函数
宏 configUSE_MALLOC_FAILED_HOOK 为 1 的话就说明使能了内存申请失败钩子函数
会调用钩子函数 vApplicationMallocFailedHook()

返回值

  • 返回 pvRerurn 值,如果内存申请成功的话就是申请到的内存首地址
  • 内存申请失败的话就返回 NULL。

20.3.2 内存释放函数

heap_1 的内存释放函数为 pvFree()

void vPortFree( void *pv )
{
	( void ) pv;
	configASSERT( pv == NULL );
}

可以看出 vPortFree()并没有具体释放内存的过程。
如果使用 heap_1,一旦申请内存成功就不允许释放!

20.4 heap_2 内存分配

不像 heap_1 那样,heap_2 提供了内存释放函数。
heap_2不会把释放的内存块合并成一个大块。
随着你不断的申请内存,内存堆就会导致内存碎片。
具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高

20.6 heap_3 内存分配

这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装
FreeRTOS 对这两个函数做了线程保护
特性

  • 需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数
    在这里插入图片描述
    注意,在 heap_3 中 configTOTAL_HEAP_SIZE 是没用的!

20.5 heap_4 内存分配

heap_4 提供了空闲内存块合并的功能。
可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。

heap_4 特性如下:

  1. 可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
  2. 不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
  3. 具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。
  4. heap_4 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存的应用

heap_4 也使用链表结构来管理空闲内存块
heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾,其中 pxEnd 是指向 BlockLink_t 的指针。

20.5.1 内存堆初始化函数

内存初始化函数 prvHeapInit()

  • 初始化静态变量 xBlockAllocatedBit,初始化完成以后此变量值为 0X80000000。对于 32 位 MCU 来说就是 0X80000000。
  • 此变量会用来标记某个内存块是被使用
  • BlockLink_t 中的成员变量 xBlockSize 是用来描述内存块大小的,在 heap_4 中其最高位表示此内存块是否被使用。
  • 在 heap_4 中一个内存块最大只能为 0x7FFFFFFF。

假设内存堆 ucHeap 的大小为 46KB,即 configTOTAL_HEAP_SIZE =46*1024

在这里插入图片描述

20.6 heap_5 内存分配

heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。

需要大容量 RAM 的应用
STM32 可以外接 SRAM 甚至大容量的 SDRAM

  • 如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一
  • 使用 heap_5 的两个都可以一起作为内存堆来用。

使用 heap_5 的话

  • 在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions ()来对内存堆做初始化处理
  • 在 vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数!

函数 vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组
此结构体在portable.h 中有定义

typedef struct HeapRegion
{
	uint8_t *pucStartAddress; //内存块的起始地址
	size_t xSizeInBytes; //内存段大小
} HeapRegion_t;

内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。

现在有三个内存段:CCM、内部SRAM、外部 SRAM,起始分别为:0X10000000、0X20000000、0x68000000,大小分别为:64KB、128KB、1MB。

那么数组就如下:

HeapRegion_t xHeapRegions[] =
{
	{ ( uint8_t * ) 0X10000000UL, 0x10000 }, //CCM 内存,起始地址 0X10000000,大小 64KB
	{ ( uint8_t * ) 0X20000000UL, 0x20000 },//内部 SRAM 内存,起始地址 0X20000000,
	//大小为 128KB
	{ ( uint8_t * ) 0X68000000UL, 0x100000},//外部 SRAM 内存,起始地址 0x68000000,
	//大小为 1MB
	{ NULL, 0 } //数组结尾
};

heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。
这些内存堆会被连接在一起,和空闲内存块链表类似

20.7 内存管理代码

//MALLOC 任务函数
void malloc_task(void *pvParameters)
{
u8 *buffer;
u8 times,i,key=0;
u32 freemem;
LCD_ShowxNum(110,170,configTOTAL_HEAP_SIZE,5,16,0);//显示内存总容量 (1)
while(1)
{
	key=KEY_Scan(0);
	switch(key)
	{
		case WKUP_PRES:
		buffer=pvPortMalloc(30); //申请内存,30 个字节 (2)
		printf("申请到的内存地址为:%#x\r\n",(int)buffer);
	break;
	case KEY1_PRES:
		if(buffer!=NULL)vPortFree(buffer); //释放内存 (3)
		buffer=NULL; (4)
		break;
	case KEY0_PRES:
		if(buffer!=NULL) //buffer 可用,使用 buffer (5)
		{
			times++;
			sprintf((char*)buffer,"User %d Times",times);//向 buffer 中填写一些数据
			LCD_ShowString(94,210,200,16,16,buffer);
		}
		break;
	}
	freemem=xPortGetFreeHeapSize(); //获取剩余内存大小 (6)
	LCD_ShowxNum(110,190,freemem,5,16,0);//显示内存总容量
	i++;
	if(i==50)
	{
		i=0;
		LED0=~LED0;
	}
	vTaskDelay(10);
	}
}

释放内存以后将 buffer 设置为 NULL

  • 函数 vPortFree()释放内存以后并不会将 buffer清零,此时 buffer 还是上次申请到的内存地址,如果此时再调用指针 buffer 的话你会发现还是可以使用的!感觉好像没有释放内存,所以这里将 buffer 清零!

附录A FreeRTOSConfig.h

/*
    FreeRTOS V9.0.0 - Copyright (C) 2016 Real Time Engineers Ltd.
    All rights reserved

    VISIT http://www.FreeRTOS.org TO ENSURE YOU ARE USING THE LATEST VERSION.

    This file is part of the FreeRTOS distribution.

    FreeRTOS is free software; you can redistribute it and/or modify it under
    the terms of the GNU General Public License (version 2) as published by the
    Free Software Foundation >>>> AND MODIFIED BY <<<< the FreeRTOS exception.

    ***************************************************************************
    >>!   NOTE: The modification to the GPL is included to allow you to     !<<
    >>!   distribute a combined work that includes FreeRTOS without being   !<<
    >>!   obliged to provide the source code for proprietary components     !<<
    >>!   outside of the FreeRTOS kernel.                                   !<<
    ***************************************************************************

    FreeRTOS is distributed in the hope that it will be useful, but WITHOUT ANY
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    FOR A PARTICULAR PURPOSE.  Full license text is available on the following
    link: http://www.freertos.org/a00114.html

    ***************************************************************************
     *                                                                       *
     *    FreeRTOS provides completely free yet professionally developed,    *
     *    robust, strictly quality controlled, supported, and cross          *
     *    platform software that is more than just the market leader, it     *
     *    is the industry's de facto standard.                               *
     *                                                                       *
     *    Help yourself get started quickly while simultaneously helping     *
     *    to support the FreeRTOS project by purchasing a FreeRTOS           *
     *    tutorial book, reference manual, or both:                          *
     *    http://www.FreeRTOS.org/Documentation                              *
     *                                                                       *
    ***************************************************************************

    http://www.FreeRTOS.org/FAQHelp.html - Having a problem?  Start by reading
    the FAQ page "My application does not run, what could be wrong?".  Have you
    defined configASSERT()?

    http://www.FreeRTOS.org/support - In return for receiving this top quality
    embedded software for free we request you assist our global community by
    participating in the support forum.

    http://www.FreeRTOS.org/training - Investing in training allows your team to
    be as productive as possible as early as possible.  Now you can receive
    FreeRTOS training directly from Richard Barry, CEO of Real Time Engineers
    Ltd, and the world's leading authority on the world's leading RTOS.

    http://www.FreeRTOS.org/plus - A selection of FreeRTOS ecosystem products,
    including FreeRTOS+Trace - an indispensable productivity tool, a DOS
    compatible FAT file system, and our tiny thread aware UDP/IP stack.

    http://www.FreeRTOS.org/labs - Where new FreeRTOS products go to incubate.
    Come and try FreeRTOS+TCP, our new open source TCP/IP stack for FreeRTOS.

    http://www.OpenRTOS.com - Real Time Engineers ltd. license FreeRTOS to High
    Integrity Systems ltd. to sell under the OpenRTOS brand.  Low cost OpenRTOS
    licenses offer ticketed support, indemnification and commercial middleware.

    http://www.SafeRTOS.com - High Integrity Systems also provide a safety
    engineered and independently SIL3 certified version for use in safety and
    mission critical applications that require provable dependability.

    1 tab == 4 spaces!
*/


#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
 *
 * See http://www.freertos.org/a00110.html.
 *----------------------------------------------------------*/

/* Ensure stdint is only used by the compiler, and not the assembler. */
#ifdef __ICCARM__
	#include <stdint.h>
	extern uint32_t SystemCoreClock;
#endif

#define configUSE_PREEMPTION			1
#define configUSE_IDLE_HOOK				1
#define configUSE_TICK_HOOK				1
#define configCPU_CLOCK_HZ				( SystemCoreClock )
#define configTICK_RATE_HZ				( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES			( 5 )
#define configMINIMAL_STACK_SIZE		( ( unsigned short ) 130 )
#define configTOTAL_HEAP_SIZE			( ( size_t ) ( 75 * 1024 ) )
#define configMAX_TASK_NAME_LEN			( 10 )
#define configUSE_TRACE_FACILITY		1
#define configUSE_16_BIT_TICKS			0
#define configIDLE_SHOULD_YIELD			1
#define configUSE_MUTEXES				1
#define configQUEUE_REGISTRY_SIZE		8
#define configCHECK_FOR_STACK_OVERFLOW	2
#define configUSE_RECURSIVE_MUTEXES		1
#define configUSE_MALLOC_FAILED_HOOK	1
#define configUSE_APPLICATION_TASK_TAG	0
#define configUSE_COUNTING_SEMAPHORES	1
#define configGENERATE_RUN_TIME_STATS	0

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES 		0
#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )

/* Software timer definitions. */
#define configUSE_TIMERS				1
#define configTIMER_TASK_PRIORITY		( 2 )
#define configTIMER_QUEUE_LENGTH		10
#define configTIMER_TASK_STACK_DEPTH	( configMINIMAL_STACK_SIZE * 2 )

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet		1
#define INCLUDE_uxTaskPriorityGet		1
#define INCLUDE_vTaskDelete				1
#define INCLUDE_vTaskCleanUpResources	1
#define INCLUDE_vTaskSuspend			1
#define INCLUDE_vTaskDelayUntil			1
#define INCLUDE_vTaskDelay				1

/* Cortex-M specific definitions. */
#ifdef __NVIC_PRIO_BITS
	/* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
	#define configPRIO_BITS       		__NVIC_PRIO_BITS
#else
	#define configPRIO_BITS       		4        /* 15 priority levels */
#endif

/* The lowest interrupt priority that can be used in a call to a "set priority"
function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY			0xf

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY	5

/* Interrupt priorities used by the kernel port layer itself.  These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
	
/* Normal assert() semantics without relying on the provision of an assert.h
header file. */
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }	
	
/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
//#define xPortSysTickHandler SysTick_Handler

#endif /* FREERTOS_CONFIG_H */


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万码无虫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值