前言

网站:

FreeRTOS官网

参考文章:

从0到1学习FreeRTOS:FreeRTOS 内核应用开发:(四)FreeRTOS 的启动流程
FreeRTOS 入门-极客笔记
FreeRTOS基础篇教程目录汇总

FreeRTOS基础篇_研究是为了理解的博客-CSDN博客

FreeRTOS+CubeMX系列第一篇——初识FreeRTOS_cubemx freertos

飞起的小田博客

教学大纲

FreeRTOS介绍

FreeRTOS(读作"free-arr-toss")是一个嵌入式系统使用的开源实时操作系统。它的主要工作是 执行任务,大部分FreeRTOS的代码都涉及优先权,调度以及执行用户自定义任务。

以前在使用 51,STM32单片机裸机(未使用操作系统)的时候一般都是在 main函数 里面用 while(1) 做一个大循环来完成所有的处理,即 应用程序 是一个 无限的循环 ,循环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对多任务系统而言,这个就是 单任务系统(也称为前后台系统)。中断服务函数就是 前台程序,大循环while(1) 就是 后台程序

裸机开发:实时性差,前后台各个任务都是排队等着轮流执行;但是这种前后台系统简单,资源消耗也少

多任务系统开发:把一个大问题(应用)“分而治之”,把大问题划分为很多小问题,然后逐步把小问题解决掉,大问题也就随之解决(这些小问题可以单独作为一个小任务来处理),这些小任务是并发处理的,注意,并不是说同一时刻一起执行很多任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多任务一样。

任务调度器:哪个任务先执行,哪个任务后执行?

FreeRTOS 是一个 抢占式 的实时多任务系统,任务调度器也是抢占式的

抢占式调度:高优先级任务可以打断低优先级任务,抢占式调度,是最高优先级的任务一旦就绪,总能得到CPU的执行权;它抢占了低优先级的运行机会,在抢占式调度系统中,总是运行 最高优先级的任务

时间片轮转调度:让相同优先级的几个任务轮流运行,每个任务运行一个时间片,任务在时间片运行后操作系统自动切换到下一个任务运行;在任务运行的时间片中,也可以提前让出CPU运行权,把它交给下一个任务运行。FreeRTOS的时间片固定为 一个时钟节拍,由 configTICK_RATE_HZ 这个宏定义

/********FreeRTOSConfig.h********/
# define configTICK_RATE_HZ			( ( TickType_t ) 1000 )

FreeRTOS文件结构

首先去官网 点击 Download FreeRTOS — 点击下载FreeRTOS 202112.00即可

FreeRTOSv10.4.1
    |
    | -- FreeRTOS
            | -- Demo	存放演示例程工程 
            | -- License	存放许可证
            | -- Source		存放实时内核源文件
                    | -- include	API头文件
                    | -- portable	提供一些会被 FreeRTOS 核心代码调用的函数
                    | -- croutine.c	 协程相关 ,以前片子资源不够的时候用,现在用的人很少了
                    | -- event_groups.c	事件组
                    | -- list.c	列表结构描述,在内核整体控制上都使用了列表格式数据处理,一切数据结构的基础
                    | -- queue.c	消息队列用于task间通信和同步
                    | -- stream_buffer.c	流缓冲区
                    | -- tasks.c	任务相关的函数
                    | -- timers.c	软定时器
    |
    | -- FreeRTOS-Plus	内核以外的附加的组件
            | -- Demo
            | -- Source
                    | -- FreeRTOS-Plus-CLI 	指令交互
    				| -- FreeRTOS-Plus-IO	提供不同硬件设备I/O引脚的通信接口
    				| -- FreeRTOS-Plus-TCP	TCP/IP协议组件
    				| -- FreeRTOS-Plus-UDP	UDP组件    
    				| -- FreeRTOS-Plus-Trace	可视化跟踪   

第1讲

FreeRTOS的启动流程

FreeRTOS启动流程

FreeRTOS主要有两种比较流行的启动方式:

  1. 在main函数中将硬件初始化 ,RTOS系统初始化,同时创建所有任务,再启动RTOS调度器
//伪代码

#include "xxx.h"

int main(void)
{
    Hardware_init();	//硬件初始化
    RTOS_init();	//RTOS初始化
    RTOS_TaskCreate(Task_n);	//创建任务1
    RTOS_TaskCreate(Task_n);	//创建任务n
    RTOS_Start();	//RTOS启动,开始任务调度
}

//任务1函数
void Task_1()
{
    for(;;)
    {
        ...
    }
}

//任务n函数
void Task_n()
{
    for(;;)
    {
        ...
    }
}
  1. 在main函数中将硬件初始化 ,RTOS系统初始化,只创建一个启动任务,再启动RTOS调度器。之后,在启动任务中创建各种应用任务,当所有任务创建完成,启动任务把自己删除。
//伪代码

#include "xxx.h"

int main(void)
{
    Hardware_init();	//硬件初始化
    RTOS_init();	//RTOS初始化
    RTOS_TaskCreate(AppTaskCreate);	//创建总任务
    RTOS_Start();	//RTOS启动,开始任务调度
}

void AppTaskCreate()
{
    //创建任务1
    RTOS_TaskCreate(Task_1);
    //创建任务n
    RTOS_TaskCreate(Task_n);
    //创建完应用任务删除自身释放内存
    RTOS_TaskDelate(AppTaskCreate);
}

//任务1函数
void Task_1()
{
    for(;;)
    {
        ...
    }
}

//任务n函数
void Task_n()
{
    for(;;)
    {
        ...
    }
}

第2讲

FreeRTOS编程风格

  • FreeRTOS使用的数据类型虽然都是标准C的数据类型,但都进行了重定义,取了个新名字

需要注意的是, char数据类型可以通过keil指定了有符号或者无符号, 默认为无符号

  • 因为STM32属于32位架构,所以CubeMX配置默认把 configUSE_16_BIT_TICKS Disable掉,但是可以通过修改 FreeRTOSConfig.h
数据类型(主要是4种) 说明
TickType_t 如果用户使能了宏定义 configUSE_16_BIT_TICKS,那么 TickType_t 定义的就是 16 位无符号数
如果没有使能,那么 TickType_t 定义的就是 32 位无符号数。 对于 32 位架构的处理器,一定要禁止此宏定义,即设置此宏定义数值为 0 即可【CubeMX配置里默认禁止了】
BaseType_t 对于 32 位架构,BaseType_t 定义的是 32 位有符号数
对于 16 位架构,BaseType_t 定义的是 16 位有符号数。 如果 BaseType_t 被定义成了 char 型,要特别注意将其设置为有符号数,因为部分函数的返回值是用负数来表示错误类型。
UBaseType_t 这个数据类型是 BaseType_t 类型的无符号版本
StackType_t 栈变量数据类型定义,这个数量类型由系统架构决定,对于 16 位系统架构,StackType_t 定义的是16 位变量,对于 32 位系统架构,StackType_t 定义的是 32 位变量。
  • FreeRTOS里的变量的命名

在FreeRTOS中,定义变量时,把变量的类型作为前缀,方便用户通过变量即可知道变量的类型

uint32_t 定义的变量都加上前缀 ul【 u 代表 unsigned 无符号,l 代表 long 长整型】
uint16_t 定义的变量都加上前缀 us【 u 代表 unsigned 无符号,s 代表 short 短整型】
uint8_t 定义的变量都加上前缀 uc。【u 代表 unsigned 无符号,c 代表 char 字符型】

指针变量会加上前缀 p

char 定义的变量只能用于 ASCII 字符,前缀使用 c

char * 定义的指针变量只能用于 ASCII 字符串,前缀使用 pc

其它比如结构体、任务句柄等前缀是x,无符号则是 ux

  • 函数

加上了 static 声明的函数,定义时要加上前缀 pr ,这个是单词 private(私人) 的缩写

带有返回值的函数,根据返回值的数据类型,加上相应的前缀,如果没有返回值,即 void 类型
,函数的前缀加上字母 v

根据文件名,文件中相应的函数定义时也将文件名加到函数命名中,比如 tasks.c 文件中函数
vTaskDelete,函数中的 task 就是文件名中的 task

  • 宏定义

在FreeRTOS中,宏用 大写字母 表示,并 配有小写字母作为前缀,前缀用于指示该宏在哪个头文件定义。

此外,有几个通用的宏定义贯穿FreeRTOS的整个代码,都是表示0与1的宏(pd前缀表示portable data便携式数据1)
pdTRUE — 1
pdFALSE — 0
pdPASS — 1
pdFAIL — 0

FreeRTOS调试方法

处理器利用率:处理器利用率其实就是系统运行的程序占用的CPU资源,表示机器在某段时间程序运行的情况,如果这段时间中,程序一直在占用CPU的使用权,那么可以认为CPU的利用率是100%;CPU的利用率越高,说明机器在这个时间上运行了很多程序,反之较少。利用率的高低与CPU强弱有直接关系。

需要了解任务的执行状态,任务栈的使用情况以及各个任务的 CPU 使用率,这时就需要用到官方提供的两个函数 vTaskListvTaskGetRunTimeStats,然后通过串口打印出来【另外有一点要特别注意,这种调试方式仅限测试目的,实际项目中不要使用,这种测试方式比较影响系统实时性】

为了获取 FreeRTOS 的任务信息,需要创建一个定时器, 这个定时器的时间基准精度要高于系统时钟节拍,这样得到的任务信息才准确。这里提供的函数仅用于测试目的,切不可将其用于实际项目,原因有两点:

  1. FreeRTOS 的系统内核没有对总的计数时间做溢出保护。
  2. 定时器中断是 50us 进入一次,比较影响系统性能。

这里使用的是 32 位变量来保存 50us 一次的计数值,最大支持计数时间(运行时间超过了 59.6 分钟将不准确):

232×50us/3600s=59.6分钟2^{32} \times 50us / 3600s = 59.6 \text{分钟}

CubeMX配置

  • 第一个宏用来使能 运行时间统计功能,第二个宏用来使能 可视化追踪功能

  • 如果第二个宏设置为1,则下面两个宏必须被定义:
  1. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()

用户程序需要 提供一个基准时钟函数,函数完成初始化基准时钟功能,这个函数要被define到宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()上。这是因为 运行时间统计需要一个比系统节拍中断频率还要高分辨率的基准定时器,否则,统计可能不精确。基准定时器中断频率要比系统节拍中断 快10~20倍。基准定时器中断 频率越快,统计越精准,但能统计的运行时间也越短(比如,基准定时器10ms中断一次,8位无符号整形变量可以计到2.55秒,但如果是1秒中断一次,8位无符号整形变量可以统计到255秒)。

  1. portGET_RUN_TIME_COUNTER_VALUE()

用户程序需要 提供一个返回基准时钟当前“时间”的函数,这个函数要被define到宏portGET_RUN_TIME_COUNTER_VALUE()上。

  • 定时50us

Hz=1(50/1000000)s=20000Hz = \frac{1}{(50/1000000)s} = 20000

Arr=100000020000=50Arr = \frac{1000000}{20000} = 50

上面那两个宏是MX封装FreeRTOS的函数的,可在 FreeRTOSConfig.h查看:

/*main.c*/

volatile uint32_t sys_time = 0UL;	//系统时间计数


int main(void)
{
    HAL_TIM_Base_Start_IT(&htim6);	//开启定时器6
    ...
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim->Instance == TIM6)
  {
	  sys_time++;
  }
}
/*AllTask.c*/

//创建一个打印任务

//打印任务 
void vPrint_CPU_function(void *pvParameters)
{
	uint8_t CPU_Run[500];	//保存任务运行时间信息
	
	for(;;)
	{
		vTaskList((char*)&CPU_Run);	
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                  任务状态  优先级  剩余栈  任务序号\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		
		vTaskGetRunTimeStats((char*)&CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                 运行计数              利用率\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		
		osDelay(1000);
	}
	vTaskDelete(NULL);
}

//实现下面两个函数

//初始化定时器
void configureTimerForRunTimeStats(void)
{
	sys_time = 0;	//清零计数时间
}

//获取当前系统的运行时间计数器的值
unsigned long getRunTimeCounterValue(void)
{
	return sys_time;	//返回计数时间
}

第3讲

系统配置说明

关于系统配置可参考官网:https://www.freertos.org/zh-cn-cmn-s/a00110.html

FreeRTOSConfig.h 根据正在构建的应用程序定制FreeRTOS内核。因此,它特定于应用程序,而不是FreeRTOS,并且应该位于应用程序目录中,而不是位于FreeRTOS内核源代码目录中。

另外,大部分配置选项在 FreeRTOS.h 文件中都有默认的配置,在应用时,把需要的配置选项放在 FreeRTOSConfig.h 文件即可。

FreeRTOS.h 里首先是引入 FreeRTOSConfig.h 头文件,然后宏定义定义是如下格式:

#ifndef INCLUDE_xTaskGetIdleTaskHandle
	#define INCLUDE_xTaskGetIdleTaskHandle 0
#endif

//意思是如果用户在 FreeRTOSConfig.h 没有定义 "INCLUDE_xTaskGetIdleTaskHandle 1" 则默认定义为 "#define INCLUDE_xTaskGetIdleTaskHandle 0"

Config开始的宏

  • 内核配置
说明
configUSE_PREEMPTION 1–抢占式调度器
0–合作式调度器【项目一般不用合作式】
configUSE_PORT_OPTIMISED_TASK_SELECTION 此配置用于优化优先级列表中要执行的最高优先级任务的算法。对CM内核的移植文件,默认已经在文件 portmacro.h 文件中使能。
通用方式–0,所有平台的移植文件都可以配置为0,纯c编写,比专用效率低,可用的优先级数量不限制
专用方式–1,部分平台支持,这些平台架构有专用的汇编指令,通过这些指令可以加快算法执行速度,比通用方式高效,有最大优先级数限制通常限制为32个
configUSE_TICKLESS_IDLE 1–使能tickless低功耗模式
0–禁能tickless低功耗模式【一般涉及到低功耗产品才使能】
configCPU_CLOCK_HZ 定义CPU的主频,单位Hz
configTICK_RATE_HZ 定义系统时钟节拍数,单位Hz,一般取1000Hz即可,设置过高会增加系统负荷
configMAX_PRIORITLES 定义可供用户使用的最大任务优先级数
configMINIMAL_STACK_SIZE 定义空闲任务的栈空间大小,单位字(即4字节)【默认是128字(512字节)】
configTOTAL_HEAP_SIZE 定义堆大小,FreeRTOS内核,用户动态内存申请,任务栈,任务创建,信号量创建,消息队列创建等都需要用到这个空间【即把单片机RAM分一部分给FreeRTOS剩下的部分由用户自己使用】
configMAX_TASK_NAME_LEN 定义任务名最大字符数,末尾的结束符’\0’也包含在内
configUSE_16_BIT_TICKS 系统时钟节拍数使用 TickType_t 数据类型定义的【默认32位单片机配置为0】
configIDLE_SHOULD_YIELD 用于使能与空闲任务同优先级的任务,只有满足以下两个条件此配置才有效:
1. 使能抢占式调度器
2. 有创建与空闲任务同优先级的任务【实际应用不建议用户使用此功能默认为0即可】
configUSE_TASK_NOTIFICATIONS 1–使能任务间直接的消息传递,包含信号量,时间标志组和消息邮箱
0–禁用此功能
configUSE_MUTEXES 1–使能互斥信号量
0–禁能互斥信号量
configUSE_RECURSIVE_MUTEXES 1–使能递归互斥信号量
0–禁能递归互斥信号量
configUSE_COUNTING_SEMAPHORES 1–使能计数信号量
0–禁能计数信号量
configQUEUE_REGISTRY_SIZE 通过此定义来设置可以注册的信号量和消息队列个数
configUSE_QUEUE_SETS 1–使能消息队列
0–禁能消息队列
configUSE_TIME_SLICING 1–使能时间片调度
0–禁能时间片调度【默认在 FreeRTOS.h里配置为1了】
  • 其他配置

钩子函数主要功能是用于函数的扩展,用户可以根据自己的需要在里面添加相关的测试函数

说明
configUSE_IDLE_HOOK 1–使能空闲任务的钩子函数
0–禁能空闲任务钩子函数
configUSE_MALLOC_FAILED_HOOK 当创建任务,信号量或者消息队列时,FreeRTOS通过函数pvPortMalloc()申请动态内存。
1–使能动态内存申请失败时的钩子函数
0–禁能动态内存申请失败时的钩子函数
configUSE_TICK_HOOK 1–使能滴答定时器中断里面执行的钩子函数
0–禁能滴答定时器中断里面执行的钩子函数
configCHECK_FOR_STACK_OVERFLOW FreeRTOS的栈溢出检测支持两种方法,为了方便描述,我们这里将其称之为方法一和方法二。
2–栈溢出检测使用方法2
1–栈溢出检测使用方法1
0–禁止栈溢出检测
configGENERATE_RUN_TIME_STATS 1–使能任务运行状态参数统计
0–禁止此特性
configUSE_TRACE_FACILITY 1–将添加额外的结构体成员和函数以此来协助可视化和跟踪
0–禁能此特性
configUSE_STATS_FORMATTING_FUNCTIONS 用户配置宏定义 configUSE_TRACE FACILITYconfigUSE STATS FORMATTING FUNCTIONS 都为1的时候,将使能函数 vTaskList()vTaskGetRanTimeStats() ,如果两者中任何一个为0,那么这两个函数都将被禁能
  • 合作式任务配置(不需要用到)
说明
configUSE_CO_ROUTINES 1–使能合作式调度相关函数
0–禁能合作式调度相关函数
configMAX_CO_ROUTINE_PRIORITIES 此参数用于定义可供用户使用的最大的合作式任务优先级数
  • 软件定时器配置
说明
configUSE_TIMERS 1–使能软件定时器
0–禁能软件定时器【它是通过滴答定时器来配置很多软件定时器的】
configTIMER_TASK_PRIORITY 配置软件定时器任务的优先级
configTIMER_QUEUE_LENGTH 配置软件定时器命令队列的长度【相当于软件定时器的个数】
configTIMER_TASK_STACK_DEPTH 配置软件定时器任务的栈空间大小
  • 中断相关

说明
configLIBRARY_LOWEST_INTERrUPT_PRIORITY 配置中断最低优先级,通常为15(因为STM32的抢占优先级最多设置为4bit,优先级最低只能设置为15),此参数用于配置SysTick与PendSV
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 配置系统可管理的最高优先级,此参数用于配置BASEPRI寄存器,设置为5则优先级数值为0,1,2,3,4的中断是不受FreeRTOS管理的,不可被屏蔽,也不能调用FreeRTOS的API接口函数,而中断优先级在5~15的中断受系统FreeRTOS管理,可以被屏蔽

理解:这3个是FreeRTOS产生的中断:

  1. xPortSysTickHandler 是用于处理系统的时钟节拍,实现任务调度和时间管理等功能
  2. xPortPendSVHandler 当系统需要切换任务时,会触发一个特殊的中断——PendSV中断,并调用xPortPendSVHandler函数。
  3. vPortSVCHandler 系统调用是通过软中断(SWI)实现的。当程序执行svc指令时,会触发软中断,并调用vPortSVCHandler函数。通过vPortSVCHandler函数的处理,FreeRTOS能够实现系统调用和任务管理等功能,例如创建任务、删除任务、延时等待、获取系统时间等。这些功能都是由内核提供的,通过系统调用的方式

include 开始的宏

INCLUDE 开头的宏允许您的应用程序将未使用的实时内核组件从您的工程中移除,这可节约嵌入式应用程序所需的任何ROM或RAM。
每个宏都采用以下形式:

INCLUDE_FunctionName

其中 FunctionName表示可以选择性地排除的API函数(或函数集) 要包含API函数,请将宏设置为1,要排除该函数,请将宏设置为0

断言配置

STM32CubeMX生成的,在 FreeRTOSConfig.h 有定义,而且在大量地方有使用断言

理解:如果条件 x 不成立,即 x 的值为 0,那么执行以下步骤:

  1. 禁用中断,防止出现其他错误
  2. 进入一个无限循环,程序会一直卡在这里,直到被复位或者重新启动 这个宏定义主要用于在程序运行时检查某些关键条件是否满足,如果不满足则强制停止程序的运行,避免出现未知的错误。通常情况下,这个宏定义会被用在嵌入式系统开发中,以确保系统的稳定性和可靠性。

在HAL库也有类似断言,在GPIO初始化那,如果参数不是那7个GPIO组就会报错

MX里参数配置

  • 根据文档可以知道哪些型号单片机支持跑FreeRTOS的:微控制器和编译器工具链支持 FreeRTOS;我们这次测试的是蓝桥杯嵌入式开发比赛的板子 STM32G431RBT6,它基于 ARM Cortex-M4F 架构,支持FreeRTOS【原本打算用标准库去学习FreeRTOS但是官方从F4系列开始就不更新标准库了所以只能使用HAL库学习】

CubeMX参数详解

lnterface:选择CMSIS_v1【版本1】

【Config parameters 选项详解】

  • API,Versions:都是一些版本信息

  • MPU/FPU:STM32处理器中的两种特殊处理单元,MPU是内存保护单元,用于保护系统内存;而FPU则是浮点运算单元,用于处理浮点数运算的指令【使不使能无所谓】

  • Kernel settings:内核设置

选项 解释
USE_PREEMPTION 设置为 1 以使用 抢占式 rtos 调度程序
设置为 0 以使用 协作式 rtos 调度程序(即时间片)
CPU_CLOCK_HZ 这必须设置为驱动用于生成内核周期性滴答中断的外设的时钟频率,这通常但不总是等于主系统时钟频率【CPU 时钟频率】
TICK_RATE_HZ 设置滴答中断频率,值以 HZ 为单位指定,直接影响到计时的分辨率,精度越高,占用CPU时间越多,范围 1~1000;多个任务可以共享一个优先级,RTOS调度器为相同优先级的任务分享CPU时间,在每一个RTOS 系统节拍中断到来时进行任务切换。高的系统节拍中断频率会降低分配给每一个任务的“时间片”持续时间。【一般默认为1000也就是1ms】
MAX_PRIORITIES 能够分配给任务的最大优先级,范围 4~32,比如设置为4,那么你可以使用的优先级号是0–1–2–3,如果程序里超出的话则强制为 MAX-1
MINIMAL_STACK_SIZE 设置分配给空闲任务的堆栈大小值在这里以 uint32 为单位(字);应该考虑线程的数量、总堆大小和系统栈大小, 栈的大小不能超过总堆的空间,当动态分配时, 最大值 = configTOTAL_HEAP_SIZE/4;当静态分配时, 最大值 = MCU ram size/4;空闲任务 优先级最低,通常用来执行一些系统维护任务,比如检查任务堆栈使用情况,定期释放不再使用的内存,管理硬件等等【这个选项只影响空闲任务不会影响用户创建的任务】
MAX_TASK_NAME_LEN 任务名的最大(ASCII)字符数,包括字符串结束符NULL(’\0’),范围 12–255
USE_16_BIT_TICKS tick 计数值保存在一个 portTickType 型的变量中。STM32 只能 0
1-则 portTickType 为 无符号 16bit
0-则 portTickType 为 无符号32bit
IDLE_SHOULD_YIELD 当任务具有空闲优先级且内核系统使用了抢占式调度器,则:
0-阻止空闲任务为其它具有空闲优先级的任务让出CPU,只有当空闲任务离开运行状态才能被抢占。
1-如果有另外一个空闲优先级的任务在准备状态,则空闲任务立刻让出CPU,让该任务运行【一般默认0】
USE_MUTEXES 1–使用互斥量
0–忽略互斥量
未使用 cmsis rtos v2 时允许使用这两个值【一般默认1】
临界区机制
USE_RECURSIVE_MUTEXES 当USE_MUTEXES=1才有意义。
1–使用递归互斥量
0–忽略递归互斥量
USE_COUNTING_SEMAPHORES 1–使用计数信号量
0–忽略计数信号量
QUEUE_REGISTRY_SIZE 通过此宏定义来设置可以注册的信号量和消息队列个数
队列注册有2个目的,都与操作系统内核的调试器有关:
1、注册队列的时候,可以给队列起一个名字当使用调试组件时通过名字可以很容易区分不同队列。
2、它包含调试器所需的信息来定位每个已注册的队列和信号量。
如果想使用内核调试器查看队列和信号量信息,必须先将这些队列和信号量进行注册。参见 vQueueAddToRegistry()vQueueUnregisterQueue(),如果用户没有使用内核方面的调试器这个宏定义是没有意义的
USE_APPLICATION_TASK_TAG 1–vTaskSetApplicationTaskTag 函数有效
仅用于高级用户
可以为每个任务分配一个“tag”值。 此值仅用于应用程序,RTOS 内核本身并不以任何方式使用它。
ENABLE_BACKWARD_COMPATIBILITY 头文件 FreeRTOS.h 包含一系列 #define 宏定义,这些宏将 FreeRTOS 8.0.0 版本之前使用的数据类型的名称映射到版本 8.0.0 中使用的名称。
这些宏可以确保RTOS内核升级到V8.0.0版本时,之前的应用代码不用做任何修改。
0–会去掉这些宏定义,需要用户确认应用代码没有用到8.0.0版本之前的(原本需要映射的)名字
相当于兼容旧版本的一个功能【一般默认1】
USE_PORT_OPTIMISED_TASK_SELECTION 可以根据不同型号的单片机优化任务调度的选择,从而有效地提高系统的效率,用于优化优先级列表中要执行的最高优先级任务的算法【由于Freertos v9 支持,强制启用 1】
USE_TICKLESS_IDLE 1-使能tickless低功耗
0-禁用tickless低功耗
它可以通过在系统处于空闲状态时关闭定时器中断来实现,从而减少系统的能耗
USE_TASK_NOTIFICATIONS 1-使能任务间直接的消息传递,包括信号量,事件标志组和消息邮箱
0-禁用此功能,每个任务节省8字节
每个RTOS任务都有 32位的通知值。RTOS任务通知是直接发送到任务的事件,它可以解除对接收任务的阻塞,并且可以更新接收任务的通知值
RECORD_STACK_HIGH_ADDRESS 1-启用时,假设堆栈向下增长,堆栈起始地址将保存到每个任务 tcb 中
  • Memory management settings:内存管理设置
选项 解释
Memory Allocation Dynamic(动态)
static(静态)
Dynamic/static(动态或者静态)【一般选择动态】
TOTAL_HEAP_SIZE rtos 内核可用的 ram 总量,范围 512字节~32000字节【stm32G4的SRAM最大是32K】
Memory Management scheme 内存管理方案,会管理你的动态内存分配后剩余的零碎【一般选择heap_4】
  • Hook function related definitions:钩子函数相关定义
选项 解释
USE_IDLE_HOOK 当FreeRTOS空闲时 ,Idle Hook选项允许用户定义一个函数来完成一些特定的工作,比如进行资源释放、状态检查等。使用该选项可以有效地利用空闲时间,而不必浪费系统资源
USE_TICK_HOOK tick hook 函数是一个钩子或回调函数, 它可以在每次FreeRTOS计时器溢出时被调用,以实现一些特定的功能。例如,用户可以使用它来检查任务的执行情况,监控任务的堆栈使用情况,以及进行资源释放等。
USE_MALLOC_FAILED_HOOK 它可以 在系统申请内存失败时被调用,以实现一些特定的功能。例如,用户可以使用它来报告内存分配错误,以帮助调试程序,或者可以采取恢复措施,比如释放空闲的资源等。
USE_DAEMON_TASK_STARTUP_HOOK 它可以在 后台任务启动时被调用,以实现一些特定的功能。例如,用户可以使用它来报告任务的启动状态,或者可以初始化一些系统资源,以便后台任务可以正常运行。
CHECK_FOR_STACK_OVERFLOW 它可以 检查任务的堆栈 使用情况,以判断是否发生堆栈溢出。如果发生堆栈溢出,FreeRTOS就会调用一个钩子函数,以便用户可以采取恢复措施,例如重新启动任务或系统等。
  • Run time and task stats gathering related definitions:任务运行信息获取配置
选项 解释
GENERATE_RUN_TIME_STATS 1-使能任务运行状态参数统计
0-禁用此功能
它可以收集有关系统运行情况的统计信息,如 系统的运行时间、任务的CPU占用情况等。这些信息可以帮助用户调试程序,或者可以帮助用户优化系统的性能。
USE_TRACE_FACILITY 1-使能此配置将添加额外的结构体成员和函数,以此来协助可视化和跟踪,在使用LAR中的FreeRTOS
0-禁用此功能
插件时需要使能这个配置,否则无法显示任务栈的使用情况
它可以跟踪系统中任务的执行情况,可以帮助用户更好地调试系统。它可以 跟踪系统中所有任务的运行状态,例如挂起状态、就绪状态、正在运行状态等,以便用户可以更好地了解系统的运行情况。
USE_STATS_FORMATTlNG_FUNCTIONS USE_TRACE_FACILITY 和 此函数都为1时将使能 vTaskList()函数和 vTaskGetRunTimeStats()函数;只要其中一个为0那这两个函数无效。
它可以 将系统的运行状态信息格式化为文本。使用这个选项,可以 方便地将系统的运行状态信息打印到控制台或文件中,以便进行调试和分析
  • Co-routine related definitions:合作式任务配置(一般资源不够的单片机才用)
选项 解释
USE_CO_ROUTINES 1-使能合作式调度相关函数
0-禁用合作式相关函数
协程是一种更灵活的任务模型,它可以更轻松地实现一些复杂的任务。使用这个选项,可以方便地将一些复杂的任务实现在FreeRTOS上,使系统更加灵活。
MAX_CO_ROUTINE_PRIORITIES 它用于指定协程的最大优先级数量。使用这个选项,可以指定系统中最多可以有多少个不同优先级的协程。这将有助于更好地调度任务,提高系统的性能。
  • Software timer definitions:软件定时器配置
选项 解释
USE_TIMERS 它允许用户在FreeRTOS中使用定时器。使用定时器,可以方便地在系统中设定指定时间段内执行任务,从而更好地调度任务,提高系统的性能。就是当单片机定时器不够时再打开
  • lnterrupt nesting behaviour configuration:中断嵌套行为配置
选项 解释
LIBRARY_LOWEST_INTERRUPT_PRIORITY 此宏定义是用来配置FreeRTOS用到的SysTick中断和PendSV中断的优先级。在NVIC分组设置为4的情况下,此宏定义的范围就是0~15,即SysTick和PendSV都配置为了最低优先级, 实际项目也推荐配置成最低优先级
LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 定义了受FreeRTOS管理的最高优先级中断。简单的说就是允许用户在这个中断服务程序里调用FreeRTOS的API的最高优先级。设置NVIC为4的情况下,配置LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY为0x01表示用户可以在抢占式优先级为1~15的中断里调用FreeRTOS的API函数,抢占式优先级为0的中断里面不允许调用
  • Added with 10.2.1 support(这个没什么用)

【lnclude parameters 选项详解】

可参考文章:FreeRTOS系列第12篇—FreeRTOS任务应用函数

函数 描述 作用
vTaskPrioritySet 改变一个任务的优先级
ux TaskPriorityGet 查询员工任务的优先级 查询任务的优先级参数是任务句柄,返回值是UBaseType_t类型
vTaskDelete 删除任务
vTas1kCleanUpResources 回收删除任务后的资源(RAM等)
vTaskSuspend 时任务进入挂起状态
vTaskDelayUntil 使任务进入阻塞状态(绝对时间)
vTaskDelay 时任务进入阻塞状态(相对时间)
xTaskGetSchedulerState 查询调度器状态
xTaskResumeFromlSR 将挂起态任务切换成就绪态(中断中)
xQueueGetMutexHolder
xSemaphoreGetMutexHolder 返回获取函数参数中互斥量的任务句柄
pcTaskGetTaskName 获取任务的名字
uxTaskGetStackHighWaterMark 返回任务启动后的最小剩余堆栈空间
xTaskGetCurrentTaskHandle 返回运行态任务的句柄
eTaskGetState 返回任务状态值
xEventGroupSetBitFromISR 设置指定的事件组标志位为1(中断中)
xTimerPendFunctionCall
xTaskAbortDelay 任务切出阻塞态进入就绪态
xTaskGetHandle 通过函数名字获取函数句柄
uxTaskGetStackHighWaterMark2 用于获取任务堆栈的高水位标记 当任务开始执行时,它的堆栈被填充了一些特定的值,如0xa5、0x5a等。当任务运行时,它的堆栈指针会向下移动,堆栈空间被使用。通过获取堆栈高水位标记,可以知道任务堆栈的最高使用位置,即任务堆栈的剩余空间, 返回的数值越大,表示任务堆栈的剩余空间越大

【Advanced settings(高级设置) 选项详解】

【User Constants(用户常量) 选项详解】

【Tasks and Queues(任务和队列) 选项详解】

【Timers and Semaphores(定时器和信号量) 选项详解】

【FreeRTOS Heap Usage(FreeRTOS堆使用) 选项详解】

选项 解释(单位Bytes)
HEAP STILL AVAILABLE 剩余字节数
TOTAL HEAP USED 已使用字节数
Total amount for tasks 任务总占用字节数
Total amount for queues 队列所占字节数
Total amount for timers 定时器所占字节数
Total amount for mutexes and semaphores 信号量与互斥量所占字节数
Total amount for events FreeRTOS事件数量
FreeRTOS tasks 各任务字节数分配情况

【Mutexes(互斥锁) 选项详解】

CubeMX配置

如果直接生成代码会弹出:

只是因为FreeRTOS使用了滴答定时器,所以我们需要在SYS那换成别的不冲突的定时器【一般使用TIM7,因为TIM6包括了DAC相关的所以没选,然后中断优先级可以改高点4左右,因为低的话可能造成系统的HALDelay函数被打断】

  • 需要注意由于改了SYS的定时器所以它默认会在 main.c 生成定时器回调函数所以用户不需要去重复写

第4讲

任务的概念和状态

任务的基本概念

从系统的角度看,任务是竞争系统资源的 最小运行单元

FreeRTOS是一个支持多任务的操作系统,在FreeRTOS中,任务 可以使用或等待CPU使用内存空间等系统资源,并独立于其他任务运行。

每个任务在自己的环境中运行,在任何时刻,只有一个任务得到运行,FreeRTOS调度器决定运行哪个任务。 调度器会不断的启动、停止每一个任务,宏观上看,所有的任务都在同时进行

在FreeRTOS中,每个任务都有自己的栈空间(一段连续的内存),用于保存任务运行环境。任务越多,需要的栈空间就越大,而一个系统能运行多少个任务,取决于系统可用的内存。

任务通常会运行在一个 死循环 中,不会退出, 如果不需要某个任务,可以调用FreeRTOS中的删除任务API函数将任务删除,释放系统资源

特别的,FreeRTOS支持相同优先级任务, 相同优先级任务之间的调度,采用的是轮询机制,每个任务分配一定的执行时间。 不同优先级任务之间的调度,执行的是抢占式调度

在任何时刻,只有 一个任务 得到运行,RTOS调度器决定运行哪个任务,在任务切入切出时 保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责

任务的状态

就绪状态 (Ready):任务已经创建,并且可以被调度器运行。当任务被创建时,它处于就绪状态。但是因为有一个同优先级或者更高优先级的任务处于运行状态而还没有真正执行(没有被阻塞和挂起)

运行状态 (Running):当前在 CPU 上运行的任务。只有一个任务能处于运行状态。

阻塞状态 (Blocked):任务被阻塞了,不能被调度器运行。当任务执行阻塞操作时,如等待信号量、邮箱、消息队列,外部中断,调用延时函数等,它将进入阻塞状态。

挂起状态 (Suspended):任务被挂起了,不能被调度器运行。当任务被调用 vTaskSuspend() 挂起时,它将进入挂起状态。(不可以通过设定超时事件而退出挂起状态,只能通过调用xTaskResume()才可以从挂起态恢复)

删除状态 (Deleted):任务已被删除,不能被调度器运行。当任务调用 vTaskDelete() 或者调度器自动删除任务时,它将进入删除状态。

系统启动调度与空闲任务

系统启动

使用 vTaskStartScheduler() 函数启动FreeRTOS调度【osKernelstart里面其实就是调用这个函数只是封装了而已】

使用这个函数要注意以下几个问题:

  1. 空闲任务和可选的定时器任务是在调用这个函数后自动创建的。
  2. 正常情况下这个函数是不会返回的,如果有返回,极有可能是用于定时器任务或者空闲任务的内存空间不足造成创建失败,此时需要加法FreeRTOS可管理的内存空间。
#define configTOTAL_HEAP_SIZE                    ((size_t)10240)

空闲任务

FreeRTOS 中有一个特殊的任务叫做空闲任务 (Idle task)。这个任务是由 FreeRTOS 自动创建的,它的优先级是最低的,并且当所有其它任务都处于阻塞状态时,调度器会自动切换到这个任务上运行。

空闲任务的主要目的是在系统空闲时执行后台操作,如调整 CPU 的频率,执行计数器或收集统计信息等。

可以通过实现 xApplicationIdleHook() 函数来指定空闲任务的具体行为,以实现自己的空闲处理逻辑。此函数在空闲任务调用时运行。此函数的默认实现为空函数,如果没有被重定义,空闲任务就不会执行其他任何操作。

空闲任务是唯一一个不允许出现阻塞情况的任务

空闲任务钩子

空闲任务钩子是一个函数,每一个空闲任务周期被调用一次。如果你想将任务程序功能运行在空闲优先级上,可以有两种选择:

  1. 在一个空闲任务钩子中实现这个功能:因为FreeRTOS必须至少有一个任务处于就绪或运行状态,因此钩子函数 不可以调用可能引起空闲任务阻塞的API函数(比如vTaskDelay()或者带有超时事件的队列或信号量函数)。
  2. 创建一个具有空闲优先级的任务去实现这个功能:这是个更灵活的解决方案,但是会带来更多RAM开销。
//创建一个空闲钩子步骤如下:
1.在CubeMX【config parameters】里使能"USE_IDLE_HOOK"选项
2.定义一个函数,函数和参数原型如下:
void vApplicationIdleHook(void);

通常, 使用这个空闲钩子函数设置CPU进入低功耗模式

任务的设计要点

  1. 中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作, 不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是, 中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候 必须考虑中断的频率、中断的处理时间 等重要因素,以便配合对应中断处理任务的工作。
  2. 做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作( 此处的死循环是指没有阻塞机制的任务循环体比如while(1)或者其他的),那么比这个任务优先级低的任务都将无法执行。【注意的是任务函数本身是一个死循环没错但是不能在里面再搞死循环这个是需要避免的】
  3. 任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出CPU使用权,这就需要我们自己 明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些。
  4. 空闲任务(idle任务)是 FreeRTOS系统中没有其他工作进行时自动进入的系统任务 。因为处理器总是需要代码来执行—— 所以至少要有一个任务处于运行态。
  5. 除此之外,还需要注意任务的执行时间。任务的执行时间一般是指两个方面, 一是任务从开始到结束的时间二是任务的周期

任务的创建

创建任务的方式:

  1. 静态创建任务:xTaskCreateStatic(),需要自行定义任务栈空间与任务控制块,一般不采用
  2. 动态创建任务,xTaskCreate(),系统动态分频任务栈空间与任务控制块,一般是使用此方式

两种动态任务创建的方式,一种是在 CubeMX中创建任务;另一种是在 工程中调用FreeRTOS源码来创建任务

CubeMX中创建任务

CubeMX默认会生成一个函数名为 StartDefaultTask 的默认任务

优先级从低到高排序
osPriorityIdle(空闲)
osPriorityLow (低)
osPriorityBelowNormal(低于正常)
osPriorityNormal(正常)
osPriorityAboveNormal(高于正常)
osPriorityHigh(高)
osPriorityRealtime(优先实时)

main.c 里自动先初始化任务,然后调用内核启动函数启动你的任务

个人习惯

  • 一般任务设置成 weak 方式,这样不修改源代码,在另一个文件里实现它
  • 任务函数名和字符串名称,习惯是设置成 相同的字符串函数名首字母大写字符串名称小写
  • 创建一个 My_Task文件夹,创建 AllTask.cAllTask.h ,这里写你创建的任务实现代码
  • 创建一个 App 文件夹,里面存放你写的外设驱动代码
  • MINIMAL_STACK_SIZE 大小一般给大点10240以上,任务看情况调试,默认128字
//在CubeMX中创建任务本质上和在工程内创建没有什么不同,都是都调用FreeRTOS源码,只不过CubeMX会对FreeRTOS源码进行二次封装

osThreadId testTaskHandle;	//任务句柄

//任务函数
void TestTASK(void const * argument)
{
    for(;;)
    {
        ...
    }
}
//参数分别是:字符串,函数名,优先级,传入的参数,栈大小
//osThreadDef也不是函数是宏定义,就是把参数赋给结构体成员
//osThread不是函数是宏定义,它使用 ##  拼接字符串变成"os_thread_def_testTask"然后把这个结构体取址赋给创建任务的函数osThreadId osThreadCreate (const osThreadDef_t *thread_def, void *argument)的第一个参数,它是结构体指针类型,创建完后返回值是一个句柄,然后把句柄赋给你定义的句柄testTaskHandle

osThreadDef(testTask, TestTASK, osPriorityNormal, 0, 500);	//参数写到一个结构体
testTaskHandle = osThreadCreate(osThread(testTask), NULL);	//调用封装好的xTaskCreate函数

调用FreeRTOS源码来创建任务

//任务创建函数原型
BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,	//任务函数
                        const char *const pcName,	//任务名
                        const configSTACK_DEPTH_TYPE usStackDepth,	//任务堆栈大小(单位word)
                        void *const pvParameters,	//任务参数
                        UBaseType_t uxPriority,	//任务优先级
                        TaskHandle_t *const pxCreatedTask 	//任务句柄
                      );
变量 描述
pxTaskCode 函数指针,指向任务函数的入口。任务永远不会返回(死循环);该参数类型为 TaskFunction_t 定义在文件 projdefs.h 中,参数类型为空指针类型并返回空类型【任务函数的形参只能是pvParameters】
pcName 任务描述,字符串形式;字符串的最大长度由 configMAX_TASK_NAME_LEN 决定默认16(包含’\0’),该宏位于 FreeRTOSConfig.h 中【用于调试时方便看是哪个任务】
usStackDepth 创建任务时,FreeRTOS内核会为每个任务分配固定的栈空间,栈空间的字数(word),而不是字节数(byte)
pvParameters void* 指针,当任务创建时,作为一个参数传递给任务,没有的话一般是写 NULL
uxPriority 任务的优先级,优先级的取值范围为[ 0, configMAX_PRIORITIES - 1], 任务优先级越高,其优先级值越大
pxCreatedTask 用于传出任务的句柄(ID)。这个句柄可以用来操作已经创建的任务,如改变任务优先级、删除任务等。如果不需要任务句柄,可以将pvCreatedTask置为NULL
返回值 如果任务创建成功,则返回 pdPASS ;如果任务创建失败,则返回相应的错误码(-1/-4/-5)。大部分创建失败的原因,都是因为FreeRTOS无法为任务分配足够的空间导致的,在实际程序中,应该判断该返回值,失败时记录错误码,便于查找问题原因。
//错误码

//错误无法分配所需的内存
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY	( -1 )
//错误队列阻塞    
errQUEUE_BLOCKED						( -4 )
//错误队列产量    
errQUEUE_YIELD							( -5 )

详解任务句柄

  • 首先看一下定义的句柄,类型是 osThreadId
osThreadId testTaskHandle;
  • 点击 osThreadId 跳转一下,可以看到它是重命名的,类型是 TaskHandle_t
typedef TaskHandle_t osThreadId;
  • 点击 TaskHandle_t 跳转一下,可以看到最终它的类型是 struct tskTaskControlBlock*,它是一个指向任务控制块结构体的指针
struct tskTaskControlBlock;
typedef struct tskTaskControlBlock* TaskHandle_t;
  • 然后我们在创建任务时是对指针进行取址 &testTaskHandle
xTaskCreate(vPrint_CPU_function,"usart1TX_function",TASK1_STACK_SIZE,NULL,TASK1_PRIORITY,&testTaskHandle);
  • 那说明参数类型是一个指针的指针,函数原型如下(省略了其他参数):
BaseType_t xTaskCreate(x,x,x,x,x,TaskHandle_t * const pxCreatedTask )
  • 为什么参数类型是一个指针的指针呢,因为我们的形参 testTaskHandle本身是一个指针,然后我们需要传递这个形参,就需要 & ,举例:
func(int **temp)
{
    **temp += 10;
}

int main(void)
{
    int a = 1;
    int *p1 = &a;
    int *p2 = p1;
    
    func(&p2);
    printf("a = %d\n",a);	//结果是11
}

1.首先在main函数里面定义一个整型变量a,并初始化为1
2.再定义一个指向a的指针p1,并初始化为 &a
3.再定义一个指向p1的指针p2,并初始化为p1
4.调用func函数,并把p2的地址作为参数传递给func函数
5.func函数中的参数是一个指向指针的指针temp,它将指向整型数值+11,因为p2指向p1,p1里面又是存放a的地址,所以p2指向a的地址,所以可以修改a的值
    
注意:
int *p2 = p1;改成int *p2 = &p1;编译是不通过的,因为P1类型是int*,而&p1的类型是int **,但是我们的p2类型是int * 所以类型不一致,想要类型一致则改成 int **p2 = &p1;
然后func函数形参类型要改成三级指针 int ***temp,函数内部改成 ***temp += 10;这样才能正常运行修改a的值
  • 言归正传继续看这个 pxCreatedTask形参 用在哪里(在xTaskCreate函数里,省略了其他参数),可以看到是直接把这个指针的指针赋值的没有&
prvInitialiseNewTask( x, x, x, x, x, pxCreatedTask, x, x );
  • prvInitialiseNewTask 函数原型(省略了其他参数),可以看到类型也是指针的指针 TaskHandle_t * const pxCreatedTask
static void prvInitialiseNewTask(x,
x,x,x,x,TaskHandle_t * const pxCreatedTask,x,x )
  • 继续看 pxCreatedTask 这个形参用在什么地方(在prvInitialiseNewTask函数里),可以看到是把一个指针类型的 pxNewTCB 赋给 pxCreatedTask
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
  • 再看看 pxNewTCB形参的类型是 TCB_t类型
static void prvInitialiseNewTask(x,x,x,x,x,x,TCB_t *pxNewTCB,x)
  • 点击 TCB_t 跳转可以看到它是一个重命名
typedef tskTCB TCB_t;
  • 点击 tsKTCB 跳转可以看到它是一个结构体,通过注释可以知道这个结构体是一个任务控制块
/*Task control block......*/
typedef struct tskTaskControlBlock 
{
	...
} tskTCB;
  • 所以最终下面代码意思就是 将新创建任务的TCB结构体的指针pxNewTCB转换为一个TaskHandle_t类型的句柄,并将其存储到pxCreatedTask所指向的地址中,而这个所指向的地址里面存储了一个TaskHandle_t类型的变量,而这个变量就是用户定义的句柄:testTaskHandle
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;

任务删除,挂起与恢复

删除任务函数详解

//函数原型
void vTaskDelete( TaskHandle_t xTaskToDelete );	//参数是任务句柄

使用这个函数要注意以下问题:

  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件里配置宏定义为
#define INCLUDE_vTaskDelete                  1
  1. 如果往此函数里面填的任务ID是 NULL(即数值0),那么删除的就是当前正在执行的任务,此任务被删除后,FreeRTOS会切换到任务就绪列表里面下一个要执行的最高优先级任务
  2. 在 FreeRTOS 中,在执行删除任务的时候,并不会释放任务的内存空间,只会 将任务添加到回收列表中,真正的系统资源回收工作在空闲任务完成,如果用户在FreeRTOS中调用了这个函数的话,一定要让空闲任务有执行的机会,否则这块内存是无法释放的,另外,创建的任务在使用中申请了动态内存,这个内存不会因为任务被删除而删除,这一点一定要注意,一定要在删除任务前将申请的动态内存释放
  3. 删除时最好判断一下句柄是否为 NULL,不为 NULL就删除

这个回收列表是一个由TCB结构体组成的链表,其中每个节点都是一个空闲任务的TCB结构体。

挂起任务函数详解

//函数原型
void vTaskSuspend( TaskHandle_t xTaskToSuspend )	//参数是任务句柄

使用这个函数要注意以下问题:

  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置宏定义为
#define INCLUDE_vTaskSuspend                 1
  1. 如果往此函数填写参数为 NULL(即数值0) 那么挂起的就是当前正在执行的任务,此任务被挂起后,FreeRTOS会切换到任务就绪列表里面下一个要执行的高优先级任务
  2. 多次调用此函数的话,只需调用一次 vTaskResume即可将任务从挂起态恢复

恢复任务函数详解

恢复任务有两种方式:普通方式中断方式

普通方式恢复任务函数原型:

void vTaskResume( TaskHandle_t xTaskToResume )	//参数是任务句柄

使用这个函数需要注意以下问题:

  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置宏定义为:
#define INCLUDE_vTaskSuspend                 1
  1. 多次调用函数 vTaskSuspend 的话,只需调用一次 vTaskResume即可将任务从挂起态恢复
  2. 此函数是用于任务代码中调用,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是 xTaskResumeFromISR()

中断方式恢复任务函数原型:

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )	//参数是任务句柄,返回值是成功返回pdTrue 失败返回pdFALSE

使用这个函数要注意以下问题:

  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置宏定义为(第一个已经默认在 FreeRTOS.h 里打开了不需要用户去打开):
INCLUDE_xTaskResumeFromISR 1
INCLUDE_vTaskSuspend 1    
  1. 多次调用函数 vTaskSuspend 的话,只需调用一次 xTaskResumeFromISR即可将任务从挂起态恢复
  2. 如果用户打算采用这个函数实现中断与任务的同步, 要注意一种情况,如果此函数的调用优先于vTaskSuspend被调用,那么此次同步会丢失,这种情况下建议使用信号量来实现同步
  3. 此函数是用于中断服务程序中调用,故不可以在任务中使用此函数
  4. 此函数有返回值的,可以通过判断返回值来是否进行任务切换

综合实验程序

实验目的:熟悉任务的创建,删除,挂起,恢复

按键1:删除任务LED2

按键2:创建任务LED2

按键3:挂起任务LED3

按键4:恢复任务LED3

注意:打印任务优先级不能比LED任务高,不然会影响LED显示

CubeMX配置

程序编写

main.c
int main(void)
{
    HardWare_init();	//硬件初始化
    AppTaskCreate();	//任务创建
}
KEY.c
//省略一部分,留下主要部分

extern osThreadId vled1_taskfunctionHandle;	//LED1任务
extern osThreadId vled2_taskfunctionHandle;	//LED2任务
extern osThreadId vled3_taskfunctionHandle;	//LED3任务
extern osThreadId vprint_cpu_taskfunctionHandle;	//打印任务
extern osThreadId vkey_taskfunctionHandle;	//按键检测与实现任务
extern void vLED2_TaskFunction(void const * argument);

//按键执行功能
void KEY_RUNFLAG(void)
{
	if(KeyData.KEY1_DOWN_FLAG)
	{
		KeyData.KEY1_DOWN_FLAG = 0;
		printf("KEY1按下\r\n");
		if(vled2_taskfunctionHandle != NULL)
		{
			//删除任务2
			vTaskDelete(vled2_taskfunctionHandle);
			vled2_taskfunctionHandle = NULL;
			printf("删除任务LED2\r\n");
		}
		else
		{
			printf("任务LED2已删除,不需要删除\r\n");
		}
	}
	if(KeyData.KEY2_DOWN_FLAG)
	{
		KeyData.KEY2_DOWN_FLAG = 0;
		printf("KEY2按下\r\n");
		if(NULL == vled2_taskfunctionHandle)
		{
			//创建LED2任务
			osThreadDef(vled2_taskfunction,vLED2_TaskFunction,osPriorityNormal,0,128);
			vled2_taskfunctionHandle = osThreadCreate(osThread(vled2_taskfunction),NULL);
			if(vled2_taskfunctionHandle != NULL)
			{
				printf("成功创建任务LED2\r\n");
			}
		}
		else
		{
			printf("任务LED2已存在,不需要创建\r\n");
		}
	}
	if(KeyData.KEY3_DOWN_FLAG)
	{
		KeyData.KEY3_DOWN_FLAG = 0;
		printf("KEY3按下,挂起任务LED3\r\n");
		vTaskSuspend(vled3_taskfunctionHandle);	//挂起任务LED3
	}
	if(KeyData.KEY4_DOWN_FLAG)
	{
		KeyData.KEY4_DOWN_FLAG = 0;
		printf("KEY4按下,恢复任务LED3\r\n");
		vTaskResume(vled3_taskfunctionHandle);
	}	
}
AllTask.c
extern osThreadId vled1_taskfunctionHandle;	//LED1任务
extern osThreadId vled2_taskfunctionHandle;	//LED2任务
extern osThreadId vled3_taskfunctionHandle;	//LED3任务

osThreadId vprint_cpu_taskfunctionHandle;	//打印任务
osThreadId vkey_taskfunctionHandle;	//按键检测与实现任务

void vPrint_CPU_TaskFunction(void const* argument);
void vKey_TaskFunction(void const* argument);

//管理任务
void AppTaskCreate(void)
{
	taskENTER_CRITICAL();
	
	//创建打印任务
	osThreadDef(vprint_cpu_taskfunction,vPrint_CPU_TaskFunction,osPriorityNormal,0,512);
	vprint_cpu_taskfunctionHandle = osThreadCreate(osThread(vprint_cpu_taskfunction),NULL);
	//创建按键任务
	osThreadDef(vkey_taskfunction,vKey_TaskFunction,osPriorityNormal,0,128);
	vkey_taskfunctionHandle = osThreadCreate(osThread(vkey_taskfunction),NULL);
	
	taskEXIT_CRITICAL();
}

//打印任务
void vPrint_CPU_TaskFunction(void const* argument)
{
	uint8_t CPU_Run[500];	//保存任务运行时间信息
	
	for(;;)
	{
		vTaskList((char*)&CPU_Run);	
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                  任务状态  优先级  剩余栈  任务序号\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		
		vTaskGetRunTimeStats((char*)&CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                 运行计数              利用率\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		
		osDelay(5000);
	}	
}


//按键任务
void vKey_TaskFunction(void const* argument)
{
	for(;;)
	{
		KEY_function();
		KEY_RUNFLAG();
		osDelay(20);
	}
}


void vLED1_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Dis(0x01,SET);
		osDelay(300);
		LED_Dis(0x01,RESET);
		osDelay(300);
	}
}

void vLED2_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Dis(0x02,SET);
		osDelay(500);
		LED_Dis(0x02,RESET);
		osDelay(500);	
	}
	
}
	
void vLED3_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Dis(0x04,SET);
		osDelay(100);
		LED_Dis(0x04,RESET);
		osDelay(100);		
	}
}


//初始化
void HardWare_init(void)
{
	LCD_Init();
	LCD_Clear(Blue);
	LCD_SetBackColor(Blue);
	LCD_SetTextColor(Black);
	LCD_DisplayStringLine(Line0,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line1,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line2,(uint8_t*)"      FreeRTOS      ");
	LCD_SetBackColor(White);
	LCD_DisplayStringLine(Line3,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line4,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line5,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line6,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line7,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line8,(uint8_t*)"                    ");
	LCD_DisplayStringLine(Line9,(uint8_t*)"                    ");
	LED_Dis(0xFF,RESET);
	printf("----FreeRTOS----\r\n");
	printf("----任务管理实验----\r\n");
	printf("按键功能如下:\r\n");
	printf("KEY1:删除LED2任务\r\n");
	printf("KEY2:重建LED2任务\r\n");
	printf("KEY3:挂起LED3任务\r\n");
	printf("KEY4:恢复LED3任务\r\n");
	HAL_TIM_Base_Start_IT(&htim6);	//开启定时器6
}

第5讲

了解调度器

简单的说,调度器就是使用相关的调度算法来决定当前需要执行的任务。

FreeRTOS操作系统支持三种调度方式: 抢占式调度时间片调度合作式调度

合作式调度

合作式调度器提供了一种单任务的系统结构:

  1. 当任务需要运行时,被添加到等待队列
  2. 任务在特定的时刻被调度运行(以周期性或者单次方式)
  3. 任务运行直到完成(高优先级任务不可抢占CPU),然后由调度器选择下一个任务

优点:调度简单,系统占用资源少(单任务结构,运行时高优先级任务不会抢占CPU,不需要给每个任务分配独立的栈空间)

缺点:系统实时性不够好

单片机资源越来越丰富,加上合作式调度器的系统实时性不够好,合作式调度已经很少使用,FreeRTOS在新的版本中已不再更新

抢占式调度

抢占式调度器提供了一种 多任务 的系统结构,高优先级任务可以抢占低优先级的CPU使用权,使得系统实时性非常好。

使用抢占式调度器时,根据任务重要程度合理分配优先级,CPU会优先执行就绪列表中优先级最高的任务。

下面图片:任务1优先级<任务2优先级<任务3优先级

所以高优先级任务一定要有阻塞,让出CPU给低优先级的任务执行否则高优先级会一直占用CPU低优先级任务不会得到运行的机会

时间片调度

时间片调度针对同优先级的任务,调度算法给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。【默认开启的】

//FreeRTOS.h
#define configUSE_TIME_SLICING 1

注意:FreeRTOS里每一个任务分配的时间是一样的,不像别的操作系统可以不同任务不同时间片

面图片:任务1优先级=任务2优先级=任务3优先级=任务4优先级

举例

可以创建3个任务ABC,优先级设为一样,每个任务是打印功能然后使用 HAL_Delay(10)延时(这里不使用os延时这样可以看到很好效果),然后时间片节拍设置为50ms(表示隔50ms切换一次任务),开始运行可以发现每个任务都是打印5次(任务里延时10msX5等于50ms),然后切换下一个任务以此循环。

如果把 时间片禁止 了就会发现只有一个任务一直在运行不会切换

任务栈大小确定

任务的栈大小公式:

number of bytes=TCB size+(4×task stack size)\text{number of bytes} = \text{TCB size} + (4 \times \text{task stack size})

TCB size 默认是112\text{TCB size 默认是112}

task stack size 就是你创建任务设置的word数 单位:字\text{task stack size 就是你创建任务设置的word数 单位:字}

一般默认是128字也就是128×4=512字节\text{一般默认是128字也就是}128\times4 = 512\text{字节}

栈空间来自
局部变量
函数形参(针对函数嵌套)
函数返回地址(针对函数嵌套)
一般函数的返回地址是专门保存到LR( Link Register)寄存器里面的,LR是需要入栈的
函数内部的状态保存
任务切换
发生中断

FreeRTOS任务栈的大小实际上是由 TCB大小和任务堆栈大小 共同决定的。TCB大小决定了任务创建时需要分配的空间大小,而每个任务堆栈在创建时需要分配4个字节的空间,因此任务栈的总大小就是TCB大小加上(4 * 任务堆栈大小)。

建议:可以事先给任务分配一个大的栈空间,然后通过调试打印方法打印栈的使用情况,运行一段时间后就有一个大概的范围,再乘以安全系数(一般是1.5~2),即可得到需要使用的栈空间了

任务栈溢出与检测

栈溢出就是用户分配的栈空间不够用了,溢出了

栈生长方向是从高地址到低地址生长(M4和M3是这种方式):

注意:栈的生长方向不同于数组的存储方向,数组的存储方向是从低地址到高地址。因此,在使用栈和数组时需要注意它们的存储方向,以避免访问越界和数据错误。

FreeRTOS提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行:

  1. 在任务切换时检测任务栈指针是否过界,如果过界了,在任务切换的时候会触发栈溢出钩子函数(钩子函数的主要作用就是对原有函数的功能进行扩展,用户可以根据自己的需要往里面添加相关的测试代码)
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )	//参数1是任务句柄,参数2是任务名称

用户可以在钩子函数里面做一些处理。这种方法不能保证所有的栈溢出都能检测到。比如 任务在执行的过程中出现过栈溢出,任务切换前栈指针又恢复到正常水平,这种情况在任务的时候是检测不到的。又比如 任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。

使用方法1需要在 FreeRTOSConfig.h 文件中配置如下宏定义(在MX选择):

configCHECK_FOR_STACK_OVERFLOW 1
  1. 任务创建的时候 将任务栈所有数据初始化为0xa5,任务切换时进行任务栈检测的时候会检测末尾的16个字节是否都是0xa5,通过这种方式来检测任务栈是否溢出了。相比方法一,这种方法的速度稍慢些,但是这样就有效地避免了方法一里面的部分情况。不过依然不能保证所有的栈溢出都能检测到,比如任务栈末尾的16个字节没有用到,即没有被修改,但是任务栈已经溢出了,这种情况是检测不到的。另外任务栈溢出后,任务栈末尾的16个字节没有修改,但是溢出部分的栈区数据被修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。

使用方法2需要在 FreeRTOSConfig.h 文件中配置如下宏定义(在MX选择):

configCHECK_FOR_STACK_OVERFLOW 2

以上的方法在产品测试可以用但是产品发布时最好关闭它

综合实验程序

实验目的:模拟栈溢出,造成硬件异常

实验方法:在任务 vkey_taskfunction 中申请过大的数组,模拟栈溢出的情况,检测到按键1按下时,对数组赋值,模拟产生系统硬件错误。检测溢出后触发钩子函数,将发送栈溢出的任务打印出来。【检测机制两种方法都试试】

CubeMX配置

  • 选择方法1,测完再换方法2测

程序编写

  • 需要在 HardFault_Handler() 函数里面进行编写

这个函数是ARM Cortex-M 系列处理器内置的一个函数。在 STM32 系列微控制器中,HardFault_Handler 函数也是预定义好的, 用于处理处理器硬件错误异常。【在 stm32g4xx_it.c里】

KEY.c
//按键执行功能
void KEY_RUNFLAG(void)
{
    if(KeyData.KEY1_DOWN_FLAG)
    {
        int16_t i;
        uint8_t Buf[1024];

        KeyData.KEY1_DOWN_FLAG = 0;
        printf("KEY1按下\r\n");
        for(i = 1023; i >= 0; i--)
        {
            Buf[i] = 0x55;
            vTaskDelay(100);
        }

    }
    ...
}
AllTask.c
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
	printf("任务%s发送栈溢出",pcTaskName);
}
stm32g4xx_it.c
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */

  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
	  printf("系统硬件错误\r\n");
	  HAL_Delay(1);
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}

实验现象

按键1按下会触发钩子函数打印 任务vkey_taskfunction发送栈溢出,而且死机只能重启,但是没触发硬件错误,这是因为

栈溢出并未导致任务的堆栈空间覆盖到 HardFault_Handler 函数所在的堆栈空间。如果任务的堆栈空间溢出仅仅覆盖到了其他堆栈空间,比如其他任务的堆栈空间,那么就不会触发 HardFault_Handler 函数。,如果触发硬件错误直接就打印 系统硬件错误且不会触发钩子函数

方法2一样

第6讲

中断优先级

NVIC回顾

STM32中有一个强大而方便的NVIC,它属于CM4内核的器件。

STM32中有两个优先级的概念: 抢占优先级响应优先级(也称子优先级),抢占优先级高的可以打断抢占优先级低的,抢占优先级一样则看响应优先级的高低,如果抢占优先级和响应优先级一样则根据它们在中断表中的排位顺序决定先处理哪一个。

注意:当你启动FreeRTOS后,中断优先级组只能配置为 4,即抢占优先级可以配置为0~15,数值越小优先级越高,关闭FreeRTOS后才可以自定义选择组【在FreeRTOSConfig.h也可以看到】

SVC,PendSV,Systick中断

SVC中断:在FreeRTOS的移植文件 ports.c 中有用到SVC中断的0号系统服务,即SVC 0;此中断在FreeRTOS中仅执行一次,用于启动第一个要执行的任务。另外,由于FreeRTOS没有配置SVC的字段优先级,默认没有配置的情况下,SVC中断的优先级就是最高的 0。

PendSV和Systick中断:任务切换和时基中断都是配置为优先级最低

不受操作系统管理的中断

FreeRTOS内核源码有多处开关全局中断的地方,这些开关全局中断会加大中断延迟时间。比如在源码的某个地方关闭了全局中断,但是此时有外部中断触发,这个中断的服务程序就需要等到再次开启全局中断后才可以得到执行。 开关中断之间的时间越长,中断延迟时间就越大,这样极其影响系统的实时性。如果这是一个紧急的中断事件,得不到计时执行的话,后果是可想而知的。

针对这种情况,FreeRTOS专门做了一个新的开关中断实现机制。 关闭中断时仅关闭受FreeRTOS管理的中断,不受FreeRTOS管理的中断不关闭,这些不受管理的中断都是高优先级的中断,用户可以在这些中断里面加入需要实时响应的程序。

实现这个功能的奥秘在于FreeRTOS开关中断使用的寄存器 basepri

basepri 寄存器用于任务抢占和中断抢占之间的优先级管理。该寄存器是一个 8 位的寄存器,它的值可以控制当前任务允许被中断的最高优先级。当它被设置为某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大优先级越低),若设置为0则不关闭任何中断,0也是默认值。

当任务进入临界区时,可以通过将 basepri 寄存器的值设置为大于等于临界区中最高优先级的值,来禁止中断抢占当前任务,从而保证临界区的原子性。 当任务退出临界区时,可以通过将 basepri 寄存器的值恢复为 0,来允许中断抢占当前任务。

注意:使用 basepri 寄存器来管理任务和中断之间的优先级关系需要特别小心,因为 不当的使用可能会导致任务饥饿、死锁等问题。因此,在使用 basepri 寄存器时,需要仔细考虑任务的优先级、中断的优先级以及它们之间的关系,以确保系统的稳定性和可靠性。

可以通过MX配置此值或者 FreeRTOSConfig.h

【一般设置为5即可,即0~4中断不受FreeRTOS管理】

如果你想哪个中断不受FreeRTOS管理可以在设置那把勾去掉然后就可以配置为 0~4了,不去掉勾的话只会显示 5~15 给你

任务优先级

任务优先级说明

  1. FreeRTOS中任务的最高优先级是通过 FreeRTOSConfig.h 文件中的 configMAX_PRIORITIES 进行配置的,用户实际可以使用的优先级范围是 0~configMAX_PRIORITIES -1。比如我们配置此宏定义为 7 ,那么用户可以使用的优先级号是 0,1,2,3,4,5,6不包含7

  1. 用户配置任务的优先级数值越小,那么此任务的优先级越低,空闲任务的优先级是 0(最低)
  2. 建议用户配置宏定义 configMAX_PRIORITIES 的最大值不要超过 32 。因为对于CM内核的移植文件,用户任务的优先级不是大于等于32的话, portmacro.h 文件中的宏定义 configUSE_PORT_OPTIMISED_TASK_SELECTION会优化优先级列表中要执行的最高优先级任务的获取算法【此宏定义默认是使能的(即默认32),用户也可以在 FreeRTOSConfig.h 文件中进行配置】

任务优先级分配方案

优先级设置多少是没有标准的,但是可以参考下面这个标准:

IRQ任务:IRQ任务是指通过中断服务程序进行触发的任务,此类任务应该设置为所有任务里面优先级最高的

高优先级后台任务:比如按键检测,触摸检测,USB消息处理,串口消息处理等,都可以归为这一类任务

低优先级的时间片调度任务:比如 emWin 的界面显示,LED数码管的显示等不需要实时执行的都可以归为这一类任务。实际应用中用户不必纠结直接将这些任务都设置为1的同优先级任务,可以设置多个优先级,只需注意这类任务不需要高实时性

空闲任务:空闲任务是系统任务

注意:IRQ任务和高优先级任务必须设置为阻塞式(调用消息等待或者延迟等函数即可),只有这样,高优先级任务才会释放CPU的使用权,从而低优先级任务才有机会得到执行

函数

获取任务优先级

//函数原型
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )	//参数是任务句柄
  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义(或者在MX使能):
#define INCLUDE_uxTaskPriorityGet            1
  1. 如果参数填 NULL(即数值0),那么获取的优先级就是当前正在执行的任务

修改任务优先级

//函数原型
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )//参数1:任务句柄 参数2:新的优先级
  1. 使用此函数需要在 FreeRTOSConfig.h 配置文件中配置如下宏定义:
#define INCLUDE_vTaskPrioritySet             1
  1. 如果第一个参数填的是 NULL,那配置的就是当前正在执行的任务
  2. 如果被修改的任务优先级修改后高于正在执行的任务,将执行任务切换,切换到修改好的高优先级任务
  3. 第二个参数值不可大于等于 FreeRTOSConfig.h 文件中的宏定义 #define configMAX_PRIORITIES 配置的数值

开关中断与临界段函数

临界段概念

代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。

进入临界段前操作寄存器basepri关闭了所有大于等于宏定义configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY所定义的中断优先级,这样临界段代码就不
会被中断干扰到,而且 实现任务切换功能的PendSV中断和滴答定时器中断是最低优先级中断,所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。退出临界段时重新操作 basepri寄存器,即打开被关闭的中断(这里我们不考虑不受FreeRTOS管理的更高优先级中断)

除了FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:

  1. 读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
  2. 调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。 总之,对于临界段要做到执行时间越短越好,否则会影响系统的实时性

开关中断函数(宏)

//在task.h里

//关闭所有受管理的中断
#define taskDISABLE_INTERRUPTS()	portDISABLE_INTERRUPTS()
//打开所有受管理的中断
#define taskENABLE_INTERRUPTS()		portENABLE_INTERRUPTS()

这两个函数不推荐使用,因为它们不支持嵌套使用!

任务进入/退出临界段函数

//任务中进入临界段
#define taskENTER_CRITICAL()		portENTER_CRITICAL()
//任务中退出临界段
#define taskEXIT_CRITICAL()			portEXIT_CRITICAL()

其实这两个函数也是调用 开关中断函数,但是这两个函数都对变量 uxCriticalNesting 进行了操作,这个变量很重要,用于临界段的嵌套计数

注意:临界段处理函数必须成对使用

中断进入/退出临界段函数

//中断进入临界段
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
//中断退出临界段
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

中断里面的临界段代码的开关中断是通过寄存器basepri实现的。这里为什么没有中断嵌套计数呢?是因为它换了另外一种实现方法, 通过保存和恢复寄存器basepri的数值就可以实现嵌套使用

注意:临界段处理函数必须成对使用

综合实验程序

初始时:

print任务优先级:3

IDLE任务优先级:0

LED3任务优先级:3

LED2任务优先级:3

LED1任务优先级:3

按键任务优先级:4

CubeMX配置

  • 可控制的中断是5~15

程序编写

任务中临界段测试

  • 这里测试所以临界段里面的代码只是执行延时5s,正常来说是放重要的代码的
KEY.c(测试1--不改按键任务优先级只系统延时5s)
//测试1(不改优先级只系统延时5s)
	if(KeyData.KEY1_DOWN_FLAG)
	{
		KeyData.KEY1_DOWN_FLAG = 0;
		printf("KEY1按下\r\n");
		printf("更改前的KEY任务的优先级为:%u\r\n",(uint16_t)uxTaskPriorityGet(NULL));
		HAL_Delay(5000);
	}

实验现象是:按下按键1时LED任务停止运行5s,5s后恢复正常【这是因为按键的优先级是4最高的所以其他低优先级的不能抢占】

KEY.c(测试2--改按键任务优先级为2)
if(KeyData.KEY1_DOWN_FLAG)
{
	KeyData.KEY1_DOWN_FLAG = 0;
	printf("KEY1按下\r\n");
	printf("更改前的KEY任务的优先级为:%u\r\n",(uint16_t)uxTaskPriorityGet(NULL));
	vTaskPrioritySet(NULL,2);
	printf("更改后的KEY任务的优先级为:%u\r\n",(uint16_t)uxTaskPriorityGet(NULL));
	HAL_Delay(5000);
}

实验现象是:按键1按下后按键任务优先级会立刻改成2,由于按键任务优先级变成比LED任务优先级还低,所以马上进行了任务切换所以HAL_Delay(5000); 不会执行到就切换了

KEY.c(测试3--改按键任务优先级为2并且加临界段)
if(KeyData.KEY1_DOWN_FLAG)
{
	KeyData.KEY1_DOWN_FLAG = 0;
	printf("KEY1按下\r\n");
	//测试任务代码临界段
	printf("更改前的KEY任务的优先级为:%u\r\n",(uint16_t)uxTaskPriorityGet(NULL));
	vTaskPrioritySet(NULL,2);
	printf("更改后的KEY任务的优先级为:%u\r\n",(uint16_t)uxTaskPriorityGet(NULL));
	printf("进入代码临界段\r\n");
	taskENTER_CRITICAL();	//进入代码临界段
	printf("延时5s,尽管KEY任务的优先级最低,但是由于进入了进阶段,任务不会切换,LED灯应该停止闪烁\r\n");
	HAL_Delay(5000);
	taskEXIT_CRITICAL();	//退出临界段
	printf("退出代码临界段\r\n");
	vTaskPrioritySet(NULL,4);
}

实验现象是按键1按下后按键任务优先级改成2,比LED任务优先级低,但是由于进入了临界段,因为临界段中禁止任务切换。所以,即使 LED 任务的优先级比按键任务高,也不能在临界段中抢占执行权,而是执行临界段里面的代码,退出临界段后,由于执行了 vTaskPrioritySet(NULL,4);按键任务的优先级改回4,这意味着按键任务的优先级此时比 LED 任务高,所以任务调度器会继续执行按键任务而不是立即切换到 LED 任务。 因此,根据任务调度器的调度策略,退出临界段后任务的切换不一定会立即发生。需要根据任务的优先级和状态来决定下一个要执行的任务。

中断中临界段测试

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    UBaseType_t uxSavedInterruptStatus;
    
    uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();	//进入临界段
    //放重要的代码
    taskEXTI_CRITICAL_FROM_ISR(uxSavedInterruptStatus);	//退出临界段
}

uxSavedInterruptStatus是一个变量,用于保存进入临界段前的中断状态。在进入临界段时,中断是被禁止的,因此这个变量的值为0。在退出临界段时,根据这个变量的值来决定是否开启中断,以保持中断的嵌套状态不变。

第7讲

调度锁,中断锁,任务锁

调度锁

调度锁就是RTOS提供的 调度器开关 函数,如果某个任务调用了调度锁开关函数, 处于调度锁开和调度锁关之间的代码在执行期间是不会被高优先级的任务抢占的,即任务调度被禁止。这一点要跟临界段的作用区分开, 调度锁只是禁止了任务调度并没有关闭任何中 断,中断还是正常执行的。而临界段进行了开关中断操作。

//调度锁开启函数原型
void vTaskSuspendAll( void )

使用这个函数要注意以下问题:

  1. 调度锁只是禁止了任务调度,并没有关闭任务中断
  2. 调度锁开启函数和关闭函数一定要成对使用
  3. 切不可在调度锁开启函数和调度锁关闭函数之间调用任何会引起任务切换的API,比如 vTaskDelayUntilvTaskDelayxQueueSend
//调度锁关闭函数
BaseType_t xTaskResumeAll( void )	//调度锁关闭后,如果需要任务切换,此函数返回pdTRUE,否则返回pdFALSE

使用这个函数要注意以下问题:

同上

综合实验程序

实验方法

任务1:优先级低,启动调度锁,调度锁开关之间使用HAL_Delay延时5s,退出调度锁后,使用HAL_Delay继续延时5s

任务2:优先级高,指示灯100ms闪烁

CubeMX配置

程序编写

AllTask.c(测试1--LED1任务只延时不调用调度锁函数)
void vLED1_TaskFunction(void const * argument)
{
	for(;;)
	{
		HAL_Delay(2000);
		HAL_Delay(5000);
	}
}

void vLED2_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Dis(0x02,SET);
		osDelay(100);
		LED_Dis(0x02,RESET);
		osDelay(100);	
	}
}

实验现象:LED2任务执行100ms闪烁,因为LED2任务优先级比较高

AllTask.c(测试2--LED1任务延时并调用调度锁函数)
void vLED1_TaskFunction(void const * argument)
{
	for(;;)
	{
		HAL_Delay(2000);
		//调度器锁开
		vTaskSuspendAll();
		HAL_Delay(5000);
		if(pdTRUE == xTaskResumeAll())
		{
			taskYIELD();	//立即任务切换
		}
	}
}

void vLED2_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Dis(0x02,SET);
		osDelay(100);
		LED_Dis(0x02,RESET);
		osDelay(100);	
	}
}

实验现象是LED2任务闪烁10次然后停止闪烁5s然后又闪烁10次以此循环

中断锁

中断锁就是RTOS 提供的 开关中断 函数,FreeRTOS没有专门的中断锁函数,使用上一讲里面介绍的临界段处理函数就可以实现同样效果。

任务锁

简单的说,为了防止当前任务的执行被其它高优先级的任务打断而提供的锁机制就是任务锁。FreeRTOS也没有专门的任务锁函数,但是使用FreeRTOS现有的功能有两种实现方法:

  1. 利用调度锁关闭任务切换
  2. 利用FreeRTOS的任务代码临界段处理函数关闭PendSV中断和Systick 中断,进而关闭任务切换。

第8讲

系统节拍

FreeRTOS 的时钟节拍

任何操作系统都需要提供一个时钟节拍,以供系统处理诸如延时、 超时等与时间相关的事件。
时钟节拍是特定的周期性中断,这个中断可以看做是 系统心跳。 中断之间的时间间隔取决于不同的应
用,一般是 1ms – 100ms。时钟的节拍中断使得内核可以将任务延迟若干个时钟节拍,以及当任务等待
事件发生时,提供等待超时等依据。 时钟节拍率越快,系统的额外开销就越大。一般来说 都是用滴答定时器来实现系统时钟节拍的

滴答定时器 Systick

SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号: 15), 滴答定时器是一个 24 位
的递减计数器,支持中断。 使用比较简单, 专门用于给操作系统提供时钟节拍。
FreeRTOS 的系统时钟节拍可以在配置文件 FreeRTOSConfig.h 里面设置:

#define configTICK_RATE_HZ                       ((TickType_t)1000)

如上所示的宏定义配置表示系统时钟节拍是 1KHz(即1ms)

延时相关函数

FreeRTOS 中的时间延迟函数主要有以下两个作用:

  1. 为周期性执行的任务提供延迟。
  2. 对于抢占式调度器,让高优先级任务可以通过时间延迟函数释放 CPU 使用权,从而让低优先级任务可以得到执行。

FreeRTOS 的时间相关函数主要是4个:

vTaskDelay函数

相对延时,vTaskDelay()指定的延时时间是从调用vTaskDelay()后开始计算,直到延时指定的时间结束。单位是 系统节拍时钟周期(不是ms)portTICK_PERIOD_MS 宏定义是用来辅助计算真实时间,此值是系统节拍时钟中断的周期,单位是毫秒【在portmacro.h有定义】

不适用与周期性执行任务的场合其它任务和中断活动,会影响到vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),因此会影响任务下一次执行的时间

//configTICK_RATE_HZ是在cubeMX里填写的频率,可在FreeRTOSconfig.h查看
#define configTICK_RATE_HZ                       ((TickType_t)1000)

//portmacro.h
#define portTICK_PERIOD_MS			( ( TickType_t ) 1000 / configTICK_RATE_HZ )

所以vTaskDelay的计算公式(可参考osDelay函数):

\text{实际vTaskDelay参数值 = 你想要延时的时间(ms) / portTICK_PERIOD_MS}

比如延时500ms,则通过计算得出vTaskDelay参数应该写500

TickType_t TicksToDelay;	//定义变量(注意类型要跟函数一致)
TicksToDelay = 500 / portTICK_PERIOD_MS;	//转换为节拍
vTaskDelay(TicksToDelay);	//阻塞500ms

osDelay

osDelay函数就是封装了 vTaskDelay ,单位是 ms(不是节拍)

vTaskDelayUntil函数

绝对延时,周期性任务可以使用此函数,以确保一个恒定的频率执行,当调用 vTaskSuspendAll() 函数挂起RTOS调度器时,不可以使用此函数。

即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒。

注意:在使用绝对延时时,如果您的任务需要执行时间超过了指定的延时时间,则任务将在计划时间之前被唤醒。这可能会导致任务的优先级被提升,从而影响系统的实时性。

//函数原型
//使用前需要在MX使能或者在FreeRTOSConfig.h把宏定义使能:#define INCLUDE_vTaskDelayUntil 1

//参数1:存储任务上次处于非阻塞状态时刻的变量地址
//参数2:周期性延迟时间(当时间等于(*pxPreviousWakeTime + xTimeIncrement)时,任务解除阻塞)
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
//用法
//可以使用portTickType定义因为在 FreeRTOS.h里宏定义了它等于TickType_t (938行左右)

void Usart1Tx_function(void *pvParameters)
{
	static TickType_t PreviousWakeTime;	//保存上一次时间
	static TickType_t TimeIncrement;	//需要多少节拍
	
	TimeIncrement = 1000 / portTICK_PERIOD_MS;	//把ms转换成节拍(如果设置的系统节拍是1ms则不需要转换直接给参数即可)
	PreviousWakeTime = xTaskGetTickCount();	//获取当前系统时间
	
	for(;;)
	{
		vTaskDelayUntil(&PreviousWakeTime,TimeIncrement);	//绝对延时,1000ms
	}
	vTaskDelete(NULL);
}

xTaskGetTickCount函数

用于获取系统当前运行的时钟节拍数,此函数用于在 任务代码 里面调用,如果在中断服务程序里面调用的话,需要使用另一个函数【不可混淆用】

//用法
printf("当前系统节拍是:%d\n",xTaskGetCount());

xTaskGetTickCountFromISR函数

用于获取系统当前运行的时钟节拍数,此函数用于在 中断服务程序 里面调用,如果在任务里面调用的话,需要使用另一个函数【不可混淆用】

//用法与上面一样

综合实验程序

任务1:HAL_Delay延时50ms,模拟传感器采集数据与被中断或高优先级任务打断的时间,printf打印任务运行次数,再通过vTaskDelay相对延时200ms

任务2:HAL_Delay延时50ms,模拟传感器采集数据与被中断或高优先级任务打断的时间,printf打印任务执行次数,再通过vTaskDelayUntil绝对延时200ms

实验分析:任务1由于采用相对延时,printf间隔250ms(50ms+200ms)打印信息;任务2由于采用绝对延时,printf间隔200ms(50ms+150ms)打印信息

程序编写

AllTask.c
void vLED1_TaskFunction(void const * argument)
{
	uint16_t Task1_cnt = 0;
	
	for(;;)
	{
		//模拟传感器采集数据与被中断或者高优先级任务打断的时间
		HAL_Delay(50);
		LED_Togg(0x01);
		//打印任务运行次数
		printf("任务1执行次数:%d\r\n",++Task1_cnt);
		//相对延时200ms
		osDelay(200);
	}
}

void vLED2_TaskFunction(void const * argument)
{
	portTickType PreviousWakeTime;	//之前的唤醒时间
	uint16_t Task2_cnt = 0;
	PreviousWakeTime = xTaskGetTickCount();	//获取当前系统时间
	
	for(;;)
	{
		//模拟传感器采集数据与被中断或者高优先级任务打断的时间
		HAL_Delay(50);
		LED_Togg(0x02);
		//打印任务运行次数
		printf("任务2执行任务次数:%d\r\n",++Task2_cnt);
		//绝对延时200ms
		osDelayUntil(&PreviousWakeTime,200);
	}
}

第9讲

链表的概念

链表是一种物理存储单元上 非连续、非顺序 的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点(链表中每一个元素称为节点)组成,节点可以在运行时动态生成。每个节点包括两个部分: 一个是存储数据元素的数据域另一个是存储下一个节点地址的指针域

链表作为C语言的一种基础数据结构,在平时写程序中用得并不多,但在操作系统中使用得非常多。如果需要读懂FreeRTOS系统的源码,必须弄懂链表,如果只是应用FreeRTOS系统,简要了解即可。

单向/双向链表

FreeRTOS里链表实现

  • 在 FreeRTOS 中,任务链表使用的是 双向循环链表

  • FreeRTOS中的 列表列表项 分别对应C语言链表中的 链表节点

ListItem_t:用来表示链表中的一个元素

MiniListItem_t:用来表示链表中初始的那个元素

List_t:用来表示一个链表

ListItem_t

//此代码来自list.h【140行左右】

//用于描述链表中的一个元素
struct xLIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			
	configLIST_VOLATILE TickType_t xItemValue;			
	struct xLIST_ITEM * configLIST_VOLATILE pxNext;		
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;	
	void * pvOwner;										
	struct xLIST * configLIST_VOLATILE pxContainer;		
	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE			
};
typedef struct xLIST_ITEM ListItem_t;	

详解

参数 解释
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE 用于检查链表的第一个列表项的完整性的常量。该常量的值为 0x12345678,用于检查第一个列表项的 xItemValue 字段是否包含了正确的值。xItemValue 字段的值应该是最高优先级任务的优先级值,如果该值不正确,说明链表的完整性已经被破坏。
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE 用于检查链表的第二个列表项的完整性的常量。该常量的值为 0x87654321,用于检查第二个列表项的 xItemValue 字段是否包含了正确的值。xItemValue 字段的值应该是最低优先级任务的优先级值,如果该值不正确,说明链表的完整性已经被破坏。
configLIST_VOLATILE TickType_t xItemValue 用于存储任务的优先级,configLIST_VOLATILE 是一个宏定义,在 FreeRTOS 的不同端口中可能会有不同的实现。它通常用于确保在访问 xItemValue 字段时使用原子操作,以防止多个任务同时访问该字段时发生竞态条件
struct xLIST_ITEM * configLIST_VOLATILE pxNext 指向下一个成员的指针
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious 指向上一个成员的指针
void * pvOwner 一个指向拥有该列表项的内核对象(如任务、信号量等)的指针
struct xLIST * configLIST_VOLATILE pxContainer 一个指向包含该列表项的双向循环链表的指针。在 FreeRTOS 中,每个列表项都属于一个双向循环链表,该指针指向该列表项所属的链表。

MiniListItem_t

  • 是一个迷你型的 Item

  • 它和 ListItem_t 的定义非常类似,关键成员少了 pvOwnerpxContainer

struct xMINI_LIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
    configLIST_VOLATILE TickType_t xItemValue;
    struct xLIST_ITEM *configLIST_VOLATILE pxNext;
    struct xLIST_ITEM *configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

List_t

//这个是管理整个链表的(精简)
typedef struct xLIST
{
	listFIRST_LIST_INTEGRITY_CHECK_VALUE				
	volatile UBaseType_t uxNumberOfItems;
	ListItem_t * configLIST_VOLATILE pxIndex;			
	MiniListItem_t xListEnd;							
	listSECOND_LIST_INTEGRITY_CHECK_VALUE				
} List_t;
参数 解释
listFIRST_LIST_INTEGRITY_CHECK_VALUE 用于检查链表的完整性,确保链表没有被破坏。这是一个常量,其值为0x4C495354UL,用于检测链表的开头是否被修改。
volatile UBaseType_t uxNumberOfItems 链表中元素的数量。定义了当前这个链表中有多少个 Item ,增加一个链表元素,这个值加1,反之,减1;
ListItem_t * configLIST_VOLATILE pxIndex 指向链表中的第一个元素,用来遍历整个链表
MiniListItem_t xListEnd 它不是链表中的一个元素,只是一个指向链表最后一个元素的标记。
listSECOND_LIST_INTEGRITY_CHECK_VALUE 用于检查链表的完整性,确保链表没有被破坏。这是一个常量,其值为0x5453494CUL,用于检测链表的结尾是否被修改。

链表的节点定义/初始化

  • 以下代码在 list.c

一个链表的初始化

【只在新建一个链表时才执行,后续插入删除不需要】

//参数:指向一个 List_t 类型的指针,该指针指向一个双向链表的头结点。
void vListInitialise( List_t * const pxList )
{
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );// MiniListItem_t 结构强转	
	pxList->xListEnd.xItemValue = portMAX_DELAY;
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );	
	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
	listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
	listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}
这段代码执行过程
传入一个表征链表的结构体指针 List_t * const pxList
在 List_t 结构中,用于标记链表最后的 xListEnd 结构是一个定义,而不是指针,这里首先将传入链表的 pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); 强转为 ListItem_t 结构,并赋值给了 pxIndex,也就是给 pxIndex 内容为这个 List 的 xListEnd 的地址
接下来便将 xListEnd 的 xItemValue 写入最大值 0xFFFFFFFF(32位CPU)
然后便将 xListEnd 的 next 和 prev 指针全部指向它自己,已达到初始化的目的
最后初始化该链表中有效元素的个数为 0 个
最后通过宏 listSET_LIST_INTEGRITY_CHECK_1_VALUElistSET_LIST_INTEGRITY_CHECK_2_VALUE 分别设置链表的完整性检查值
链表就被成功地初始化了

初始化一个链表元素

  • 将元素的容器指针给赋值成为 NULL
//初始化节点
void vListInitialiseItem( ListItem_t * const pxItem )
{
	pxItem->pxContainer = NULL;

	listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
	listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

链表的插入

  • 以下代码在 list.c

插入新节点需要先执行 vListInitialiseItem 函数再执行插入函数

尾插

//参数1:一个指向列表的指针
//参数2:一个指向要插入的列表项的指针
void vListInsertEnd( List_t *const pxList, ListItem_t *const pxNewListItem )
{
    ListItem_t *const pxIndex = pxList->pxIndex;

    listTEST_LIST_INTEGRITY( pxList );
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

    pxNewListItem->pxNext = pxIndex;
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;

    mtCOVERAGE_TEST_DELAY();

    pxIndex->pxPrevious->pxNext = pxNewListItem;
    pxIndex->pxPrevious = pxNewListItem;

    pxNewListItem->pxContainer = pxList;

    ( pxList->uxNumberOfItems )++;
}
这段代码执行过程
首先获取链表的 pxIndex 结构指针,此指针在链表初始化的时候,是指向了 xListEnd
然后,使用 listTEST_LIST_INTEGRITYlistTEST_LIST_ITEM_INTEGRITY 两个宏来检查链表的完整性和新元素的完整性。这两个宏通常用于调试。
将新元素的 pxNext 指针设置为链表的索引节点,将 pxPrevious 指针设置为链表索引节点的前一个节点(即之前最后一个节点)
执行 mtCOVERAGE_TEST_DELAY() 宏,用于代码覆盖率测试
将新元素插入到链表中:
1.将新元素的前一个节点的 pxNext 指针指向新元素
2.将链表索引节点的 pxPrevious 指针指向新元素
将新元素的 pxContainer 指针设置为链表本身,以表示该元素属于该链表
最后,将链表的元素数量加 1

升序插入

  • 如果两个节点的辅助值相同,则新节点在旧节点的后面插入
//参数1:指向链表的指针,表示要将新的节点插入到哪个链表中。
//参数2:指向新节点的指针,表示要插入的新节点
void vListInsert( List_t *const pxList, ListItem_t *const pxNewListItem )
{
    ListItem_t *pxIterator;
    const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

    listTEST_LIST_INTEGRITY( pxList );
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

    if( xValueOfInsertion == portMAX_DELAY )
    {
        pxIterator = pxList->xListEnd.pxPrevious;
    }
    else
    {


        for( pxIterator = ( ListItem_t * ) & ( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext )
        {
            //这里没有什么可做的,只是迭代到想要的插入位置
        }
    }

    pxNewListItem->pxNext = pxIterator->pxNext;
    pxNewListItem->pxNext->pxPrevious = pxNewListItem;
    pxNewListItem->pxPrevious = pxIterator;
    pxIterator->pxNext = pxNewListItem;

    pxNewListItem->pxContainer = pxList;

    ( pxList->uxNumberOfItems )++;
}
这段代码执行过程
首先定义一个指向链表节点的指针,表示在链表中查找要插入位置的迭代器。
xValueOfInsertion 表示新节点的值,用于在链表中查找插入位置。
进行完整性检查,确保链表和节点的数据结构没有被意外地改变
然后根据新节点的值 xValueOfInsertion 在链表中查找插入位置
将新节点插入到链表中,更新前后节点的指针【这里for循环找,直到找到一个大于xValueOfInsertion的值才退出,那新节点就会插在相同节点的后面】
将新节点的容器指针 pxContainer 指向链表的指针 pxList
增加链表的节点数 uxNumberOfItems

链表的删除

  • 从指定元素中的 pxContainer 获取到该元素所属的链表结构;再将元素从链表中摘除
UBaseType_t uxListRemove( ListItem_t *const pxItemToRemove )
{

    List_t *const pxList = pxItemToRemove->pxContainer;

    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

    mtCOVERAGE_TEST_DELAY();

    if( pxList->pxIndex == pxItemToRemove )
    {
        pxList->pxIndex = pxItemToRemove->pxPrevious;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    pxItemToRemove->pxContainer = NULL;
    ( pxList->uxNumberOfItems )--;

    return pxList->uxNumberOfItems;
}

综合实验程序

创建一个任务,在任务里面进行链表测试

把硬件初始化都注释,只需保留内核的

程序编写

AllTask.c
//定义列表
xList List;
//定义列表项
xListItem List_Item1;
xListItem List_Item2;
xListItem List_Item3;

void vLED1_TaskFunction(void const * argument)
{
	//列表初始化
	vListInitialise(&List);
	//列表项初始化
	vListInitialiseItem(&List_Item1);
	List_Item1.xItemValue = 1;
	vListInitialiseItem(&List_Item2);
	List_Item2.xItemValue = 2;
	vListInitialiseItem(&List_Item3);
	List_Item3.xItemValue = 3;
	//将列表项1,2,3按升序插入列表
	vListInsert(&List,&List_Item1);
	vListInsert(&List,&List_Item2);
	vListInsert(&List,&List_Item3);
	//将列表项2移除
	uxListRemove(&List_Item2);
	//将列表项2插入列表尾部
	vListInsertEnd(&List,&List_Item2);
	
	for(;;)
	{
		osDelay(1);
	}
}

软件仿真调试

  • 首先设置断点在 MX_FREERTOS_Init 函数那和 vListInitialise 函数那,点击进入仿真,点击 Watch1添加 4 个变量进行查看

  • 进入仿真后默认是分配了地址给这几个变量

  • 点击运行,则运行到 vListInitialise 函数那,然后按 F10,执行完初始化函数,可以看到 Num初始化为0,ItemValue 初始化为最大值为0xFFFFFFFF,pxPrevious,pxNext都指向EndEnd 则等于 Index

  • 然后继续按 F10,执行完列表项初始化,可以看到 ItemValue 的值已经初始化好,因为 vListInitialiseItem 函数里面只是把 pxContainer 赋值为 NULL,所以地址没其他变化

  • 继续按一次 F10,进行插入 List_Item1

  • 继续按一次 F10,进行插入 List_Item2,可以看到2是插在1后面

  • 继续按一次 F10,进行插入 List_Item3,可以看到3是插在2后面

  • 继续按 F10,删除 List_Item2,可以看到 pxContainer已经指向 NULL,列表的总元素个数变成 2

  • 继续 F10,把 List_Item2 插入到尾部,可以看到 1的下一个是3,3的下一个是2,2的下一个是索引节点

  • 测试完记得把之前注释的硬件初始化还原

第10讲

消息队列的概念

消息队列的概念及其作用

消息队列就是通过 RTOS 内核提供的服务,任务或中断服务子程序可以将一个消息( 注意,FreeRTOS 消息队列传递的是实际数据(复制方式),并不是数据地址,RTX,uCOS-II 和 uCOS-III 是传递的地址)放入到队列,同样,一个或者多个任务可以通过 RTOS 内核服务从队列中得到消息;常用于任务间通信,是一种 异步通信方式。

通常,先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入到消息队列的消息,即 先进先出的原则(FIFO),FreeRTOS
的消息队列支持 先进先出(FIFO)后进先出(LIFO) 两种数据存取方式。

FreeRTOS中消息队列特性:

  1. 消息支持先进先出方式排队,支持异步读写工作方式
  2. 读写队列均支持超时机制
  3. 消息支持后进先出方式排队,向队首发送消息(LIFO)
  4. 可以允许不同长度(不超过队列节点最大值)的任意类型消息
  5. 一个任务能够从任意一个消息队列接收和发送消息
  6. 多个任务能够从同一个消息队列接收和发送消息
  7. 当队列使用结束后,可以通过删除队列函数进行删除

在FreeRTOS里当一个任务向消息队列发送数据时,它会将实际数据复制到消息队列中,并在接收数据的任务接收该消息时返回这些数据。 这种方法需要更多的内存分配和数据复制

这样 RTX,uCOS-II 和 uCOS-III 更加高效,但是 FreeRTOS 则任务之间的数据传递更加安全,因为每个任务都有自己的内存空间,这可以防止一个任务意外地覆盖另一个任务的数据

任务能够从队列中读取消息,当队列中的消息为空时,读取消息的任务将被阻塞。用户可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过指定的阻塞时间,即使队列中没有有效数据, 任务也会自动从阻塞态转为就绪态

在裸机编程时,使用全局数组的确比较方便,但是在加上 RTOS 后就是另一种情况了。 相比消息队列,使用全局数组主要有如下四个问题:

  1. 使用消息队列可以让 RTOS 内核有效地管理任务,而全局数组是无法做到的,任务的超时等机制需要用户自己去实现
  2. 使用了全局数组就要防止多任务的访问冲突,而使用消息队列则处理好了这个问题,用户无需担心
  3. 使用消息队列可以有效地解决中断服务程序与任务之间消息传递的问题,使用全局数组任务则需要不断去监测标志位以获取数据
  4. FIFO 机制更有利于数据的处理

FreeRTOS任务间消息队列的实现

任务间消息队列的实现是指各个任务之间使用消息队列实现任务间的通信。

如下图:

运行条件
创建消息队列,可以存放10个消息
创建2个任务Task1和 Task2,任务 Task1向消息队列放数据,任务 Task2从消息队列获取数据
消息采用 FIFO 方式
运行过程主要有以下两种情况
任务 Task1 向消息队列放数据,任务 Task2从消息队列取数据,如果放数据的速度 快于 取数据的速度,那么会出现消息队列存放慢的情况,FreeRTOS的消息存放函数 xQueueSend 支持超时等待,用户可以设置超时等待,直到有空间可以存放消息或者设置的超时时间溢出
任务 Task1向消息队列放数据,任务 Task2 从消息队列取数据,如果放数据的速度 慢于 取数据的速度,那么会出现消息队列为空的情况,FreeRTOS的消息获取函数 xQueueReceive支持超时等待,用户可以设置超时等待,直到消息队列中有消息或者数组的超时时间溢出

FreeRTOS中断方式消息队列的实现

FreeRTOS 中断方式消息队列的实现是指中断函数和 FreeRTOS 任务之间使用消息队列

如下图:

运行条件
创建消息队列,可以存放10个消息
创建1个任务Task1和一个串口接收中断
消息采用 FIFO 方式
运行过程主要有以下两种情况
中断服务程序向消息队列放数据,任务Task1从消息队列取数据,如果放数据的速度 快于 取数据的速度,那么会出现消息队列存放满的情况。由于中断服务程序里面的消息队列发送函数 xQueueSendFromISR不支持超时设置,所以发送前要通过含 xQueueIsQueueFullFromISR 检测消息队列是否满
中断服务程序向消息队列放数据,任务 Task1 从消息队列取数据,如果放数据的速度 慢于 取数据的速度,那么会出现消息队列存为空的情况。在FreeRTOS的任务中可以通过函数 xQueueReceive 获取消息,因为此函数可以设置超时等待,直到消息队列中有消息存放或者设置的超时时间溢出

实际应用中,中断方式的消息机制要注意以下四个问题:

  1. 中断函数的执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应
  2. 实际应用中, 建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效地保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行
  3. 中断服务程序中一定要调用专用于中断的消息队列函数,即以 FromISR 结尾的函数
  4. 在操作系统中实现中断服务程序与裸机编程的区别
  • 如果 FreeRTOS 工程的中断函数中没有调用FreeRTOS 的消息队列 API 函数,与裸机编程是一样的。
  • 如果 FreeRTOS 工程的中断函数中调用了 FreeRTOS 的消息队列的 API 函数,退出的时候要检测是否有高优先级任务就绪,如果有就绪的,需要在退出中断后进行任务切换,这点与裸机编程稍有区别

消息队列API

以下函数可在 queue.h 找到定义,在 queue.c 找到实现,一共 24个函数

官网:FreeRTOS - FreeRTOS 队列 API 函数

队列API名 描述
xQueueCreate 创建一个新队列并返回:
成功–可引用此队列的句柄
失败–NULL【动态创建】
xQueueCreateStatic 创建一个新队列并返回 可以引用该队列的句柄【静态创建,不怎么用】
vQueueDelete 删除队列 — 释放分配用于存储放置在队列中的项目的所有内存
xQueueSend 在队列中发布消息【FIFO】
xQueueSendFromISR 在队列中发布消息【中断】
xQueueSendToBack 往队列尾部发布消息【FIFO】,这个跟上面一样
xQueueSendToBackFromISR 往队列尾部发布消息【中断】
xQueueSendToFront 往队列头部发布消息【LIFO】
xQueueSendToFrontFromISR 往队列头部发布消息【中断】
xQueueReceive 从队列中接收消息
xQueueReceiveFromISR 从队列中接收消息【中断】
uxQueueMessagesWaiting 返回队列中存储的消息数
uxQueueMessagesWaitingFromISR 返回队列中存储的消息数【中断】
uxQueueSpacesAvailable 返回队列中的可用空间数
xQueueReset 将队列重置为其原始的空状态
xQueueOverwrite 即使队列已满的情况下也将写入队列, 同时覆盖队列中已经 存在的数据【适用于长度为1的队列】
xQueueOverwriteFromISR 即使队列已满的情况下也将写入队列, 同时覆盖队列中已经 存在的数据【适用于长度为1的队列】【中断】
xQueuePeek 从队列中接收消息,而无须从队列中删除该消息。 消息由副本接收,因此必须提供适当大小的缓冲区
xQueuePeekFromISR 从队列中接收消息,而无须从队列中删除该消息。 消息由副本接收,因此必须提供适当大小的缓冲区【中断】
vQueueAddToRegistry 为队列指定名称,并将队列添加到注册表
vQueueUnregisterQueue 从队列注册表中删除队列
pcQueueGetName 从队列的句柄中查找队列名称
xQueueIsQueueFullFromISR 查询队列以确定队列是否已满【中断】
xQueueIsQueueEmptyFromISR 查询队列以确定队列是否为空

消息队列创建,删除

了解一下消息队列控制块(句柄)

typedef struct QueueDefinition
{
    //指向队列缓冲区的起始地址
    int8_t *pcHead;	
    //指向下一个可供写入的缓冲区地址
    int8_t *pcWriteTo;	
    //一个匿名的联合体,用于保存指向等待队列和信号量的指针
    union
    {
        QueuePointers_t xQueue;
        SemaphoreData_t xSemaphore;
    } u;
	//一个链表,用于保存等待发送的任务列表
    List_t xTasksWaitingToSend;
    //一个链表,用于保存等待接收的任务列表
    List_t xTasksWaitingToReceive;
	//记录队列中当前等待接收的消息数量
    volatile UBaseType_t uxMessagesWaiting;
    //队列中元素(消息)的数量(即缓冲区的总长度)
    UBaseType_t uxLength;
    //队列中每个元素(消息)的大小(以字节为单位)
    UBaseType_t uxItemSize;
	//接收锁,用于控制读取缓冲区的并发访问
    volatile int8_t cRxLock;
    //发送锁,用于控制写入缓冲区的并发访问
    volatile int8_t cTxLock;

#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
    //标志位,指示队列是否是静态分配的
    uint8_t ucStaticallyAllocated;
#endif

#if ( configUSE_QUEUE_SETS == 1 )
    //如果队列是QueueSet的一部分,则该指针指向QueueSet容器
    struct QueueDefinition *pxQueueSetContainer;
#endif

#if ( configUSE_TRACE_FACILITY == 1 )
    //队列在FreeRTOS内部的编号,用于跟踪调试
    UBaseType_t uxQueueNumber;
    //队列类型,用于跟踪调试
    uint8_t ucQueueType;
#endif

} xQUEUE;
//重命名为Queue_t
typedef xQUEUE Queue_t;
//定义一个消息队列
osMessageQId myQueue01Handle;

通过点击 osMessageQId 跳转可以知道最终它是一个什么类型

osMessageQId --> typedef QueueHandle_t osMessageQId; --> typedef struct QueueDefinition * QueueHandle_t;

创建队列

//参数1:队列长度,即消息个数
//参数2:每个消息大小,单位字节
//返回值:创建成功返回消息队列的句柄 如果由于FreeRTOSConfig.h文件中configTOTAL_HEAP_SIZE大小不足,无法为此消息队列提供所需空间会返回NULL
QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize );

使用这个函数要注意以下问题:

FreeRTOS 的消息传递是数据的复制,而不是传递的数据地址,这点要特别注意。 每一次传递都是 uxItemSize 个字节

删除消息队列

//参数:队列句柄
void vQueueDelete( QueueHandle_t xQueue );

说明:消息队列删除后,系统会清空此队列的全部消息,且不能再次使用此队列

任务中消息队列发送

//参数1:消息队列句柄
//参数2:要传递数据地址,每次发送都是将消息队列创建函数xQueueCreate所指定的单个消息大小复制到消息队列空间中(如果发送的是变量需要加&)
//参数3:等待消息队列有空间的最大等待时间,单位是:系统时钟节拍
//返回值:消息成功发送返回pdTRUE 失败返回errQUEUE_FULL
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait);

使用这个函数需要注意以下问题:

  1. FreeRTOS 的消息传递是数据的复制,而不是传递的数据地址
  2. 假设消息队列空间是10个字节,但是消息数据是4个字节那在发送时还是会发生10个字节不会以数据的大小而改变这个, xQueueSend()函数的第二个参数是固定长度的,长度取决于创建队列时指定的单个消息大小
  3. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数
  4. 如果消息队列已经满且第三个参数为 0,那么此函数会立即返回
  5. 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第三个参数配置为 portMAX_DELAY,那么此发送函数会永久等待直到消息队列有空间可以使用
  6. 消息队列还有两个函数 xQueueSendToBackxQueueSendToFront,函数 xQueueSendToBack
    实现的是 FIFO 方式的存取,函数xQueueSendToFront 实现的是 LIFO 方式的读写。 我们这里说的函数 xQueueSend 等效于 xQueueSendToBack,即实现的是 FIFO 方式的存取
  7. 发送的数据大小包括\r\n

中断中消息队列发送

//参数1:消息队列句柄
//参数2:要传递数据地址, 每次发送都是将消息队列创建函数 xQueueCreate 所指定的单个消息大小复制到消息队列空间中
//参数3:用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是 pdTRUE,说明有高优先级任务要执行,否则没有
//返回值:如果消息成功发送返回 pdTRUE,否则返回 errQUEUE_FULL
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * const pvItemToQueue,BaseType_t * const pxHigherPriorityTaskWoken)

使用这个函数要注意以下问题:

  1. FreeRTOS 的消息传递是数据的复制,而不是传递的数据地址。 正因为这个原因,用户在创建消息队列时单个消息大小不可太大,因为一定程度上面会增加中断服务程序的执行时间
  2. 此函数是用于中断服务程序中调用的,故不可以在任务代码中调用此函数
  3. 消息队列还有两个函数 xQueueSendToBackFromISRxQueueSendToFrontFromISR,函数
    xQueueSendToBackFromISR 实现的是 FIFO 方式的存取,函数 xQueueSendToFrontFromISR 实现的是 LIFO 方式的读写。 我们这里说的函数 xQueueSendFromISR 等效于xQueueSendToBackFromISR,即实现的是 FIFO 方式的存取

消息队列接收

//参数1:消息队列句柄
//参数2:从消息队列中复制出数据后所储存的缓冲地址,缓冲区空间要大于等于消息队列创建函数 xQueueCreate 所指定的单个消息大小,否则取出的数据无法全部存储到缓冲区,从而造成内存溢出。
//参数3:消息队列为空时,等待消息队列有数据的最大等待时间,单位系统时钟节拍
//返回值:如果接收到消息返回 pdTRUE,否则返回 pdFALSE
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );

使用这个函数要注意以下问题:

  1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数
  2. 如果消息队列为空且第三个参数为 0,那么此函数会立即返回
  3. 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第三个参数配置为 portMAX_DELAY,那么此函数会永久等待直到消息队列有数据

任务与任务程序

按键1:打印任务执行情况

按键2:向队列1发送单个数据

按键3:向队列2发送字符串

创建4个任务,分别是 LED任务按键任务队列1接收任务队列2接收任务

CubeMX配置

程序编写

  • xQueueCreate(1,sizeof(uint32_t)) 可以写成 xQueueCreate(1,4),因为uint32_t是4位,但是不能写成3否则接收数据是负数乱码
AllTask.c
extern osThreadId vled1_taskfunctionHandle;
extern osThreadId queue1rx_taskfunctionHandle;
extern osThreadId queue2rx_taskfunctionHandle;
extern osThreadId vkey_taskfunctionHandle;

//定义队列变量
QueueHandle_t xQueue1 = NULL;
QueueHandle_t xQueue2 = NULL;

//队列创建
void Queue_Create(void)
{
	//创建消息队列1接收1个消息
	xQueue1 = xQueueCreate(1,sizeof(uint32_t));
	if(NULL == xQueue1)
	{
		printf("创建消息队列1失败\r\n");
	}
	else
	{
		printf("创建消息队列1成功\r\n");
	}
	//创建消息队列2接收2个消息
	xQueue2 = xQueueCreate(2,16);
	if(NULL == xQueue2)
	{
		printf("创建消息队列2失败\r\n");
	}
	else
	{
		printf("创建消息队列2成功\r\n");
	}
}

//消息队列2任务
void Queue2RX_TaskFunction(void const * argument)
{
	uint8_t ucRX_Data[16] = {0};	//接收队列2数据数组
	
	for(;;)
	{
		if(pdTRUE == xQueueReceive(xQueue2,ucRX_Data,portMAX_DELAY))	//永远等待它不会超时的
		{
			printf("成功接收消息队列2的字符串:%s\r\n",ucRX_Data);
		}
	}
}

//消息队列1任务
void Queue1RX_TaskFunction(void const * argument)
{
	uint32_t ucRx_Data;	//存放接收的数据
	const TickType_t RX_BlockTime = pdMS_TO_TICKS(1000);	//1s
	
	for(;;)
	{
		if(pdTRUE == xQueueReceive(xQueue1,&ucRx_Data,RX_BlockTime))
		{
			printf("成功接收消息队列1的数据:%d\r\n",ucRx_Data);
		}
		else
		{
			printf("接收消息队列1的数据出现超时!!!\r\n");
		}
	}
}

//按键任务
void vKey_TaskFunction(void const* argument)
{
	for(;;)
	{
		KEY_function();
		KEY_RUNFLAG();
		osDelay(20);
	}
}


void vLED1_TaskFunction(void const * argument)
{
	for(;;)
	{
		LED_Togg(0x01);
		osDelay(1000);
	}
}
KEY.c
extern osThreadId vled1_taskfunctionHandle;
extern osThreadId queue1rx_taskfunctionHandle;
extern osThreadId queue2rx_taskfunctionHandle;
extern osThreadId vkey_taskfunctionHandle;
extern QueueHandle_t xQueue1;
extern QueueHandle_t xQueue2;

void KEY_RUNFLAG(void)
{
	uint8_t CPU_Run[500];	//保存任务运行时间信息
	static uint32_t ucSend_Data = 1;	//向消息队列1发送的数据
	 const TickType_t SendBlockTime = pdMS_TO_TICKS(10);
	if(KeyData.KEY1_DOWN_FLAG)	//打印任务情况
	{
		KeyData.KEY1_DOWN_FLAG = 0;
		vTaskList((char*)&CPU_Run);	
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                  任务状态  优先级  剩余栈  任务序号\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		
		vTaskGetRunTimeStats((char*)&CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");
		printf("任务名                                 运行计数              利用率\r\n");
		printf("%s",CPU_Run);
		printf("-----------------------------------------------------------------------------------------\r\n");		
	}
	if(KeyData.KEY2_DOWN_FLAG)	//向队列1发送单个数据
	{
		KeyData.KEY2_DOWN_FLAG = 0;
		if(pdTRUE == xQueueSend(xQueue1,&ucSend_Data,SendBlockTime))
		{
			printf("成功向消息队列1发送数据:%d\r\n",ucSend_Data);
		}
		else
		{
			printf("向消息队列1发送数据出现超时!!!\r\n");
		}
		//更新要发送的数据
		ucSend_Data += 1000;
	}
	if(KeyData.KEY3_DOWN_FLAG)
	{
		KeyData.KEY3_DOWN_FLAG = 0;
		if(pdTRUE == xQueueSend(xQueue2,"MCUSTM32G431\r\n",0))
		{
			printf("成功向消息队列2发送字符串:%s\r\n","MCUSTM32G431");
		}
		else
		{
			printf("按键3向消息队列2发送字符串出现超时!!!\r\n");
		}
	}
	if(KeyData.KEY4_DOWN_FLAG)
	{
		KeyData.KEY4_DOWN_FLAG = 0;
		if(pdTRUE == xQueueSend(xQueue2,"Yang5201314",0))
		{
			printf("成功向消息队列2发送字符串:%s\r\n","Yang5201314");
		}
		else
		{
			printf("按键4向消息队列2发送字符串出现超时!!!\r\n");
		}
	}	
}

实验现象

因为消息队列1任务是一直在判断是否接收到数据,如果没有接收到则会打印 接收消息队列1的数据出现超时!!!,间隔是1s(任务里有写),当按下按键2则发送32位的数据,然后消息队列1任务就接收打印,按键3则发送字符串,因为接收任务那是使用无限等待的,所以不会返回FALSE,直到接收到字符串数据才打印

中断与任务程序

MX创建队列注意

osMessageQId myQueue01Handle;

osMessageQDef(myQueue01, 10, ucUSART1_RX_BUFF);
myQueue01Handle = osMessageCreate(osMessageQ(myQueue01), NULL);

这个消息大小可以是 uint8_t,uint16_t...,也可以是结构体名称,数组名称(需要在程序里定义)

需要注意 Item Size 不能直接填数字,原因是:

首先看它底层,它这个最终是使用 sizeof() 来计算的所以如果给数字,那它只能是4(int)

创建3个任务,分别是LED,按键,队列1任务

创建一个消息队列名字为:myQueue01

配置 USART1中断

实验分析:在串口1中断里进行消息队列的发送,然后在队列1任务里进行打印接收的数据

CubeMX配置

程序编写

my_usart1.h
#ifndef __MY_USART1_H
#define __MY_USART1_H
#include "AllTask.h"

#define RX_MAX_LEN 10


extern uint8_t ucUSART1_RX_BUFF[RX_MAX_LEN];
extern uint8_t ucRx_Len;
extern bool Rx_Over_Flag;


#endif
my_usart1.c
  1. 将串口接收数据放在队列发送数据后面,这样可以避免在发送数据时接收到新的数据导致数据丢失
  2. 将任务切换放在最后,这样可以避免在任务切换时新的中断到来导致数据丢失
  3. 添加了队列满的处理,可以选择丢弃数据或者等待队列可用
#include "my_usart1.h"

uint8_t ucUSART1_RX_BUFF[RX_MAX_LEN] = {0};
uint8_t ucRx_Len = 0;
bool Rx_Over_Flag = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if(huart == &huart1)
    {
        //向队列1发送数据
        if(xQueueSendFromISR(myQueue01Handle, ucUSART1_RX_BUFF, &xHigherPriorityTaskWoken) != pdPASS)
        {
            //如果队列满了,可以选择丢弃数据或者等待队列可用
            //xQueueReset(myQueue01Handle);
            //xQueueSendFromISR(myQueue01Handle, ucUSART1_RX_BUFF, &xHigherPriorityTaskWoken);
			printf("队列满了\r\n");
        }
        //继续通过串口中断接收字符
        HAL_UART_Receive_IT(&huart1,(uint8_t*)ucUSART1_RX_BUFF, RX_MAX_LEN);
    }
    //如果有高优先级任务就绪,执行一次任务切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
AllTask.c
//消息队列1任务
void Queue1RX_TaskFunction(void const * argument)
{
	uint8_t ucRx_Data[10] = {0};	//存放接收的数据
	
	//接收中断,够RX_MAX_LEN个才会触发中断
	HAL_UART_Receive_IT(&huart1,(uint8_t*)ucUSART1_RX_BUFF,RX_MAX_LEN);
	for(;;)
	{
		if(pdPASS == xQueueReceive(myQueue01Handle,&ucRx_Data,portMAX_DELAY))
		{
			printf("成功接收消息队列1的数据:%s\r\n",ucRx_Data);
		}
	}
}

第11讲

信号量的概念与分类

消息队列 是实现任务与任务或任务与中断间通信的数据结构,可类比裸机编程中的数组

信号量 是实现任务与任务或任务与中断间通信的机制,可以类比裸机编程中的标志位

比如:比如有个 30 人的电脑机房,我们就可以创建信号量的初始化值是 30,表示 30 个可用资源,不理解的初学者表示信号量还有初始值?是的,信号量说白了就是 共享资源的数量。 另外我们要求一个同学使用一台电脑,这样每有一个同学使用一台电脑,那么信号量的数值就减一,直到 30 台电脑都被占用,此时信号量的数值就是 0。 如果此时还有几个同学没有电脑可以使用,那么这几个同学就得等待,直到有同学离开。 有一个同学离开,那么信号量的数值就加 1,有两个就加 2,依此类推。刚才没有电脑用的同学此时就有电脑可以用了,有几个同学用,信号量就减几,直到再次没有电脑可以用,这么一个过程就是使用信号量来管理共享资源的过程。

信号量可以实现任务与任务或任务与中断间的 同步功能(二值信号量)资源管理(计数信号量)临界资源的互斥访问(互斥信号量)

信号量是一个 非负正数,二值信号量与互斥信号量取值范围为 0-1,计数信号量取值范围是 0-N(N>1) , 0表示信号量为空,所有试图获取它的任务都将处于阻塞状态,直到超时退出或其他任务释放信号量 正数表示有一个或多个信号量供获取

平时使用信号量主要实现以下两个功能:

平时使用信号量主要实现以下两个功能:

  1. 两个任务之间或者中断函数跟任务之间的同步功能,这个和前面章节讲解的事件标志组是类似的。其实就是共享资源为 1 的时候。
  2. 多个共享资源的管理,就像上面举的机房上机的例子。针对这两种功能,FreeRTOS 分别提供了二值信号量和计数信号量,其中二值信号量可以理解成计数信号量的一种特殊形式,即初始化为仅有一个资源可以使用,只不过 FreeRTOS 对这两种都提供了 API函数,而像 RTX,uCOS-II 和 III 是仅提供了一个信号量功能,设置不同的初始值就可以分别实现二值信-号量和计数信号量。 当然,FreeRTOS 使用计数信号量也能够实现同样的效果。
信号量的分类
二值信号量(重点同步应用,类似裸机的Flag)
计数信号量(重点资源管理)
互斥信号量(重点互斥访问)
递归互斥信号量(简单了解)

二值信号量的定义与应用

定义:信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。

创建二值信号量时,系统会为创建的二值信号量分配内存,二值信号量创建完成后的示意图为:

从上图可以看出,二值信号量是一种 长度为1,消息大小为0的特殊消息队列

因为这个队列只有空或满两种状态,而且消息大小为0,因此在运用时,只需要知道队列中是否有消息即可,而无需关注消息是什么。

在嵌入式系统中,二值信号量是 任务与任务 或者 任务与中断同步的重要手段

二值信号量也可以用于 临界资源的访问,但不建议,因为存在 任务优先级翻转 问题,这个将在下一讲的 互斥信号量(具有优先级继承机制)中进行详细讲解

任务与任务中同步的应用场景

假设有一个温湿度传感器,每1s采集一次数据,那么让它在液晶屏中显示数据,这个周期也是1s,如果液晶屏刷新的周期是100ms,那么此时的温湿度数据还没更新,液晶屏根本无须刷新,只需要在1s后温湿度数据更新时刷新即可,否则CPU就是白白做了多次的无效数据更新操作,造成CPU资源浪费。如果液晶屏刷新的周期是10s,那么温湿度的数据都变化了10次,液晶屏才来更新数据,那么这个产品测得的结果就是不准确的,所以还是需要同步协调工作,在温湿度采集完毕之后进行液晶屏数据的刷新,这样得到的结果才是最准确的,并且不会浪费CPU的资源。

上面例子虽然也可以像裸机直接设置一个标志位来进行判断,但是拿信号量来实现的好处是可以解决多任务之间的同步和互斥问题

任务与中断中同步的应用场景

在串口接收中,我们不知道什么时候有数据发送过来,但如果设置一个任务专门时刻查询是否有数据到来,将会浪费CPU资源,所以在这种情况下使用二值信号量是很好的办法: 当没有数据到来时,任务进入阻塞态,不参与任务的调度;等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后在运行时处理数据,这样系统的资源就会得到很好的利用

二值信号量的运作机制

任务间二值信号量的实现

任务间二值信号量的实现是指各个任务之间使用信号量实现任务的同步功能。下面我们通过如下的框图来说明一下FreeRTOS二值信号量的实现:

  • 首先创建2个任务Task1和Task2
  • 创建二值信号量默认的初始值是0,也就是没有可用资源

运行过程:任务Task1运行过程中调用函数 xSemaphoreTake 获取信号量资源,但是由于创建二值信号的初始值是0,没有信号量可以用,任务Task1将由运行态转到阻塞状态。运行的过程中任务Task2通过函数 xSemaphoreGive 释放信号量,任务Task1由阻塞态进入到就绪态,在调度器的作用下由就绪态又进入到运行态,实现Task1与Task2的同步功能。

中断方式二值信号量的实现

FreeRTOS中断方式二值信号量的实现是指中断与任务间使用信号量实现同步功能。下面我们通过如下的框图来说明一下FreeRTOS中断方式二值信号量的实现:

  • 创建1个任务Task1和一个串口接收中断
  • 二值信号量的初始值为0,串口中断调用函数 xSemaphoreGiveFromISR 释放信号量,任务Task1调用函数 xSemaphoreTake 获取信号量资源

运行过程:任务Task1运行过程中调用函数 xSemaphoreTake,由于信号量的初始值是0,没有信号量资源可用,任务Task1由运行态进入到阻塞态。Task1阻塞的情况下,串口接收到数据进入到了串口中断服务程序,在串口中断服务程序中调用函数 xSemaphoreGiveFromlSR 释放信号量资源,信号量数值加1,此时信号量计数值为1,任务Task1由阻塞态进入到就绪态,在调度器的作用下由就绪态又进入到运行态,任务Task1获得信号量后,信号量数值减1,此时信号量计数值又变成了0。再次循环执行时,任务Task1调用函数 xSemaphoreTake 由于没有资源可用再次进入到挂起态,等待串口释放二值信号量资源,如此往复循环。

实际应用中,中断方式的消息机制要注意以下四个问题

  1. 中断函数的 执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应
  2. 实际应用中,建议 不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效地保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行
  3. 中断服务程序中一定要 调用专用于二值信号量设置函数,即以FromISR结尾的函数
  4. 如果FreeRTOS工程的中断函数中调用了FreeRTOS的二值信号量的API函数退出的时候 要检测是否有高优先级任务就绪,如果有就绪的,需要在退出中断后进行任务切换

常用的API函数

使用二值信号量的典型流程如下:

  1. 创建二值信号量
  2. 释放二值信号量(0–>1)
  3. 获取二值信号量(1–>0)
  4. 删除二值信号量

二值信号量创建与删除

  • 二值信号量控制块(句柄)

二值信号量的句柄为消息队列的句柄,因为二值信号量是一种长度为1,消息大小为0的特殊消息队列; 变量xBinarySem 是一个指向 osSemaphoreId 类型的指针,用于保存信号量的句柄或者标识符

//定义二值信号量
osSemaphoreId xBinarySem = NULL;

通过点击 osSemaphoreId 跳转可以看到最后它其实也是一个消息队列句柄

osSemaphoreId --> typedef SemaphoreHandle_t osSemaphoreId; --> typedef QueueHandle_t SemaphoreHandle_t; --> typedef struct QueueDefinition * QueueHandle_t;
  • 二值信号量的创建
//函数原型
//函数描述:创建二值信号量
//返回值:创建成功会返回二值信号量的句柄,如果由于 FreeRTOSConfig.h文件中heap大小不足,无法为此二值信号量提供所需的空间会返回NULL
QueueHandle_t xSemaphoreCreateBinary(void)

此函数是基于消息队列函数实现:

//使用示例
xBinarySem = xSemaphoreCreateBinary();
if(NULL == xBinarySem)
{
    printf("创建二值信号量失败\r\n");
}
else
{
    printf("创建二值信号量成功\r\n");
}
  • 二值信号量删除
//函数原型
//函数描述:用于删除二值信号量
void vSemaphoreDelete(void);
  • 任务中二值信号量释放
//函数原型
//函数描述:用于任务中二值信号量释放
//参数:信号量句柄
//返回值:如果信号量释放成功返回pdTRUE,否则返回pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了
BaseType_t xSemaphoreGive(SemaphoreData_t xSemaphore);

使用这个函数需要注意下面问题:

  1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是 xSemaphoreGiveFromISR

  2. 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary()--二值, xSemaphoreCreateMutex()--互斥 或者 xSemaphoreCreateCounting()--计数 创建了信号量

  3. 此函数不支持使用 xSemaphoreCreateRecursiveMutex()--递归互斥 创建的信号量

//使用示例
printf("发送同步信号\r\n");
xResult = xSemaphoreGive(xBinarySem);
if(pdTRUE == xResult)
{
    printf("成功发送二值信号量同步信号,次数=%d\r\n",++GiveCnt);
}
else
{
    printf("发送二值信号量同步信号失败\r\n");
}
  • 中断中二值信号量释放
//函数原型
//函数描述:用于中断服务程序中释放信号量
//参数1:信号量句柄
//参数2:用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数是pdTRUE,说明有高优先级任务要执行,否则没有
//返回值:如果信号量释放成功返回pdTRUE,否则返回errQUEUE_FULL
BaseType_t xSemaphoreGiveFromISR( SemaphoreData_t xSemaphore, BaseType_t * const pxHigherPriorityTaskWoken );

使用这个函数需要注意下面问题:

  1. 此函数是基于消息队列函数 xQueueGiveFromISR 实现的:
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )	xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
  1. 此函数是用于中断服务程序中调用的,故不可以任务代码中调用此函数,任务代码中中使用的是 xSemaphoreGive
  2. 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary() 或者 xSemaphoreCreateCounting() 创建了信号量。
  3. 此函数不支持使用 xSemaphoreCreateMutex() 创建的信号量。
//使用示例
void HAL_UART_RxCpltCallback(UART_HandleTypeDefe *huart)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if(huart->Instance == huart3.Instance)
    {
        //发送同步信号
        xSemaphoreGiveFromISR(myBinarySem01Handle,&xHigherPriorityTaskWoken);
        //如果有高优先级任务就绪,执行一次任务切换
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}
  • 任务中二值信号量获取
//函数原型
//参数1:信号量句柄
//参数2:是没有信号量可用时,等待信号量可用的最大等待时间,单位系统节拍
//返回值:如果创建成功会获取信号量返回pdTRUE,否则返回pdFALSE
BaseType_t xSemaphoreTake(SemaphoreData_t xSemaphore, TickType_t xBlockTime);

使用这个函数需要注意下面问题:

  1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是 xSemaphoreTakeFromISR
  2. 如果消息队列为空且第2个参数为0,那么此函数会立即返回
  3. 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为1且第2个参数配置为 portMAX_DELAY,那么此函数会永久等待直到信号量可用。(一般串口我们设置为永久等待)
//使用示例
uint8_t ucUSART1_RX_BUFF[10] = {0};

void BinarySem_SyncReceive_Task(void const *argument)
{
    BaseType_t xResult;
    uint16_t TakeCnt = 0;	//获取计数
    
    for(;;)
    {
        //通过串口中断接收10个字符
        HAL_UART_Receive_IT(&huart1,(uint8_t*)ucUSART1_RX_BUFF,10);
        printf("等待同步信号,无限等待\r\n");
        xResult = xSemaphoreTake(myBinarySem01Handle,portMAX_DELAY);
        if(pdTRUE == xResult)
        {
            printf("成功接收到二值信号量同步信号次数 = %d\r\n",++TakeCnt);
            printf("接收达到的串口数据:%s\r\n",ucUSART1_RX_BUFF);
        }
    }
}

任务与任务程序

创建3个任务:LED,按键,信号量同步

手动创建二值信号量

MX配置

程序编写

app_freertos.c(自动创建的)

这里我们调用API手动创建二值信号量

//定义二值信号量
osSemaphoreId xBinartSem = NULL;

void MX_FREERTOS_Init(void)
{
    ...
    xBinartSem = xSemaphoreCreateBinary();
    if(NULL == xBinartSem)
    {
	    printf("创建二值信号量失败\r\n");
    }
    else
    {
	    printf("创建二值信号量成功\r\n");
    }        
}
AllTask.c
extern osSemaphoreId xBinartSem;

//同步信号任务
void Binarysem_Function(void const * argument)
{
	BaseType_t xResult;
	uint16_t TakeCnt = 0;	//获取计数
	for(;;)
	{
		printf("等待同步信号,无限等待\r\n");
		xResult = xSemaphoreTake(xBinartSem,portMAX_DELAY);	//获取二值信号量
		
		if(pdTRUE == xResult)
		{
			printf("成功接收到二值信号量同步信号,次数=%d\r\n",++TakeCnt);
		}
	}
}
KEY.c
extern osSemaphoreId xBinartSem;
uint16_t GiveCnt = 0;	//释放计数

void KEY_RUNFLAG(void)
{
	if(KeyData.KEY2_DOWN_FLAG)	
	{
		KeyData.KEY2_DOWN_FLAG = 0;
		printf("发送同步信号\r\n");
		xResult = xSemaphoreGive(xBinartSem);	//释放二值信号量
		
		if(pdTRUE == xResult)
		{
			printf("成功发送二值信号量同步信号,次数 = %d\r\n",++GiveCnt);
		}
		else
		{
			printf("发送二值信号量同步信号失败\r\n");
		}
	}    
}

中断与任务程序

MX创建二值信号量

//MX生成的创建二值信号量代码
osSemaphoreId myBinarySem01Handle; 

osSemaphoreDef(myBinarySem01);
myBinarySem01Handle = osSemaphoreCreate(osSemaphore(myBinarySem01), 1);

需要注意的是我们需要手动添加一些代码在它创建完后面:

if(NULL == myBinarySem01Handle)
{
    printf("创建二值信号量失败\r\n");
}
else
{
    printf("创建二值信号量成功\r\n");
}
xSemaphoreTake(myBinarySem01Handle, 0);	//MX生成的创建二值信号量时默认为1所以需要去获取一次

因为它默认生成的代码是默认是1(即默认是释放),所以需要创建完二值信号量后获取一次(信号量变成0),原因可以通过点击 osSemaphoreCreate 函数跳转源码查看:

程序编写

my_usart1.c
extern osSemaphoreId myBinarySem01Handle;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	
	if(&huart1 == huart)
	{
		//发送同步信号
		xSemaphoreGiveFromISR(myBinarySem01Handle,&xHigherPriorityTaskWoken);
		//如果有高优先级任务则进行一次切换
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	}
}
AllTask.c
extern osSemaphoreId myBinarySem01Handle;

void Binarysem_Function(void const * argument)
{
	BaseType_t xResult;
	uint16_t TakeCnt = 0;	//获取计数
	for(;;)
	{
		//通过串口中断接收10个字符
		HAL_UART_Receive_IT(&huart1,(uint8_t*)ucUSART1_RX_BUFF,RX_MAX_LEN);
		printf("等待同步信号,无限等待\r\n");
		xResult = xSemaphoreTake(myBinarySem01Handle,portMAX_DELAY);
		printf("num:%ld\r\n",xResult);
		if(pdTRUE == xResult)
		{
			printf("成功接收到二值信号量同步信号,次数:%d\r\n",++TakeCnt);
			printf("接收到的串口数据:%s\r\n",ucUSART1_RX_BUFF);
		}
	}
}

第12讲

计数信号量的定义与应用

待写2023.3.23

内存管理

FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口( API)却是统一的。这样做可以增加系统的灵活性: 用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。

FreeRTOS 的内存管理模块 通过对内存的申请、释放操作,来管理用户和系统对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题

统一的上层接口(API)

void *pvPortMalloc( size_t xSize ); //内存申请函数
void vPortFree( void *pv ); //内存释放函数
void vPortInitialiseBlocks( void ); //初始化内存堆函数
size_t xPortGetFreeHeapSize( void ); //获取当前未分配的内存堆大小
size_t xPortGetMinimumEverFreeHeapSize( void ); //获取未分配的内存堆历史最小值

FreeRTOS 提供的内存管理都是从内存堆中分配内存的。 创建任务、消息队列、事件等操作都使用到分配内存的函数,这是系统中默认使用内存管理函数从内存堆中分配内存给系统核心组件使用。

内存管理方案 描述
heap_1.c 只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是 内存利用率不高, 某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。
① 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS 的应用程序都符合这个条件) 。
② 函数的执行时间是确定的并且不会产生内存碎片。
heap_2.c 采用一种 最佳匹配算法,支持释放申请的内存, 但是它不能把相邻的两个小的内存块合成一个大的内存块, 对于每次申请内存大小都比较固定的,这个方式是没有问题的, 而对于每次申请并不是固定内存大小的则会造成内存碎片
① 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的应用程序。
② 如果我们的应用程序中的队列、任务、信号量、 等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片。
③ 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多
④ 不能用于那些内存分配和释放是随机大小的应用程序。
heap_3.c 简单的封装了标准 C 库中的 malloc()free() 函数, 并且能满足常用的编译器。 重新封装后的 malloc()和 free()函数具有 保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。
① 需要链接器设置一个堆, malloc()和 free()函数由编译器提供
② 具有不确定性
③ 很可能增大 RTOS 内核的代码大小
注意:configTOTAL_HEAP_SIZE 宏定义不起作用
heap_4.c 采用 最佳匹配 算法以及 合并 算法
① 可用于重复删除任务,队列,信号量,互斥量等的应用程序
② 可用于分配和释放随机字节内存的应用程序,不会产生严重的内存碎片
③ 具有不确定性,但是效率比标准c库的malloc函数高得多
heap_5.c 采用 最佳匹配 算法和 合并 算法,并且 允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配

FreeRTOS的疑问

临界段和调度锁?

在 FreeRTOS 中, 临界段只是禁止其他中断打断当前任务,而并没有禁止其他任务抢占当前任务的 CPU 时间。因此,如果当前任务在临界区内被一个高优先级任务抢占,那么当前任务会被挂起,等待高优先级任务执行完毕后才能继续执行。 临界段的作用是保护共享资源,在临界区内对共享资源的访问是原子的,即不会被其他中断打断,但是并没有保护当前任务不被其他任务抢占如果需要保护当前任务不被其他任务抢占,可以使用调度锁来实现。 因此,在使用临界段时,需要注意该机制只能保证共享资源的原子性,而不能保证当前任务不被其他任务抢占。如果需要保证当前任务不被其他任务抢占,可以使用调度锁或者其他机制来实现。【所以单单使用临界段访问共享资源还是有风险的最好配合调度锁】

中断中的临界段?

uxSavedInterruptStatus这个变量的值不是决定中断是否打开的关键因素。它的作用是在退出临界段时恢复进入临界段之前的中断状态,从而保持中断的嵌套状态不变。具体来说,当进入临界段时,taskENTER_CRITICAL_FROM_ISR()函数会将中断嵌套计数器加1,如果此时中断是打开的,那么函数会将uxSavedInterruptStatus变量的值设置为1,表示进入临界段前中断是打开的。当退出临界段时,taskEXIT_CRITICAL_FROM_ISR()函数会根据中断嵌套计数器的值来决定是否开启中断,如果中断嵌套计数器为0,即没有嵌套临界段的情况,那么函数会将中断开启或关闭,具体的开启或关闭动作取决于uxSavedInterruptStatus变量的值。如果uxSavedInterruptStatus变量的值为1,表示进入临界段前中断是打开的,那么函数会在退出临界段时重新开启中断。如果uxSavedInterruptStatus变量的值为0,表示进入临界段前中断是关闭的,那么函数会在退出临界段时保持中断关闭状态。因此,uxSavedInterruptStatus变量的值不是决定中断是否打开的关键因素,而是用于恢复中断状态的。 默认情况下,中断是打开状态,还有注意的是不受FreeRTOS管理的中断还是会打断进入临界段的任务

所以在使用临界段函数时,要尽可能地缩小临界区,以减少禁止中断的时间,从而避免对系统的响应性产生影响

中断优先级跟任务优先级区别

简单的说,这两个之间没有任何关系, 不管中断的优先级是多少,中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序

HAL_Delay在FreeRTOS作用

HAL_Delay 函数在FreeRTOS里也可以使用,但是它不会造成阻塞,它是在那空等时间到

任务控制块作用

TCB是任务控制块(Task Control Block)的缩写,是一个用于管理任务的数据结构。TCB结构体中保存了任务的各种信息,如任务的状态、优先级、堆栈指针、等待事件、任务延时等等。

在FreeRTOS中,每个任务都有一个对应的TCB结构体,这个结构体可以在任务创建时分配内存并初始化,然后在任务运行时被用来管理任务的各种状态和信息。任务的创建、删除、挂起、恢复等操作都会对任务的TCB结构体进行修改,从而影响任务的运行。

通过结构体里面这些信息,FreeRTOS可以实现任务的调度、切换、挂起、恢复等操作,从而实现多任务系统的功能

为什么cubemx以前生成的代码是 typedef void* TaskHandle_t;,后来改成 typedef struct tskTaskControlBlock* TaskHandle_t; 为什么作者要改成这样?

原因是因为这个改变可以提高代码的可读性和可维护性。

typedef void* TaskHandle_t;的定义中,TaskHandle_t是一个指向任意类型数据的指针,这意味着它可以指向任何类型的数据,而不仅仅是指向任务控制块。这样的定义虽然可以工作,但是在代码中使用起来不够直观,容易引起混淆和错误。 相比之下,typedef struct tskTaskControlBlock* TaskHandle_t;的定义更加明确和准确,它直接将TaskHandle_t定义为一个指向任务控制块的指针类型。这样的定义可以提高代码的可读性和可维护性,使得代码更加易于理解和修改。此外,使用结构体指针的方式来定义任务句柄,还可以避免在代码中进行不必要的类型转换,从而提高代码的安全性和可靠性。

vPrintString函数的使用

如果需要这个函数可以在CubeMX里把 USE_TRACE_FACILITYUSE_STATS_FORMATTING_FUNCTIONS 使能,然后在应用程序中包含 FreeRTOS/Source/debug/printf-stdarg.c 文件

vPrintString 是一个RTOS内核中的输出函数,用于将一个字符串输出到控制台或者其他设备上。它的优点是可以在RTOS内核中使用,输出的字符串可以和RTOS其他任务的输出混合在一起,方便调试和监控。【默认没有换行,需要换行可以使用 vPrintStringAndNewLine 函数】

相对延时和绝对延时区别?

举例:

运行条件:

  1. 有一个 bsp_KeyScan 函数,这个函数处理时间大概耗时 2ms。
  2. 有两个任务,一个任务 Task1 是用的 vTaskDelay 延迟,延迟 10ms,另一个任务 Task2 是用的
    vTaskDelayUntil 延迟,延迟 10ms。
  3. 不考虑任务被抢占而造成的影响

SysTick 的优先级配置为最低,那延迟的话系统时间会不会有偏差?

答案是不会的,因为 SysTick 只是当次响应中断被延迟了,而 SysTick 是硬件定时器,它一直在计时,这一次的溢出产生中断与下一次的溢出产生中断的时间间隔是一样的,至于系统是否响应还是延迟响应, 这个与 SysTick 无关,它照样在计时。

在CubeMX里把MAX_PRIORITIES选项写了7为什么程序里使用8也能正常运行

在FreeRTOS中,MAX_PRIORITIES选项定义了任务优先级的最大值。根据FreeRTOS文档的说明,MAX_PRIORITIES的默认值是5,最大值是256。在Cubemx里将其设置为7,实际上就是将其设置为了7。 如果在程序中使用了大于7的优先级,程序仍然可以正常运行,因为FreeRTOS并不会检查任务优先级的范围。如果超出了范围,FreeRTOS会将其限制在MAX_PRIORITIES-1的范围内,因此在你的例子中,使用了8的优先级实际上被限制在了7。 因此,你在程序中使用8的优先级并不会有任何问题,但仍然建议使用符合规范的优先级范围,以避免潜在的问题。,可以使用 uxTaskPriorityGet() 函数查询任务优先级

绝对不可以在优先级为0的中断服务例程中调用RTOSAPI函数

在FreeRTOS中,优先级为0的中断服务例程通常是用于处理紧急事件的,例如硬件故障、系统崩溃等。在这种情况下,RTOS内部的一些操作可能已经被中断打断,因此在中断服务例程中调用RTOSAPI函数可能会导致意想不到的行为,例如死锁、资源争用等问题。此外,优先级为0的中断服务例程的执行时间应该尽量短,以保证系统的实时性和稳定性。因此,为了避免不必要的风险和影响,最好避免在优先级为0的中断服务例程中调用RTOSAPI函数。

FreeRTOS执行任务过程

FreeRTOS 使用一种称为 调度器 的算法,它可以在任务之间自动切换,以确保系统资源合理分配。它会检查正在运行的任务,如果发现任务函数是一个死循环,它会把CPU时间分配给其他任务,以便它们也能够正常运行。比如,假设系统中有3个任务,A,B 和 C,A的优先级最高,B的优先级次之,C的优先级最低。如果A中的任务函数是一个死循环,则调度器会把CPU时间分配给B和C,以便它们也能够正常运行。

首先执行的最高优先级的任务Task1,Task1 会一直运行 直到遇到系统阻塞式的 API 函数,比如延迟,事件标志等待,信号量等待, Task1 任务会被挂起,也就是释放CPU的执行权,让低优先级的任务得到执行。

裸机开发跟FreeRTOS死循环

在裸机开发中,死循环会一直占用CPU时间,从而阻止其他功能得以执行。然而,在FreeRTOS中,死循环不会阻塞其他任务,因为FreeRTOS中的调度器会根据任务的优先级和时间片来决定任务的运行时间,从而实现多任务的同时运行。

为什么要空闲任务?

因为FreeRTOS一旦启动,就必须要保证系统中 每时每刻都有一个任务处于运行状态(Runing),并且空闲任务不可以被 挂起与删除,空闲任务的 优先级最低的,以便系统中其他任务能随时抢占空闲任务的CPU使用权。(这些都是系统必要的东西,无需用户自己实现,处理完这些系统才真正启动)

FreeRTOS里抢占式rtos调度程序与协作式rtos调度程序区别

抢占式RTOS调度程序是指调度器会把任务 按照优先级排序 ,并自动从优先级最高的任务开始执行,当优先级高的任务结束后自动切换到优先级次高的任务执行;而协作式RTOS调度程序是指 调度器不会自动切换任务,而是由被调度的任务自行切换,任务可以选择是否执行其他任务。

FreeRTOS里互斥量,递归互斥量,计数信号量什么意思

互斥量(Mutex)是FreeRTOS中提供的一种互斥机制,它可以帮助多个任务访问共享资源时保持互斥, 即一次只有一个任务可以访问该资源,以防止两个任务同时访问同一资源;从而避免了任务之间的竞争,保证了系统的稳定性。
递归互斥量(Recursive Mutex)是FreeRTOS中提供的一种特殊的互斥量,它 可以被同一个任务多次加锁,而不会出现死锁的情况,从而可以帮助任务在访问共享资源时保持互斥性。

计数信号量(Counting Semaphore)是FreeRTOS中提供的一种特殊的计数器,它可以用来控制多个任务同时访问共享资源的数量,从而可以有效地提高系统的效率。

FreeRTOS里钩子函数什么意思

钩子函数是FreeRTOS中的一种特殊函数,它可以被任务和调度器调用,以实现在任务切换、调度器启动和关闭等特定情况下执行特定操作的功能。它可以被用户用于实现一些自定义的功能,比如跟踪任务的运行情况、监控任务的堆栈使用情况等。

钩子函数之所以被称为钩子函数,是因为它们可以像钩子一样,钩住任务的执行流程,从而实现特定的功能。

FreeRTOS里抢占优先级为什么是1~15

因为0是单片机最高的优先级,一般系统保留给内部使用,以便系统可以在重要情况下响应中断,而用户则应尽量从优先级1开始,这样可以保证系统的稳定性和正常运行。

换算

16位的系统中(比如8086微机) 1字 (word)= 2字节(byte)= 16(bit)
32位的系统中(比如win32) 1字(word)= 4字节(byte)=32(bit)
64位的系统中(比如win64)1字(word)= 8字节(byte)=64(bit)

FreeRTOS里临界区什么意思

FreeRTOS临界区可以理解为一个特殊的区域, 只有当一个任务进入临界区,其他任务才能进入,而在一个任务在临界区内完成操作之前,其他任务都无法进入这个区域。例如当你去银行取钱时,你必须进入取款机区域,而在你取完钱之前,其他人都不能进这个区域。【也就是互斥量功能】

创建任务时并不需要写进入临界区退出临界区,但是在使用临界区机制时,必须显式地进入临界区和退出临界区。

FreeRTOS函数

函数/宏定义 HAL代码(封装) 作用
void vTaskDelete( TaskHandle_t xTaskToDelete ) / 参数类型为任务句柄,一般参数写 NULL,表示删除调用者任务而不是删除指定任务
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char *const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask ) osThreadId osThreadCreate (const osThreadDef_t *thread_def, void *argument) 创建任务函数,经常用
void vTaskStartScheduler( void ) osStatus osKernelStart (void) 启动调度器,在创建完任务后必须执行的操作不然任务不能运行
void vTaskDelay( const TickType_t xTicksToDelay ) osStatus osDelay (uint32_t millisec) 相对延时,任务会进入阻塞状态,单位是系统节拍时钟周期(不是ms或者s),此函数不适用与周期性执行任务的场合。此外,其它任务和中断活动,会影响 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),因此会影响任务下一次执行的时间
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) / 绝对延时
TickType_t xTaskGetTickCount( void ) / 获取系统当前运行的时钟节拍数(任务函数中使用)
TickType_t xTaskGetTickCountFromISR( void ) / 获取系统当前运行的时钟节拍数(中断服务函数中使用)
vTaskList / 列出系统中所有任务的详细信息,包括任务名、任务状态、任务优先级、任务堆栈使用情况等
vTaskGetRunTimeStats / 用于获取系统中任务的运行时间统计信息,包括每个任务的运行时间、任务占用 CPU 的百分比、任务切换次数等
void vTaskDelete( TaskHandle_t xTaskToDelete ) / 删除任务
void vTaskSuspend( TaskHandle_t xTaskToSuspend ) / 挂起任务
void vTaskResume( TaskHandle_t xTaskToResume ) / 普通方式恢复任务
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) / 中断方式恢复任务
/ void HardFault_Handler(void) STM自带的一个用于处理硬件错误异常的中断服务函数
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName) / 任务栈溢出时自动调用的钩子函数,参数 xTask 是指出现栈溢出的任务的句柄,参数pcTaskName则是该任务的名称
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ) / 获取任务优先级
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ) / 修改任务函数
#define taskDISABLE_INTERRUPTS() portDISABLE_INTERRUPTS() / 关闭所有受管理的中断(不推荐使用)
#define taskENABLE_INTERRUPTS() portENABLE_INTERRUPTS() / 打开所有受管理的中断(不推荐使用)
#define taskENTER_CRITICAL() portENTER_CRITICAL() / 进入临界段(任务中)
#define taskEXIT_CRITICAL() portEXIT_CRITICAL() / 退出临界段(任务中)
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() / 中断进入临界段
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x ) / 中断退出临界段
#define taskYIELD() portYIELD() / 强制当前任务放弃CPU的使用权,让其他同优先级的任务得到执行机会
pdMS_TO_TICKS( xTimeInMs ) / 把用户的毫秒转换成系统节拍数

FreeRTOS坑

今天创建了两个任务一个是串口打印一个是LED闪烁,但是一直是高优先级的在运行另一个不允许,查了原因原来是CubeMX默认生成的任务导致,把默认的任务函数去掉不实现它即可(它是weak的)

使用 printf 造成挂死,原因是任务分配的栈大小太小了,默认是 128,改成 512 即可

还有打印都是框框问号原因是重定向函数里 HAL_UART_Transmit 函数写错,里面参数 ch必须是 &ch