[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》笔记
[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》笔记
- 第3章 初识 FreeRTOS
- 第5章 裸机系统与多任务系统
- 第6章 数据结构—列表与列表项讲解
- 第7章 任务的定义与任务切换的实现
- 第8章 临界段的保护
- 第9章 空闲任务与阻塞延时的实现
- 第10章 支持多优先级
- 第11章 任务延时列表的实现
- 第12章 支持时间片
- 第16章 任务管理
- 第17章 消息队列
- 第18章 信号量
- 第19章 互斥量
- 第20章 事件
- 第21章 软件定时器
- 第22章 任务通知
- 第23章 内存管理
第3章 初识 FreeRTOS
FreeRTOS学习记录(变量命名规则全解、文件介绍)
1.FreeRTOS 由美国的 Richard Barry 于 2003 年发布,Richard Barry 是 FreeRTOS 的拥有者和维护者,
2.FreeRTOS 于 2018 年被亚马逊收购,改名为 AWS FreeRTOS,版本号升级为 V10,且开源协议也由原来的 GPLv2+修改为 MIT,与 GPLv2+相比,MIT 更加开放,你完全可以理解为是为所欲为的免费。V9 以前的版本还是维持原样,V10 版本相比于 V9 就是加入了一些物联网相关的组件,内核基本不变。
3.当你修改了 FreeRTOS 内核源码的时候,就必须将修改的这部分开源,反馈给社区,其它应用部分不用开源。免费的意思是无论你是个人还是公司,都可以免费地使用,不需要掏一分钱。
4.FreeRTOS 和 OpenRTOS 拥有的代码是一样的,但是可从官方获取到的服务却是不一样的。FreeRTOS 号称免费,OpenRTOS 号称收费,它们的具体区别见表格 3-1。
5.SaveRTOS 也是基于 FreeRTOS,但是 SaveRTOS 为某些特定的领域做了安全相关的设计,有关 SaveRTOS 获得的安全验证具体见表格 3-2。当然,SaveRTOS 也是需要收费,人家这些安全验证可不是白做的。
6.FreeRTOS 的源码和相应的官方书籍均可从官网 www.freertos.org 获得可以下载 FreeRTOS 最新版本的源码。如果想下载以往版本的可从托管网址:https://sourceforge.net/projects/freertos/files/FreeRTOS/,点击 PDF Books 可以下载 FreeRTOS 官方的两本电子书,分别为 FreeRTOSV10.0.0 Reference Manual.pdf 和 Mastering_the_FreeRTOS_Real_Time_Kernel-A_HandsOn_Tutorial_Guide.pdf,一本是 API 参考手册,另外一本是手把手入门教程。不过都是英文的,英文不好的朋友看起来还是有些吃力
7.学习一个 RTOS,搞懂它的编程的风格很重要,这可以大大提供我们阅读代码的效率。下面我们就以 FreeRTOS 里面的数据类型、变量名、函数名和宏这几个方面做简单介绍。
8.在编程的时候,如果用户没有明确指定 char 的符号类型,那么编译器会默认的指定char 型的变量为无符号或者有符号。正是因为这个原因,在 FreeRTOS 中,我们都需要明确的指定变量 char 是有符号的还是无符号的。在 keil 中,默认 char 是无符号的,但是也可以配置为有符号的,具体配套过程见图3-3
9.FreeRTOS 中详细的数据类型重定义在 portmacro.h 这个头文件中实现
10.在 FreeRTOS 中,定义变量的时候往往会把变量的类型当作前缀加在变量上,这样的好处是让用户一看到这个变量就知道该变量的类型。比如 char 型变量的前缀是 c,short 型变量的前缀是 s,long 型变量的前缀是 l, portBASE_TYPE 类型变量的前缀是 x。还有其他的数据类型,比如数据结构,任务句柄,队列句柄等定义的变量名的前缀也是 x。如果一个变量是无符号型的那么会有一个前缀 u,如果是一个指针变量则会有一个前缀 p。因此,当我们定义一个无符号的 char 型变量的时候会加一个 uc 前缀,当定义一个char 型的指针变量的时候会有一个 pc 前缀。
11.函数名包含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数(static定义)则会加一个 prv(private)的前缀。特别的,在函数名中加入了函数所在的文件名,这大大的帮助了用户提高寻找函数定义的效率和了解函数作用的目的
12.宏均是由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义,部分举例具体见表格3-4。
13.这里有个地方要注意的是信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。
第5章 裸机系统与多任务系统
1.轮询系统统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,大概的伪代码具体见代码清单 5-1。轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。
2.相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台,main 函数里面的无限循环我们称为后台
3.相比前后台系统中后台顺序执行的程序主体,在多任务系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为任务。每个任务都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流,不用担心每个功能模块之间是否存在干扰。加入了操作系统,我们的编程反而变得简单了。整个系统随之带来的额外开销就是操作系统占据的那一丁点的 FLASH 和 RAM。现如今,单片机的 FLASH 和 RAM 是越来越大,完全足以抵挡 RTOS 那点开销。
4.无论是裸机系统中的轮询系统、前后台系统和多任务系统,我们不能一锤子的敲定孰优孰劣,它们是不同时代的产物,在各自的领域都还有相当大的应用价值,只有合适才是最好。
模型 | 事件响应 | 事件处理 | 特点 |
---|---|---|---|
轮询系统 | 主程序 | 主程序 | 轮询响应事件,轮询处理事件 |
前后台系统 | 中断 | 主程序 | 实时响应事件,轮询处理事件 |
多任务系统 | 中断 | 任务 | 实时响应事件,实时处理事件 |
第6章 数据结构—列表与列表项讲解
1.双向链表与单向链表的区别就是节点中有两个节点指针,分别指向前后两个节点,其它完全一样。
2.在很多公司的嵌入式面试中,通常会问到链表和数组的区别。
1)链表是通过节点把离散的数据链接成一个表,通过对节点的插入和删除操作从而实现对数据的存取。
2)而数组是通过开辟一段连续的内存来存储数据,这是数组和链表最大的区别。
3)数组的每个成员对应链表的节点,成员和节点的数据类型可以是标准的 C 类型或者是用户自定义的结构体。数组有起始地址和结束地址,而链表是一个圈,没有头和尾之分,但是为了方便节点的插入和删除操作会人为的规定一个根节点。
3.FreeRTOS 中与链表相关的操作均在 list.h 和 list.c 这两个文件中实现
列表与列表项是学习FreeRTOS的基础,在FreeRTOS中有大量应用,所以这一部分必须得理解深入。FreeRTOS-深入分析列表与列表项
第7章 任务的定义与任务切换的实现
1.在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。
2.在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中。
3.默认为 128,单位为字,即 512字节,这也是 FreeRTOS 推荐的最小的任务栈。是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。
4.在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行
5.将栈顶指针向下做 8 字节对齐。在 Cortex-M3(Cortex-M4 或Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑)。
6.配置 PendSV 和 SysTick 的中断优先级为最低。SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。
7.在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址。向量表通常是从内部 FLASH 的起始地址开始存放,那么可知 memory:0x00000000 处存放的就是 MSP 的值。这个可以通过仿真时查看内存的值证实。将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于0x200008DB,与图7-7 查询到的值吻合。将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler的时候,向量表已经初始化完毕,MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。
8.CPS 指令把全局中断打开。为了快速地开关中断, Cortex-M内核 专门设置了一条 CPS 指令,有 4 种用法:
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
9.PRIMASK 和 FAULTMAST 是 Cortex-M内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI。设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。
10.当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示返回 thumb 状态,bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式。
11.portYIELD 的实现很简单,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV中断服务函数,在里面实现任务切换。
12.将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP),因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使用的是 PSP 还是 MSP),因此需要入栈保护。R3 保存的是当前正在运行的任务(准确来说是上文,因为接下来即将要切换到新的任务)的 TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运行函数 vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量,所以为了保险起见,R3 也入栈保护起来。
13.在关中断方面,FreeRTOS 与其它的 RTOS 关中断不同,而是操作BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉(除了硬 FAULT)。
第8章 临界段的保护
1.在Cortex-M内核中,假如有8个优先级,优先级取值范围是0~7,但数值最大的优先级7却代表着最低的逻辑优先级
对于具有 4 位优先级(0-15)的 ARM Cortex-M 微控制器:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY 15
在这个例子中(对cortex来说,0最高):
configMAX_SYSCALL_INTERRUPT_PRIORITY 设置为 5,意味着优先级为5、6、7、…、15的中断可以调用FreeRTOS API函数
configKERNEL_INTERRUPT_PRIORITY 设置为 15,意味着内核的时钟中断具有最低的优先级,允许所有其他中断抢占它。
假设:configMAX_SYSCALL_INTERRUPT_PRIORITY 为5, MCU串口中断优先级被设置为6,那么串口中断就可以调用含有FromISR的函数, 如果Timer中断优先级为4,那么timer中断处理函数就不能调用FromISR
2.在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。
3.不带返回值的关中断函数,不能嵌套,不能在中断里面使用。带返回值的关中断函数,可以嵌套,可以在中断里面使用。
4.临界段用一句话概括就是一段在执行的时候不能被中断的代码段。在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作
5.那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。既然这样,FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制。
6.直接的开关中断方式不支持嵌套调用例子说明
比如下面的例子:
void FunctionA()
{
taskDISABLE_INTERRUPTS(); 关闭中断
FunctionB(); 调用函数B
FunctionC(); 调用函数C
taskENABLE_INTERRUPTS(); 打开中断
}
void FunctionB()
{
taskDISABLE_INTERRUPTS(); 关闭中断
代码
taskENABLE_INTERRUPTS(); 打开中断
}
工程中调用了FunctionA就会出现执行完FunctionB后中断被打开的情况,此时FunctionC将不被保护了。
7.嵌套使用举例
void FunctionB()
{
taskENTER_CRITICAL()
临界段代码
taskEXIT_CRITICAL();
}
void FunctionA()
{
taskENTER_CRITICAL();
FunctionB();
FunctionC();
taskEXIT_CRITICAL();
}
在非中断场合嵌套调用:taskENTER_CRITICAL(); 和 taskEXIT_CRITICAL(); 支持嵌套调用,系统会记录嵌套深度,只有在最外层退出时才会真正恢复中断和调度。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
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();
}
}
在中断场合,带中断保护版本,可以嵌套
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
替代方案:在某些情况下,可以使用其他同步机制(如信号量、互斥锁)来替代临界区,以减少对系统实时性的影响。
在 FreeRTOS 中,对临界段的保护出现在两种场合,一种是在中断场合一种是在非中断场合,
第9章 空闲任务与阻塞延时的实现
1.在FreeRTOS 中,空闲任务是系统在【启动调度器】的时候创建的优先级最低的任务,空闲任 务主体主要是做一些系统内存的清理工作。但是为了简单起见,我们本章实现的空闲任务只是对一个全局变量进行计数。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作。也可以在空闲任务中喂狗(看门狗)。
2.vTaskDelay ()函数
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。
3.修改 vTaskSwitchContext()函数
任务切换。调用 taskYIELD()会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。
4.SysTick 中断服务函数
在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪,如果不为 0 就继续延时。如果一个任务要延时,一开始 xTicksToDelay 肯定不为 0,当 xTicksToDelay 变为0 的时候表示延时结束
第10章 支持多优先级
1.在 FreeRTOS 中,数字优先级越小,逻辑优先级也越小,这与隔壁的 RT-Thread 和 μC/OS 刚好相反。
2.查 找 最 高 优 先 级 的 就 绪 任 务 有 两 种 方 法 , 具 体 由configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏控制,定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法,但是通用方法我们也讲解下。
2.优化的方法,这得益于 Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第一次出现 1 的位的前面的零的个数。
第11章 任务延时列表的实现
1.在 FreeRTOS 中,有一个任务延时列表(实际上有两个,为了方便讲解原理,我们假装合并为一个,其实两个的作用是一样的),当任务需要延时的时候,则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量:xNextTaskUnblockTime 的值。
2.xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定性的方法相比,FreeRTOS 这个 xNextTaskUnblockTime全局变量设计的非常巧妙。
3.任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较,如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器xTickCount 的值,然后进行任务切换。
4.FreeRTOS 定义了两个任务延时列表,当系统时基计数器xTickCount 没有溢出时,用一条列表,当 xTickCount 溢出后,用另外一条列表。么意思?xTimeToWake 等于系统时基计数器 xTickCount 的值加上任务需要延时的时间xTicksToWait。举例:如果当前 xTickCount 的值等于 0xfffffffdUL,xTicksToWait 等于0x03,那么 xTimeToWake = 0xfffffffdUL + 0x03 = 1,显然得出的值比任务需要延时的时间0x03 还小,这肯定不正常,说明溢出了,这个时候需要将任务插入到溢出任务延时列表。xTimeToWake 没有溢出,则将任务插入到正常任务延时列表。
5.那什么情况下就绪列表的链表里面会有多个任务节点?即同一优先级下有多个任务?这个就是我们下一章“支持时间片”要讲的内容。
第12章 支持时间片
1.FreeRTOS 与隔壁的 RT-Thread 和 μC/OS 一样,都支持时间片的功能。所谓时间片就是同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间我们叫时间片。在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只能是一个 tick。与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。其实 FreeRTOS 的这种时间片功能不能说是真正意义的时间片,因为它不能随意的设置时间为多少个 tick,而是默认一个 tick,然后默认在每个 tick 中断周期中进行任务切换而已。
FreeRTOS的学习记录(基础知识)
第16章 任务管理
1.从系统的角度看,任务是竞争系统资源的最小运行单元。FreeRTOS 是一个支持多任务的操作系统。在 FreeRTOS 中,任务可以使用或等待 CPU、使用内存空间等系统资源,并独 立 于 其 它 任 务 运 行 , 任 何 数 量 的 任 务 可 以 共 享 同 一 个 优 先 级 , 如 果 宏configUSE_TIME_SLICING 定义为 1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
2.作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM。
3.FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。
4.就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。挂起态→就绪态:把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
freertos源码分析DAY8 (任务管理)
第17章 消息队列
1.实现了任务接收来自其他任务或中断的不固定长度的消息
2.先进先出原则(FIFO),但是也支持后进先出原则(LIFO)。
3.无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。
第18章 信号量
1.抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。
2.互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。
3.用作同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞,任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态
4.还记得我们经常说的中断要快进快出吗,在 FreeRTOS 中我们用信号量用于同步,任务与任务的同步,中断与任务的同步,可以大大提高效率。
5.可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。
第19章 互斥量
1.本来按照信号量的特性,每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。
2.当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。
3.信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。而 FreeRTOS 提供的互斥量可以通过优先级继承算法,可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。
4.优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。
5.互斥量更适合于:可能会引起优先级翻转的情况。
6.递归互斥量更适用于:任务可能会多次获取互斥量的情况下。这样可以避免同一任务多次递归持有而造成死锁的问题。
7.互斥量的 API 函数实际上都是宏,它使用现有的队列机制这些宏定义在 semphr.h 文件中,如果使用互斥量,需要包含 semphr.h 头文件。所以 FreeRTOS 的互斥量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,
第20章 事件
1.事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。
2.如 果 宏configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就是 16 位的,其中有 8 个位用来存储事件组;而如果宏 configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是32 位 的 , 其 中 有 24 个 位 用来存储 事件组 。
3.每一位代表一个事件,任务通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。事件的“逻辑或”也被称作是独立型同步,指的是任务感兴趣的所有事件任一件发生即可被唤醒;事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。
4.事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。
5.但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。
第21章 软件定时器
1.软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务),比如 vTaskDelay()以及 其它能阻 塞任务运 行的函数 ,
2.FreeRTOS 提供的软件定时器支持单次模式和周期模式
3.软件定时器的定时过程中是极有可能被其它中断所打断,因为软件定时器的执行上下文环境是任务。
第22章 任务通知
1.FreeRTOS 从 V8.2.0 版本开始提供任务通知这个功能,每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为 1 的队列(可以保存一个 32位整数或指针值)。
2.FreeRTOS 提供以下几种方式发送通知给任务 :
1)发送通知给任务, 如果有通知未读,不覆盖通知值。
2)发送通知给任务,直接覆盖通知值。
3)发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用。
4)发送通知给任务,递增通知值,可以当做计数信号量使用。
3.当然,凡是都有利弊,不然的话 FreeRTOS 还要内核的 IPC 通信机制干嘛,消息通知虽然处理更快,RAM 开销更小,但也有以下限制 :
1)只能有一个任务接收通知消息,因为必须指定接收通知的任务。
2)只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。
4.顾名思义,任务通知是属于任务中附带的资源,所以在任务被创建的时候,任务通知也被初始化的,而在分析队列和信号量的章节中,我们知道在使用队列、信号量前,必须先创建队列和信号量,目的是为了创建队列数据结构。
5.只有在任务中可以等待通知,而不允许在中断中等待通知。
6.任务通知是任务控制块的资源,那它也算任务控制块中的成员变量
第23章 内存管理
1.在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。
2.在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。
3.在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:
1)这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。
2)它们的实现可能非常的大,占据了相当大的一块代码空间。
3)他们几乎都不是安全的。
4)它们并不是确定的,每次调用这些函数执行的时间可能都不一样。
5)它们有可能产生碎片。
6)这两个函数会使得链接器配置得复杂。
7)如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。