Morrison.J Android Dev Engineer

FreeRTOS官方指导文档阅读笔记

2019-11-18
Jasper

基于 161204_Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf,可以从FreeRTOS官网下载

本笔记是即时的,一边阅读一边做笔记。它既包含重要的内容和阅读过程中产生的疑问,不包含完整的知识梳理。参考FreeRTOS官网

1. pdf版本说明

This is the 161204 copy which does not yet cover FreeRTOS V9.0.0, FreeRTOS V10.0.0, or low power tick-less operation. Check http://www.FreeRTOS.org regularly for additional documentation and updates to this book. See http://www.FreeRTOS.org/FreeRTOS-V9.html for information on FreeRTOS V9.x.x. See https://www.freertos.org/FreeRTOS-V10.html for information on FreeRTOS V10.x.x. Applications created using FreeRTOS V9.x.x onwards can allocate all kernel objects statically at compile time, removing the need to include a heap memory manager.

2. 序言

不知道为何,现在我读书都喜欢先看看序言,它能让我对一本书有大致的了解,明白能从书中获得哪些知识,以及让我快速地订制读书计划。

2-1. FreeRTOS基本介绍

FreeRTOS以任务为单位,当系统空闲时,也自动调度空闲任务idle task。idle task会进行一些后台系统动作,诸如处理能力计算、后台检查或者将处理器置于低功耗模式。

电源管理方面,freeRTOS还有一个tick-less模式。这个模式比正常的低功耗模式拥有更低的电量消耗,而且低功耗的时间持续更长。

更灵活的中断处理,freeRTOS允许将中断处理服务例程延迟在开发者编写的task application中运行,或者在freeRTOS的后台程序中运行。

多任务处理,混合多任务处理。通过简单的设计就可以让一个持续的、循环的、事件驱动的处理过程在一个application中实现。另外,硬实时和软实时的实现是可以通过选择适当的任何和优先级。

所谓硬实时就是100%满足实时要求,软实时就是基本上满足实时性。当我们将一个要求硬实时的任务的优先级设置为最高级时,那么可以认为这个的确是一个硬实时实现。

2-2. FreeRTOS Features

列举几个比较关心的feature

  1. 非常灵活的任务优先级控制
  2. 非常灵活的轻量级的任务通知机制
  3. 队列
  4. 二进制和计数信号量
  5. 互斥与递归互斥
  6. 软定时器
  7. 事件组
  8. 钩子函数
  9. idle hook function
  10. 栈溢出检查
  11. 跟踪记录
  12. 任务运行时统计信息收集
  13. 通过软件进行管理的中断堆栈

基本都列出来,我发觉都挺有用的,也许是带着项目的心在阅读,与平时的学习还是不大一样。

2-3. 开发实验平台

开发实验平台很多,比如可以选择所支持的一种SOC进行(STM32),也可以选择使用Windows模拟器。

windows模拟器相关,参考 http://www.microsoft.com/express ,它不提供实时性。

3. FreeRTOS源码及工程概览

本章的目的是熟悉freeRTOS的源码结构和Demo,以及熟悉创建一个freeRTOS工程所需要的基本知识。

3-1. 理解FreeRTOS

3-1-1. FreeRTOS port

FreeRTOS支持20多种编译器,并支持超过3种架构的处理器(比如 ARM就是一种处理器架构)。每一种支持的编译器或者处理器都被称作FreeRTOS的一个port,比如GCC-ARM编译器就是一个port,STM32也是一个port,一个是编译器,另一个则是处理器。

3-1-2. 编译

FreeRTOS包含通用代码和port特定代码,每一个port都有对应的demo,而且这些demo都是开箱即用的,无需任何修改就可以编译通过。

3-1-3. FreeRTOSConfig.h

这是一个独立的配置头文件,可以选择性的开启或者关闭一个功能,比如选择某种系统调度策略。它应当放置在application目录下,而不是FreeRTOS系统源码目录。也就是,每一个demo中有一个FreeRTOSConfig.h。

3-1-4. FreeRTOS通用文件

必选文件:

tasks.c, and list.c

可选文件:

  • queue.c
    包含queue和semaphore(信号量)
  • timers.c
    软件时钟,如果application确切使用时钟,则必须加入编译
  • event_groups.c
    事件组,如果确切使用,则必须加入编译
  • croutine.c
    特别针对小型微控制器

3-1-5. FreeRTOS特定port文件

保存在FreeRTOS/Source/portable目录,内部存在一定的等级关系,编译器是上一级目录,处理器则是下一级目录。

三个目录/文件必须添加到编译器 include path中,包括:

  • FreeRTOS/Source/include
  • FreeRTOS/Source/portable/[compiler]/[architecture]
  • FreeRTOSConfig.h

3-1-6. FreeRTOS头文件引用

要使用FreeRTOS API,必须包含FreeRTOS.h,然后就是根据具体的需要加入‘task.h’, ‘queue.h’, ‘semphr.h’, ‘timers.h’ or ‘event_groups.h’。

3-2. 关于Demo与工程创建

FreeRTOS是基于Windows进行开发和编译的,所以,当demo在linux上进行编译的时候,可能会出现一些莫名其妙的警告信息。

参考Demo有很多好处,最基本的就是演示了FreeRTOS工程如何构建,需要添加哪些头文件。还能达到开箱即用的目的,给出了FreeRTOS API的调用样例。

该章节还介绍了如何基于一个Demo创建一个工程,以及如何从零创建一个工程。基于Demo创建就比较简单,而从零创建,则应当先让工程正常编译,而且可以”裸奔”的之后,再加入FreeRTOS支持。

对于V8.x的版本,heap_n.c需要开发者自己添加,n表示不同的动态内存管理策略。 从V9.0.0开始,heap_n.c不需要额外选择添加,而是通过FreeRTOSConfig.h文件进行配置,决定是否开启动态内存管理功能。由此可见,V9.x之后,内存管理策略已经可以通用,无需开发者选择。

3-3. 数据类型与FreeRTOS的编码风格

在微处理器上,数据类型往往不能通用,需要用户自定义两个数据类型;FreeRTOS的编码风格决定了FreeRTOS的API看上去是怎样的,都代表了什么意思,熟悉它,有助于阅读源码以及开发。

3-3-1. 数据类型

每一个port都有一个独立的portmacro.h,里面定义了两种数据类型。

  • TickType_t
    Tick表示一个系统滴答。一个系统滴答来自一个周期性的系统中断,叫Tick中断,两个Tick中断之间的时间就是一个滴答周期。而从系统启动到当前所经历的滴答周期叫滴答数,即Tick count。TickType_t就是保存这个Tick count的数据类型。TickType_t的位宽依赖于配置configUSE_16_BIT_TICKS(0/1),可以是无符号16-bit或者无符号32-bit。以1ms作为一个滴答周期,32-bit的TickType_t可以用8年,10ms则可以用80年,才会溢出。
  • BaseType_t
    基本的数据类型,默认与处理器的位宽等同。由于它非常影响系统性能和使用效率,没有特殊需要不应当修改。

FreeRTOS在使用char时,必定明确指明了有符号还是无符号,而不是默认由编译器决定,因为每个编译器对char的默认处理不尽相同。

3-3-2. 变量名称

FreeRTOS在变量名称前面加上前缀的方式表示变量属于什么类型,这仅仅是一个参考前缀,使得代码更加易于阅读。

‘c’ for char, ‘s’ for int16_t (short), ‘l’ int32_t (long), and ‘x’ for BaseType_t and any other non-standard types.

3-3-3. 函数名称

格式:前缀 + 所属文件名 + 函数功能

举例:xQueueReceive() returns a variable of type BaseType_t and is defined within queue.c.

4. 内存管理

FreeRTOS拥有5种内存管理方式,FreeRTOS V9.x 之后支持静态分配也可以在运行时动态分配。这里仅仅熟悉V8版本的内存管理方式,如有需要,可以参考官网关于V9 V10的更新内容。

FreeRTOS的内存对象都是动态分配的,创建是分配,不使用时删除。这里所说的动态内存分配,与通俗的C概念的动态内存分配存在实质上的差异,通俗的C概念的动态内存分配并不适合real-time应用,由于都是动态,所以,这个说法也适用于FreeRTOS。

通俗C概念的动态内存分配具有以下特点,诸如:超大内存分配、线程安全、每次分配都不是确定的、能对付内存碎片、是的链接器复杂化、很难调试和分析问题。

旧版的FreeRTOS使用内存池的动态内存分配方案,系统将内存预先分成大小不同的内存块,当应用申请时,将合适的块分配给应用。但是,这种方案无法在小RAM的设备上使用,RAM使用率太低。

在阅读下文之前,先了解几个概念:

应用(Application):相对于FreeRTOS的kernel的application,叫应用,严格来说,一个运行中的嵌入式设备,具有一个内核和一个应用。
任务(Tack):应用可以创建一个或者多个任务,既可以动态创建也可以静态创建。
array:本文将整个可用堆内存为array,所有堆内存的分配都在array上进行。
变量内存:变量内存是在编译器链接时决定的,内存地址唯一确定。变量内存与堆内存是相互独立的,而且共同占据了RAM区。

4-1. Heap_1

特点:应用开始去调度任务之前一次性分配,只分配不释放,以任务为单位进行分配,具有确定性。

系统开始只是给内核对象和必要的用于创建任务的行为分配内存,当任务创建时,系统开始调度任务之前,任务所需要的内存都将分配完成。应用在运行过程中,不会释放任何内存,所分配的内容伴随应用的整个生命周期。

即,内存的分配在任务创建时已经决定了。

所有任务都是系统开始调度之前,一致性创建出来,此时内存已经是确定的。

任务的内存分为TCB和Stack,即任务管理和任务栈。

4-2. Heap_2

特点:以任务为单位进行分配,分配的内存可以释放,具有不确定性。

任务在创建时,一次性分配整个任务需要的内存。当任务删除时,一次性删除任务占用的内存。内核会将空闲的内存进行分割,以适应任务的创建。所以,该方案会产生内存碎片。

此方案适用于需要频繁创建和删除任务的业务需求,而且这些任务是重复的任务,前后任务的内存占用是一样的。如果不一样,内存碎片问题将无法控制。

4-3. Heap_3

特点:模拟标准的C概念的内存分配方案,内存大小不是确定的,需要保证线程安全,设备所使用的堆最大值也无法确定且由链接器决定。

该方案模拟标准的malloc和free函数进行动态内存分配,分配具有不确定性,非常容易产生内存碎片。而且需要保证线程安全,保证安全的方式是禁止任务调度。

configTOTAL_HEAP_SIZE配置变得无效,堆大小由链接器根据代码在编译时确定。

4-4. Heap_4

特点:运行时动态分配、自动合并空闲内存

类似前面的分配方式,内核为每一个task分配TCB和Stack。当任务删除后,内核自动合并TCB和Stack为一块自由内存,这一点与heap_2不相同,heap_2不会进行合并。

当有任务继续申请内存时,内核将在自由内存中分配适当大小的内存给任务使用。当任务释放对象时,所动态申请的内存又回归自由内存空间,并自动与相邻的空闲空间合并。

heap_4支持应用指定动态分配内存的起始位置,可以选择是快速的内部RAM,也可以选择慢速的外部RAM。如果不选择,则内存起始地址是由链接器自动安排的。

configAPPLICATION_ALLOCATED_HEAP=1,并且ucHeap必须由应用在源文件中定义。

GCC默认将array的起始地址存放在.my_heap(memory段),而IAR编译器则将其固定为0x20000000。

4-5. Heap_5

特点:策略与heap_4完全相同,可以应用分散的array。

当设备的RAM不是独立一块RAM时,即RAM的地址不具备连续性,整个array由多个地区区间组合而成。此时,Heap_5将作为唯一的可选内存管理方案。

说白了,它能将不同的地址拼接起来,是的内存分配就像在一块连续的array上操作一样。

Heap_5的修正版本,堆的起始地址应当有链接器自动分配。链接器将所有的变量都分配固定的RAM地址,如果变量的RAM地址与heap地址重叠,则程序会跑飞。但是,开发者不知道链接器需要多大的RAM,于是,最佳做法是将heap的起始地址交由链接器决定。如果链接器所使用的RAM超过了RAM最大值,则链接不能成功完成。

诀窍就是使用ucHeap唯一堆结构体的第一个元素,编译器自动将该变量分配RAM,而它又将作为堆内存使用(故意弄重叠,但是我们只给堆使用)。如果ucHeap定义得过大,链接将会失败,于是需要我们在编译阶段动态调整ucHeap的值。

说白了,我们欺骗了链接器,将变量空间当做堆空间使用。

4-6. Heap相关通用函数

xPortGetFreeHeapSize():获取剩余堆大小
xPortGetMinimumEverFreeHeapSize ():获取从未分配过的内存大小
configUSE_MALLOC_FAILED_HOOK:该配置允许应用定义一个堆内存分配失败的回调函数,函数名固定为void vApplicationMallocFailedHook( void );

5. 任务管理

目标:了解任务的创建、运行时间分配、任务的优先级、任务的状态、任务的函数原型等等

5-1. 任务函数原型

void ATaskFunction( void *pvParameters );

该函数承载了任务的所有内容,内部一般是一个无限循环。在函数的尾部,即无限循环的外边,vTaskDelete( NULL );用于删除任务。

在V9.0.0版本之后,任务内存支持静态分配,除非configSUPPORT_DYNAMIC_ALLOCATION is set to 1 in FreeRTOSConfig.h;之前的版本必须将一个heap_n.c编译进来。

5-2. 顶级任务状态

所谓顶级,就是最基本的任务状态。

一个application可以包含多个任务,当只存在一个任务时,很明显,它只需要两个状态,要么是运行,要么是非运行状态。

换入:从非运行 - 运行状态
换出:从运行 - 非运行状态

非运行状态包括:阻塞状态、休眠状态、就绪状态

5-3. 任务的创建

创建任务包含静态创建(>9.0)和动态创建(8.x)

xTaskCreate():动态创建任务
xTaskCreateStatic(): 静态创建任务,并静态分配内存

根据前面关于函数原型的描述,x前缀表示该函数会返回一个BaseType_t类型的数据。

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, 
                        const char * const pcName, 
                        uint16_t usStackDepth, 
                        void *pvParameters, 
                        UBaseType_t uxPriority, 
                        TaskHandle_t *pxCreatedTask );

pvTaskCode、pcName表示任务的主体函数(见任务函数原型)和名称,名称具有最大允许的长度。

usStackDepth 表示任务占据的Stack大小,属于Heap内存的一部分,单位是BaseType_t,最大不超过uint16_t。对于idle任务,configMINIMAL_STACK_SIZE指明了它的默认占用,这也是一个任务最低需求内存。

任务的Stack是开发者人为计算的,如何评估和确定一个任务的Stack大小呢?

pvParameters:转给任务的参数,即传给上面提到的任务函数原型

uxPriority:任务优先级

pxCreatedTask:任务的指针,可以用于在创建任务后设置任务的优先级或者删除任务

任务可以在另一个任务运行时创建。

同一个任务函数可以用于创建多个任务,每个任务的名称不相同。

5-4. 任务优先级

任务优先级决定了任务获得CPU的时间的长短,多个任务可以拥有相同的优先级。

注意:FreeRTOS的任务调度器,在同一时间只会将最高优先级的任务安排运行,如果希望多任务并行,将它们都设为最高优先级吧。
注意:事件驱动型任务除外。

任务最大优先级:configMAX_PRIORITIES compile time configuration constant within FreeRTOSConfig.h

configMAX_PRIORITIES可能会被默认作为到任务的优先级,取决于方法的类型:

Generic Method:
通用方法是指一般的C函数,它的优先级比较高,默认是configMAX_PRIORITIES。 configUSE_PORT_OPTIMISED_TASK_SELECTION = 0,或者未定义,或者Generic Method是唯一的FreeRTOS任务方法。

Architecture Optimized Method:
架构特定函数,是指在特定的处理器架构上有特殊优化的函数。如果有这样的函数存在,configMAX_PRIORITIES的最大值被限定为32.使用架构特定函数,需要将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1.注意:并非所有的处理器都有之类函数。

5-5. 时间测量和滴答中断

滴答中断用于选择任务和任务切换

configTICK_RATE_HZ 需要在FreeRTOSConfig.h进行定义,比如100Hz就是10毫秒。

当频率小于1KHZ时,FreeRTOS提供pdMS_TO_TICKS()函数来转换某个时间长度为滴答数,比如计算200ms是多少滴答数,可以TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );

5-6. 非运行状态

FreeRTOS任务调度器只会调度最高优先级任务,对于低级任务,只有通过事件驱动才能得到调度。

并不是所有的非运行状态的任务都有可能被FreeRTOS任务调度器自动调度,能被任务调度器自动调度的任务必须是就绪态且优先级是最高的任务。
如果低优先级任务期望获得调度机会,则必须重写为事件驱动型任务。

5-6-1. 阻塞状态

当一个任务在等待事件时,将进入阻塞状态。这种任务称为事件驱动型任务,它能实现真正的不同优先级多任务并行执行。
事件驱动有两种类型,一是时间驱动,比如任务在等待一个10ms的时间,时间达到时立即转换为就绪态。二是同步事件驱动,任务在等待一个事件的到来,事件到来后立即转换为就绪态。

阻塞状态下的任务不会被FreeRTOS任务调度器主动调度,即使它是最高优先级。

同步事件包括:
queues, binary semaphores, counting semaphores, mutexes, recursive mutexes, event groups and direct to task notifications。

一个正在运行的任务,随时可以将自己设置为阻塞状态。

5-6-2. 休眠状态

休眠状态也是不会被主动调度的,必须通过vTaskSuspend()函数进入,也必须通过vTaskResume() or xTaskResumeFromISR()函数退出该状态。

5-6-3. 就绪态

FreeRTOS任务调度器总是工作于就绪态和运行中的任务,来回切换它们。

5-7. 延时函数

vTaskDelayUntil():严格的定时函数,单位是滴答数。它可以指定从某个滴答开始,后续的起始滴答系统自动设定为当前滴答。(慎用)
vTaskDelay:一般的延时函数,延时周期从调用函数开始,至经历了指定的滴答数结束。

如果画一张图更好理解,但是我懒。

5-8. 任务调度的完整性概述

任务调度发生在:1. 事件驱动;2.滴答产生。

问题:如果事件驱动导致任务调度,事件还未处理完成,此时,滴答到来,是否立即调度其它任务?

验证结果:是的,系统只保证事件传到到任务,并调度。但是不保证时间任务的运行时间。

实践经验:尽量将重要任务的优先级设置为较高优先级。

5-9. 空闲任务与空闲任务hook

vTaskStartScheduler()一旦调用后,系统至少应当有一个任务在运行。当application创建的所有任务都处于阻塞状态时(或者其它不可被调度状态)时,idle task就是唯一一个task。idle task优先级最低,且是0.

idle task是否必定会占用一个滴答呢,当有其它任务处于就绪态时,应当立即换出idle task。

V8.0立即调度;V9.x中,当configIDLE_SHOULD_YIELD=1,表示无需等待tick结束,立即退出idle task,直接调度就绪态任务

配置 configUSE_IDLE_HOOK=1 表示使能空闲任务钩子函数。

可以为idle task设置hook函数,当该任务运行时,自动调用hook函数。

hook函数不应当是死循环,不应当阻塞和休眠。

5-10. 修改任务的优先级

任务的外部和内部都可以修改优先级。

任务内部修改优先级,必定是任务获得运行时进行修改。获得运行机会可以是最高优先级被调度器自动调度,或者是事件驱动型任务被调度。

Q:在外部修改一个就绪态的低优先级任务为最高优先级任务,且比当前正在运行的任务的优先级还要高,则它会不会立即被调度,而无需等到tick结束? A:是的,将会立即被调度,无需等待tick结束

经验预测:保留最高优先级不被任何任务设置,当我们需要一个事件驱动的任务在事件发生后,正在处理的任务不被打断,可以通过设置优先级为最高优先级,处理业务完成后,将优先级设置会原来的较低优先级。

5-11. 线程本地存储

本pdf没有说明,参考https://www.freertos.org/thread-local-storage-pointers.html

线程私有的本地变量,比如errno。

创建线程私有本地变量需要使用特定的结构体,特定的API.

5-12. 调度策略

在这里,总结性地了解调度策略,调度细节。

5-12-1. Fixed Priority Pre-emptive Scheduling with Time Slicing

翻译为:基于时间片的固定优先级抢占式调度策略

优先级是固定的(Fixed Priority),无法改变自身和其它任务的优先级。

抢占式(Pre-emptive),表示一旦有更高优先级的任务进入就绪态,则立即换出当前正在运行的任务。

基于时间片,表示对于同等优先级的任务,系统平均分配处理器时间,每一个任务获得时间片是尽可能相等。

另外,对于idle task,一旦进入idle task,当有新任务到来时,是等待当前时间片用完还是立即换出新的任务。它由configIDLE_SHOULD_YIELD配置值确定。

5-12-2. Prioritized Pre- emptive Scheduling (without Time Slicing )

非基于时间片的固定优先级抢占式调度策略,与前面的基于时间片的策略类似,只是这里的策略不是基于时间片的。

不基于时间片,意外着,一旦一个高优先级任务进入运行状态,除非有更高优先级的任务进入就绪态,否则,系统不会换出当前执行中的任务。

经验预测:这种策略一般使用于每个任务都具有不同优先级的情况。

5-12-3. Co- operative Scheduling

合作式调度策略。

该策略中,运行中的任务不会被抢占,除非任务进入阻塞状态或者主动释放执行权。所以,时间片策略在此是没有意义的。
当一个运行中的任务放弃执行权或者阻塞时,就绪态队列中的任务,高优先级的进入运行状态。

6. 队列管理器

队列相关知识包括,task-to-task, task-to-interrupt, and interrupt-to-task。本节只涉及task-to-task,其它在中断部分章节涉及。

6-1. 队列的特点

队列可以存储固定大小的数据项。

队列的长度是在创建的时候就决定下来了。

队列采用FIFO的管理方式。

可以往队列后端添加项,也可以将项插入到队列的前端,并覆盖前端原有的数据项。

队列的数据流是以二进制传输的。

将数据放入队列采用复制的方式,意味着数据是真实存在于队列中。
可以将数据或者指针放入队列,但如果是放指针,则应当避免操作野指针。
FreeRTOS总是以最高权限运行任务,所以不存在指针到了另一个任务中无权读取数据的情况。

队列可以有多个读写者,常见的方式是多个写者,一个读者。

6-2. 队列阻塞读

任务在等待一个队列可读时阻塞,读取过程中阻塞。如果等待的时间超时,任务退出阻塞状态。

如果存在多个读者,当队列可读时,高优先级的任务获得读取权,其它任务继续阻塞。如果相同优先级,则等待时间最长的那个任务获得读取权。

6-3. 队列阻塞写

类似读,写也有超时。写时阻塞。多任务写时,高优先级获得写权,相同优先级,则等待时间最长的那个任务获得写入权。

6-4. 多队列阻塞

FreeRTOS支持等待多个队列,这些队列通过一个Set保存。

6-5. 使用队列

xQueueSendToFront() or xQueueSendToBack()

中断中使用:xQueueSendToFrontFromISR() and xQueueSendToBackFromISR()

经验:当队列读取任务的优先级高于写入任务的优先级,则任务最多拥有一个item。因为队列一旦可读,高优先级任务将获得写权限,被系统调度。
同时,写任务可以不设置写超时时间,因为一旦写任务获得运行权,说明读任务已经将数据读走,队列此时一定是空的。

6-6. 使用队列读取多个数据源

队列可以用于从多个数据源读取数据,比如HMI、UART、CAN bus、其它任务等等。

可以设置读写方的优先级不相同,如果读方优先级高,则写入到队列中的数据将立即被读走。如果写入方的优先级较高,则只有当没有数据正在写入,或者队列已满时,读取方才会读取一个item的数据,这种情况下,当写入方很频繁时,队列总是满的。

6-7. 使用队列传递大数据

大数据无法直接放进队列,可以给队列传递指针,需要处理数据同步问题。

比如网络数据,就需要用到大数据指针,将其存到队列中。当network接口接收到网络数据时,会向TCP/IP task发出任务通知,并告知任务接收到的数据保存位置的首地址,以及数据的大小。

6-8. 从多个队列中接收数据

从多个队列接收数据的效率是较低的,如果不是特别的需求,不应当滥用该功能。

从多个队列接收数据,一般发生在这样的场景。从第三方集成的模块,它们都内定了一个自定义的队列,我们需要从这个队列中接收数据,当有多个这样的第三方模块时,我们就有必要从多个队列中接收数据。此时使用队列集合就显得特别方便。

开启队列集合功能:configUSE_QUEUE_SETS=1

队列集合既可以添加队列,也可以添加信号量,队列集合保存它们的指针。

由于队列集合可以保存不同数据类型的队列,当我们从队列集合中获得可读队列的指针时,应当判断该队列的数据类型,判断方法很简单,将获得队列与先前加入队列集合的队列进行==判断即可。

6-9. 邮箱

使用队列可以实现邮箱机制,当一个队列在读取的时候不会自动删除队列中的内容,并且队列可以被所有任务读取,它就成为一个邮箱。

7. 软件定时器管理器

相对于处理器的定时器,是由任务实现的定时器。

软件定时器是可选的,configUSE_TIMERS宏控制。

定时器需要回调函数来定时执行。void ATimerCallback( TimerHandle_t xTimer );
回调函数执行在特定任务(定时器后台任务)的上下文,要求尽执行时间可能短,且不能进入阻塞状态。

类别:一次性定时器,自动装载定时器。

7-1. 定时器上下文

回调函数运行在定时器后台任务上下文中(timer service task),这是一个系统任务,不应当阻塞这个任务。

这个后台任务是在调度器开始工作之前由系统自动创建的,任务的优先级和Stack深度可配置:configTIMER_TASK_PRIORITY and configTIMER_TASK_STACK_DEPTH。

定时器命令队列是提供其它向定时器传输命令的队列,诸如开始定时器、停止定时器、重置定时器。队列的长度是可以定制的,configTIMER_QUEUE_LENGTH。开发者不需要特别的往该队列传递命令,而是由FreeRTOS API自动完成。

定时器后台任务的调度,与其它任务的调度是一样的,由于的优先级可以配置,开发者应当考虑它是否配置为较高的优先级。

7-2. 定时器操作相关

定时器ID是定时器的唯一标识符,可以通过API更改或者获取定时器ID,而且该API不是通过定时器命令队列,而是直接调用到定时器任务上下文。

这是合理的,当前任务可以调用API访问定时器,说明定时器任务处于非运行状态,修改它的上下文是可行的。

可以动态修改定时的定时周期。

8. 中断管理器

硬件中断会抢占CPU时间,任务只能在没有ISR的时候运行,而且没有任何的方式可以让任务抢占ISR。

关键字:

ISR:Interrupt Service Routine,中断服务例程

8-1. 从ISR中调用FreeRTOS API

FreeRTOS提供了两套API,其中带FromISR后缀的API表示可以再在ISR中安全调用的API。

至于为什么这么做,就是为了简单。明确限定一个函数是在ISR中调用可以避免诸如:是否处于ISR的判断、函数变量有效性、决定使用哪种上下文。

缺点也非常明显:FreeRTOS拥有两套API,而且ISR API有限,某些可能需要的API无法在ISR中调用。该问题往往存在于集成第三方库时遇到。

解决方案:

  • 将中断延迟到一个task中执行,那么中断中的调用都是任务上下文,而不是ISR上下文。
  • 如果所在的port支持中断嵌套,则统一使用ISR API。因为ISR API也可以在任务上下文使用,反之则不行。
  • 第三方库往往使用的FreeRTOS抽象层的API,我们可以测试这些抽象的API在第三方库中实际是运行在任务上下文还是ISR上下文,根据情况改成特定上下文的API即可。

8-1-1. The xHigherPriorityTaskWoken Parameter

可选参数,不用设置为NULL;若使用,默认值为pdFALSE。

当上下文切换是由中断引发的,中断发生前的上下文与中断返回后的上下可能不是同一个任务的上下文。
有的API可以将某个任务变成就绪态,比如xQueueSendToBack(),可以使得接收端的任务变成就绪态。
此时,高优先级的任务会被换入,获得执行权,具体策略依赖于这些API是从任务上下文调用还是从中断上下文调用:

  • 从任务上下文调用
    如果configUSE_PREEMPTION=1,立即切换上下文
  • 从中断上下文调用
    不会立即切换上下文,且pxHigherPriorityTaskWoken参数会被置位,提醒开发者上下文应当发生切换

在中断上下文不发生上下文切换的理由:

  • 避免资源浪费,比如UART中断,每个字节切换一次和接收到一串字符再中断一次存在巨大性能差异
  • 提供开发者可控的上下文切换,中断是随机不可控的,但是上下文切换可以
  • 可移植性强,在所有port都可通用
  • 高效,可以在ISR调用多个FreeRTOS API;兼容某些只能在ISR尾部切换上下文的处理器

8-1-2. The portYIELD_FROM_ISR() and portEND_SWITCHING_ISR() Macros

这两个宏是用于在ISR请求上下文切换的,它依赖上文的xHigherPriorityTaskWoken作为参数,如果参数为pdTRUE,则可能执行上下文切换,并切换到正处于运行状态的任务上下文,即使这个上下文已经不是进入中断前的上下文。(后面半句有点多余,切换到任务上下文就完事。)

8-2. 延迟中断处理过程

类似Linux的中断下半部,使得ISR仅消耗尽可能短的时间。

这样做的好处有很多,比如:系统能更快地响应其它中断、task中可以使用所有FreeRTOS API、避免更多的中断嵌套的发生。

一旦将中断处理过程延迟到任务中执行,目标任务的优先级影响到处理程序的及时性,优先级越高,越及时。

到底需要不要延迟中断处理,主要看紧迫程度。而有的行为是禁止在ISR中执行的,比如申请内存,打印log到交互终端。

8-3. 使用Binary Semaphores进行同步

Binary Semaphores(二值信号量)用于同步ISR和task,task一般等待一个Binary Semaphores,而在ISR中将Binary Semaphores传递给任务。如果希望该处理过程快速得到执行,可以给这个任务设置较高的优先级。

8-4. Counting Semaphores

类似二值信号量,计数信号量中的值是会不断增加的,每’take’一次减少1,直到减少为0,表示信号量中没有信号。

一般用于资源统计,当不为0表示资源可用,且表明了资源的数量;等于0则表示没有资源(资源以及用完了)。

8-5. 推迟中断处理工作到FreeRTOS后台任务

开发者既可以为每一个中断创建一个处理任务,也可以将延迟的中断处理放到FreeRTOS的一个后台任务中进行集中处理。

其实这个后台任务就是定时器管理器,将任务推到定时器管理器,并安排立即执行。相关的函数与添加定时器类似。

8-6. 在ISR中使用队列

开发者可以在ISR中发送events或者数据到队列中,且是发往任务。

队列是ISR往任务传递数据的好帮手,但是当数据过于频繁时也存在缺点。

FreeRTOS工程中的很多例子都在中断中使用了队列来接收UART数据,这不是一个搞笑的设计,在实际的工程中应当避免这样的用法。

更佳的设计方案:

  • 使用DMA来接收数据,该功能不会产生软件开销,一次传输完成后任务会收到通知
  • 将每次接收到的单个字符保存到线程安全RAM空间,它是FreeRTOS+TCP的一部分
  • 将接收到的字符串直接在ISR中处理,并保存到队列中

FreeRTOS+ 是FreeRTOS的拓展功能

8-7. 中断嵌套

中断嵌套中,最重要的中断优先级,以及中断与任务的调度关系。

硬件确定了那个ISR会投入运行,而软件则决定了哪个任务会投入运行。任务无法抢占ISR。

中断嵌套功能可以通过配置 configMAX_SYSCALL_INTERRUPT_PRIORITY and configMAX_API_CALL_INTERRUPT_PRIORITY 来获得支持。

configMAX_SYSCALL_INTERRUPT_PRIORITY:最高的线程安全FreeRTOS API可被调用的中断优先级,更高优先级的中断则无法调用FreeRTOS API。

configKERNEL_INTERRUPT_PRIORITY:tick中断的优先级,它应当尽量低。

如果一个FreeRTOS port没有定义configMAX_SYSCALL_INTERRUPT_PRIORITY常量,则任何调用线程安全FreeRTOS API的ISR都应当运行在configKERNEL_INTERRUPT_PRIORITY指定的优先级上。

每一个中断源都包含数字优先级和逻辑优先级。

数字优先级是表面的数字值,而逻辑优先级是表示真正的优先级高低,有的SOC把数字大的优先级定义为较高逻辑优先级,数字较小的优先级定义了较低的逻辑优先级,有的则正好相反。逻辑优先级高的中断获得优先执行权。

ARM Cortex-M SOC中,0是最高优先级,配置时务必小心谨慎。
它具有最低的数值等于255优先级,tick interrupt的数字优先级就应当设置为255。
同时,它不允许将configMAX_SYSCALL_INTERRUPT_PRIORITY设置为0

9. 资源管理器

资源,最重要的就是资源的同步问题,本章主要涉及资源同步问题。

原子操作、可重入函数等都是跟C概念一样的东西。

互斥技术:
当一个任务尝试去访问一个非线程安全的数据时,就需要使用互斥技术。

9-1. 临界区

调用taskENTER_CRITICAL() and taskEXIT_CRITICAL()可以将一段代码设置为临界区。临界区中的代码只能被一个任务调用。

比如:
vPrintString函数用于打印信息到终端,所有任务都有可能会调用到这段代码,将内部设置成一个临界区,避免了多个任务同时往终端发送数据而导致乱码。
但是,我们不应当这么做,因为输出数据到终端耗费相当多的时间,请使用其它更有的解决方案。

临界区是一个非常粗糙的同步解决方案,它会禁中断(优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断)。且只有中断才能在临界区发生任务切换。

9-2. 锁定调度器

锁定调度器类似于临界区,不同的是,锁定调度器不会禁止中断。

9-3. 互斥量与二值信号量

互斥量是一种特殊的二值信号量,用户在任务间共享资源。通过configUSE_MUTEXES=1配置生效。

互斥量与linux中互斥量概念相同。

应当注意优先级反转问题和死锁的产生。

优先级反转:
高优先级的任务在等待一个被低优先级持有的互斥量,高优先级任务由此进入阻塞状态,低优先级任务从而处于运行状态。这个现象违背了高优先级先拥有运行权的优先级控制逻辑,称为优先级反转。

解决方案:优先级集成。当低优先级持有一个高优先级正在等待的互斥量时,低优先级会临时抬升优先级到与高优先级任务相同的优先级,避免处于中间优先级的任务获得执行权。当低优先级释放互斥量时,则自动将自身的优先级返回到原来的优先级。

递归互斥量:
死锁一般产生于多个任务之间,但是同一个任务之内也可能产生死锁。假如一个任务obtain一个互斥量,然后调用一个lib中的函数,它take相同的互斥量,然后等待该互斥量,进入阻塞状态。此时,任务就会陷入自己等待自己持有的互斥量情况,永远处于阻塞状态。

解决方案就是使用递归互斥量,该互斥量允许多个take行为,一旦互斥量被give,则所有的take者将获得通知。

9-4. 互斥量与任务调度

假设多个任务take一个互斥量,当这个互斥量give的时候,高优先级的任务立即获得执行权。如果这些任务的优先级是同一个优先级,则下一个时间片到来时,根据默认调度策略换入其中一个任务去运行。

9-5. 看门狗任务

看门狗任务用于检测系统死锁的产生,它拥有私有的独立资源,其它任务必须经由看门狗任务才能访问该资源。

10. 事件组

事件组(events group)是事件的组合,通过一个组合来管理一堆事件,从而更方便任务与任务之间的同步和事件通信。

任务可以等待事件组中的一个或者多个事件,每个事件之间,既可以是与的关系(所有事件都生效时唤醒任务),也可以是或的关系(其中一个事件生效时唤醒任务)。

比如FreeRTOS+TCP中就使用了事件组,将建立、连接、可读、可写分别当做独立事件放到事件组中。事件组中的事件也可以分别被多个任务监听,比如一个任务监听socket可读,另一个则监听socket可写,可读时读取网络数据,可写时写入网络数据。

事件组中的事件标志是通过一个EventBits_t数据类型,每一个二进制位表示一个事件,0表示事件无效,1表示事件有效。

特别的:
configUSE_16_BIT_TICKS=1,事件组中包含8个事件。
configUSE_16_BIT_TICKS=0,事件组中包含24个事件。

10-1. 使用事件组进行任务同步

可以使用事件组保持2个或者多个任务之间的同步逻辑。

假设有4个任务A、B、C、D,A先接收到一个event,然后委派工作给BCD三个任务去做,A必须等待BCD完成各自的任务之后,才能接收下一个event。那么使用事件组同步,将非常方便。

类似上面提到的在TCP中的事件组应用,读任务和写任务都必须等待对方完成工作后再去关闭socket。

11. 任务通知

configUSE_TASK_NOTIFICATIONS to 1

前面已经提到,任务通信的工具可以使用中间对象,比如:队列、事件组、各种信号量。

通过中间对象进行任务交互,数据仅仅是传递到中间对象,并不会直接传递到对方任务中。

任务通知则不一样,它可以直接将数据传到到另一个任务。

每一个任务都有一个‘Notification State’,可以两种状态中的一种,‘Pending’ or ‘Not-Pending’。

每个任务都有一个‘Notification Value’,是特定的32-bit无符号整数。

Pending:有通知达到
Not-Pending:无通知达到或者通知以及被读取

11-1. 任务通知优缺点

任务通知优点:

  • 快,比中间对象快
  • 占用RAM少,只有固定的8Bytes

缺点:

  • 无法通过通知发送event或者数据到ISR,反过来可以
  • 中间对象可以被任何获得handle的任务/ISR访问,但是通知却只能有本任务访问
  • 无法保存多个数据,而队列却可以
  • 无法一次通知多个任务,而中间对象可以
  • 中间对象可以等待对方获取内容后再写入数据,但是通知会直接覆盖

11-2. 使用任务通知

可以关注一下给出的三个例子:UART、ADC、TCP

12. 开发者支持

跟踪application的运行状态、提供优化依据、问题追踪

12-1. configASSERT()

断言,可以在开发和阶段打开这个宏,代码会增加,会占用更多的内存,当关闭这个宏后,代码会自动在编译阶段移除,不影响实际发布。

12-2. FreeRTOS+Trace

非常强大的系统跟踪器,一个由Percepio提供的实时的统计、分析和跟踪工具。

它的结果以图表的方式显示出来,所提供的信息对分析、故障排查、优化具有重要作用。

12-3-1. Malloc failed hook

申请内存失败回调,前面提到过,系统在创建任务、队列、信号量、事件组等都会动态申请内存。

12-3-2. Stack overflow hook

任务栈溢出异常,它与TCB一并保存在heap中。

12-3-3. 获取任务运行时和任务状态信息

任务运行时统计信息提供各个任务消耗的时间,该功能会增加了上下文切换时间。

12-3-4. The Run-Time Statistics Clock

运行时统计信息所需要的时钟,这个时钟应当比tick 周期要短,频率要快10-100倍。

它最好是一个处理器的32-bit的外部时钟,如果没有,则可以通过软件的方式实现。

软件方式:

  • 设计一个软中断,将中断的次数作为clock计数(这个方案非常耗性能)
  • 使用一个16-bit的定时器作为32-bit的低16bit计数,时钟中断的次数作为32-bit的高16位,以此代替32-bit外部时钟

13. 总结

FreeRTOS不复杂,很多概念与通用操作系统相同,可以把它当做一个简化版的通用操作系统。但它是一个RTOS,高优先级的任务获得执行权。ISR可以中断任务,任务无法中断ISR(这一点通用操作系统也一样)。

FreeRTOS的加入,使得板级开发编程基于操作系统开发,RTOS丰富的系统功能和特性给IoT设备赋予了多任务功能,以及更丰富和复杂的开发需求。

下一步:

  • 阅读v9.0/V10.0更新
  • 实践操作,根据业务需要运行Demo
  • 灵活使用FreeRTOS+Trace及调试接口

Comments

Content