更多干货推荐可以去牛客网看看,他们现在的IT题库内容很丰富,属于国内做的很好的了,而且是课程+刷题+面经+求职+讨论区分享,一站式求职学习网站,最最最重要的里面的资源全部免费!!!点击进入--------------》跳转接口
更多干货推荐可以去牛客网看看,他们现在的IT题库内容很丰富,属于国内做的很好的了,而且是课程+刷题+面经+求职+讨论区分享,一站式求职学习网站,最最最重要的里面的资源全部免费!!!点击进入--------------》跳转接口
目录
- 第一章FreeRTOS 简介
- 1.1 初识FreeRTOS
- 1.1.1 什么是FreeRTOS?
- 1.1.2 为什么选择FreeRTOS?
- 1.1.3 FreeRTOS 特点
- 1.1.4 商业许可
- 1.3 FreeRTOS 源码初探
- 1.3.1 FreeRTOS 源码下载
- 1.3.2 FreeRTOS 文件预览
- 第二章FreeRTOS 移植
- 2.1 准备工作
- 2.1.1 准备基础工程
- 2.1.2 FreeRTOS 系统源码
- 2.2 FreeRTOS 移植
- 2.2.1 向工程中添加相应文件
- 2.2.2 修改SYSTEM 文件
- 2.3 移植验证实验
- 2.3.1 实验程序设计
- 2.3.2 实验程序运行结果分析
- 第三章FreeRTOS 系统配置
- 3.1 FreeRTOSConfig.h 文件
- 3.1 “INCLUDE_”开始的宏
- 3.2 “config”开始的宏
- 第四章FreeRTOS 中断配置和临界段
- 4.1 Cortex-M 中断
- 4.1.1 中断简介
- 4.1.2 中断管理简介
- 4.1.3 优先级分组定义
- 4.1.4 优先级设置
- 4.1.5 用于中断屏蔽的特殊寄存器
- 4.2 FreeRTOS 中断配置宏
- 4.2.1 configPRIO_BITS
- 4.2.2 configLIBRARY_LOWEST_INTERRUPT_PRIORITY
- 4.2.3 configKERNEL_INTERRUPT_PRIORITY
- 4.2.4 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
- 4.2.5 configMAX_SYSCALL_INTERRUPT_PRIORITY
- 4.3 FreeRTOS 开关中断
- 4.4 临界段代码
- 4.4.1 任务级临界段代码保护
- 4.4.2 中断级临界段代码保护
- 4.5 FreeRTOS 中断测试实验
- 4.5.1 实验程序设计
- 4.5.2 实验程序运行结果
- 第五章FreeRTOS 任务基础知识
- 5.1 什么是多任务系统?
- 5.2 FreeRTOS 任务与协程
- 5.2.1 任务(Task)的特性
- 5.2.2 协程(Co-routine)的特性
- 5.3 任务状态
- 5.4 任务优先级
- 5.5 任务实现
- 5.6 任务控制块
- 5.7 任务堆栈
- 第六章FreeRTOS 任务相关API 函数
- 6.1 任务创建和删除API 函数
- 6.2 任务创建和删除实验(动态方法)
- 6.2.1 实验程序设计
- 6.2.2 程序运行结果分析
- 6.3 任务创建和删除实验(静态方法)
- 6.3.1 实验程序设计
- 6.3.2 程序运行结果分析
- 6.4 任务挂起和恢复API 函数
- 6.5 任务挂起和恢复实验
- 6.5.1 实验程序设计
- 6.5.2 程序运行结果分析
- 第七章FreeRTOS 列表和列表项
- 7.1 什么是列表和列表项?
- 7.1.1 列表
- 7.1.2 列表项
- 7.1.3 迷你列表项
- 7.2 列表和列表项初始化
- 7.2.1 列表初始化
- 7.2.2 列表项初始化
- 7.3 列表项插入
- 7.3.1 列表项插入函数分析
- 7.3.2 列表项插入过程图示
- 7.4 列表项末尾插入
- 7.4.1 列表项末尾插入函数分析
- 7.4.2 列表项末尾插入图示
- 7.5 列表项的删除
- 7.6 列表的遍历
- 7.7 列表项的插入和删除实验
- 7.7.1 实验程序设计
- 7.7.2 程序运行结果分析
- 第八章FreeRTOS 调度器开启和任务相关函数详解
- 8.1 阅读本章所必备的知识
- 8.2 调度器开启过程分析
- 8.2.1 任务调度器开启函数分析
- 8.2.2 内核相关硬件初始化函数分析
- 8.2.3 启动第一个任务
- 8.2.4 SVC 中断服务函数
- 8.2.5 空闲任务
- 8.3 任务创建过程分析
- 8.3.1 任务创建函数分析
- 8.3.2 任务初始化函数分析
- 8.3.3 任务堆栈初始化函数分析
- 8.3.4 添加任务到就绪列表
- 8.4 任务删除过程分析
- 8.5 任务挂起过程分析
- 8.6 任务恢复过程分析
- 第九章FreeRTOS 任务切换
- 9.1 PendSV 异常
- 9.2 FreeRTOS 任务切换场合
- 9.2.1 执行系统调用
- 9.2.2 系统滴答定时器(SysTick)中断
- 9.3 PendSV 中断服务函数
- 9.4 查找下一个要运行的任务
- 9.6 FreeRTOS 时间片调度
- 9.6.2 程序运行结果分析
- 第十章FreeRTOS 系统内核控制函数
- 10.1 内核控制函数预览
- 10.2 内核控制函数详解
第一章FreeRTOS 简介
1.1 初识FreeRTOS
1.1.1 什么是FreeRTOS?
我们看一下FreeRTOS 的名字,可以分为两部分:Free 和RTOS,Free 就是免费的、自由的、不受约束的意思,RTOS 全称是Real Time Operating System,中文名就是实时操作系统。RTOS 不是指某一个确定的系统,而是指一类系统。比如UCOS,FreeRTOS,RTX,RT-Thread 等这些都是RTOS 类操作系统。
操作系统允许多个任务同时运行,这个叫做多任务,实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务,任务调度在各个任务之间的切换非常快!这就给人们造成了同一时刻有多个任务同时运行的错觉。
操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配同样的运行时间,时间到了就轮到下一个任务,Unix 操作系统就是这样的。RTOS 的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的,实时环境中要求操作系统必须对某一个事件做出实时的响应,因此系统任务调度器的行为必须是可预测的。像FreeRTOS 这
种传统的RTOS 类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此任务优先级来决定下一刻应该运行哪个任务。
FreeRTOS 是RTOS 系统的一种,FreeRTOS 十分的小巧,可以在资源有限的微控制器中运行,当然了,FreeRTOS 不仅局限于在微控制器中使用。但从文件数量上来看FreeRTOS 要比UCOSII 和UCOSIII 小的多。
1.1.2 为什么选择FreeRTOS?
1、FreeRTOS 免费。
2、许多其他半导体厂商产品的SDK 包就使用FreeRTOS 作为其操作系统,尤其是WIFI、蓝牙这些带协议栈的芯片或模块。
3、许多软件厂商也使用FreeRTOS 做本公司软件的操作系统,比如著名的TouchGFX,其所有的例程都是基于FreeRTOS 操作系统的。ST 公司的所有要使用到RTOS 系统的例程也均采用了FreeRTOS,由此可见免费的力量啊!
3、简单,FreeRTOS 的文件数量很少,和UCOS系统相比要少很多!
4、文档相对齐全,在FreeRTOS 的官网(www.freertos.org)上可以找到所需的文档和源码,但是所有的文档都是英文版本的,而且下载pdf 文档的时候是要收费的。
5、FreeRTOS 被移植到了很多不同的微处理器上,比如我们使用的STM32,F1、F3、F4 和最新的F7 都有移植,这个极大的方便了我们学习和使用。
6、社会占有量很高,EEtimes 统计的2015 年RTOS 系统占有量中FreeRTOS 已经跃升至第一位。
1.1.3 FreeRTOS 特点
FreeRTOS 是一个可裁剪的小型RTOS 系统,其特点包括:
●FreeRTOS 的内核支持抢占式,合作式和时间片调度。
●SafeRTOS 衍生自FreeRTOS,SafeRTOS 在代码完整性上相比FreeRTOS 更胜一筹。
●提供了一个用于低功耗的Tickless 模式。
●系统的组件在创建时可以选择动态或者静态的RAM,比如任务、消息队列、信号量、软件定时器等等。
●已经在超过30 种架构的芯片上进行了移植。
●FreeRTOS-MPU 支持Corex-M 系列中的MPU 单元,如STM32F103。
●FreeRTOS 系统简单、小巧、易用,通常情况下内核占用4k-9k 字节的空间。
●高可移植性,代码主要C 语言编写。
●支持实时任务和协程(co-routines 也有称为合作式、协同程序,本教程均成为协程)。
●任务与任务、任务与中断之间可以使用任务通知、消息队列、二值信号量、数值型信号量、递归互斥信号量和互斥信号量进行通信和同步。
●创新的事件组(或者事件标志)。
●具有优先级继承特性的互斥信号量。
●高效的软件定时器。
●强大的跟踪执行功能。
●堆栈溢出检测功能。
●任务数量不限。
●任务优先级不限。
1.1.4 商业许可
FreeRTOS 衍生出来了另外两个系统:OpenRTOS 和SafeTROS,FreeRTOS 开源许可协议允许在商业应用中使用FreeRTOS 系统,并且不需要公开你的私有代码。如果有以下需求的话可以使用OpenRTOS:
1、你不能接受FreeRTOS 的开源许可协议条件,具体参见表1.4.1。
2、你需要技术支持。
3、你想获得开发帮助
4、你需要法律保护或者其他的保护。
使用OpenRTOS 的话需要遵守商业协议,FreeRTOS 的开源许可和OpenRTOS 的商业许可区别如表1.4.1 所示:
OpenRTOS 是FreeRTOS 的商业化版本,OpenRTOS 的商业许可协议不包含任何GPL 条款。
还有另外一个系统:SafeRTOS,SafeRTOS 看名字有个Safe,安全的意思!SafeRTOS 也是FreeRTOS 的衍生版本,只是SafeRTOS 过了一些安全认证,比如IEC61508。
1.3 FreeRTOS 源码初探
1.3.1 FreeRTOS 源码下载
1.3.2 FreeRTOS 文件预览
从图1.3.1.5 可以看出FreeRTOS 源码中有两个文件夹,4 个HTML 格式的网页和一个txt 文档,HTML 网页和txt 文档就不用介绍了,看名字就知道是什么东西了,重点在于上面那两个文件夹:FreeRTOS 和FreeRTOS-Plus,这两个文件夹里面的东西就是FreeRTOS 的源码。我们知道苹果从Iphone6 以后分为了Iphone6 和Iphone6 Plus两个版本,区别就是Plus 比普通的功能多一点,配置强大一点。现在FreeRTOS 也这么分,是不是Plus 版本比FreeRTOS 功能强一点啊,强大到哪里?是不是源码都不同了呀?
1、FreeRTOS 文件夹
打开FreeRTOS 文件夹,如图1.3.2.1 所示:
图1.3.2.1 中有三个文件夹,Demo、License 和Source,从名字上就可以很容易的得出他们都是些什么。
●Demo 文件夹
Demo 文件夹里面就是FreeRTOS 的相关例程,打开以后如图1.3.2.2 所示:
可以看出FreeRTOS 针对不同的MCU 提供了非常多的Demo,其中就有ST 的F1、F4 和F7 的相关例程,这对于我们学习来说是非常友好的,我们在移植的时候就会参考这些例程。
●License 文件夹
这个文件夹里面就是相关的许可信息,要用FreeRTOS 做产品的得仔细看看,尤其是要出口的产品。
●Source 文件夹
看名字就知道了,这个就是FreeRTOS 的本尊了,打开后如图1.3.2.3 所示:
图1.3.2.3 就是FreeRTOS 的源码文件,也是我们以后打交道的,可以看出,相比于UCOS来说FreeRTOS 的文件非常少!include 文件夹是一些头文件,移植的时候是需要的,下面的这些.C 文件就是FreeRTOS 的源码文件了,移植的时候肯定也是需要的。
重点来看一下portable这个文件夹,我们知道FreeRTOS 是个系统,归根结底就是个纯软件的东西,它是怎么和硬件联系在一起的呢?软件到硬件中间必须有一个桥梁,portable 文件夹里面的东西就是FreeRTOS系统和具体的硬件之间的连接桥梁!不同的编译环境,不同的MCU,其桥梁应该是不同的,打开portable 文件夹,如图1.3.2.4 所示:
从图1.3.2.4 中可以看出FreeRTOS 针对不同的编译环境和MCU 都有不同的“桥梁”,我们这里就以MDK 编译环境下的STM32F103 为例。MemMang 这个文件夹是跟内存管理相关的,我们移植的时候是必须的,具体内容我们后面会专门有一章来讲解。Keil 文件夹里面的东西肯定也是必须的,但是我们打开Keil 文件夹以后里面只有一个文件:See-also-the-RVDS-directory.txt。
这个txt 文件是什么鬼?别急嘛!看文件名字“See-also-the-RVDS-directory”,意思就是参考RVDS文件夹里面的东西!哎,好吧,再打开RVDS 文件夹,如图1.3.2.5 所示:
RVDS 文件夹针对不同的架构的MCU 做了详细的分类,STM32F103 就参考ARM_CM3,打开ARM_CM3 文件夹,如图1.3.2.6 所示:
ARM_CM3 有两个文件,这两个文件就是我们移植的时候所需要的!
2、FreeRTOS-Plus 文件夹
上面我们分析完了FreeRTOS 文件夹,接下来看一下FreeRTOS-Plus,打开以后如图1.3.2.7所示:
同样,FreeRTOS-Plus 也有Demo 和Source,Demo 就不看了,肯定是一些例程。我们看一下Source,打开以后如图1.3.2.8 所示:
可以看出,FreeRTOS-Plus 中的源码其实并不是FreeRTOS 系统的源码,而是在FreeRTOS系统上另外增加的一些功能代码,比如CLI、FAT、Trace 等等。就系统本身而言,和FreeRTOS里面的一模一样的,所以我们如果只是学习FreeRTOS 这个系统的话,FreeRTOS-Plus 就没必要看了。
第二章FreeRTOS 移植
以ALIENTEK 的STM32F103 开发板为例。
2.1 准备工作
2.1.1 准备基础工程
要移植FreeRTOS,肯定需要一个基础工程,基础工程越简单越好,这里我们就用基础例程中的跑马灯实验来作为基础工程。
2.1.2 FreeRTOS 系统源码
FreeRTOS 系统源码在上一章已经详细的讲解过如何获取了,这里我们会将FreeRTOS 的系统源码放到开发板光盘中去,路径为:6,软件资料->14,FreeRTOS 学习资料->FreeRTOS 源码。
2.2 FreeRTOS 移植
2.2.1 向工程中添加相应文件
1、添加FreeRTOS 源码
在基础工程中新建一个名为FreeRTOS 的文件夹,如图2.2.1.1 所示:
创建FreeRTOS 文件夹以后就可以将FreeRTOS 的源码添加到这个文件夹中,添加完以后如图2.2.1.2 所示:
在1.3.2 小节中详细的讲解过portable 文件夹,我们只需要留下keil、MemMang 和RVDS这三个文件夹,其他的都可以删除掉,完成以后如图2.2.1.3 所示:
2、向工程分组中添加文件
打开基础工程,新建分组FreeRTOS_CORE 和FreeRTOS_PORTABLE,然后向这两个分组中添加文件,如图2.2.1.4 所示:
分组FreeRTOS_CORE 中的文件在什么地方就不说了,打开FreeRTOS 源码一目了然。重点来说说FreeRTOS_PORTABLE 分组中的port.c 和heap_4.c 是怎么来的,port.c 是RVDS 文件夹下的ARM_CM3 中的文件,因为STM32F103 是Cortex-M3 内核的,因此要选择ARM_CM3中的port.c 文件。heap_4.c 是MemMang 文件夹中的,前面说了MemMang 是跟内存管理相关
的,里面有5 个c 文件:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这5 个c 文件是五种不同的内存管理方法,都可以用来作为FreeRTOS 的内存管理文件,只是它们的实现原理不同,各有利弊。这里我们选择heap_4.c,至于原因,后面会有一章节专门来讲解FreeRTOS 的内存管理,到时候大家就知道原因了。这里就先选择heap_4.c,毕竟本章的重点是FreeRTOS 的移植。
3、添加相应的头文件路径
添加完FreeRTOS 源码中的C 文件以后还要添加FreeRTOS 源码的头文件路径,头文件路径如图2.2.1.5 所示:
头文件路径添加完成以后编译一下,看看有没有什么错误,结果会发现提示打不开“FreeRTOSConfig.h”这个文件,如图2.2.1.6 所示:
这是因为缺少FreeRTOSConfig.h 文件,这个文件在哪里找呢?你可以自己创建,显然这不是一个明智的做法。我们可以找找FreeRTOS 的官方移植工程中会不会有这个文件,打开FreeRTOS 针对STM32F103 的移植工程文件,文件夹是CORTEX_STM32F103_Keil,打开以后如图2.2.1.7 所示
果然!官方的移植工程中有这个文件,我们可以使用这个文件,但是建议大家使用我们例程中的FreeRTOSConf.h 文件,这个文件是FreeRTOS 的系统配置文件,不同的平台其配置不同,但是我们提供的例程中的这个文件肯定是针对ALIENTEK 开发板配置正确的。这个文件复制到什么地方大家可以自行决定,这里我为了方便放到了FreeRTOS 源码中的include 文件夹下。
FreeRTOSConfig.h 是何方神圣?看名字就知道,他是FreeRTOS 的配置文件,一般的操作系统都有裁剪、配置功能,而这些裁剪及配置都是通过一个文件来完成的,基本都是通过宏定义来完成对系统的配置和裁剪的,关于FreeRTOS 的配置文件FreeRTOSConfig.h 后面也会有一章节来详细的讲解。
到这里我们再编译一次,没有错误!如图2.2.1.8 所示:
如果还有错误的话大家自行根据错误类型查找和修改错误!
2.2.2 修改SYSTEM 文件
SYSTEM 文件夹里面的文件一开始是针对UCOS 而编写的,所以如果使用FreeRTOS 的话就需要做相应的修改。本来打算让SYSTEM 文件夹也支持FreeRTOS,但是这样的话会导致SYSTEM 里面的文件太过于复杂,这样非常不利于初学者学习,所以这里就专门针对FreeRTOS修改了SYSTEM 里面的文件。
1、修改sys.h 文件
sys.h 文件修改很简单,在sys.h 文件里面用宏SYSTEM_SUPPORT_OS 来定义是否使用OS,我们使用了FreeRTOS,所以应该将宏SYSTEM_SUPPORT_OS 改为1。
//0,不支持os
//1,支持os
#define SYSTEM_SUPPORT_OS 1 //定义系统文件夹是否支持OS
2、修改usart.c 文件
usart.c 文件修改也很简单,usart.c 文件有两部分要修改,一个是添加FreeRTOS.h 头文件,默认是添加的UCOS 中的includes.h 头文件,修改以后如下:
//如果使用os,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "FreeRTOS.h" //os 使用
#endif
另外一个就是USART1 的中断服务函数,在使用UCOS 的时候进出中断的时候需要添加OSIntEnter()和OSIntExit(),使用FreeRTOS 的话就不需要了,所以将这两行代码删除掉,修改以后如下:
void USART1_IRQHandler(void) //串口1 中断服务程序
{u8 Res;if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){Res =USART_ReceiveData(USART1); //读取接收到的数据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;}}}}
}
3、修改delay.c 文件
delay.c 文件修改的就比较大了,因为涉及到FreeRTOS 的系统时钟,delay.c 文件里面有4个函数,先来看一下函数SysTick_Handler(),此函数是滴答定时器的中断服务函数,代码如下:
extern void xPortSysTickHandler(void);
//systick 中断服务函数,使用OS 时用到
void SysTick_Handler(void)
{if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行{xPortSysTickHandler();}
}
FreeRTOS 的心跳就是由滴答定时器产生的,根据FreeRTOS 的系统时钟节拍设置好滴答定时器的周期,这样就会周期触发滴答定时器中断了。在滴答定时器中断服务函数中调用FreeRTOS 的API 函数xPortSysTickHandler()。
delay_init()是用来初始化滴答定时器和延时函数,代码如下:
//初始化延迟函数
//SYSTICK 的时钟固定为AHB 时钟,基础例程里面SYSTICK 时钟频率为AHB/8
//这里为了兼容FreeRTOS,所以将SYSTICK 的时钟频率改为AHB 的频率!
//SYSCLK:系统时钟频率
void delay_init()
{u32 reload;SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);//选择外部时钟HCLKfac_us=SystemCoreClock/1000000; //不论是否使用OS,fac_us 都需要使用reload=SystemCoreClock/1000000; //每秒钟的计数次数单位为Mreload*=1000000/configTICK_RATE_HZ; //根据configTICK_RATE_HZ 设定溢出//时间reload 为24 位寄存器,最大值://16777216,在72M 下,约合0.233s 左右fac_ms=1000/configTICK_RATE_HZ; //代表OS 可以延时的最少单位SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启SYSTICK 中断SysTick->LOAD=reload; //每1/configTICK_RATE_HZ 秒中断//一次SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
}
前面我们说了FreeRTOS 的系统时钟是由滴答定时器提供的,那么肯定要根据FreeRTOS 的系统时钟节拍来初始化滴答定时器了,delay_init()就是来完成这个功能的。FreeRTOS 的系统时钟节拍由宏configTICK_RATE_HZ 来设置,这个值我们可以自由设置,但是一旦设置好以后我们就要根据这个值来初始化滴答定时器,其实就是设置滴答定时器的中断周期。在基础例程中
滴答定时器的时钟频率设置的是AHB 的1/8,这里为了兼容FreeRTOS 将滴答定时器的时钟频率改为了AHB,也就是72MHz!这一点一定要注意!
接下来的三个函数都是延时的,代码如下:
/延时nus
//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 数
void delay_xms(u32 nms)
{u32 i;for(i=0;i<nms;i++) delay_us(1000);
}
delay_us()是us 级延时函数,delay_ms 和delay_xms()都是ms 级的延时函数,delay_us()和delay_xms()不会导致任务切换。delay_ms()其实就是对FreeRTOS 中的延时函数vTaskDelay()的简单封装,所以在使用delay_ms()的时候就会导致任务切换。
delay.c 修改完成以后编译一下,会提示如图2.2.2.1 所示错误:
图2.2.2.1 的错误提示表示在port.c、delay.c 和stm32f10x_it.c 中三个重复定义的函数:SysTick_Handler()、SVC_Handler()和PendSV_Handler(),这三个函数分别为滴答定时器中断服务函数、SVC 中断服务函数和PendSV 中断服务函数,将stm32f10x_it.c 中的三个函数屏蔽掉,如图2.2.2.2 所示:
再次编译代码,应该没有错误了,如果还是错误的话自行根据错误类型修改!至此,SYSTEM文件夹就修改完成了。
2.3 移植验证实验
2.3.1 实验程序设计
1、实验目的
编写简单的FreeRTOS 应用代码,测试FreeRTOS 的移植是否成功。鉴于大家还没正式学习FreeRTOS,可以直接将本实验代码复制粘贴到自己的移植工程中。
2、实验设计
本实验设计四个任务:start_task()、led0_task ()、led1_task ()和float_task(),这四个任务的任务功能如下:
- start_task():用来创建其他三个任务。
- led0_task ():控制LED0 的闪烁。
- led1_task ():控制LED1 的闪烁。
- float_task():简单的浮点测试任务,用于测试STM32F4 的FPU 是否工作正常。
3、实验工程
FreeRTOS 实验2-1 FreeRTOS 移植实验。
4、实验程序与分析
●任务设置
#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); //任务函数
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化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);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}//LED0 任务函数
void led0_task(void *pvParameters)
{while(1){LED0=~LED0;vTaskDelay(500);}
}//LED1 任务函数
void led1_task(void *pvParameters)
{while(1){LED1=0;vTaskDelay(200);LED1=1;vTaskDelay(800);}
}
led0_task()和led1_task()任务很简单,就是让LED0 和LED1 周期性闪烁。
关于具体的函数的调用方法这些不要深究,后面会有详细的讲解!
2.3.2 实验程序运行结果分析
编译并下载代码到STM32F103 开发板中,下载进去以后会看到LED0 和LED1 开始闪烁。
LED0 均匀闪烁,那是因为我们在LED0 的任务代码中设置好的LED0 亮500ms,灭500ms。
LED1 亮的时间短,灭的时间长,这是因为在LED1 的任务代码中设置好的亮200ms,灭800ms。
第三章FreeRTOS 系统配置
在实际使用FreeRTOS 的时候我们时常需要根据自己需求来配置FreeRTOS,而且不同架构
的MCU 在使用的时候配置也不同。FreeRTOS 的系统配置文件为FreeRTOSConfig.h,在此配置
文件中可以完成FreeRTOS 的裁剪和配置,这是非常重要的一个文件,本章就来讲解这个这个
文件,本章分为如下几部分:
3.1 FreeRTOSConfig.h 文件
3.2 “INCLUDE_”开始的宏
3.3 “config”开始的宏
3.1 FreeRTOSConfig.h 文件
FreeRTOS 的配置基本是通过在FreeRTOSConfig.h 中使用“#define”这样的语句来定义宏
定义实现的。在FreeRTOS 的官方demo 中,每个工程都有一个FreeRTOSConfig.h 文件,我们
在使用的时候可以参考这个文件,甚至直接复制粘贴使用。
3.1 “INCLUDE_”开始的宏
使用“INCLUDE_”开头的宏用来表示使能或除能FreeRTOS 中相应的API 函数,作用就
是用来配置FreeRTOS 中的可选API 函数的。比如当宏INCLUDE_vTaskPrioritySet 设置为0 的
时候表示不能使用函数vTaskPrioritySet() ,当设置为1 的时候就表示可以使用函数
vTaskPrioritySet()。这个功能其实就是条件编译,在文件tasks.c 中有如下图3.1.1 所示的代码。
从图3.1.1 可以看出当满足条件:NCLUDE_vTaskPrioritySet == 1 的时候,函数
vTaskPrioritySet()才会被编译,注意,这里为了缩小篇幅将函数vTaskPrioritySet()的内容进行了
折叠。FreeRTOS 中的裁剪和配置就是这种用条件编译的方法来实现的,不止FreeRTOS 这么干,
很多的协议栈、RTOS 系统和GUI 库等都是使用条件编译的方法来完成配置和裁剪的。条件编
译的好处就是节省空间,不需要的功能就不用编译,这样就可以根据实际需求来减少系统占用
的ROM 和RAM 大小,根据自己所使用的MCU 来调整系统消耗,降低成本。
下面来看看“INCLUDE_”开始的都有哪些宏,它们的功能都是什么。
1、INCLUDE_xSemaphoreGetMutexHolder
如果要使用函数xQueueGetMutexHolder()的话宏INCLUDE_xSemaphoreGetMutexHolder 必
须定义为1。
2、INCLUDE_xTaskAbortDelay
如果要使用函数xTaskAbortDelay()的话将宏INCLUDE_xTaskAbortDelay 定义为1。
3、INCLUDE_vTaskDelay
如果要使用函数vTaskDelay()的话需要将宏INCLUDE_vTaskDelay 定义为1。
4、INCLUDE_vTaskDelayUntil
如果要使用函数vTaskDelayUntil()的话需要将宏INCLUDE_vTaskDelayUntil 定义为1。
5、INCLUDE_vTaskDelete
如果要使用函数vTaskDelete()的话需要将宏INCLUDE_vTaskDelete 定义为1。
6、INCLUDE_xTaskGetCurrentTaskHandle
如果要使用函数xTaskGetCurentTaskHandle() 的话需要将宏
INCLUDE_xTaskGetCurrentTaskHandle 定义为1。
7、INCLUDE_xTaskGetHandle
如果要使用函数xTaskGetHandle()的话需要将宏INCLUDE_xTaskGetHandle 定义为1。
8、INCLUDE_xTaskGetIdleTaskHandle
如果要使用函数xTaskGetIdleTaskHandle() 的话需要将宏
INCLUDE_xTaskGetIdleTaskHandle 定义为1。
9、INCLUDE_xTaskGetSchedulerState
如果要使用函数xTaskGetSchedulerState()的话需要将宏INCLUDE_xTaskGetSchedulerState
定义为1。
10、INCLUDE_uxTaskGetStackHighWaterMark
如果要使用函数uxTaskGetStackHighWaterMark() 的话需要将宏
INCLUDE_uxTaskGetStackHighWaterMark 定义为1。
11、INCLUDE_uxTaskPriorityGet
如果要使用函数uxTaskPriorityGet()的话需要将宏INCLUDE_uxTaskPriorityGet 定义为1。
12、INCLUDE_vTaskPrioritySet
如果要使用函数vTaskPrioritySet()的话需要将宏INCLUDE_vTaskPrioritySet 定义为1。
13、INCLUDE_xTaskResumeFromISR
如果要使用函数xTaskResumeFromISR()的话需要将宏INCLUDE_xTaskResumeFromISR 和
INCLUDE_vTaskSuspend 都定义为1。
14、INCLUDE_eTaskGetState
如果要使用函数eTaskGetState()的话需要将宏INCLUDE_eTaskGetState 定义为1。
15、INCLUDE_vTaskSuspend
如果要使用函数vTaskSuspend() 、vTaskResume() 、prvTaskIsTaskSuspended() 、
xTaskResumeFromISR()的话宏INCLUDE_vTaskSuspend 要定义为1。
如果要使用函数xTaskResumeFromISR() 的话宏INCLUDE_xTaskResumeFromISR 和
INCLUDE_vTaskSuspend 都必须定义为1。
16、INCLUDE_xTimerPendFunctionCall
如果要使用函数xTimerPendFunctionCall() 和xTimerPendFunctionCallFromISR() 的话宏
INCLUDE_xTimerPendFunctionCall 和configUSE_TIMERS 都必须定义为1。
3.2 “config”开始的宏
“config”开始的宏和“INCLUDE_”开始的宏一样,都是用来完成FreeRTOS 的配置和裁
剪的,接下来我们就看一下这些“config”开始的宏。
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 中就有如图3.2.1
所示定义:
从图3.2.1 可以看出当宏configAPPLICATION_ALLOCATED_HEAP 定义为1 的话需要用
户自行堆内存ucHeap,否则的话就是编译器来分配的。
2、configASSERT
断言,类似C 标准库中的assert()函数,调试代码的时候可以检查传入的参数是否合理,
FreeRTOS 内核中的关键点都会调用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__)
当参数x 错误的时候就通过串口打印出发生错误的文件名和错误所在的行号,调试代码的
可以使用断言,当调试完成以后尽量去掉断言,防止增加开销!
3、configCHECK_FOR_STACK_OVERFLOW
设置堆栈溢出检测,每个任务都有一个任务堆栈,如果使用函数xTaskCreate()创建一个任
务的话那么这个任务的堆栈是自动从FreeRTOS 的堆(ucHeap)中分配的,堆栈的大小是由函数
xTaskCreate()的参数usStackDepth 来决定的。如果使用函数xTaskCreateStatic()创建任务的话任
务堆栈是由用户设置的,参数pxStackBuffer 为任务堆栈,一般是一个数组。
堆栈溢出是导致应用程序不稳定的主要因素,FreeRTOS 提供了两种可选的机制来帮助检
测和调试堆栈溢出,不管使用哪种机制都要设置宏configCHECK_FOR_STACK_OVERFLOW。
如果使能了堆栈检测功能的话,即宏configCHECK_FOR_STACK_OVERFLOW 不为0,那么用
户必须提供一个钩子函数(回调函数),当内核检测到堆栈溢出以后就会调用这个钩子函数,此
钩子函数原型如下:
void vApplicationStackOverflowHook( TaskHandle_t xTask,char * pcTaskName );
参数xTask 是任务句柄,pcTaskName 是任务名字,要注意的是堆栈溢出太严重的话可能会
损毁这两个参数,如果发生这种情况的话可以直接查看变量pxCurrentTCB 来确定哪个任务发
生了堆栈溢出。有些处理器可能在堆栈溢出的时候生成一个fault 中断来提示这种错误,另外,
堆栈溢出检测会增加上下文切换的开销,建议在调试的时候使用。
configCHECK_FOR_STACK_OVERFLOW==1,使用堆栈溢出检测方法1。
上下文切换的时候需要保存现场,现场是保存在堆栈中的,这个时候任务堆栈使用率很可
能达到最大值,方法一就是不断的检测任务堆栈指针是否指向有效空间,如果指向了无效空间
的话就会调用钩子函数。方法一的优点就是快!但是缺点就是不能检测所有的堆栈溢出。
configCHECK_FOR_STACK_OVERFLOW==2,使用堆栈溢出检测方法2。
使用方法二的话在创建任务的时候会向任务堆栈填充一个已知的标记值,方法二会一直检
测堆栈后面的几个bytes(标记值)是否被改写,如果被改写的话就会调用堆栈溢出钩子函数,方
法二也会使用方法一中的机制!方法二比方法一要慢一些,但是对用户而言还是很快的!方法
二能检测到几乎所有的堆栈溢出,但是也有一些情况检测不到,比如溢出值和标记值相同的时
候。
3、configCPU_CLOCK_HZ
设置CPU 的频率。
4、configSUPPORT_DYNAMIC_ALLOCATION
定义为1 的话在创建FreeRTOS 的内核对象的时候所需要的RAM 就会从FreeRTOS 的堆中
动态的获取内存,如果定义为0 的话所需的RAM 就需要用户自行提供,默认情况下宏
configSUPPORT_DYNAMIC_ALLOCATION 为1。
5、configENABLE_BACKWARD_COMPATIBILITY
FreeRTOS.h 中由一些列的#define 宏定义,这些宏定义都是一些数据类型名字,如下图3.2.2
所示:
在V8.0.0 之前的FreeRTOS 中会使用到这些数据类型,这些宏保证了你的代码从V8.0.0 之
前的版本升级到最新版本的时候不需要做出修改,默认情况下宏
configENABLE_BACKWARD_COMPATIBILITY 为1。
6、configGENERATE_RUN_TIME_STATS
设置为1 开启时间统计功能,相应的API 函数会被编译,为0 时关闭时间统计功能。如果宏configGENERATE_RUN_TIME_STATS 为1 的话还需要定义表3.2.1 中的宏。
宏 | 描述 |
---|---|
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() | 此宏用来初始化一个外设来作为时间统计的基准时钟。 |
portGET_RUN_TIME_COUNTER_VALUE()或portALT_GET_RUN_TIME_COUNTER_VALUE(Time) | 此宏用来返回当前基准时钟的时钟值。 |
7、configIDLE_SHOULD_YIELD
此宏定义了与空闲任务(idle Task)处于同等优先级的其他用户任务的行为,当为0 的时候空闲任务不会为其他处于同优先级的任务让出CPU 使用权。当为1 的时候空闲任务就会为处于同等优先级的用户任务让出CPU 使用权,除非没有就绪的用户任务,这样花费在空闲任务上的时间就会很少,但是这种方法也带了副作用,见图3.2.3。
图3.2.3 中有三个用户任务:A、B、C,还有一个空闲任务I,用户任务和空闲任务处于同
一优先级,任务切换发生在T0~T7 时刻。T0~T1 之间的时间为一个时间片,从图中可以看出一
开始任务B、C 都执行了一个完成的时间片,在T2 时刻空闲任务I 开始执行,I 任务运行了一
段时间以后被A 任务抢走了CPU 使用权,A 任务运行到T3 时刻发生任务切换,B 任务开始运
行。可以看出其实任务I 和任务A 一起使用了一个时间片,所以任务A 运行的时间就比其他任
务少!
一般建议大家关闭这个功能,毕竟空闲任务用不了多少时间,而且现在的MCU 性能都很
强!
8、configKERNEL_INTERRUPT_PRIORITY、
configMAX_SYSCALL_INTERRUPT_PRIORITY、
configMAX_API_CALL_INTERRUPT_PRIORITY
这三个宏和FreeRTOS 的中断配置有关,后面会有专门的章节来讲解!
9、configMAX_CO_ROUTINE_PRIORITIES
设置可以分配给协程的最大优先级,也就是协程的优先级数。设置号以后协程的优先级可
以从0 到configMAX_CO_ROUTINE_PRIORITIES-1 ,其中0 是最低的优先级,
configMAX_CO_ROUTINE_PRIORITIES-1 为最高的优先级。
10、configMAX_PRIORITIES
设置任务的优先级数量,设置好以后任务就可以使用从0 到configMAX_PRIORITIES-1 的
优先级,其中0 是最低优先级,configMAX_PRIORITIES-1 是最高优先级。注意和UCOS 的区
别,UCOS 中0 是最高优先级!
11、configMAX_TASK_NAME_LEN
设置任务名最大长度。
12、configMINIMAL_STACK_SIZE
设置空闲任务的最小任务堆栈大小,以字为单位,不是字节。比如在STM32 上设置为100
的话,那么真正的堆栈大小就是100*4=400 字节。
13、configNUM_THREAD_LOCAL_STORAGE_POINTERS
设置每个任务的本地存储指针数组大小,任务控制块中有本地存储数组指针,用户应用程
序可以在这些本地存储中存入一些数据。
14、configQUEUE_REGISTRY_SIZE
设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需
要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会再内核
调试器中看到,如果不使用内核调试器的话此宏设置为0 即可。
15、configSUPPORT_STATIC_ALLOCATION
当此宏定义为1,在创建一些内核对象的时候需要用户指定RAM,当为0 的时候就会自使
用heap.c 中的动态内存管理函数来自动的申请RAM。
16、configTICK_RATE_HZ
设置FreeRTOS 的系统时钟节拍频率,单位为HZ,此频率就是滴答定时器的中断频率,需
要使用此宏来配置滴答定时器的中断,前面在讲delay.c 文件的时候已经说过了。本教程中我们
将此宏设置为1000,周期就是1ms。
17、configTIMER_QUEUE_LENGTH
此宏是配置FreeRTOS 软件定时器的,FreeRTOS 的软件定时器API 函数会通过命令队列向
软件定时器任务发送消息,此宏用来设置这个软件定时器的命令队列长度。
18、configTIMER_TASK_PRIORITY
设置软件定时器任务的任务优先级。
19、configTIMER_TASK_STACK_DEPTH
设置定时器服务任务的任务堆栈大小。
20、configTOTAL_HEAP_SIZE
设置堆大小,如果使用了动态内存管理的话,FreeRTOS 在创建任务、信号量、队列等的时
候就会使用heap_x.c(x 为1~5) 中的内存申请函数来申请内存。这些内存就是从堆
ucHeap[configTOTAL_HEAP_SIZE]中申请的,堆的大小由configTOTAL_HEAP_SIZE 来定义。
21、configUSE_16_BIT_TICKS
设置系统节拍计数器变量数据类型,系统节拍计数器变量类型为TickType_t ,当
configUSE_16_BIT_TICKS 为1 的时候TickType_t 就是16 位的,当configUSE_16_BIT_TICKS
为0 的话TickType_t 就是32 位的。
22、configUSE_APPLICATION_TASK_TAG
此宏设置为1 的话函数configUSE_APPLICATION_TASK_TAGF() 和xTaskCallApplicationTaskHook()就会被编译。
23、configUSE_CO_ROUTINES
此宏为1 的时候启用协程,协程可以节省开销,但是功能有限,现在的MCU 性能已经非
常强大了,建议关闭协程。
24、configUSE_COUNTING_SEMAPHORES
设置为1 的时候启用计数型信号量,相关的API 函数会被编译。
25、configUSE_DAEMON_TASK_STARTUP_HOOK
当宏configUSE_TIMERS 和configUSE_DAEMON_TASK_STARTUP_HOOK 都为1 的时需
要定义函数vApplicationDaemonTaskStartupHook(),函数原型如下:
void vApplicationDaemonTaskStartupHook( void )
26、configUSE_IDLE_HOOK
为1 时使用空闲任务钩子函数,用户需要实现空闲任务钩子函数,函数的原型如下:
void vApplicationIdleHook( void )
27、configUSE_MALLOC_FAILED_HOOK
为1 时使用内存分配失败钩子函数,用户需要实现内存分配失败钩子函数,函数原型如下;
void vApplicationMallocFailedHook( void )
28、configUSE_MUTEXES
为1 时使用互斥信号量,相关的API 函数会被编译。
29、configUSE_PORT_OPTIMISED_TASK_SELECTION
FreeRTOS 有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的
方法,也就是硬件方法,使用MCU 自带的硬件指令来实现。
通用方法:
●当宏configUSE_PORT_OPTIMISED_TASK_SELECTION 为0,或者硬件不支持的时候。
●希望所有硬件通用的时候。
●全部用C 语言来实现,但是效率比特殊方法低。
●不限制最大优先级数目的时候。
特殊方法:
●不是所有的硬件都支持。
●当宏configUSE_PORT_OPTIMISED_TASK_SELECTION 为1 的时候。
●硬件拥有特殊的指令,比如计算前导零(CLZ)指令。
●比通用方法效率高。
●会限制优先级数目,一般是32 个。
STM32 有计算前导零的指令,所以我们可以使用特殊方法,即将宏
configUSE_PORT_OPTIMISED_TASK_SELECTION 定义为1。计算前导零的指令在UCOSIII 也
用到了,也是用来查找下一个要运行的任务的。
30、configUSE_PREEMPTION
为1 时使用抢占式调度器,为0 时使用协程。如果使用抢占式调度器的话内核会在每个时
钟节拍中断中进行任务切换,当使用协程的话会在如下地方进行任务切换:
●一个任务调用了函数taskYIELD()。
●一个任务调用了可以使任务进入阻塞态的API 函数。
●应用程序明确定义了在中断中执行上下文切换。
31、configUSE_QUEUE_SETS
为1 时启用队列集功能。
32、configUSE_RECURSIVE_MUTEXES
为1 时使用递归互斥信号量,相关的API 函数会被编译。
33、configUSE_STATS_FORMATTING_FUNCTIONS
宏configUSE_TRACE_FACILITY 和configUSE_STATS_FORMATTING_FUNCTIONS 都为
1 的时候函数vTaskList()和vTaskGetRunTimeStats()会被编译。
34、configUSE_TASK_NOTIFICATIONS
为1 的时候使用任务通知功能,相关的API 函数会被编译,开启了此功能的话每个任务会
多消耗8 个字节。
35、configUSE_TICK_HOOK
为1 时使能时间片钩子函数,用户需要实现时间片钩子函数,函数的原型如下:
void vApplicationTickHook( void )
36、configUSE_TICKLESS_IDLE
为1 时使能低功耗tickless 模式。
37、configUSE_TIMERS
为1 时使用软件定时器,相关的API 函数会被编译,当宏configUSE_TIMERS 为1 的话,
那么宏configTIMER_TASK_PRIORITY 、configTIMER_QUEUE_LENGTH 和
configTIMER_TASK_STACK_DEPTH 必须定义。
38、configUSE_TIME_SLICING
默认情况下,FreeRTOS 使用抢占式调度器,这意味着调度器永远都在执行已经就绪了的最
高优先级任务,优先级相同的任务在时钟节拍中断中进行切换,当宏configUSE_TIME_SLICING
为0 的时候不会在时钟节拍中断中执行相同优先级任务的任务切换,默认情况下宏
configUSE_TIME_SLICING 为1。
39、configUSE_TRACE_FACILITY
为1 启用可视化跟踪调试,会增加一些结构体成员和API 函数。
FreeRTOS 的配置文件基本就这些,还有一些其他的配置宏由于使用的比较少这里并没有列出来,这些配置具体使用到的时候在具体查看就行了。
第四章FreeRTOS 中断配置和临界段
FreeRTOS 的中断配置是一个很重要的内容,需要根据所使用的MCU 来具体配置。这需要
了解MCU 架构中有关中断的知识,本章会结合Cortex-M 的NVIC 来讲解STM32 平台下的
FreeRTOS 中断配置,本章分为如下几部分:
4.1 Cortex-M 中断
4.2 FreeRTOS 中断配置宏
4.3 FreeRTOS 开关中断
4.4 临界段代码
4.5 FreeRTOS 中断测试实验
4.1 Cortex-M 中断
4.1.1 中断简介
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后CPU 就会中断当前
的流程转而去处理中断服务,Cortex-M 内核的MCU 提供了一个用于中断管理的嵌套向量中断
控制器(NVIC)。
Cotex-M3 的NVIC 最多支持240 个IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个Systick(滴
答定时器)定时器中断和多个系统异常。
4.1.2 中断管理简介
Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器,这些寄存器大多数都在
NVIC 和系统控制块(SCB)中,CMSIS 将这些寄存器定义为结构体。以STM32F103 为例,打开
core_cm3.h,有两个结构体,NVIC_Type 和SCB_Type,如下:
typedef struct
{__IO uint32_t ISER[8]; /*!< Offset: 0x000 Interrupt Set Enable Register */uint32_t RESERVED0[24];__IO uint32_t ICER[8]; /*!< Offset: 0x080 Interrupt Clear Enable Register */uint32_t RSERVED1[24];__IO uint32_t ISPR[8]; /*!< Offset: 0x100 Interrupt Set Pending Register */uint32_t RESERVED2[24];__IO uint32_t ICPR[8]; /*!< Offset: 0x180 Interrupt Clear Pending Register */uint32_t RESERVED3[24];__IO uint32_t IABR[8]; /*!< Offset: 0x200 Interrupt Active bit Register */uint32_t RESERVED4[56];__IO uint8_t IP[240]; /*!< Offset: 0x300 Interrupt Priority Register (8Bit wide) */uint32_t RESERVED5[644];__O uint32_t STIR; /*!< Offset: 0xE00 Software Trigger Interrupt Register */} NVIC_Type;
typedef struct
{__I uint32_t CPUID; /*!< Offset: 0x00 CPU ID Base Register */__IO uint32_t ICSR /*!< Offset: 0x04 Interrupt Control State Register */__IO uint32_t VTOR; /*!< Offset: 0x08 Vector Table Offset Register */__IO uint32_t AIRCR; /*!< Offset: 0x0C Application Interrupt / Reset Control Register */__IO uint32_t SCR; /*!< Offset: 0x10 System Control Register */__IO uint32_t CCR; /*!< Offset: 0x14 Configuration Control Register */__IO uint8_t SHP[12]; /*!< Offset: 0x18 System Handlers Priority Registers (4-7, 8-11, 12-15)*/__IO uint32_t SHCSR; /*!< Offset: 0x24 System Handler Control and State Register */__IO uint32_t CFSR; /*!< Offset: 0x28 Configurable Fault Status Register */__IO uint32_t HFSR; /*!< Offset: 0x2C Hard Fault Status Register */__IO uint32_t DFSR; /*!< Offset: 0x30 Debug Fault Status Register */__IO uint32_t MMFAR; /*!< Offset: 0x34 Mem Manage Address Register */__IO uint32_t BFAR; /*!< Offset: 0x38 Bus Fault Address Register */__IO uint32_t AFSR; /*!< Offset: 0x3C Auxiliary Fault Status Register */__I uint32_t PFR[2]; /*!< Offset: 0x40 Processor Feature Register */__I uint32_t DFR; /*!< Offset: 0x48 Debug Feature Register */__I uint32_t ADR; /*!< Offset: 0x4C Auxiliary Feature Register */__I uint32_t MMFR[4]; /*!< Offset: 0x50 Memory Model Feature Register */__I uint32_t ISAR[5]; /*!< Offset: 0x60 ISA Feature Register */
} SCB_Type;
NVIC 和SCB 都位于系统控制空间(SCS)内,SCS 的地址从0XE000E000 开始,SCB 和NVIC
的地址也在core_cm3.h 中有定义,如下:
#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE + 0x0100) /*!< NVIC Base Address */
#define SCB_BASE (SCS_BASE + 0x0D00) /*!< System Control Block Base Address */#define SCB ((SCB_Type * ) SCB_BASE ) /*!< SCB configuration struct */
#define NVIC ((NVIC_Type* ) NVIC_BASE ) /*!< NVIC configuration struct *//
以上的中断控制寄存器我们在移植FreeRTOS 的时候是不需要关心的,这里只是提一下,
大家要是感兴趣的话可以参考Cortex-M 的权威指南,我们重点关心的是是三个中断屏蔽寄存
器:PRIMASK、FAULTMASK 和BASEPRI,这三个寄存器后面会详细的讲解。
4.1.3 优先级分组定义
当多个中断来临的时候处理器应该响应哪一个中断是由中断的优先级来决定的,高优先级
的中断(优先级编号小)肯定是首先得到响应,而且高优先级的中断可以抢占低优先级的中断,
这个就是中断嵌套。Cortex-M 处理器的有些中断是具有固定的优先级的,比如复位、NMI、
HardFault,这些中断的优先级都是负数,优先级也是最高的。
Cortex-M 处理器有三个固定优先级和256 个可编程的优先级,最多有128 个抢占等级,但
是实际的优先级数量是由芯片厂商来决定的。但是,绝大多数的芯片都会精简设计的,以致实
际上支持的优先级数会更少,如8 级、16 级、32 级等,比如STM32 就只有16 级优先级。在设
计芯片的时候会裁掉表达优先级的几个低端有效位,以减少优先级数,所以不管用多少位来表
达优先级,都是MSB 对齐的,如图4.1.3.1 就是使用三位来表达优先级。
在图4.1.3.1 中,Bit0~Bit4 没有实现,所以读它们总是返回零,写如它们的话则会忽略写入
的值。因此,对于3 个位的情况,可是使用的优先级就是8 个:0X00(最高优先级)、0X20、0X40、
0X60、0X80、0XA0、0XC0 和0XE0。注意,这个是芯片厂商来决定的!不是我们能决定的,
比如STM32 就选择了4 位作为优先级!
有读者可能就会问,优先级配置寄存器是8 位宽的,为什么却只有128 个抢占等级?8 位
不应该是256 个抢占等级吗?为了使抢占机能变得更可控,Cortex-M 处理器还把256 个优先级
按位分为高低两段:抢占优先级(分组优先级)和亚优先级(子优先级),NVIC 中有一个寄存器是
“应用程序中断及复位控制寄存器(AIRCR)”,AIRCR 寄存器里面有个位段名为“优先级组”,
如表4.1.3.1 所示:
表4.1.3.1 中PRIGROUP 就是优先级分组,它把优先级分为两个位段:MSB 所在的位段(左
边的)对应抢占优先级,LSB 所在的位段(右边的)对应亚优先级,如表4.1.3.2 所示。
在看一下STM32 的优先级分组情况,我们前面说了STM32 使用了4 位,因此最多有5 组
优先级分组设置,这5 个分组在msic.h 中有定义,如下:
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority0 bits for subpriority */
可以看出STM32 有5 个分组,但是一定要注意!STM32 中定义的分组0 对应的值是7!
如果我们选择分组4,即NVIC_PriorityGroup_4 的话,那4 位优先级就都全是抢占优先级了,
没有亚优先级,那么就有0~15 共16 个优先级。而移植FreeRTOS 的时候我们配置的就是组4,如图4.1.3.2 所示:
如果使用ALIENTEK 的基础例程的话默认配置的组2,所以在将基础例程中的外设驱动移
植到FreeRTOS 下面的时候需要修改优先级配置。主要是FreeRTOS 的中断配置没有处理亚优
先级这种情况,所以只能配置为组4,直接就16 个优先级,使用起来也简单!
4.1.4 优先级设置
每个外部中断都有一个对应的优先级寄存器,每个寄存器占8 位,因此最大宽度是8 位,
但是最小为3 位。4 个相临的优先级寄存器拼成一个32 位寄存器。如前所述,根据优先级组的
设置,优先级又可以分为高、低两个位段,分别抢占优先级和亚优先级。STM32 我们已经设置
位组4,所以就只有抢占优先级了。优先级寄存器都可以按字节访问,当然也可以按半字/字来
访问,有意义的优先级寄存器数目由芯片厂商来实现,如表4.1.4.1 和4.1.4.2 所示:
上面说了,4 个相临的寄存器可以拼成一个32 位的寄存器,因此地址0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为0xE000_ED20 的32 位寄存器。这一点很重要!因为FreeRTOS 在设置PendSV 和SysTick 的中断优先级的时候都是直接操作的地址0xE000_ED20。
4.1.5 用于中断屏蔽的特殊寄存器
在4.1.2 小节中说了我们在STM32 上移植FreeRTOS 的时候需要重点关注PRIMASK、
FAULTMASK 和BASEPRI 这三个寄存器,本节就来学习一下这三个寄存器。
1、PRIMASK 和FAULTMASK 寄存器
在许多应用中,需要暂时屏蔽所有的中断一执行一些对时序要求严格的任务,这个时候就
可以使用PRIMASK 寄存器,PRIMASK 用于禁止除NMI 和HardFalut 外的所有异常和中断,
汇编编程的时候可以使用CPS(修改处理器状态)指令修改PRIMASK 寄存器的数值:
CPSIE I; //清除PRIMASK(使能中断)
CPSID I; //设置PRIMASK(禁止中断)
PRIMASK 寄存器还可以通过MRS 和MSR 指令访问,如下:
MOVS R0, #1
MSR PRIMASK, R0 ;//将1 写入PRIMASK 禁止所有中断
以及:
MOVS R0, #0
MSR PRIMASK, R0 ;//将0 写入PRIMASK 以使能中断
UCOS 中的临界区代码代码保护就是通过开关中断实现的(UCOSIII 也可以使用禁止任务调
度的方法来实现临界区代码保护,这里不讨论这种情况),而开关中断就是直接操作PRIMASK
寄存器的,所以在UCOS 中关闭中断的时候时关闭了除复位、NMI 和HardFault 以外的所有中
断!
FAULTMASK 比PRIMASK 更狠,它可以连HardFault 都屏蔽掉,使用方法和PRIMASK 类
似,FAULTMASK 会在退出时自动清零。
汇编编程的时候可以利用CPS 指令修改FAULTMASK 的当前状态:
CPSIE F ;清除FAULTMASK
CPSID F ;设置FAULTMASK
还可以利用MRS 和MSR 指令访问FAULTMASK 寄存器:
MOVS R0, #1
MSR FAULTMASK, R0 ;将1 写入FAULTMASK 禁止所有中断
以及:
MOVS R0, #0
MSR FAULTMASK, R0 ;将0 写入FAULTMASK 使能中断
2、BASEPRI 寄存器
PRIMASK 和FAULTMASK 寄存器太粗暴了,直接关闭除复位、NMI 和HardFault 以外的
其他所有中断,但是在有些场合需要对中断屏蔽进行更细腻的控制,比如只屏蔽优先级低于某
一个阈值的中断。那么这个作为阈值的优先级值存储在哪里呢?在BASEPRI 寄存器中,不过
如果向BASEPRI 写0 的话就会停止屏蔽中断。比如,我们要屏蔽优先级不高于0X60 的中断,
则可以使用如下汇编编程:
MOV R0,#0X60
MSR BASEPRI, R0
如果需要取消BASEPRI 对中断的屏蔽,可以使用如下代码:
MOV R0, #0
MSR BASEPRI, R0
注意!FreeRTOS 的开关中断就是操作BASEPRI 寄存器来实现的!它可以关闭低于某个阈
值的中断,高于这个阈值的中断就不会被关闭!
4.2 FreeRTOS 中断配置宏
4.2.1 configPRIO_BITS
此宏用来设置MCU 使用几位优先级,STM32 使用的是4 位,因此此宏为4!
4.2.2 configLIBRARY_LOWEST_INTERRUPT_PRIORITY
此宏是用来设置最低优先级,前面说了,STM32 优先级使用了4 位,而且STM32 配置的
使用组4,也就是4 位都是抢占优先级。因此优先级数就是16 个,最低优先级那就是15。所以
此宏就是15,注意!不同的MCU 此值不同,具体是多少要看所使用的MCU 的架构,本教程
只针对STM32 讲解!
4.2.3 configKERNEL_INTERRUPT_PRIORITY
此宏用来设置内核中断优先级,此宏定义如下:
#define configKERNEL_INTERRUPT_PRIORITY( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
宏configKERNEL_INTERRUPT_PRIORITY 为,宏
configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移8-configPRIO_BITS 位,也就是左移4
位。为什么要左移4 位呢?前面我们说了,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 )
可以看出,portNVIC_PENDSV_PRI 和portNVIC_SYSTICK_PRI 都是使用了宏
configKERNEL_INTERRUPT_PRIORITY ,为什么宏portNVIC_PENDSV_PRI 是宏
configKERNEL_INTERRUPT_PRIORITY 左移16 位呢?宏portNVIC_SYSTICK_PRI 也同样是
左移24 位。4.1.4 小节讲过了,PendSV 和SysTcik 的中断优先级设置是操作0xE000_ED20 地址
的,这样一次写入的是个32 位的数据,SysTick 和PendSV 的优先级寄存器分别对应这个32
位数据的最高8 位和次高8 位,不就是一个左移16 位,一个左移24 位了。
PendSV 和SysTick 优先级是在哪里设置的呢?在函数xPortStartScheduler()中设置,此函数
在文件port.c 中,函数如下:
BaseType_t xPortStartScheduler( void )
{configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );#if( configASSERT_DEFINED == 1 ){volatile uint32_t ulOriginalPriority;volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * )( portNVIC_IP_REGISTERS_OFFSET_16 +portFIRST_USER_INTERRUPT_NUMBER );volatile uint8_t ucMaxPriorityValue;ulOriginalPriority = *pucFirstUserPriorityRegister;*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;ucMaxPriorityValue = *pucFirstUserPriorityRegister;configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY &ucMaxPriorityValue ) );ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY &ucMaxPriorityValue;ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ){ulMaxPRIGROUPValue--;ucMaxPriorityValue <<= ( uint8_t ) 0x01;}ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;*pucFirstUserPriorityRegister = ulOriginalPriority;}#endif /* conifgASSERT_DEFINED */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置PendSV 中断优先级portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置SysTick 中断优先级vPortSetupTimerInterrupt();uxCriticalNesting = 0;prvStartFirstTask();return 0;
}
上述代码中红色部分就是设置PendSV 和SysTick 优先级的,它们是直接向地址
portNVIC_SYSPRI2_REG 写入优先级数据,portNVIC_SYSPRI2_REG 是个宏,在文件port.c 中
由定义,如下:
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
可以看到宏portNVIC_SYSPRI2_REG 就是地址0XE000ED20!同时也可以看出在FreeRTOS
中PendSV 和SysTick 的中断优先级都是最低的!
4.2.4 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
此宏用来设置FreeRTOS 系统可管理的最大优先级,也就是我们在4.1.5 小节中讲解
BASEPRI 寄存器说的那个阈值优先级,这个大家可以自由设置,这里我设置为了5。也就是高
于5 的优先级(优先级数小于5)不归FreeRTOS 管理!
4.2.5 configMAX_SYSCALL_INTERRUPT_PRIORITY
此宏是configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移4 位而来的,原因
和宏configKERNEL_INTERRUPT_PRIORITY 一样。此宏设置好以后,低于此优先级的中断可
以安全的调用FreeRTOS 的API 函数,高于此优先级的中断FreeRTOS 是不能禁止的,中断服
务函数也不能调用FreeRTOS 的API 函数!
以STM32 为例,有16 个优先级,0 为最高优先级,15 为最低优先级,配置如下:
●configMAX_SYSCALL_INTERRUPT_PRIORITY == 5
●configKERNEL_INTERRUPT_PRIORITY==15
结果如图4.2.5.1 所示:
由于高于configMAX_SYSCALL_INTERRUPT_PRIORITY 的优先级不会被FreeRTOS 内核
屏蔽,因此那些对实时性要求严格的任务就可以使用这些优先级,比如四轴飞行器中的壁障检
测。
4.3 FreeRTOS 开关中断
FreeRTOS 开关中断函数为portENABLE_INTERRUPTS ()和portDISABLE_INTERRUPTS(),
这两个函数其实是宏定义,在portmacro.h 中有定义,如下:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
可以看出开关中断实际上是通过函数vPortSetBASEPRI(0)和vPortRaiseBASEPRI()来实现
的,这两个函数如下:
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{__asm{msr basepri, ulBASEPRI}
}/*-----------------------------------------------------------*/static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;__asm{msr basepri, ulNewBASEPRIdsbisb}
}
函数vPortSetBASEPRI()是向寄存器BASEPRI 写入一个值,此值作为参数ulBASEPRI 传
递进来,portENABLE_INTERRUPTS()是开中断,它传递了个0 给vPortSetBASEPRI(),根据我
们前面讲解BASEPRI 寄存器可知,结果就是开中断。
函数vPortRaiseBASEPRI() 是向寄存器BASEPRI 写入宏
configMAX_SYSCALL_INTERRUPT_PRIORITY ,那么优先级低于
configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽!
4.4 临界段代码
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设
的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要
关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS 系统本身就有很多的临界段代码,
这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段
代码保护。
FreeRTOS 与临界段代码保护有关的函数有4 个:taskENTER_CRITICAL() 、
taskEXIT_CRITICAL() 、taskENTER_CRITICAL_FROM_ISR() 和
taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在task.h 文件中有定义。这四
个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
4.4.1 任务级临界段代码保护
taskENTER_CRITICAL()和taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临
界段,一个是退出临界段,这两个函数是成对使用的,这函数的定义如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
而portENTER_CRITICAL()和portEXIT_CRITICAL()也是宏定义,在文件portmacro.h 中有
定义,如下:
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
函数vPortEnterCritical()和vPortExitCritical()在文件port.c 中,函数如下:
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();}
}
可以看出在进入函数vPortEnterCritical()以后会首先关闭中断,然后给变量uxCriticalNesting
加一,uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数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);}
}
(1)、进入临界区。
(2)、退出临界区。
(1)和(2)中间的代码就是临界区代码,注意临界区代码一定要精简!因为进入临界区会关闭
中断,这样会导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应!
4.4.2 中断级临界段代码保护
函数taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()中断级别
临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于
configMAX_SYSCALL_INTERRUPT_PRIORITY!原因前面已经说了。这两个函数在文件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)dsbisb}return ulReturn; (3)
}
(1)、先读出BASEPRI 的值,保存在ulReturn 中。
(2)、将configMAX_SYSCALL_INTERRUPT_PRIORITY 写入到寄存器BASEPRI 中。
(3)、返回ulReturn,退出临界区代码保护的时候要使用到此值!
中断级临界代码保护使用方法如下:
//定时器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); //清除中断标志位
}
(1)、进入临界区。
(2)、退出临界区。
4.5 FreeRTOS 中断测试实验
4.5.1 实验程序设计
1、实验目的
上面我们讲了在FreeRTOS 中优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断会被屏蔽掉,高于的就不会,那么本节我们就写个简单的例程测试一下。使用两个定时
器,一个优先级为4,一个优先级为5,两个定时器每隔1s 通过串口输出一串字符串。然后在
某个任务中关闭中断一段时间,查看两个定时器的输出情况。。
2、实验设计
本实验设计了两个任务start_task()和interrupt_task(), 这两个任务的任务功能如下:
start_task():创建另外一个任务。
interrupt_task() :中断测试任务,任务中会调用FreeRTOS 的关中断函数
portDISABLE_INTERRUPTS()来将中断关闭一段时间。
3、实验工程
FreeRTOS 实验4-1 FreeRTOS 中断测试实验。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define INTERRUPT_TASK_PRIO 2 //任务优先级
#define INTERRUPT_STK_SIZE 256 //任务堆栈大小
TaskHandle_t INTERRUPTTask_Handler; //任务句柄
void interrupt_task(void *p_arg); //任务函数
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDTIM3_Int_Init(10000-1,7200-1); //初始化定时器3,定时器周期1STIM5_Int_Init(10000-1,7200-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 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(); //退出临界区
}//中断测试任务函数
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);}
}
(1)、创建一个任务来执行开关中断的动作,任务函数为interrupt_task()。
(2)、当任务interrupt_task()运行5 次以后关闭中断。
(3) 、调用函数portDISABLE_INTERRUPTS() 关闭中断。优先级低于
configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断都会被关闭,高于的不会受任何影响。
(4)、调用函数delay_xms()延时5S,此函数是对delay_us()的简单封装,delay_xms()会用来
模拟关闭中断一段时间,此函数不会引起任务调度!
(5)、调用函数portENABLE_INTERRUPTS()重新打开中断。
●中断初始化及处理过程
//通用定时器3 中断初始化
//这里时钟选择为APB1 的2 倍,而APB1 为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能//定时器TIM3 初始化TIM_TimeBaseStructure.TIM_Period = arr; //自动重装载值TIM_TimeBaseStructure.TIM_Prescaler =psc; //定时器分频TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3 中断,允许更新中断//中断优先级NVIC 设置NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4; //先占优先级4 级(1)NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0 级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能NVIC_Init(&NVIC_InitStructure); //初始化NVIC 寄存器TIM_Cmd(TIM3, ENABLE); //使能TIMx
}//通用定时器5 中断初始化
//这里时钟选择为APB1 的2 倍,而APB1 为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器5!
void TIM5_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //时钟使能//定时器TIM5 初始化TIM_TimeBaseStructure.TIM_Period = arr; //自动重装载值TIM_TimeBaseStructure.TIM_Prescaler =psc; //定时器分频TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure);TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE ); //使能指定的TIM5 中断,允许更新中断//中断优先级NVIC 设置NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM5 中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5; //先占优先级5 级(2)NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0 级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能NVIC_Init(&NVIC_InitStructure); //初始化NVIC 寄存器TIM_Cmd(TIM5, ENABLE); //使能TIM5}//定时器3 中断服务函数
void TIM3_IRQHandler(void)
{if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断{printf("TIM3 输出.......\r\n"); (3)}TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}//定时器5 中断服务函数
void TIM5_IRQHandler(void)
{if(TIM_GetITStatus(TIM5,TIM_IT_Update)==SET) //溢出中断{printf("TIM5 输出.......\r\n"); (4)}TIM_ClearITPendingBit(TIM5,TIM_IT_Update); //清除中断标志位
}
(1)、设置定时器3 的抢占优先级为4,高于configMAX_SYSCALL_INTERRUPT_PRIORITY,
因此在调用函数portDISABLE_INTERRUPTS()关闭中断的时候定时器3 是不会受影响的。
(2)、设置定时器5 的抢占优先级为5,等于configMAX_SYSCALL_INTERRUPT_PRIORITY,
因此在调用函数portDISABLE_INTERRUPTS()关闭中断的时候定时器5 中断肯定会被关闭的。
(3)和(4)、定时器3 和定时5 串口输出信息。
4.5.2 实验程序运行结果
编译并下载代码到开发板中,打开串口调试助手查看数据输出,结果如图4.5.2.1 所示:
从图4.5.2.1 可以看出,一开始没有关闭中断,所以TIM3 和TIM5 都正常运行,红框所示
部分。当任务interrupt_task()运行了5 次以后就关闭了中断,此时由于TIM5 的中断优先级为5,
等于configMAX_SYSCALL_INTERRUPT_PRIORITY,因此TIM5 被关闭。但是,TIM3 的中断
优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY,不会被关闭,所以TIM3 正常运
行,绿框所示部分。中断关闭5S 以后就会调用函数portENABLE_INTERRUPTS()重新打开中
断,重新打开中断以后TIM5 恢复运行,蓝框所示部分。
第五章FreeRTOS 任务基础知识
RTOS 系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习RTOS 系统的工程
师或者学生主要就是为了使用RTOS 的多任务处理功能,初步上手RTOS 系统首先必须掌握的
也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理的重要性。由于任务相关的知
识很多,所以接下来我们将用几章的内容来讲解FreeRTOS 的任务。本章先学习一下FreeRTOS
的任务基础知识,本章是后面学习的基础,所以一定要掌握本章关于FreeRTOS 任务管理的基
础知识,本章分为如下几部分:
5.1 什么是多任务系统
5.2 FreeRTOS 任务与协程
5.3 初次使用
5.3 任务状态
5.4 任务优先级
5.5 任务实现
5.6 任务控制块
5.7 任务堆栈
5.1 什么是多任务系统?
回想一下我们以前在使用51、AVR、STM32 单片机裸机(未使用系统)的时候一般都是在
main 函数里面用while(1)做一个大循环来完成所有的处理,即应用程序是一个无限的循环,循
环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对于多任务
系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环
while(1)作为后台程序,如图5.1.1 所示:
前后台系统的实时性差,前后台系统各个任务(应用程序)都是排队等着轮流执行,不管你
这个程序现在有多紧急,没轮到你就只能等着!相当于所有任务(应用程序)的优先级都是一样
的。但是前后台系统简单啊,资源消耗也少啊!在稍微大一点的嵌入式应用中前后台系统就明
显力不从心了,此时就需要多任务系统出马了。
多任务系统会把一个大问题(应用)“分而治之”,把大问题划分成很多个小问题,逐步的把
小问题解决掉,大问题也就随之解决了,这些小问题可以单独的作为一个小任务来处理。这些
小任务是并发处理的,注意,并不是说同一时刻一起执行很多个任务,而是由于每个任务执行
的时间很短,导致看起来像是同一时刻执行了很多个任务一样。多个任务带来了一个新的问题,
究竟哪个任务先运行,哪个任务后运行呢?完成这个功能的东西在RTOS 系统中叫做任务调度
器。不同的系统其任务调度器的实现方法也不同,比如FreeRTOS 是一个抢占式的实时多任务
系统,那么其任务调度器也是抢占式的,运行过程如图5.1.2 所示:
在图5.1.2 中,高优先级的任务可以打断低优先级任务的运行而取得CPU 的使用权,这样
就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的
优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把CPU 的使
用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。
5.2 FreeRTOS 任务与协程
再FreeRTOS 中应用既可以使用任务,也可以使用协程(Co-Routine),或者两者混合使用。
但是任务和协程使用不同的API函数,因此不能通过队列(或信号量)将数据从任务发送给协程,
反之亦然。协程是为那些资源很少的MCU 准备的,其开销很小,但是FreeRTOS 官方已经不打
算再更新协程了,所以本教程只讲解任务。
5.2.1 任务(Task)的特性
在使用RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环
境,不依赖于系统中其他的任务或者RTOS 调度器。任何一个时间点只能有一个任务运行,具
体运行哪个任务是由RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任
务。任务不需要了解RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执
行的时候其上下文环境(寄存器值,堆栈内容等)和任务上一次退出的时候相同。为了做到这一
点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再
次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行。
任务特性:
1、简单。
2、没有使用限制。
3、支持抢占
4、支持优先级
5、每个任务都拥有堆栈导致了RAM 使用量增大。
6、如果使用抢占的话的必须仔细的考虑重入的问题。
5.2.2 协程(Co-routine)的特性
协程是为那些资源很少的MCU 而做的,但是随着MCU 的飞速发展,性能越来越强大,现
在协程几乎很少用到了!但是FreeRTOS 目前还没有把协程移除的计划,但是FreeRTOS 是绝对
不会再更新和维护协程了,因此协程大家了解一下就行了。在概念上协程和任务是相似的,但
是有如下根本上的不同:
1、堆栈使用
所有的协程使用同一个堆栈(如果是任务的话每个任务都有自己的堆栈),这样就比使用任
务消耗更少的RAM。
2、调度器和优先级
协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程。
3、宏实现
协程是通过宏定义来实现的。
4、使用限制
为了降低对RAM 的消耗做了很多的限制。
5.3 任务状态
FreeRTOS 中的任务永远处于下面几个状态中的某一个:
●运行态
当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在
使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处
于运行态。
●就绪态
处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,
但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
●阻塞态
如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调
用了函数vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事
件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过
这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
●挂起态
像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的
任务没有超时时间。任务进入和退出挂起态通过调用函数vTaskSuspend()和xTaskResume()。
任务状态之间的转换如图5.4.1 所示:
5.4 任务优先级
每个任务都可以分配一个从0~(configMAX_PRIORITIES-1) 的优先级,
configMAX_PRIORITIES 在文件FreeRTOSConfig.h 中有定义,前面我们讲解FreeRTOS 系统配
置的时候已经讲过了。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令
选择下一个要运行的任务,Cortex-M 处理器是支持该指令的) ,并且宏
configUSE_PORT_OPTIMISED_TASK_SELECTION 也设置为了1 ,那么宏
configMAX_PRIORITIES 不能超过32 !也就是优先级不能超过32 级。其他情况下宏
configMAX_PRIORITIES 可以为任意值,但是考虑到RAM 的消耗,宏configMAX_PRIORITIES
最好设置为一个满足应用的最小值。
优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES-1 的优
先级最高。空闲任务的优先级最低,为0。
FreeRTOS 调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说
就是处于就绪态的最高优先级的任务才会运行。当宏configUSE_TIME_SLICING 定义为1 的时
候多个任务可以共用一个优先级,数量不限。默认情况下宏configUSE_TIME_SLICING 在文件
FreeRTOS.h 中已经定义为1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器
获取运行时间。
5.5 任务实现
在使用FreeRTOS 的过程中,我们要使用函数xTaskCreate()或xTaskCreateStatic()来创建任
务,这两个函数的第一个参数pxTaskCode,就是这个任务的任务函数。什么是任务函数?任务
函数就是完成本任务工作的函数。我这个任务要干嘛?要做什么?要完成什么样的功能都是在
这个任务函数中实现的。比如我要做个任务,这个任务要点个流水灯,那么这个流水灯的程序
就是任务函数中实现的。FreeRTOS 官方给出的任务函数模板如下:
void vATaskFunction(void *pvParameters) (1)
{for( ; ; ) (2){--任务应用程序-- (3)vTaskDelay(); (4)}/* 不能从任务函数中返回或者退出,从任务函数中返回或退出的话就会调用configASSERT(),前提是你定义了configASSERT()。如果一定要从任务函数中退出的话那一定要调用函数vTaskDelete(NULL)来删除此任务。*/vTaskDelete(NULL); (5)}
(1)、任务函数本质也是函数,所以肯定有任务名什么的,不过这里我们要注意:任务函数
的返回类型一定要为void 类型,也就是无返回值,而且任务的参数也是void 指针类型的!任务
函数名可以根据实际情况定义。
(2)、任务的具体执行过程是一个大循环,for(; ; )就代表一个循环,作用和while(1)一样,笔
者习惯用while(1)。
(3)、循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
(4)、FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让FreeRTOS 发生任务
切换的API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用
的就是FreeRTOS 的延时函数。
(5)、任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用
函数vTaskDelete(NULL)删除此任务!
FreeRTOS 的任务函数和UCOS 的任务函数模式基本相同的,不止FreeRTOS,其他RTOS
的任务函数基本也是这种方式的。
5.6 任务控制块
FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结
构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数xTaskCreate()创建任务的时候就
会自动的给每个任务分配一个任务控制块。在老版本的FreeRTOS 中任务控制块叫做tskTCB,
新版本重命名为TCB_t,但是本质上还是tskTCB,本教程后面提到任务控制块的话均用TCB_t
表示,此结构体在文件tasks.c 中有定义,如下:
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack; //任务堆栈栈顶#if ( portUSING_MPU_WRAPPERS == 1 )xMPU_SETTINGS xMPUSettings; //MPU 相关设置#endifListItem_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,//如果是动态创建的就为pdFALSEuint8_t ucStaticallyAllocated;#endif#if( INCLUDE_xTaskAbortDelay == 1 )uint8_t ucDelayAborted;#endif
} tskTCB;//新版本的FreeRTOS 任务控制块重命名为TCB_t,但是本质上还是tskTCB,主要是为了兼容
//旧版本的应用。
typedef tskTCB TCB_t;
可以看出来FreeRTOS 的任务控制块中的成员变量相比UCOSIII 要少很多,而且大多数与
裁剪有关,当不使用某些功能的时候与其相关的变量就不参与编译,任务控制块大小就会进一
步的减小。
5.7 任务堆栈
FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调
度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,
等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着
从上次中断的地方开始运行。
创建任务的时候需要给任务指定堆栈,如果使用的函数xTaskCreate()创建任务(动态方法)
的话那么任务堆栈就会由函数xTaskCreate()自动创建,后面分析xTaskCreate()的时候会讲解。
如果使用函数xTaskCreateStatic()创建任务(静态方法)的话就需要程序员自行定义任务堆栈,然
后堆栈首地址作为函数的参数puxStackBuffer 传递给函数,如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,const char * const pcName,const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,StackType_t * const puxStackBuffer, (1)StaticTask_t * const pxTaskBuffer )
(1)、任务堆栈,需要用户定义,然后将堆栈首地址传递给这个参数。
堆栈大小:
我们不管是使用函数xTaskCreate()还是xTaskCreateStatic()创建任务都需要指定任务堆栈大
小。任务堆栈的数据类型为StackType_t,StackType_t 本质上是uint32_t,在portmacro.h 中有定
义,如下:
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE longtypedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
可以看出StackType_t 类型的变量为4 个字节,那么任务的实际堆栈大小就应该是我们所
定义的4 倍。
第六章FreeRTOS 任务相关API 函数
上一章我们学习了FreeRTOS 的任务基础知识,本章就正式学习如何使用FreeRTOS 中有
关任务的API 函数。本来本章想讲解FreeRTOS 的任务原理知识的,但是很多初学者还没使用
过FreeRTOS,甚至其他的RTOS 系统都没有使用过,所以一上来就是苦涩的原理很可能会吓跑
一大批初学者。所以本章做了调整,先学习怎么用,先知其然,后面在知其所以然。使用过以
后再学习原理、看源码就会轻松很多。本章分为如下几部分:
6.1 任务创建和删除API 函数
6.2 任务创建和删除实验(动态方法)
6.3 任务创建和删除实验(静态方法)
6.4 任务挂起和恢复API 函数
6.5 任务挂起和恢复实验
6.1 任务创建和删除API 函数
FreeRTOS 最基本的功能就是任务管理,而任务管理最基本的操作就是创建和删除任务,
FreeRTOS 的任务创建和删除API 函数如表6.1.1.1 所示:
1、函数xTaxkCreate()
此函数用来创建一个任务,任务需要RAM 来保存与任务有关的状态信息(任务控制块),任
务也需要一定的RAM 来作为任务堆栈。如果使用函数xTaskCreate()来创建任务的话那么这些
所需的RAM 就会自动的从FreeRTOS 的堆中分配,因此必须提供内存管理文件,默认我们使用
heap_4.c 这个内存管理文件,而且宏configSUPPORT_DYNAMIC_ALLOCATION 必须为1。如
果使用函数xTaskCreateStatic()创建的话这些RAM 就需要用户来提供了。新创建的任务默认就
是就绪态的,如果当前没有比它更高优先级的任务运行那么此任务就会立即进入运行态开始运
行,不管在任务调度器启动前还是启动后,都可以创建任务。此函数也是我们以后经常用到的,
本教程所有例程均用此函数来创建任务,函数原型如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,const char * const pcName,const uint16_t usStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask )
参数:
pxTaskCode:任务函数。
pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过。
configMAX_TASK_NAME_LEN。
usStackDepth:任务堆栈大小,注意实际申请到的堆栈是usStackDepth 的4 倍。其中空闲任
务的任务堆栈大小为configMINIMAL_STACK_SIZE。
pvParameters: 传递给任务函数的参数。
uxPriotiry: 任务优先级,范围0~ configMAX_PRIORITIES-1。
pxCreatedTask: 任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是
任务的任务堆栈。此参数就用来保存这个任务句柄。其他API 函数可能会使
用到这个句柄。
返回值:
pdPASS: 任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败,因为堆内存不足!
2、函数xTaskCreateStatic()
此函数和xTaskCreate()的功能相同,也是用来创建任务的,但是使用此函数创建的任务所
需的RAM 需要用用户来提供。如果要使用此函数的话需要将宏
configSUPPORT_STATIC_ALLOCATION 定义为1。函数原型如下:
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 )
参数:
pxTaskCode:任务函数。
pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过。
configMAX_TASK_NAME_LEN。
usStackDepth:任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出,
一般是个数组,此参数就是这个数组的大小。
pvParameters: 传递给任务函数的参数。
uxPriotiry: 任务优先级,范围0~ configMAX_PRIORITIES-1。
puxStackBuffer: 任务堆栈,一般为数组,数组类型要为StackType_t 类型。
pxTaskBuffer: 任务控制块。
返回值:
NULL:任务创建失败,puxStackBuffer 或pxTaskBuffer 为NULL 的时候会导致这个
错误的发生。
其他值: 任务创建成功,返回任务的任务句柄。
3、函数xTaskCreateRestricted()
此函数也是用来创建任务的,只不过此函数要求所使用的MCU 有MPU(内存保护单元),
用此函数创建的任务会受到MPU 的保护。其他的功能和函数xTaxkCreate()一样。
BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition,TaskHandle_t * pxCreatedTask )
参数:
pxTaskDefinition: 指向一个结构体TaskParameters_t,这个结构体描述了任务的任务函数、
堆栈大小、优先级等。此结构体在文件task.h 中有定义。
pxCreatedTask: 任务句柄。
返回值:
pdPASS: 任务创建成功。
其他值: 任务未创建成功,很有可能是因为FreeRTOS 的堆太小了。
4、函数vTaskDelete()
删除一个用函数xTaskCreate()或者xTaskCreateStatic()创建的任务,被删除了的任务不再存
在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄!如果此任务
是使用动态方法创建的,也就是使用函数xTaskCreate()创建的,那么在此任务被删除以后此任
务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数vTaskDelete()删除
任务以后必须给空闲任务一定的运行时间。
只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务
的内存需要用户自行释放掉,比如某个任务中用户调用函数pvPortMalloc()分配了500 字节的内
存,那么在此任务被删除以后用户也必须调用函数vPortFree()将这500 字节的内存释放掉,否
则会导致内存泄露。此函数原型如下:
vTaskDelete( TaskHandle_t xTaskToDelete )
参数:
xTaskToDelete: 要删除的任务的任务句柄。
返回值:
无
6.2 任务创建和删除实验(动态方法)
6.2.1 实验程序设计
1、实验目的
上一小节讲解了FreeRTOS 的任务创建和删除的API 函数,本小节就来学习如何使用这些
API 函数,本小节学习xTaskCreate()和vTaskDelete()这两个函数的使用
2、实验设计
本实验设计三个任务:start_task、task1_task 和task2_task ,这三个任务的任务功能如下:
start_task:用来创建其他两个任务。
task1_task :当此任务运行5 此以后就会调用函数vTaskDelete()删除任务task2_task,此任
务也会控制LED0 的闪烁,并且周期性的刷新LCD 指定区域的背景颜色。
task2_task :此任务普通的应用任务,此任务也会控制LED1 的闪烁,并且周期性的刷新
LCD 指定区域的背景颜色。
3、实验工程
FreeRTOS 实验6-1 FreeRTOS 任务创建和删除实验(动态方法)。
4、实验程序与分析
●任务设置
#define START_TASK_PRIO 1 //任务优先级 (1)
#define START_STK_SIZE 128 //任务堆栈大小 (2)
TaskHandle_t StartTask_Handler; //任务句柄 (3)
void start_task(void *pvParameters); //任务函数 (4)#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define TASK2_TASK_PRIO 3 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };
(1) 、start_task 任务的任务优先级,此处用宏来表示,以后所有的任务优先级都用宏来表
示。创建任务设置优先级的时候就用这个宏,当然了也可以直接在创建任务的时候指定任务优
先级。
(2)、start_task 任务的任务堆栈大小。
(3)、start_task 任务的任务句柄。
(4)、start_task 任务的任务函数声明。
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDLCD_Init(); //初始化LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/F407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 6-1");LCD_ShowString(30,50,200,16,16,"Task Creat and Del");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(1)(const char* )"start_task", //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度(2)
}
(1)、调用函数xTaskCreate()创建tart_task 任务,函数中的各个参数就是上面的任务设置中
定义的,其他任务的创建也用这种方法。
(2)、调用函数vTaskStartScheduler()开启FreeRTOS 的任务调度器,FreeRTOS 开始运行。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters) (1)
{taskENTER_CRITICAL(); //进入临界区//创建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 )task2_task,(const char* )"task2_task",(uint16_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )&Task2Task_Handler);vTaskDelete(StartTask_Handler); //删除开始任务(2)taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters) (3)
{u8 task1_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR = BLUE;LCD_ShowString(6,111,110,16,16,"Task1 Run:000");while(1){task1_num++; //任务执1 行次数加1 注意task1_num1 加到255 的时候会清零!!LED0=!LED0;printf("任务1 已经执行:%d 次\r\n",task1_num);if(task1_num==5){vTaskDelete(Task2Task_Handler);//任务1 执行5 次删除任务2 (4)printf("任务1 删除了任务2!\r\n");}LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
//task2 任务函数
void task2_task(void *pvParameters) (5)
{u8 task2_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR = BLUE;LCD_ShowString(126,111,110,16,16,"Task2 Run:000");while(1){task2_num++; //任务2 执行次数加1 注意task1_num2 加到255 的时候会清零!!LED1=!LED1;printf("任务2 已经执行:%d 次\r\n",task2_num);LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
(1)、start_task 任务的任务函数,在此任务函数中我们创建了另外两个任务task1_task 和
task2_task。start_task 任务的职责就是用来创建其他的任务或者信号量、消息队列等的,当创建
完成以后就可以删除掉start_task 任务。
(2)、删除start_task 任务,注意函数vTaskDelete()的参数就是start_task 任务的任务句柄
StartTask_Handler。
(3)、task1_task 任务函数(任务1),任务比较简单,每隔1 秒钟task1_num 加一并且LED0
反转,串口输出任务运行的次数,其实就是task1_num 的值。当task1_task 运行5 次以后就调
用函数vTaskDelete()删除任务task2_task。
(4)、任务task1_task 运行了5 次,调用函数vTaskDelete()删除任务task2_task。
(5)、task2_task 任务函数(任务2),和task1_task 差不多。
简单的总结分析一下此例程的流程,因为这是我们使用FreeRTOS 写的第一个程序,很多
习惯是我们后面要用到的。比如使用任务宏定义任务优先级,堆栈大小等,一般有关一个任务
的东西我们的放到一起,比如任务堆栈、任务句柄、任务函数声明等,这样方便修改。这些东
西可以放到一个.h 头文件里面去,只是例程里面任务数比较少,所以就直接放到main.c 文件里
面了,要是工程比较大的话最好做一个专用的头文件来管理。
在main 函数中一开始肯定是初始化各种硬件外设,初始化完外设以后调用函数
xTaskCreate()创建一个开始任务,注意创建开始任务是在调用函数vTaskStartScheduler()开启任
务调度器之前,这样当后面开启任务调度器以后就会直接运行开始任务了。其他任务的创建就
放到开始任务的任务函数中,由于开始任务的职责就是创建其他应用任务和信号量、队列等这
些内核对象的,所以它只需要执行一次,当这些东西创建完成以后就可以删除掉开始任务了。
看过我们的UCOS 教程的话就会发现这个过程和UCOS 里面一样的。
6.2.2 程序运行结果分析
编译程序并下载到开发板中,查看任务1 和任务2 的运行情况,下载完成以后以后LCD 显
示如图6.2.2.1 所示:
图中左边的框为任务1 的运行区域,右边的框为任务2 的运行区域,可以看出任务2 运行
了5 次就停止了,而任务1 运行了12 次了。打开串口调试助手,显示如图6.2.2.2 所示:
从图6.2.2.2 中可以看出,一开始任务1 和任务2 是同时运行的,由于任务2 的优先级比任
务1 的优先级高,所以任务2 先输出信息。当任务1 运行了5 次以后任务1 就删除了任务2,
最后只剩下了任务1 在运行了。
6.3 任务创建和删除实验(静态方法)
6.3.1 实验程序设计
1、实验目的
上一小节我们讲了使用函数xTaskCreate()来创建任务,本节在上一小节的基础上做简单的
修改,使用函数xTaskCreateStatic()来创建任务,也就是静态方法,任务的堆栈、任务控制块就
需要由用户来指定了。
2、实验设计
参考实验:FreeRTOS 实验6-1 FreeRTOS 任务创建和删除实验(动态方法)。
3、实验工程
FreeRTOS 实验6-2 FreeRTOS 任务创建和删除实验(动态方法)。
4、实验程序与分析
●系统设置
使用静态方法创建任务的时候需要将宏configSUPPORT_STATIC_ALLOCATION 设置为1,
在文件FreeRTOSConfig.h 中设置,如下所示:
#define configSUPPORT_STATIC_ALLOCATION 1 //静态内存
宏configSUPPORT_STATIC_ALLOCATION 定义为1 以后编译一次,会提示我们有两个函
数未定义,如图6.3.1.1 所示:
这个在我们讲FreeRTOS 的配置文件FreeRTOSConfig.h 的时候就说过了,如果使用静态方
法的话需要用户实现两个函数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()来获取这些内存。
●任务设置
#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); //任务函数#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
StackType_t Task1TaskStack[TASK1_STK_SIZE]; //任务堆栈
StaticTask_t Task1TaskTCB; //任务控制块
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define TASK2_TASK_PRIO 3 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
StackType_t Task2TaskStack[TASK2_STK_SIZE]; //任务堆栈
StaticTask_t Task2TaskTCB; //任务控制块
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数//LCD 刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };
(1)、静态创建任务需要用户提供任务堆栈,这里定义一个数组作为任务堆栈,堆栈数组为
StackType_t 类型。
(2)、定义任务控制块,注意任务控制块类型要用StaticTask_t,而不是TCB_t 或tskTCB!
这里已经要切记!
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDLCD_Init(); //初始化LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 6-2");LCD_ShowString(30,50,200,16,16,"Task Creat and Del");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务StartTask_Handler=xTaskCreateStatic((TaskFunction_t)start_task, //任务函数(1)(const char* )"start_task", //任务名称(uint32_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(StackType_t* )StartTaskStack, //任务堆栈(2)(StaticTask_t* )&StartTaskTCB); //任务控制块(3)vTaskStartScheduler(); //开启任务调度
}
(1)、调用函数xTaskCreateStatic()创建任务。
(2)、将定义的任务堆栈数组传递给函数。
(3)、将定义的任务控制块传递给函数。
可以看出在用法上xTaskCreateStatic()和xTaskCreate()没有太大的区别,大多数的参数都相
同。学习过UCOS 的同学应该会对函数xTaskCreateStatic()感到熟悉,因为UCOS 中创建任务的
函数和xTaskCreateStatic()类似,也需要用户来指定任务堆栈和任务控制块的内存的,然后将其
作为参数传递给任务创建函数。不过我们后面所有的例程不管是创建任务、信号量还是队列都
使用动态方法。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建TASK1 任务Task1Task_Handler=xTaskCreateStatic((TaskFunction_t )task1_task, (1)(const char* )"task1_task",(uint32_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(StackType_t* )Task1TaskStack,(StaticTask_t* )&Task1TaskTCB);//创建TASK2 任务Task2Task_Handler=xTaskCreateStatic((TaskFunction_t )task2_task, (2)(const char* )"task2_task",(uint32_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(StackType_t* )Task2TaskStack,(StaticTask_t* )&Task2TaskTCB);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{u8 task1_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR = BLUE;LCD_ShowString(6,111,110,16,16,"Task1 Run:000");while(1){task1_num++; //任务执1 行次数加1 注意task1_num1 加到255 的时候会清零!!LED0=!LED0;printf("任务1 已经执行:%d 次\r\n",task1_num);if(task1_num==5){vTaskDelete(Task2Task_Handler);//任务1 执行5 次删除任务2printf("任务1 删除了任务2!\r\n");}LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
//task2 任务函数
void task2_task(void *pvParameters)
{u8 task2_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR = BLUE;LCD_ShowString(126,111,110,16,16,"Task2 Run:000");while(1){task2_num++; //任务2 执行次数加1 注意task1_num2 加到255 的时候会清零!!LED1=!LED1;printf("任务2 已经执行:%d 次\r\n",task2_num);LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
(1)、使用静态任务创建函数xTaskCreateStatic()来创建任务task1_task。
(2)、使用静态任务创建函数xTaskCreateStatic()来创建任务task2_task。
6.3.2 程序运行结果分析
参考6.2.2 小节。
6.4 任务挂起和恢复API 函数
有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。这个时候要是使用任
务删除和重建的方法的话那么任务中变量保存的值肯定丢失了!FreeRTOS 给我们提供了解决
这种问题的方法,那就是任务挂起和恢复,当某个任务要停止运行一段时间的话就将这个任务
挂起,当要重新运行这个任务的话就恢复这个任务的运行。FreeRTOS 的任务挂起和恢复API 函
数如表6.2.1.1 所示:
1、函数vTaskSuspend()
此函数用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态。退出挂
起态的唯一方法就是调用任务恢复函数vTaskResume()或xTaskResumeFromISR()。,函数原型如
下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend)
参数:
xTaskToSuspend:要挂起的任务的任务句柄,创建任务的时候会为每个任务分配一个任务
句柄。如果使用函数xTaskCreate() 创建任务的话那么函数的参数
pxCreatedTask 就是此任务的任务句柄,如果使用函数xTaskCreateStatic()
创建任务的话那么函数的返回值就是此任务的任务句柄。也可以通过函
数xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。
注意!如果参数为NULL 的话表示挂起任务自己。
返回值:
无。
2、函数vTaskResume()
将一个任务从挂起态恢复到就绪态,只有通过函数vTaskSuspend()设置为挂起态的任务才
可以使用vTaskRexume()恢复!函数原型如下:
void vTaskResume( TaskHandle_t xTaskToResume)
参数:
xTaskToResume:要恢复的任务的任务句柄。
返回值:
无。
3、函数xTaskResumeFromISR()
此函数是vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务。函数原型如下:
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)
参数:
xTaskToResume: 要恢复的任务的任务句柄。
返回值:
pdTRUE: 恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打
断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换。
pdFALSE: 恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的
任务),这意味着在退出中断服务函数的以后不需要进行上下文切换。
6.5 任务挂起和恢复实验
6.5.1 实验程序设计
1、实验目的
学习使用FreeRTOS 的任务挂起和恢复相关API 函数,包括vTaskSuspend()、vTaskResume()
和xTaskResumeFromISR()。
2、实验设计
本实验设计4 个任务:start_task、key_task、task1_task 和task2_task,这四个任务的任务功
能如下:
start_task:用来创建其他3 个任务。
key_task:按键服务任务,检测按键的按下结果,根据不同的按键结果执行不同的操作。
task1_task:应用任务1。
task2_task: 应用任务2。
实验需要四个按键,KEY0、KEY1、KEY2 和KEY_UP,这四个按键的功能如下:
KEY0: 此按键为中断模式,在中断服务函数中恢复任务2 的运行。
KEY1: 此按键为输入模式,用于恢复任务1 的运行。
KEY2: 此按键为输入模式,用于挂起任务2 的运行。
KEY_UP: 此按键为输入模式,用于挂起任务1 的运行。
3、实验工程
FreeRTOS 实验6-3 FreeRTOS 任务挂起和恢复实验。
4、实验程序与分析
●任务设置
实验中任务优先级、堆栈大小和任务句柄等的设置如下:
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define KEY_TASK_PRIO 2 //任务优先级
#define KEY_STK_SIZE 128 //任务堆栈大小
TaskHandle_t KeyTask_Handler; //任务句柄
void key_task(void *pvParameters); //任务函数#define TASK1_TASK_PRIO 3 //任务优先级#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define TASK2_TASK_PRIO 4 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键EXTIX_Init(); //初始化外部中断LCD_Init(); //初始化LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 6-3");LCD_ShowString(30,50,200,16,16,"Task Susp and Resum");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务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(); //开启任务调度
}
在main 函数中我们主要完成硬件的初始化,在硬件初始化完成以后创建了任务start_task()
并且开启了FreeRTOS 的任务调度。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters) (1)
{taskENTER_CRITICAL(); //进入临界区//创建KEY 任务xTaskCreate((TaskFunction_t )key_task,(const char* )"key_task",(uint16_t )KEY_STK_SIZE,(void* )NULL,(UBaseType_t )KEY_TASK_PRIO,(TaskHandle_t* )&KeyTask_Handler);//创建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 )task2_task,(const char* )"task2_task",(uint16_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )&Task2Task_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//key 任务函数
void key_task(void *pvParameters)
{u8 key;while(1){key=KEY_Scan(0);switch(key){case WKUP_PRES:vTaskSuspend(Task1Task_Handler); //挂起任务1 (2)printf("挂起任务1 的运行!\r\n");break;case KEY1_PRES:vTaskResume(Task1Task_Handler); //恢复任务1 (3)printf("恢复任务1 的运行!\r\n");break;case KEY2_PRES:vTaskSuspend(Task2Task_Handler);//挂起任务2 (4)printf("挂起任务2 的运行!\r\n");break;}vTaskDelay(10); //延时10ms}
}
//task1 任务函数
void task1_task(void *pvParameters) (5)
{u8 task1_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR = BLUE;LCD_ShowString(6,111,110,16,16,"Task1 Run:000");while(1){task1_num++; //任务执1 行次数加1 注意task1_num1 加到255 的时候会清零!!LED0=!LED0;printf("任务1 已经执行:%d 次\r\n",task1_num);LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
//task2 任务函数
void task2_task(void *pvParameters) (6)
{u8 task2_num=0;POINT_COLOR = BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR = BLUE;LCD_ShowString(126,111,110,16,16,"Task2 Run:000");while(1){task2_num++; //任务2 执行次数加1 注意task1_num2 加到255 的时候会清零!!LED1=!LED1;printf("任务2 已经执行:%d 次\r\n",task2_num);LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
(1)、start_task 任务,用于创建其他3 个任务。
(2)、在key_tssk 任务里面,KEY_UP 被按下,调用函数vTaskSuspend()挂起任务1。
(3)、KEY1 被按下,调用函数vTaskResume()恢复任务1 的运行。
(4)、KEY2 被按下,调用函数vTaskSuspend()挂起任务2。
(5)、任务1 的任务函数,用于观察任务挂起和恢复的过程。
(6)、任务2 的任务函数,用于观察任务挂起和恢复的过程(中断方式)。
●中断初始化及处理过程
//外部中断初始化程序
//初始化PE4 为中断输入.
void EXTIX_Init(void)
{EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;KEY_Init(); // 按键端口初始化RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //使能复用功能时钟//GPIOE4 中断线以及中断初始化配置下降沿触发GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);EXTI_InitStructure.EXTI_Line=EXTI_Line4;EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;EXTI_InitStructure.EXTI_LineCmd = ENABLE;EXTI_Init(&EXTI_InitStructure); //初始化外设EXTI 寄存器NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x06; //抢占优先级6 (1)NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道NVIC_Init(&NVIC_InitStructure); //初始化外设NVIC 寄存器
}
//任务句柄
extern TaskHandle_t Task2Task_Handler;
//外部中断4 服务程序
void EXTI4_IRQHandler(void)
{BaseType_t YieldRequired;delay_xms(20); //消抖if(KEY0==0){YieldRequired=xTaskResumeFromISR(Task2Task_Handler);//恢复任务2printf("恢复任务2 的运行!\r\n");if(YieldRequired==pdTRUE){/*如果函数xTaskResumeFromISR()返回值为pdTRUE,那么说明要恢复的这个任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),所以在退出中断的时候一定要进行上下文切换!*/portYIELD_FROM_ISR(YieldRequired);}}EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4 上的中断标志位
}
//任务句柄
extern TaskHandle_t Task2Task_Handler;
//外部中断4 服务程序
void EXTI4_IRQHandler(void)
{BaseType_t YieldRequired;delay_xms(20); //消抖if(KEY0==0){YieldRequired=xTaskResumeFromISR(Task2Task_Handler);//恢复任务2 (2)printf("恢复任务2 的运行!\r\n");if(YieldRequired==pdTRUE){/*如果函数xTaskResumeFromISR()返回值为pdTRUE,那么说明要恢复的这个任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),所以在退出中断的时候一定要进行上下文切换!*/portYIELD_FROM_ISR(YieldRequired); (3)}}EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4 上的中断标志位
}
(1)、设置中断优先级,前面在讲解FreeRTOS 中断的时候就讲过,如果中断服务函数要使
用FreeRTOS 的API 函数的话那么中断优先级一定要低于
configMAX_SYSCALL_INTERRUPT_PRIORITY!这里设置为6。
(2)、调用函数xTaskResumeFromISR()来恢复任务2 的运行。
(3)、根据函数xTaskResumeFromISR()的返回值来确定是否需要进行上下文切换。当返回值
为pdTRUE 的时候就需要调用函数portYIELD_FROM_ISR()进行上下文切换,否则的话不需要。
6.5.2 程序运行结果分析
编译并下载程序到开发板中,通过按不同的按键来观察任务的挂起和恢复的过程,如图
6.5.2.1 所示:
从图6.5.2.1 可以看出,一开始任务1 和任务2 都正常运行,当挂起任务1 或者任务2 以后
任务1 或者任务2 就会停止运行,直到下一次重新恢复任务1 或者任务2 的运行。重点是,保
存任务运行次数的变量都没有发生数据丢失,如果用任务删除和重建的方法这些数据必然会丢
失掉的!
第七章FreeRTOS 列表和列表项
要想看懂FreeRTOS 源码并学习其原理,有一个东西绝对跑不了,那就是FreeRTOS 的列表
和列表项。列表和列表项是FreeRTOS 的一个数据结构,FreeRTOS 大量使用到了列表和列表项,
它是FreeRTOS 的基石。要想深入学习并理解FreeRTOS,那么列表和列表项就必须首先掌握,
否则后面根本就没法进行。本章我们就来学习一下FreeRTOS 的列表和列表项,包括对列表和
列表项的操作,本章分为如下几部分:
7.1 什么是列表和列表项
7.2 列表和列表项的初始化
7.3 列表项的插入
7.4 列表项末尾插入
7.5 列表项的删除
7.6 列表项的遍历
7.7 列表项的插入和删除实验
7.1 什么是列表和列表项?
7.1.1 列表
列表是FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS
中的任务。与列表相关的全部东西都在文件list.c 和list.h 中。在list.h 中定义了一个叫List_t 的
结构体,如下:
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,在初始化列表的时候会这两个变量中
写入一个特殊的值,默认不开启这个功能。以后我们在学习列表的时候不讨论这个功能!
(2)、uxNumberOfItems 用来记录列表中列表项的数量。
(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、列表中最后一个列表项,用来表示列表结束,此变量类型为MiniListItem_t,这是一个
迷你列表项,关于列表项稍后讲解。
列表结构示意图如图7.1.1.1 所示:
注意!图7.1.1.1 中并未列出用于列表完整性检查的成员变量。
7.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)、用法和列表一样,用来检查列表项完整性的。以后我们在学习列表项的时候不讨
论这个功能!
(2)、xItemValue 为列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向前一个列表项,和pxNext 配合起来实现类似双向链表的功能。
(5)、pvOwner 记录此链表项归谁拥有,通常是任务控制块。
(6)、pvContainer 用来记录此列表项归哪个列表。注意和pvOwner 的区别,在前面讲解任务
控制块TCB_t 的时候说了在TCB_t 中有两个变量xStateListItem 和xEventListItem,这两个变量
的类型就是ListItem_t,也就是说这两个成员变量都是列表项。以xStateListItem 为例,当创建
一个任务以后xStateListItem 的pvOwner 变量就指向这个任务的任务控制块,表示xSateListItem
属于此任务。当任务就绪态以后xStateListItem 的变量pvContainer 就指向就绪列表,表明此列
表项在就绪列表中。举个通俗一点的例子:小王在上二年级,他的父亲是老王。如果把小王比
作列表项,那么小王的pvOwner 属性值就是老王,小王的pvContainer 属性值就是二年级。
列表项结构示意图如图7.1.2.1 所示:
注意!图7.1.2.1 中并未列出用于列表项完整性检查的成员变量!
7.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 指向上一个列表项。
可以看出迷你列表项只是比列表项少了几个成员变量,迷你列表项有的成员变量列表项都
有的,没感觉有什么本质区别啊?那为什么要弄个迷你列表项出来呢?那是因为有些情况下我
们不需要列表项这么全的功能,可能只需要其中的某几个成员变量,如果此时用列表项的话会
造成内存浪费!比如上面列表结构体List_t 中表示最后一个列表项的成员变量xListEnd 就是
MiniListItem_t 类型的。
迷你列表项结构示意图如图7.1.3.1 所示:
注意!图7.1.3.1 中并未列出用于迷你列表项完整性检查的成员变量!
7.2 列表和列表项初始化
7.2.1 列表初始化
新创建或者定义的列表需要对其做初始化处理,列表的初始化其实就是初始化列表结构体
List_t 中的各个成员变量,列表的初始化通过使函数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)
}
(1)、xListEnd 用来表示列表的末尾,而pxIndex 表示列表项的索引号,此时列表只有一个
列表项,那就是xListEnd,所以pxIndex 指向xListEnd。
(2)、xListEnd 的列表项值初始化为portMAX_DELAY,portMAX_DELAY 是个宏,在文件
portmacro.h 中有定义。根据所使用的MCU 的不同,portMAX_DELAY 值也不相同,可以为0xffff
或者0xffffffffUL,本教程中为0xffffffffUL。
(3)、初始化列表项xListEnd 的pxNext 变量,因为此时列表只有一个列表项xListEnd,因
此pxNext 只能指向自身。
(4)、同(3)一样,初始化xListEnd 的pxPrevious 变量,指向xListEnd 自身。
(5)、由于此时没有其他的列表项,因此uxNumberOfItems 为0,注意,这里没有算xListEnd。
(6) 和(7) 、初始化列表项中用于完整性检查字段,只有宏
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 为1 的时候才有效。同样的根据所选的
MCU 不同其写入的值也不同,可以为0x5a5a 或者0x5a5a5a5aUL。STM32 是32 位系统写入的
是0x5a5a5a5aUL,列表初始化完以后如图7.2.1.1 所示:
注意,图7.2.1.1 为了好分析,将xListEnd 中的各个成员变量都写了出来!
7.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 这两个列
表项中的其他成员变量在做初始化,任务创建过程后面会详细讲解。
7.3 列表项插入
7.3.1 列表项插入函数分析
列表项的插入操作通过函数vListInsert()来完成,函数原型如下:
void vListInsert( List_t * const pxList,ListItem_t * const pxNewListItem )
参数:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
返回值:
无
函数vListInsert()的参数pxList 决定了列表项要插入到哪个列表中,pxNewListItem 决定了
要插入的列表项,但是这个列表项具体插入到什么地方呢?要插入的位置由列表项中成员变量
xItemValue 来决定。列表项的插入根据xItemValue 的值按照升序的方式排列!接下来我们来具
体看一下函数vListInsert()的整个运行过程,函数代码如下:
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)、这一行和下一行代码用来检查列表和列表项的完整性的。其实就是检查列表和列表项中用于完整性检查的变量值是否被改变。这些变量的值在列表和列表项初始化的时候就被写入
了,这两行代码需要实现函数configASSERT()!
(3)、要插入列表项,第一步就是要获取该列表项要插入到什么位置!如果要插入的列表项
的值等于portMAX_DELAY,也就是说列表项值为最大值,这种情况最好办了,要插入的位置
就是列表最末尾了。
(4)、获取要插入点,注意!列表中的xListEnd 用来表示列表末尾,在初始化列表的时候
xListEnd 的列表值也是portMAX_DELAY,此时要插入的列表项的列表值也是portMAX_DELAY。
这两个的顺序该怎么放啊?通过这行代码可以看出要插入的列表项会被放到xListEnd 前面。
(5)、要插入的列表项的值如果不等于portMAX_DELAY 那么就需要在列表中一个一个的找
自己的位置,这个for 循环就是找位置的过程,当找到合适列表项的位置的时候就会跳出。由
于这个for 循环是用来寻找列表项插入点的,所以for 循环体里面没有任何东西。这个查找过程
是按照升序的方式查找列表项插入点的。
(6)、经过上面的查找,我们已经找到列表项的插入点了,从本行开始接下来的四行代码就
是将列表项插入到列表中,插入过程和数据结构中双向链表的插入类似。像FreeRTOS 这种
RTOS 系统和一些协议栈都会大量用到数据结构的知识,所以建议大家没事的时候多看看数据
结构方面的书籍,否则的话看源码会很吃力的。
(7)、列表项已经插入到列表中了,那么列表项的成员变量pvContainer 也该记录此列表项
属于哪个列表的了。
(8)、列表的成员变量uxNumberOfItems 加一,表示又添加了一个列表项。
7.3.2 列表项插入过程图示
上一小节我们分析了列表项插入函数vListInsert(),本小节我们就通过图片来演示一下这个
插入过程,本小节我们会向一个空的列表中插入三个列表项,这三个列表项的值分别为40,60
和50。
1、插入值为40 的列表项
在一个空的列表List 中插入一个列表值为40 的列表项ListItem1,插入完成以后如图7.3.2.1
所示:
注意观察插入完成以后列表List 和列表项ListItem1 中各个成员变量之间的变化,比如列
表List 中的uxNumberOfItems 变为了1,表示现在列表中有一个列表项。列表项ListItem1 中的
pvContainer 变成了List,表示此列表项属于列表List。通过图7.3.2.1 可以看出,列表是一个环
形的,即环形列表!
2、插入值为60 的列表项
接着再插入一个值为60 的列表项ListItem2,插入完成以后如图7.3.2.2 所示:
上面再讲解函数vListInsert()的时候说过了列表项是按照升序的方式插入的,所以ListItem2
肯定是插入到ListItem1 的后面、xListEnd 的前面。同样的,列表List 的uxNumberOfItems 再次
加一变为2 了,说明此时列表中有两个列表项。
3、插入值为50 的列表项
在上面的列表中再插入一个值为50 的列表项ListItem3,插入完成以后如图7.3.2.3 所示:
按照升序排列的方式,ListItem3 应该放到ListItem1 和ListItem2 中间,大家最好通过对照
这三幅图片来阅读函数vListInsert()的源码,这样就会对函数有一个直观的认识。
7.4 列表项末尾插入
7.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)之间的代码就是将要插入的列表项插入到列表末尾。使用函数
vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表
项成员变量xItemValue 来确定。vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表
中的xListEnd 成员变量表示列表末尾的,那么函数vListInsertEnd()插入一个列表项是不是就是
插到xListEnd 的前面或后面啊?这个是不一定的,这里所谓的末尾要根据列表的成员变量
pxIndex 来确定的!前面说了列表中的pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的
列表项就是要遍历的开始列表项,也就是说pxIndex 所指向的列表项就代表列表头!由于是个
环形列表,所以新的列表项就应该插入到pxIndex 所指向的列表项的前面。
(3)、标记新的列表项pxNewListItem 属于列表pxList。
(4)、记录列表中的列表项数目的变量加一,更新列表项数目。
7.4.2 列表项末尾插入图示
跟函数vListInsert()一样,我们也用图片来看一下函数vListInsertEnd()的插入过程。
1、默认列表
在插入列表项之前我们先准备一个默认列表,如图7.4.2.1 所示:
注意图7.4.2.1 中列表的pxIndex 所指向的列表项,这里为ListItem1,不再是xListEnd 了。
3、插入值为50 的列表项
在上面的列表中插入一个值为50 的列表项ListItem3,插入完成以后如图7.4.2.2 所示:
列表List 的pxIndex 指向列表项ListItem1,因此调用函数vListInsertEnd()插入ListItem3 的
话就会在ListItem1 的前面插入。
7.5 列表项的删除
有列表项的插入,那么必然有列表项的删除,列表项的删除通过函数uxListRemove()来完
成,函数原型如下:
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
参数:
pxItemToRemove: 要删除的列表项。
返回值:返回删除列表项以后的列表剩余列表项数目。
注意,列表项的删除只是将指定的列表项从列表中删除掉,并不会将这个列表项的内存给
释放掉!如果这个列表项是动态分配内存的话。函数uxListRemove()的源码如下:
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)、返回新列表的当前列表项数目。
7.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; \ (5)
}
(1)、pxTCB 用来保存pxIndex 所指向的列表项的pvOwner 变量值,也就是这个列表项属于
谁的?通常是一个任务的任务控制块。pxList 表示要遍历的列表。
(2)、列表的pxIndex 变量指向下一个列表项。
(3)、如果pxIndex 指向了列表的xListEnd 成员变量,表示到了列表末尾。
(4)、如果到了列表末尾的话就跳过xListEnd,pxIndex 再一次重新指向处于列表头的列表
项,这样就完成了一次对列表的遍历。
(5)、将pxIndex 所指向的新列表项的pvOwner 赋值给pxTCB。
此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。
7.7 列表项的插入和删除实验
上面我们通过分析源码的方式了解了FreeRTOS 的列表和列表项的操作过程,但是实际上
究竟是不是这样的、我们的分析有没有错误?最好的检验方法就是写一段测试代码测试一下,
观察在对列表和列表项操作的时候其变化情况和我们分析的是否一致。
7.7.1 实验程序设计
1、实验目的
学习使用FreeRTOS 列表和列表项相应的操作函数的使用,观察这些操作函数的运行结果
和我们理论分析的是否一致。
2、实验设计
本实验设计3 个任务:start_task、task1_task 和list_task,这三个任务的任务功能如下:
start_task:用来创建其他2 个任务。
task1_task:应用任务1,控制LED0 闪烁,用来提示系统正在运行。
task2_task: 列表和列表项操作任务,调用列表和列表项相关的API 函数,并且通过串口
输出相应的信息来观察这些API 函数的运行过程。
实验需要用到KEY_UP 按键,用于控制任务的运行。
3、实验工程
FreeRTOS 实验7-1 FreeRTOS 列表项的插入和删除实验。
4、实验程序与分析
●任务设置
实验中任务优先级、堆栈大小和任务句柄等的设置如下:
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define LIST_TASK_PRIO 3 //任务优先级
#define LIST_STK_SIZE 128 //任务堆栈大小
TaskHandle_t ListTask_Handler; //任务句柄
void list_task(void *pvParameters); //任务函数
●列表和列表项的定义
//定义一个测试用的列表和3 个列表项
List_t TestList; //测试用列表
ListItem_t ListItem1; //测试用列表项1
ListItem_t ListItem2; //测试用列表项2
ListItem_t ListItem3; //测试用列表项3
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键LCD_Init(); //初始化LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 7-1");LCD_ShowString(30,50,200,16,16,"list and listItem");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务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(); //开启任务调度
}
●任务函数
任务函数start_task()和task1_task()都比较简单,这里为了缩减篇幅就不列出来了,重点看
一下任务函数list_task(),函数如下:
//list 任务函数
void list_task(void *pvParameters)
{//第一步:初始化列表和列表项vListInitialise(&TestList);vListInitialiseItem(&ListItem1);vListInitialiseItem(&ListItem2);vListInitialiseItem(&ListItem3);ListItem1.xItemValue=40; //ListItem1 列表项值为40ListItem2.xItemValue=60; //ListItem2 列表项值为60ListItem3.xItemValue=50; //ListItem3 列表项值为50//第二步:打印列表和其他列表项的地址printf("/**********列表和列表项地址***********/\r\n");printf("项目地址\r\n");printf("TestList %#x \r\n",(int)&TestList);printf("TestList->pxIndex %#x \r\n",(int)TestList.pxIndex);printf("TestList->xListEnd %#x \r\n",(int)(&TestList.xListEnd));printf("ListItem1 %#x \r\n",(int)&ListItem1);printf("ListItem2 %#x \r\n",(int)&ListItem2);printf("ListItem3 %#x \r\n",(int)&ListItem3);printf("/****************结束*****************/\r\n");printf("按下KEY_UP 键继续!\r\n\r\n\r\n");while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第三步:向列表TestList 添加列表项ListItem1,并通过串口打印所有//列表项中成员变量pxNext 和pxPrevious 的值,通过这两个值观察列表//项在列表中的连接情况。vListInsert(&TestList,&ListItem1); //插入列表项ListItem1printf("/*********添加列表项ListItem1**********/\r\n");printf("项目地址\r\n");printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));printf("/**********前后向连接分割线***********/\r\n");printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));printf("/*****************结束****************/\r\n");printf("按下KEY_UP 键继续!\r\n\r\n\r\n");while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第四步:向列表TestList 添加列表项ListItem2,并通过串口打印所有//列表项中成员变量pxNext 和pxPrevious 的值,通过这两个值观察列表//项在列表中的连接情况。vListInsert(&TestList,&ListItem2); //插入列表项ListItem2printf("/*********添加列表项ListItem2**********/\r\n");printf("项目地址\r\n");printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));printf("/***********前后向连接分割线**********/\r\n");printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));printf("/****************结束*****************/\r\n");printf("按下KEY_UP 键继续!\r\n\r\n\r\n");while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第五步:向列表TestList 添加列表项ListItem3,并通过串口打印所有//列表项中成员变量pxNext 和pxPrevious 的值,通过这两个值观察列表//项在列表中的连接情况。vListInsert(&TestList,&ListItem3); //插入列表项ListItem3printf("/*********添加列表项ListItem3**********/\r\n");printf("项目地址\r\n");printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));printf("/**********前后向连接分割线***********/\r\n");printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));printf("/*****************结束****************/\r\n");printf("按下KEY_UP 键继续!\r\n\r\n\r\n");while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第六步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext 和//pxPrevious 的值,通过这两个值观察列表项在列表中的连接情况。uxListRemove(&ListItem2); //删除ListItem2printf("/**********删除列表项ListItem2*********/\r\n");printf("项目地址\r\n");printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));printf("/***********前后向连接分割线**********/\r\n");printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));printf("/****************结束*****************/\r\n");printf("按下KEY_UP 键继续!\r\n\r\n\r\n");while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第七步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext 和//pxPrevious 的值,通过这两个值观察列表项在列表中的连接情况。TestList.pxIndex=TestList.pxIndex->pxNext;//pxIndex 向后移一项,//这样pxIndex 就会指向ListItem1。vListInsertEnd(&TestList,&ListItem2); //列表末尾添加列表项ListItem2printf("/******在末尾添加列表项ListItem2*******/\r\n");printf("项目地址\r\n");printf("TestList->pxIndex %#x \r\n",(int)TestList.pxIndex);printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));printf("/***********前后向连接分割线**********/\r\n");printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));printf("/****************结束*****************/\r\n\r\n\r\n");while(1){LED1=!LED1;vTaskDelay(1000); //延时1s,也就是1000 个时钟节拍}
}
任务函数list_task()通过调用与列表和列表项相关的API 函数来对列表和列表项做相应的
操作,并且通过串口打印出每调用一个API 函数以后列表和列表项的连接信息,通过这些信息
我们可以直观的判断出列表项在插入、删除和末尾插入的时候这个列表的变化情况。
7.7.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,然后按照任务函数list_task()中的步
骤一步步的测试分析。
●第一步和第二步
第一步和第二步是用来初始化列表和列表项的,并且通过串口输出列表和列表项的地址,
这一步是开发板复位后默认运行的,串口调试助手信息如下所示:
由于这些列表和列表项地址前六位都为0X200000,只有最低2 位不同,所以我们就用最低
2 位代表这些列表和列表项的地址。注意!列表和列表项的地址在不同的硬件平台或编译软件
上是不同的,以自己的实际实验结果为准!简单的分析一下图7.7.2.1 可以得到如下信息:
1、列表TestList 地址为b4。
2、列表项ListItem1、ListItem2 和ListItem3 的地址分别为c8、dc 和f0。
3、列表TestList 的xListEnd 地址为bc。
4、列表TestList 的pxIndex 指向地址bc,而这个地址正是迷你列表项xListEnd,说明pxIndex
指向xListEnd,这个和我们分析列表初始化函数vListInitialise()的时候得到的结果是一致的。
●第三步
按一下KEY_UP 键,执行第三步,第三步是向列表TestList 中插入列表项ListItem1,列表
项ListItem1 的成员变量xItemValue 的值为40。第三步执行完以后串口调试助手输出如图7.7.2.2
所示:
分析图7.7.2.2 可以得出一下信息:
1、xListEnd 的pxNext 指向地址c8,而c8 是ListItem1 的地址,说明xListEnd 的pxNext 指
向ListItem1。
2、列表项ListItem1 的pxNext 指向地址bc,而bc 是xListEnd 的地址,说明ListItem1 的
pxNext 指向xListEnd。
3、xListEnd 的pxPrevious 指向地址c8,而c8 是ListItem1 的地址,说明xListEnd 的pxPrevious
指向ListItem2。
4、ListItem1 的pxPrevious 指向地址bc,bc 是xListEnd 的地址,说明ListItem1 的pxPrevious
指向xListEnd。
用简易示意图表示这一步的话如图7.7.2.3 所示:
●第四步
按一下KEY_UP 键,执行第四步,第四步是向列表TestList 中插入列表项ListItem2,列表
项ListItem2 的成员变量xItemValue 的值为60。第四步执行完以后串口调试助手输出如图7.7.2.4
所示:
分析图7.7.2.4 可以得出一下信息:
1、xListEnd 的pxNext 指向ListItem1。
2、ListItem1 的pxNext 指向ListItem2。
3、ListItem2 的pxNext 指向xListEnd。
4、列表项的pxPrevious 分析过程类似,后面的步骤中就不做分析了,只看pxNext 成员变
量。用简易示意图表示这一步的话如图7.7.2.5 所示:
●第五步
按一下KEY_UP 键,执行第五步,第五步是向列表TestList 中插入列表项ListItem3,列表
项ListItem3 的成员变量xItemValue 的值为50。第四步执行完以后串口调试助手输出如图7.7.2.6
所示:
分析图7.7.2.6 可以得出一下信息:
1、xListEnd 的pxNext 指向ListItem1。
2、ListItem1 的pxNext 指向ListItem3。
3、ListItem3 的pxNext 指向ListItem2。
4、ListItem2 的pxNext 指向xListEnd。
用简易示意图表示这一步的话如图7.7.2.7 所示:
通过这几步可以看出列表项的插入是根据xItemValue 的值做升序排列的,这个和我们分析
函数vListInsert()得到的结果一样,说明我们的分析是对的!
●第六步和第七步
这两步是观察函数uxListRemove()和vListInsertEnd()的运行过程的,分析过程和前五步一
样。这里就不做分析了,大家自行根据串口调试助手输出的信息做分析。
第八章FreeRTOS 调度器开启和任务相关函数详解
我们已经学会了FreeRTOS 的任务创建和删除,挂起和恢复等基本操作,并且也学习了分
析FreeRTOS 源码所必须掌握的知识:列表和列表项。但是任务究竟如何被创建、删除、挂起
和恢复的?系统是怎么启动的等等这些我们还不了解,一个操作系统最核心的内容就是多任务
管理,所以我们非常有必要去学习一下FreeRTOS 的任务创建、删除、挂起、恢复和系统启动
等,这样才能对FreeRTOS 有一个更深入的了解。本章分为如下几部分:
8.1 阅读本章所必备的知识
8.2 调度器开启过程分析
8.3 任务创建过程分析
8.4 任务删除过程分析
8.5 任务挂起过程分析
8.6 任务恢复过程分析
8.1 阅读本章所必备的知识
本章和下一章要讲解的内容和Cortex-M 处理器的内核架构联系非常紧密!阅读本章必须先
对Cortex-M 处理器的架构有一定的了解,在学习本章的时候一定要配合《权威指南》来学习,
推荐大家仔细阅读《权威指南》中的如下章节:
1、第3 章技术综述,通过阅读本章可以对Cortex-M 处理器的架构有一个大体的了解。
2、第4 章架构,强烈建议仔细阅读本章内容,尤其是要理解其中讲解到的各个寄存器。
3、第5 章指令集,本章和下一章的内容会涉及到一些有关ARM 的汇编指令,在阅读的
时遇到不懂的指令可以查阅《权威指南》的第5 章中相关指令的讲解。
4、第7 章异常和中断,大概了解一下。
5、第8 章深入了解异常处理,强烈建议仔细阅读!
6、第10 章OS 支持特性,强烈建议仔细阅读!
《权威指南》中的其他章节大家依据个人爱好来阅读,由于《权威指南》讲解的内容非常
的“底层”,所以看起来可能会感觉晦涩难懂,如果看不懂的话不要着急,看不懂的地方就跳过,
先对Cortex-M 的处理器有一个大概的了解就行了。笔者第一次看宋岩翻译的那本《ARM Cortex-M3 权威指南》的时候就一点都没看懂,在后面的工作中因为工作需要才硬着头皮看的,不知道
看了多少遍,反正书已经翻烂了,现在看第三版的《权威指南》估计也就能看懂个40%~50%吧。
8.2 调度器开启过程分析
在本节中会涉及到ARM 的汇编指令,有关涉及到的ARM 指令的详细使用情况请参考《权
威指南》的“第5 章指令集”。《权威指南》的这一章节对Cortex-M3/M4 内核的所有指令做了
非常详细的接收,包括指令的含义、使用方法和参考案例等等。
8.2.1 任务调度器开启函数分析
前面的所有例程中我们都是在main()函数中先创建一个开始任务start_task,后面紧接着调
用函数vTaskStartScheduler()。这个函数的功能就是开启任务调度器的,这个函数在文件tasks.c
中有定义,缩减后的函数代码如下:
void vTaskStartScheduler( void )
{BaseType_t xReturn;xReturn = xTaskCreate( prvIdleTask, (1)"IDLE", configMINIMAL_STACK_SIZE,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),&xIdleTaskHandle );
#if ( configUSE_TIMERS == 1 ) //使用软件定时器使能{if( xReturn == pdPASS ){xReturn = xTimerCreateTimerTask(); (2)}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_TIMERS */if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。{portDISABLE_INTERRUPTS(); (3)
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能NEWLIB{_impure_ptr = &( pxCurrentTCB->xNewLib_reent );}
#endif /* configUSE_NEWLIB_REENTRANT */xNextTaskUnblockTime = portMAX_DELAY;xSchedulerRunning = pdTRUE; (4)xTickCount = ( TickType_t ) 0U;portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)if( xPortStartScheduler() != pdFALSE ) (6){//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的}else{//不会运行到这里,除非调用函数xTaskEndScheduler()。}}else{//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建//空闲任务或者定时器任务的时候没有足够的内存。configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}//防止编译器报错,比如宏INCLUDE_xTaskGetIdleTaskHandle 定义为0 的话编译器就会提//示xIdleTaskHandle 未使用。( void ) xIdleTaskHandle;
}
(1)、创建空闲任务,如果使用静态内存的话使用函数xTaskCreateStatic()来创建空闲任务,
优先级为tskIDLE_PRIORITY,宏tskIDLE_PRIORITY 为0,也就是说空闲任务的优先级为最
低。
(2)、如果使用软件定时器的话还需要通过函数xTimerCreateTimerTask()来创建定时器服务
任务。定时器服务任务的具体创建过程是在函数xTimerCreateTimerTask()中完成的,这个函数
很简单,大家就自行查阅一下。
(3)、关闭中断,在SVC 中断服务函数vPortSVCHandler()中会打开中断。
(4)、变量xSchedulerRunning 设置为pdTRUE,表示调度器开始运行。
(5)、当宏configGENERATE_RUN_TIME_STATS 为1 的时候说明使能时间统计功能,此时
需要用户实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器
/计数器。
(6)、调用函数xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、
FPU 单元和PendSV 中断等等。
8.2.2 内核相关硬件初始化函数分析
FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到PendSV 中断,这些
硬件的初始化由函数xPortStartScheduler()来完成,缩减后的函数代码如下:
BaseType_t xPortStartScheduler( void )
{/******************************************************************//****************此处省略一大堆的条件编译代码**********************//*****************************************************************/portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2)vPortSetupTimerInterrupt(); (3)uxCriticalNesting = 0; (4)prvStartFirstTask(); (5)//代码正常执行的话是不会到这里的!return 0;
}
(1)、设置PendSV 的中断优先级,为最低优先级。
(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时
器的中断,函数比较简单,大家自行查阅分析。
(4)、初始化临界区嵌套计数器。
(5)、调用函数prvStartFirstTask()开启第一个任务。
8.2.3 启动第一个任务
经过上面的操作以后我们就可以启动第一个任务了,函数prvStartFirstTask()用于启动第一
个任务,这是一个汇编函数,函数源码如下:
__asm void prvStartFirstTask( void )
{PRESERVE8ldr 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)nopnop
}
(1)、将0XE000ED08 保存在寄存器R0 中。一般来说向量表应该是从起始地址(0X00000000)
开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提
供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器
(VTOR)的可编程寄存器。VTOR 寄存器的地址就是0XE000ED08,通过这个寄存器可以重新定
义向量表,比如在STM32F103 的ST 官方库中会通过函数SystemInit()来设置VTOR 寄存器,
代码如下:
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //VTOR=0x08000000+0X00
通过上面一行代码就将向量表开始地址重新定义到了0X08000000,向量表的起始地址存储
的就是MSP 初始值。关于向量表和向量表重定位的详细内容请参阅《权威指南》的“第7 章
异常和中断”的7.5 小节。
(2)、读取R0 中存储的地址处的数据并将其保存在R0 寄存器,也就是读取寄存器VTOR
中的值,并将其保存在R0 寄存器中。这一行代码执行完就以后R0 的值应该为0X08000000。
(3)、读取R0 中存储的地址处的数据并将其保存在R0 寄存器,也就是读取地址0X08000000
处存储的数据,并将其保存在R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针
MSP 的初始值,这一行代码执行完以后寄存器R0 就存储MSP 的初始值。现在来看(1)、(2)、
(3)这三步起始就是为了获取MSP 的初始值而已!
(4)、复位MSP,R0 中保存了MSP 的初始值,将其赋值给MSP 就相当于复位MSP。
(5)和(6)、使能中断,关于这两个指令的详细内容请参考《权威指南》的“第4 章架构”
的第4.2.3 小节。
(7)和(8)、数据同步和指令同步屏障,这两个指令的详细内容请参考《权威指南》的“第5
章指令集”的5.6.13 小节。
(9),调用SVC 指令触发SVC 中断,SVC 也叫做请求管理调用,SVC 和PendSV 异常对于
OS 的设计来说非常重要。SVC 异常由SVC 指令触发。关于SVC 的详细内容请参考《权威指
南》的“第10 章OS 支持特性”的10.3 小节。在FreeRTOS 中仅仅使用SVC 异常来启动第一
个任务,后面的程序中就再也用不到SVC 了。
8.2.4 SVC 中断服务函数
在函数prvStartFirstTask()中通过调用SVC 指令触发了SVC 中断,而第一个任务的启动就
是在SVC 中断服务函数中完成的,SVC 中断服务函数应该为SVC_Handler() ,但是
FreeRTOSConfig.h 中通过#define 的方式重新定义为了xPortPendSVHandler(),如下:
#define xPortPendSVHandler PendSV_Handler
函数vPortSVCHandler()在文件port.c 中定义,这个函数也是用汇编写的,函数源码如下:
__asm void vPortSVCHandler( void )
{PRESERVE8ldr 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)orr r14, #0xd ; (8)bx r14 (9)
}
(1)、获取pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向TCB_t 的指针,这个
指针永远指向正在运行的任务。这里先获取这个指针存储的地址,比如我现在的代码测试出来
这个指针是存放在0X20000044,如图8.2.4.1 所示。
(2)、取R3 所保存的地址处的值赋给R1。通过这一步就获取到了当前任务的任务控制块的
存储地址。比如当前我的程序中这个地址就为0X20000EE8,如图8.2.4.2 所示:
(3)、取R3 所保存的地址处的值赋给R0,我们知道任务控制块的第一个字段就是任务堆栈
的栈顶指针pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20000EE8)得到
的就是栈顶指针所指向的地址,当前我的程序中这个栈顶指针(pxTopOfStack)所指向的地址为
0X20000E98,如图8.2.4.3 所示
可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对
应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄
存器值!
(4)、R4~R11,R14 这些寄存器出栈。这里使用了指令LDMIA,LDMIA 指令是多加载/存储指令,不过这里使用的是具有回写的多加载/存储访问指令,用法如下:
LDMIA Rn! , {reg list}
表示从Rn 指定的存储器位置读取多个字,地址在每次读取后增加(IA),Rn 在传输完成以后
写回。对于STM32 来说地址一次增加4 字节,比如如下代码:
LDR R0, =0X800
LDMIA R0!, {R2~R4}
上面两行代码就是将0X800 地址的数据赋值给寄存器R2,0X804 地址的数据赋值给寄存
器R3,0X8008 地址的数据赋值给R4 寄存器,然后,重点来了!此时R0 为800A!通过这一
步我们就从任务堆栈中将R4~R11 这几个寄存器的值给恢复了。
这里有朋友就要问了,R0~R3,R12,PC,xPSR 这些寄存器怎么没有恢复?这是因为这些寄
存器会在退出中断的时候MCU 自动出栈(恢复)的,而R4~R11 需要由用户手动出栈。这个我们
在分析PendSV 中断服务函数的时候会讲到。到这步以后我们来看一下堆栈的栈顶指针指到哪
里了?如图8.2.4.5 所示:
从图8.3.4.5 可以看出恢复R4~R11 和R14 以后堆栈的栈顶指针应该指向地址0X20000EB8,
也就是保存寄存器R0 值的存储地址。退出中断服务函数以后进程栈指针PSP 应该从这个地址
开始恢复其他的寄存器值。
(5)、设置进程栈指针PSP,PSP=R0=0X20000EB8,如图8.2.4.6 所示:
(6)、设置寄存器R0 为0。
(7)、设置寄存器BASEPRI 为R0,也就是0,打开中断!
(8)、R14 寄存器的值与0X0D 进行或运算,得到的结果就是R14 寄存器的新值。表示退出
异常以后CPU 进入线程模式并且使用进程栈!
(9)、执行此行代码以后硬件自动恢复寄存器R0~R3、R12、LR、PC 和xPSR 的值,堆栈使
用进程栈PSP,然后执行寄存器PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开
始运行!
8.2.5 空闲任务
在8.2.1 小节讲解函数vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,
这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他
的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是FreeRTOS 系统自动创建
的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅
是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他
的事情,如下:
1、判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任
务控制块的内存。
2、运行用户设置的空闲任务钩子函数。
3、判断是否开启低功耗tickless 模式,如果开启的话还需要做相应的处理
空闲任务的任务优先级是最低的,为0,任务函数为prvIdleTask(),有关空闲任务的详细内
容我们后面会有专门的章节讲解,这里大家只要知道有这个任务就行了。
8.3 任务创建过程分析
8.3.1 任务创建函数分析
前面学了任务创建可以使用动态方法或静态方法(不讨论使用MPU 的情况),它们分别使用
函数xTaskCreate()和xTaskCreateStatic()。本节我们就以函数xTaskCreate()为例来分析一下
FreeRTOS 的任务创建过程,函数xTaskCreateStatic()类似,这里不做分析。函数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,使用
(1)中申请到的任务堆栈。
(4)、如果任务控制块内存申请失败的话就释放前面已经申请成功的任务堆栈的内存。
(5)、标记任务堆栈和任务控制块是使用动态内存分配方法得到的。
(6)、使用函数prvInitialiseNewTask()初始化任务,这个函数完成对任务控制块中各个字段
的初始化工作!
(7)、使用函数prvAddNewTaskToReadyList()将新创建的任务加入到就绪列表中。
8.3.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 ) );}
#endifpxTopOfStack = 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;}
#endifpxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode,\ (13)pvParameters );if( ( void * ) pxCreatedTask != NULL ){*pxCreatedTask = ( TaskHandle_t ) pxNewTCB; (14)}else{mtCOVERAGE_TEST_MARKER();}
}
(1) 、如果使能了堆栈溢出检测功能或者追踪功能的话就使用一个定值
tskSTACK_FILL_BYTE 来填充任务堆栈,这个值为0xa5U。
(2)、计算堆栈栈顶pxTopOfStack,后面初始化堆栈的时候需要用到。
(3)、保存任务的任务名。
(4)、任务名数组添加字符串结束符’\0’。
(5)、判断任务优先级是否合法,如果设置的任务优先级大于configMAX_PRIORITIES,则
将优先级修改为configMAX_PRIORITIES-1。
(6)、初始化任务控制块的优先级字段uxPriority。
(7)、使能了互斥信号量功能,需要初始化相应的字段。
(8)和(9)、初始化列表项xStateListItem 和xEventListItem,任务控制块结构体中有两个列表
项,这里对这两个列表项做初始化。
(10)和(12)、设置列表项xStateListItem 和xEventListItem 属于当前任务的任务控制块,也就
是设置这两个列表项的字段pvOwner 为新创建的任务的任务控制块。
(11)、设置列表项xEventListItem 的字段xItemValue 为configMAX_PRIORITIES- uxPriority,
比如当前任务优先级3,最大优先级为32,那么xItemValue 就为32-3=29,这就意味着xItemValue
值越大,优先级就越小。上一章学习列表和列表项的时候我们说过,列表的插入是按照
xItemValue 的值升序排列的。
(12)、初始化线程本地存储指针,如果使能了这个功能的话。
(13)、调用函数pxPortInitialiseStack()初始化任务堆栈。
(14)、生成任务句柄,返回给参数pxCreatedTask,从这里可以看出任务句柄其实就是任务
控制块。
8.3.3 任务堆栈初始化函数分析
在任务初始化函数中会对任务堆栈初始化,这个过程通过调用函数pxPortInitialiseStack()来
完成,函数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 -= 8; (6)return pxTopOfStack;
}
堆栈是用来在进行上下文切换的时候保存现场的,一般在新创建好一个堆栈以后会对其先
进行初始化处理,即对Cortex-M 内核的某些寄存器赋初值。这些初值就保存在任务堆栈中,保
存的顺序按照:xPSR、R15(PC)、R14(LR)、R12、R3R0、R11R14。
(1)、寄存器xPSR 值为portINITIAL_XPSR,其值为0x01000000。xPSR 是Cortex-M 的一
个内核寄存器,叫做程序状态寄存器,0x01000000 表示这个寄存器的bit24 为1,表示处于Thumb
状态,即使用的Thumb 指令。
(2)、寄存器PC 初始化为任务函数pxCode。
(3)、寄存器LR 初始化为函数prvTaskExitError。
(4)、跳过4 个寄存器,R12,R3,R2,R1,这四个寄存器不初始化。
(5)、寄存器R0 初始化为pvParameters,一般情况下,函数调用会将R0~R3 作为输入参数,
R0 也可用作返回结果,如果返回值为64 位,则R1 也会用于返回结果(在《权威指南》“第8
章深入了解异常处理”的8.1.2 小节中有讲解,P188),这里的pvParameters 是作为任务函数的
参数,保存在寄存器R0 中。
(6)、跳过8 个寄存器,R11、R10、R8、R7、R6、R5、R4。
经过上面的初始化之后,此时的堆栈结果如图8.3.3.1 所示:
注意,图8.3.3.1 中以STM32 为例,堆栈为向下增长模式。
8.3.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()来完成,函数如下:
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{taskENTER_CRITICAL();{uxCurrentNumberOfTasks++; (1)if( pxCurrentTCB == NULL )//正在运行任务块为NULL,说明没有任务运行!{pxCurrentTCB = pxNewTCB;//将新任务的任务控制块赋值给pxCurrentTCB//新创建的任务是第一个任务!!!if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ){prvInitialiseTaskLists(); (2)}else{mtCOVERAGE_TEST_MARKER();}}else{if( xSchedulerRunning == pdFALSE ){//新任务的任务优先级比正在运行的任务优先级高。if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB; (3)}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}uxTaskNumber++; //uxTaskNumber 加一,用作任务控制块编号。
#if ( configUSE_TRACE_FACILITY == 1 ){pxNewTCB->uxTCBNumber = uxTaskNumber;}
#endif /* configUSE_TRACE_FACILITY */prvAddTaskToReadyList( pxNewTCB ); (4)}taskEXIT_CRITICAL();if( xSchedulerRunning != pdFALSE ){//新任务优先级比正在运行的任务优先级高if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ){taskYIELD_IF_USING_PREEMPTION(); (5)}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}
}
(1)、变量uxCurrentNumberOfTasks 为全局变量,用来统计任务数量。
(2)、变量uxCurrentNumberOfTasks 为1 说明正在创建的任务是第一个任务!那么就需要先
初始化相应的列表,通过调用函数prvInitialiseTaskLists()来初始化相应的列表。这个函数很简
单,本质就是调用上一章讲的列表初始化函数vListInitialise()来初始化几个列表,大家可以自行
分析一下。
(3)、新创建的任务优先级比正在运行的任务优先级高,所以需要修改pxCurrentTCB 为新
建任务的任务控制块。
(4)、调用函数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()将任务添加到就绪
列表末尾。
(5)、如果新任务的任务优先级最高,而且调度器已经开始正常运行了,那么就调用函数
taskYIELD_IF_USING_PREEMPTION()完成一次任务切换。
8.4 任务删除过程分析
前面我们已经学习了如何使用FreeRTOS 的任务删除函数vTaskDelete(),本节我们来详细
的学习一下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)、查看任务是否正在等待某个事件(如信号量、队列等),因为如果任务等待某个事件的
话这个任务会被放到相应的列表中,这里需要将其从相应的列表中删除掉。
(4)、要删除的是当前正在运行的任务,
(5)、要删除任务,那么任务的任务控制块和任务堆栈所占用的内存肯定要被释放掉(如果使
用动态方法创建的任务),但是当前任务正在运行,显然任务控制块和任务堆栈的内存不能被立
即释放掉!必须等到当前任务运行完成才能释放相应的内存,所以需要打一个“标记”,标记出
有任务需要处理。这里将当前任务添加到列表xTasksWaitingTermination 中,如果有任务要删除
自身的话都会被添加到列表xTasksWaitingTermination 中。那么问题来了?内存释放在哪里完成
呢?空闲任务!空闲任务会依次将需要释放的内存都释放掉。
(6)、uxDeletedTasksWaitingCleanUp 是一个全局变量,用来记录有多少个任务需要释放内存。
(7)、调用任务删除钩子函数,钩子函数的具体内容需要用户自行实现。
(8)、删除的是别的任务,变量uxCurrentNumberOfTasks 减一,也就是当前任务数减一。
(9)、因为是删除别的任务,所以可以直接调用函数prvDeleteTCB()删除任务控制块。
(10)、重新计算一下还要多长时间执行下一个任务,也就是下一个任务的解锁时间,防止有
任务的解锁时间参考了刚刚被删除的那个任务。
(11)、如果删除的是正在运行的任务那么删除完以后肯定需要强制进行一次任务切换。
8.5 任务挂起过程分析
挂起任务使用函数vTaskSuspend(),函数源码如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{TCB_t *pxTCB;taskENTER_CRITICAL();{//如果参数为NULL 的话说明挂起自身pxTCB = prvGetTCBFromHandle( xTaskToSuspend ); (1)traceTASK_SUSPEND( pxTCB );//将任务从就绪或者延时列表中删除,并且将任务放到挂起列表中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();}vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ); (4)}taskEXIT_CRITICAL();if( xSchedulerRunning != pdFALSE ){taskENTER_CRITICAL();{prvResetNextTaskUnblockTime(); (5)}taskEXIT_CRITICAL();}else{mtCOVERAGE_TEST_MARKER();}if( pxTCB == pxCurrentTCB ){if( xSchedulerRunning != pdFALSE ){configASSERT( uxSchedulerSuspended == 0 );portYIELD_WITHIN_API(); (6)}else{if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) ==\ (7)uxCurrentNumberOfTasks ){pxCurrentTCB = NULL; (8)}else{vTaskSwitchContext(); (9)}}}else{mtCOVERAGE_TEST_MARKER();}
}
(1)、通过函数prvGetTCBFromHandle()获取要删除任务的任务控制块。
(2)、将任务从任务就绪列表延时列表中删除。
(3)、查看任务是否正在等待某个事件(如信号量、队列等),如果任务还在等待某个事件的
话就将其从相应的事件列表中删除。
(4)、将任务添加到挂起任务列表尾,挂起任务列表为xSuspendedTaskList,所有被挂起的任
务都会被放到这个列表中。
(5)、重新计算一下还要多长时间执行下一个任务,也就是下一个任务的解锁时间。防止有
任务的解锁时间参考了刚刚被挂起的那个任务。
(6)、如果刚刚挂起的任务是正在运行的任务,并且任务调度器运行正常,那么这里就需要
调用函数portYIELD_WITHIN_API()强制进行一次任务切换。
(7)、pxCurrentTCB 指向正在运行的任务,但是正在运行的任务要挂起了,所以必须给
pxCurrentTCB 重新找一个“对象”。也就是查找下一个将要运行的任务,本来这个工作是由任
务切换函数来完成的,但是程序运行到这一行说明任务调度器被挂起了,任务切换函数也无能
为力了,必须手动查找下一个要运行的任务了。调用函数listCURRENT_LIST_LENGTH()判断
一下系统中所有的任务是不是都被挂起了,也就是查看列表xSuspendedTaskList 的长度是不是
等于uxCurrentNumberOfTasks。如果等于的话就说明系统中所有的任务都被挂起了(实际上不存
在这种情况,因为最少都有一个空闲任务是可以运行的,空闲任务执行期间不会调用任何可以
阻塞或者挂起空闲任务的API 函数,为的就是保证系统中永远都有一个可运行的任务)。
(8)、如果所有任务都被挂起的话pxCurrentTCB 就只能等于NULL 了,这样当有新任务被
创建的时候pxCurrentTCB 就可以指向这个新任务。
(9)、有其他的没有被挂起的任务,调用vTaskSwitchContext()获取下一个要运行的任务,函数vTaskSwitchContext()会在下一章详细讲解。
8.6 任务恢复过程分析
任务恢复函数有两个vTaskResume()和xTaskResumeFromISR(),一个是用在任务中的,一
个是用在中断中的,但是基本的处理过程都是一样的,我们就以函数vTaskResume()为例来讲解
一下任务恢复详细过程。
void vTaskResume( TaskHandle_t xTaskToResume )
{TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume; (1)configASSERT( xTaskToResume );//函数参数不可能为NULL。if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) ) (2){taskENTER_CRITICAL(); (3){if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) (4){traceTASK_RESUME( pxTCB );( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (5)prvAddTaskToReadyList( pxTCB ); (6)if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) (7){taskYIELD_IF_USING_PREEMPTION(); (8)}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL(); (9)}else{mtCOVERAGE_TEST_MARKER();}
}
(1)、根据参数获取要恢复的任务的任务控制块,因为不存在恢复正在运行的任务这种情况
所以参数也不可能为NULL(你强行给个为NULL 的参数那也没办法),这里也就不需要使用函
数prvGetTCBFromHandle()来获取要恢复的任务控制块,prvGetTCBFromHandle()会处理参数为
NULL 这种情况。
(2)、任务控制块不能为NULL 和pxCurrentTCB,因为不存在说恢复当前正在运行的任务。
(3)、调用函数taskENTER_CRITICAL()进入临界段
(4)、调用函数prvTaskIsTaskSuspended()判断要恢复的任务之前是否已经被挂起了,恢复的
肯定是被挂起的任务,没有挂起就不用恢复。
(5)、首先将要恢复的任务从原来的列表中删除,任务被挂起以后都会放到任务挂起列表
xSuspendedTaskList 中。
(6)、将要恢复的任务添加到就绪任务列表中。
(7)、要恢复的任务优先级高于当前正在运行的任务优先级。
(8) 、因为要恢复的任务其优先级最高,所以需要调用函数
taskYIELD_IF_USING_PREEMPTION()来完成一次任务切换。
(9)、调用函数taskEXIT_CRITICAL()退出临界区。
第九章FreeRTOS 任务切换
RTOS 系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执
行顺序,任务切换效率的高低也决定了一款系统的性能,尤其是对于实时操作系统。而对于想
深入了解FreeRTOS 系统运行过程的同学其任务切换是必须掌握的知识点。本章我们就来学习
一下FreeRTOS 的任务切换过程,本章分为如下几部分:
9.1 PendSV 异常
9.2 FreeRTOS 任务切换场合
9.3 PendSV 中断服务函数
9.4 查找下一个要运行的任务
9.1 PendSV 异常
本小节参考自《权威指南》的“第10 章OS 支持特性”的第10.4 小节。
PendSV(可挂起的系统调用)异常对OS 操作非常重要,其优先级可以通过编程设置。可以
通过将中断控制和壮态寄存器ICSR 的bit28,也就是PendSV 的挂起位置1 来触发PendSV 中
断。与SVC 异常不同,它是不精确的,因此它的挂起壮态可在更高优先级异常处理内设置,且
会在高优先级处理完成后执行。
利用该特性,若将PendSV 设置为最低的异常优先级,可以让PendSV 异常处理在所有其他
中断处理完成后执行,这对于上下文切换非常有用,也是各种OS 设计中的关键。
在具有嵌入式OS 的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任
务,这两个任务会交替执行,如图9.1.1 所示:
上下文切换被触发的场合可以是:
⚫ 执行一个系统调用
⚫ 系统滴答定时器(SysTick)中断。
在OS 中,任务调度器决定是否应该执行上下文切换,如图9.1.1 中任务切换都是由SysTick
中断中执行,每次它都会决定切换到一个不同的任务中。
若中断请求(IRQ)在SysTick 异常前产生,则SysTick 异常可能会抢占IRQ 的处理,在这种
情况下,OS 不应该执行上下文切换,否则中断请求IRQ 处理就会被延迟,而且在真实系统中
延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。对于Cortex-M3 和Cortex-M4 处理器,当存在活跃的异常服务时,设计默认不允许返回到线程模式,若存在
活跃中断服务,且OS 试图返回到线程模式,则将触发用法fault,如图9.1.2 所示。
图9.1.2 ISR 执行期间的上下文切换会延迟中断服务
在一些OS 设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时可
以检查栈帧中的压栈xPSR 或NVIC 中的中断活跃壮态寄存器。不过,系统的性能可能会受到
影响,特别时当中断源在SysTick 中断前后持续产生请求时,这样上下文切换可能就没有执行
的机会了。
为了解决这个问题,PendSV 异常将上下文切换请求延迟到所有其他IRQ 处理都已经完成
后,此时需要将PendSV 设置为最低优先级。若OS 需要执行上下文切换,他会设置PendSV 的
挂起壮态,并在PendSV 异常内执行上下文切换。如图9.1.3 所示:
图9.1.3 中事件的流水账记录如下:
(1) 任务A 呼叫SVC 来请求任务切换(例如,等待某些工作完成)
(2) OS 接收到请求,做好上下文切换的准备,并且pend 一个PendSV 异常。
(3) 当CPU 退出SVC 后,它立即进入PendSV,从而执行上下文切换。
(4) 当PendSV 执行完毕后,将返回到任务B,同时进入线程模式。
(5) 发生了一个中断,并且中断服务程序开始执行
(6) 在ISR 执行过程中,发生SysTick 异常,并且抢占了该ISR。
(7) OS 执行必要的操作,然后pend 起PendSV 异常以作好上下文切换的准备。
(8) 当SysTick 退出后,回到先前被抢占的ISR 中,ISR 继续执行
(9) ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换。
(10) 当PendSV 执行完毕后,回到任务A,同时系统再次进入线程模式。
讲解PendSV 异常的原因就是让大家知道,FreeRTOS 系统的任务切换最终都是在PendSV
中断服务函数中完成的,UCOS 也是在PendSV 中断中完成任务切换的。
9.2 FreeRTOS 任务切换场合
在9.1 小节中讲解PendSV 中断的时候提到了上下文(任务)切换被触发的场合:
●可以执行一个系统调用
●系统滴答定时器(SysTick)中断。
9.2.1 执行系统调用
执行系统调用就是执行FreeRTOS 系统提供的相关API 函数,比如任务切换函数taskYIELD(),
FreeRTOS 有些API 函数也会调用函数taskYIELD(),这些API 函数都会导致任务切换,这些API 函
数和任务切换函数taskYIELD()都统称为系统调用。函数taskYIELD()其实就是个宏,在文件task.h
中有如下定义:
#define taskYIELD() portYIELD()
函数portYIELD()也是个宏,在文件portmacro.h 中有如下定义:
#define portYIELD() \
{ \portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ (1)\__dsb( portSY_FULL_READ_WRITE ); \__isb( portSY_FULL_READ_WRITE ); \
}
(1)、通过向中断控制和壮态寄存器ICSR 的bit28 写入1 挂起PendSV 来启动PendSV 中断。
这样就可以在PendSV 中断服务函数中进行任务切换了。
中断级的任务切换函数为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()来完成任务切换的。
9.2.2 系统滴答定时器(SysTick)中断
FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务
函数如下:
void SysTick_Handler(void)
{if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行{xPortSysTickHandler();}
}
在滴答定时器中断服务函数中调用了FreeRTOS 的API 函数xPortSysTickHandler(),此函数
源码如下:
void xPortSysTickHandler( void )
{vPortRaiseBASEPRI(); (1){if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器xTickCount 的值{portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (2)}}vPortClearBASEPRIFromISR(); (3)
}
(1)、关闭中断
(2)、通过向中断控制和壮态寄存器ICSR 的bit28 写入1 挂起PendSV 来启动PendSV 中
断。这样就可以在PendSV 中断服务函数中进行任务切换了。
(3)、打开中断。
9.3 PendSV 中断服务函数
前面说了FreeRTOS 任务切换的具体过程是在PendSV 中断服务函数中完成的,本节我们就
来学习一个PendSV 的中断服务函数,看看任务切换过程究竟是怎么进行的。PendSV 中断服务
函数本应该为PendSV_Handler(),但是FreeRTOS 使用#define 重定义了,如下:
#define xPortPendSVHandler PendSV_Handler
函数xPortPendSVHandler()源码如下:
__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8mrs r0, psp (1)isbldr r3, =pxCurrentTCB (2)ldr r2, [r3] (3)stmdb r0!, {r4-r11, r14} (4)str r0, [r2] (5)stmdb sp!, {r3,r14} (6)mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (7)msr basepri, r0 (8)dsbisbbl vTaskSwitchContext (9)mov r0, #0 (10)msr basepri, r0 (11)ldmia sp!, {r3,r14} (12)ldr r1, [r3] (13)ldr r0, [r1] (14)ldmia r0!, {r4-r11} (15)msr psp, r0 (16)isbbx r14 (17)nop
}
(1)、读取进程栈指针,保存在寄存器R0 里面。
(2)和(3),获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器R2 里面。
(4)、保存r4~r11 和R14 这几个寄存器的值。
(5)、将寄存器R0 的值写入到寄存器R2 所保存的地址中去,也就是将新的栈顶保存在任务
控制块的第一个字段中。此时的寄存器R0 保存着最新的堆栈栈顶指针值,所以要将这个最新
的栈顶指针写入到当前任务的任务控制块第一个字段,而经过(2)和(3)已经获取到了任务控制块,
并将任务控制块的首地址写如到了寄存器R2 中。
(6)、将寄存器R3 和R14 的值临时压栈,寄存器R3 中保存了当前任务的任务控制块,而
接下来要调用函数vTaskSwitchContext(),为了防止R3 和R14 的值被改写,所以这里临时将R3
和R14 的值先压栈。
(7)和(8)、关闭中断,进入临界区
(9)、调用函数vTaskSwitchContext() ,此函数用来获取下一个要运行的任务,并将
pxCurrentTCB 更新为这个要运行的任务。
(10)和(11)、打开中断,退出临界区。
(12)、刚刚保存的寄存器R3 和R14 的值出栈,恢复寄存器R3 和R14 的值。注意,经过(12)
步,此时pxCurrentTCB 的值已经改变了,所以读取R3 所保存的地址处的数据就会发现其值改
变了,成为了下一个要运行的任务的任务控制块。
(13)和(14)、获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器R0 中。
(15)、R4~R11,R14 出栈,也就是即将运行的任务的现场。
(16)、更新进程栈指针PSP 的值。
(17)、执行此行代码以后硬件自动恢复寄存器R0~R3、R12、LR、PC 和xPSR 的值,确定
异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。
很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器PC 值会被恢复为即将运行的
任务的任务函数,新的任务开始运行!至此,任务切换成功。
9.4 查找下一个要运行的任务
在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();}
}
(1)、如果调度器挂起那就不能进行任务切换。
(2)、调用函数taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务。
taskSELECT_HIGHEST_PRIORITY_TASK()本质上是一个宏,在tasks.c 中有定义。
FreeRTOS 中查找下一个要运行的任务有两种方法:一个是通用的方法,另外一个就是使用
硬件的方法,这个在我们讲解FreeRTOSCofnig.h 文件的时候就提到过了,至于选择哪种方法通
过宏configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为1 的时候就使
用硬件的方法,否则的话就是使用通用的方法,我们来看一下这两个方法的区别。
1、通用方法
顾名思义,就是所有的处理器都可以用的方法,方法如下:
#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; \
}
(1)、在前面的8.2.4 小节中说了pxReadyTasksLists[]为就绪任务列表数组,一个优先级一个
列表,同优先级的就绪任务都挂到相对应的列表中。uxTopReadyPriority 代表处于就绪态的最高
优先级值,每次创建任务的时候都会判断新任务的优先级是否大于uxTopReadyPriority,如果大
于的话就将这个新任务的优先级赋值给变量uxTopReadyPriority。函数prvAddTaskToReadyList()
也会修改这个值,也就是说将某个任务添加到就绪列表中的时候都会用uxTopReadyPriority 来
记录就绪列表中的最高优先级。这里就从这个最高优先级开始判断,看看哪个列表不为空就说
明哪个优先级有就绪的任务。函数listLIST_IS_EMPTY()用于判断某个列表是否为空,
uxTopPriority 用来记录这个有就绪任务的优先级。
(2)、已经找到了有就绪任务的优先级了,接下来就是从对应的列表中找出下一个要运行的
任务,查找方法就是使用函数listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列
表项,然后将获取到的列表项所对应的任务控制块赋值给pxCurrentTCB,这样我们就确定了下
一个要运行的任务了。
可以看出通用方法是完全通过C 语言来实现的,肯定适用于不同的芯片和平台,而且对于
任务数量没有限制,但是效率肯定相对于使用硬件方法的要低很多。
2、硬件方法
硬件方法就是使用处理器自带的硬件指令来实现的,比如Cortex-M 处理器就带有的计算前
导0 个数指令:CLZ,函数如下:
#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 ] ) ); \
}
(1) 、通过函数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 的个数,如下例子:
二进制数1000 0000 0000 0000 的前导零个数就为0。
二进制数0000 1001 1111 0001 的前导零个数就是4。
得到uxTopReadyPriority 的前导零个数以后在用31 减去这个前导零个数得到的就是处于就
绪态的最高优先级了,比如优先级30 为此时的处于就绪态的最高优先级,30 的前导零个数为
1,那么31-1=30,得到处于就绪态的最高优先级为30。
(2)、已经找到了处于就绪态的最高优先级了,接下来就是从对应的列表中找出下一个要运
行的任务,查找方法就是使用函数listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一
个列表项,然后将获取到的列表项所对应的任务控制块赋值给pxCurrentTCB,这样我们就确定
了下一个要运行的任务了。
可以看出硬件方法借助一个指令就可以快速的获取处于就绪态的最高优先级,但是会限制
任务的优先级数,比如STM32 只能有32 个优先级,不过32 个优先级已经完全够用了。要知道
FreeRTOS 是支持时间片的,每个优先级可以支持无限多个任务。
9.6 FreeRTOS 时间片调度
前面多次提到FreeRTOS 支持多个任务同时拥有一个优先级,这些任务的调度是一个值得
考虑的问题,不过这不是我们要考虑的。在FreeRTOS 中允许一个任务运行一个时间片(一个时
钟节拍的长度)后让出CPU 的使用权,让拥有同优先级的下一个任务运行,至于下一个要运行
哪个任务?在9.4 小节里面已经分析过了,FreeRTOS 中的这种调度方法就是时间片调度。图
9.6.1 展示了运行在同一优先级下的执行时间图,在优先级N 下有3 个就绪的任务。
1、任务3 正在运行。
2、这时一个时钟节拍中断(滴答定时器中断)发生,任务3 的时间片用完,但是任务3 还
没有执行完。
3、FreeRTOS 将任务切换到任务1,任务1 是优先级N 下的下一个就绪任务。
4、任务1 连续运行至时间片用完。
5、任务3 再次获取到CPU 使用权,接着运行。
6、任务3 运行完成,调用任务切换函数portYIELD()强行进行任务切换放弃剩余的时间片,
从而使优先级N 下的下一个就绪的任务运行。
7、FreeRTOS 切换到任务1。
8、任务1 执行完其时间片。
要使用时间片调度的话宏configUSE_PREEMPTION 和宏configUSE_TIME_SLICING 必须
为1。时间片的长度由宏configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的
中断周期,比如本教程中configTICK_RATE_HZ 为1000,那么一个时间片的长度就是1ms。时
间片调度发生在滴答定时器的中断服务函数中,前面讲解滴答定时器中断服务函数的时候说了
在中断服务函数SysTick_Handler()中会调用FreeRTOS 的API 函数xPortSysTickHandler(),而函
数xPortSysTickHandler() 会引发任务调度,但是这个任务调度是有条件的,函数
xPortSysTickHandler()如下:
void xPortSysTickHandler( void )
{vPortRaiseBASEPRI();{if( xTaskIncrementTick() != pdFALSE ){portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;}}vPortClearBASEPRIFromISR();
}
上述代码中红色部分表明只有函数xTaskIncrementTick()的返回值不为pdFALSE 的时候就
会进行任务调度!查看函数xTaskIncrementTick()会发现有如下条件编译语句:
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;
}
(1)、当宏configUSE_PREEMPTION 和宏configUSE_PREEMPTION 都为1 的时候下面的
代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为1,缺一不可!
(2)、判断当前任务所对应的优先级下是否还有其他的任务。
(3)、如果当前任务所对应的任务优先级下还有其他的任务那么就返回pdTRUE。
从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数
xTaskIncrementTick() 就会返回pdTURE ,由于函数返回值为pdTURE 因此函数
xPortSysTickHandler()就会进行一次任务切换。
9.6 时间片调度实验
9.6.1 实验程序设计
1、实验目的
学习使用FreeRTOS 的时间片调度。
2、实验设计
本实验设计三个任务:start_task、task1_task 和task2_task ,其中task1_task 和task2_task
的任务优先级相同,都为2,这三个任务的任务功能如下:
start_task:用来创建其他2 个任务。
task1_task :控制LED0 灯闪烁,并且通过串口打印task1_task 的运行次数。
task2_task :控制LED1 灯闪烁,并且通过串口打印task2_task 的运行次数。
3、实验工程
FreeRTOS 实验9-1 FreeRTOS 时间片调度。
4、实验程序与分析
●系统设置
为了观察方便,将系统的时钟节拍频率设置为20,也就是将宏configTICK_RATE_HZ 设置
为20:
#define configTICK_RATE_HZ (20)
这样设置以后滴答定时器的中断周期就是50ms 了,也就是说时间片值为50ms,这个时间
片还是很大的,不过大一点我们到时候观察的时候方便。
●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define TASK1_TASK_PRIO 2 //任务优先级 (1)
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define TASK2_TASK_PRIO 2 //任务优先级 (2)
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数
(1)和(2)、任务task1_task 和task2_task 的任务优先级设置为相同的,这里都设置为2。
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDLCD_Init(); //初始化LCDPOINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 9-1");LCD_ShowString(30,50,200,16,16,"FreeRTOS Round Robin");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2016/11/25");//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )"start_task", //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_0PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}
在main 函数中我们主要完成硬件的初始化,在硬件初始化完成以后创建了任务start_task
并且开启了FreeRTOS 的任务调度。
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建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 )task2_task,(const char* )"task2_task",(uint16_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )&Task2Task_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{u8 task1_num=0;while(1){task1_num++; //任务1 执行次数加1 注意task1_num1 加到255 的时候会清零!!LED0=!LED0;taskENTER_CRITICAL(); //进入临界区printf("任务1 已经执行:%d 次\r\n",task1_num);taskEXIT_CRITICAL(); //退出临界区//延时10ms,模拟任务运行10ms,此函数不会引起任务调度delay_xms(10); (1)}
}
//task2 任务函数
void task2_task(void *pvParameters)
{u8 task2_num=0;while(1){task2_num++; //任务2 执行次数加1 注意task2_num1 加到255 的时候会清零!!LED1=!LED1;taskENTER_CRITICAL(); //进入临界区printf("任务2 已经执行:%d 次\r\n",task2_num);taskEXIT_CRITICAL(); //退出临界区//延时10ms,模拟任务运行10ms,此函数不会引起任务调度delay_xms(10); (2)}
}
(1)、调用函数delay_xms()延时10ms。在一个时间片内如果任务不主动放弃CPU 使用权的
话那么就会一直运行这一个任务,直到时间片耗尽。在task1_task 任务中我们通过串口打印字
符串的方式提示task1_task 在运行,但是这个过程对于CPU 来说执行速度很快,不利于观察,
所以这里通过调用函数delay_xms()来默认任务占用10ms 的CPU。函数delay_xm()不会引起任
务调度,这样的话相当于task1_task 的执行周期>10ms,基本可以看作等于10ms,因为其他的
函数执行速度还是很快的。一个时间片的长度是50ms,任务执行所需的时间以10ms 算,理论
上在一个时间片内task1_task 可以执行5 次,但是事实上很少能执行5 次,基本上是4 次。
(2)、同(1)。
9.6.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,开发板上电,串口调试助手显示如
图9.6.2.1 所示:
从图9.6.2.1 可以看出,不管是task1_task 还是task2_task 都是连续执行4,5 次,和前面程
序设计的一样,说明在一个时间片内一直在运行一个任务,当时间片用完后就切换到下一个任
务运行。注意,接收到的信息后面显示的时间是串口调试助手统计的接收到数据的时间,并不
是开发板真实的运行时间,这个时间戳值仅供参考。
第十章FreeRTOS 系统内核控制函数
FreeRTOS 中有一些函数只供系统内核使用,用户应用程序一般不允许使用,这些API 函
数就是系统内核控制函数。本章我们就来学习一下这些内核控制函数,本章分为如下几部分:
10.1 内核控制函数预览
10.2 内核控制函数详解
10.1 内核控制函数预览
顾名思义,内核控制函数就是FreeRTOS 内核所使用的函数,一般情况下应用层程序不使
用这些函数,在FreeRTOS 官网可以找到这些函数,如图10.1.1 所示:
这些函数的含义如表10.1.1 所示:
10.2 内核控制函数详解
1、函数taskYIELD()
此函数用于进行任务切换,此函数本质上是一个宏,此函数的详细讲解请参考9.2.1 小节。
2、函数taskENTER_CRITICAL()
进入临界区,用于任务函数中,本质上是一个宏,此函数的详细讲解请参考4.4.1 小节。
3、函数taskEXIT_CRITICAL()
退出临界区,用于任务函数中,本质上是一个宏,此函数的详细讲解请参考4.4.1 小节。
4、函数taskENTER_CRITICAL_FROM_ISR()
进入临界区,用于中断服务函数中,此函数本质上是一个宏,此函数的详细讲解请参考4.4.2
小节。
5、函数taskEXIT_CRITICAL_FROM_ISR()
退出临界区,用于中断服务函数中,此函数本质上是一个宏,此函数的详细讲解请参考4.4.2
小节。
6、函数taskDISABLE_INTERRUPTS()
关闭可屏蔽的中断,此函数本质上是一个宏,此函数的详细讲解请参考4.3 小节。
7、函数taskENABLE_INTERRUPTS()
打开可屏蔽的中断,此函数本质上是一个宏,此函数的详细讲解请参考4.3 小节。
8、函数vTaskStartScheduler()
启动任务调度器,此函数的详细讲解请参考8.3 小节。
9、函数vTaskEndScheduler()
关闭任务调度器,FreeRTOS 中对于此函数的解释如图10.2.1 所示:
可以看出此函数仅用于X86 架构的处理器,调用此函数以后所有系统时钟就会停止运行,
所有创建的任务都会自动的删除掉(FreeRTOS 对此函数的解释是会自动删除所有的任务,但是
在FreeRTOS 的源码中没有找到相关的处理过程,有可能要根据实际情况编写相关代码,亦或
是X86 的硬件会自动处理?笔者不了解X86 架构),多任务性能关闭。可以调用函数
vTaskStartScheduler()来重新开启任务调度器。此函数在文件tasks.c 中有如下定义:
void vTaskEndScheduler( void )
{portDISABLE_INTERRUPTS(); //关闭中断xSchedulerRunning = pdFALSE; //标记任务调度器停止运行vPortEndScheduler(); //调用硬件层关闭中断的处理函数
}
函数vPortEndScheduler()在port.c 中有定义,这个函数在移植FreeRTOS 的时候要根据实际
使用的处理器来编写,此处没有实现这个函数,只是简单的加了一行断言,函数如下:
void vPortEndScheduler( void )
{configASSERT( uxCriticalNesting == 1000UL );
}
10、函数vTaskSuspendAll()
挂起任务调度器,调用此函数不需要关闭可屏蔽中断即可挂起任务调度器,此函数在文件
tasks.c 中有如下定义:
void vTaskSuspendAll( void )
{++uxSchedulerSuspended;
}
可看出,此函数只是简单的将变量uxSchedulerSuspended 加一,uxSchedulerSuspended 是挂
起嵌套计数器,调度器挂起是支持嵌套的。使用函数xTaskResumeAll()可以恢复任务调度器,
调用了几次vTaskSuspendAll()挂起调度器,同样的也得调用几次xTaskResumeAll()才会最终恢
复任务调度器。
假设现在有这样一种情况,任务1 的优先级为10,此时任务1 由于等待队列(关于队列的
知识后面会有专门的章节讲)TestQueue 而处于阻塞态。但是有段其他的代码调用函数
vTaskSuspendAll()挂起了任务调度器,在还没有调用xTaskResumeAll()恢复任务调度器之前,有
个在外部中断发生了,在中断服务程序里面调用函数xQueueSendFromISR()向任务1 发送了队
列TestQueue。如果任务调度器没有阻塞的话函数xQueueSendFromISR()会使任务1 进入就绪
态,也就是将任务1 添加到优先级10 对应的就绪列表pxReadyTasksLists[10]中,这样当任务切
换的时候任务1 就会运行。但是现在任务调度器由于函数vTaskSuspendAll()而挂起,这个时候
任务1 就不是添加到任务就绪列表pxReadyTasksLists[10]中了,而是添加到另一个叫做
xPendingReadyList 的列表中,xPendingReadyList 是个全局变量,在文件tasks.c 中有定义。当调
用函数xTaskResumeAll()恢复调度器的时候就会将挂到列表xPendingReadyList 中的任务重新移
动到它们所对应的就绪列表pxReadyTasksLists 中。
11、函数xTaskResumeAll()
此函数用于将任务调度器从挂起壮态恢复,缩减后的函数代码如下:
BaseType_t xTaskResumeAll( void )
{TCB_t *pxTCB = NULL;BaseType_t xAlreadyYielded = pdFALSE;configASSERT( uxSchedulerSuspended );taskENTER_CRITICAL(); (1){--uxSchedulerSuspended; (2)if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) (3){if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U ){while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE ) (4){pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY\ (5)( ( &xPendingReadyList ) );( void ) uxListRemove( &( pxTCB->xEventListItem ) ); (6)( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (7)prvAddTaskToReadyList( pxTCB ); (8)if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) (9){xYieldPending = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}/************************************************************************//****************************省略部分代码********************************//************************************************************************/if( xYieldPending != pdFALSE ) (10){
#if( configUSE_PREEMPTION != 0 ){xAlreadyYielded = pdTRUE; (11)}
#endiftaskYIELD_IF_USING_PREEMPTION(); (12)}else{mtCOVERAGE_TEST_MARKER();}}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL(); (13)return xAlreadyYielded; (14)
}
(1)、进入临界区。
(2)、调度器挂起嵌套计数器uxSchedulerSuspended 减一。
(3)、如果uxSchedulerSuspended 为0 说明所有的挂起都已经解除,调度器可以开始运行了。
(4)、while()循环处理列表xPendingReadyList,只要列表xPendingReadyList 不为空,说明还
有任务挂到了列表xPendingReadyList 上,这里需要将这些任务从列表xPendingReadyList 上移
除并添加到这些任务所对应的就绪列表中。
(5)、遍历列表xPendingReadyList,获取挂到列表xPendingReadyList 上的任务对应的任务
控制块。
(6)、将任务从事件列表上删除。
(7)、将任务从壮态列表上移除。
(8)、调用函数prvAddTaskToReadyList()将任务添加到就绪列表中。
(9)、判断任务的优先级是否高于当前正在运行的任务,如果是的话需要将xYieldPending 标
记为pdTRUE,表示需要进行任务切换。
(10)、根据(9)得出需要进行任务切换。
(11)、标记在函数xTaskResumeAll()中进行了任务切换,变量xAlreadyYielded 用于标记在
函数xTaskResumeAll()中是否有进行任务切换。
(12)、调用函数taskYIELD_IF_USING_PREEMPTION()进行任务切换,此函数本质上是一
个宏,其实最终调用是通过调用函数portYIELD()来完成任务切换的。
(13)、退出临界区。
(14)、返回变量xAlreadyYielded,如果为pdTRUE 的话表示在函数xTaskResumeAll()中进
行了任务切换,如果为pdFALSE 的话表示没有进行任务切换。
12、函数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 );
}
(1)、函数参数xTicksToJump 是要加上的时间值,系统节拍计数器xTickCount 加上这个时
间值得到新的系统时间。关于xTicksToJump 这个时间值的确定后面在讲解FreeRTOS 的低功耗
模式的时候会详细的讲解。