FreeRTOS

还在更新中……

一、Mastering-the-freeRTOS 书籍翻译

1. 编码风格:


1. 数据类型
TickType_t
freeRTOS内置内核系统周期滴答中断。
从任务开始走过滴答周期的个数,叫滴答数值,它用于测量时间。
两个相邻的系统滴答的时间,叫滴答周期。
描述任务时间的基本单位是滴答周期。
TickType_t 就是程序中任务时间变量的类型。

BaseType_t
这是内核中效率最高的数据类型。通常,这个类型在64-bit架构上就是64-bit的类型的数据;32位架构就是32位数据类型;16位架构就是16位数据类型;8位架构就是8位长度的数据类型。
BaseType_t 通常用于数值受限的返回值数据类型,以及 pdTRUE/pdFALSE 布尔类型。

2. 变量名称
变量名前缀有以下情形:
c 代表 char;
s 代表 short int16_t;
l 代表 long int32_t;
x 代表 BaseType_t 以及其他非标准数据类型(结构体、任务句柄、队列句柄等);
若变量是无符号整型,那么还要加上前缀 'u' ,若变量是指针变量,那么要加 'p'。

3. 函数名称
函数名称的前缀信息由含函数返回值数据类型信息和函数定义所在的文件信息组成。比如:
vTaskPrioritySet() 返回值是void类型,定义在 tasks.c 文件中。
xQueueReceive() 返回值是 BaseType_t 类型,定义在 queue.c 文件中。
pvTimerGetTimerID() 返回值是 void 指针类型,定义在 timer.c 文件中。
文件私有函数名称的前缀是'prv'.

4. 格式
Tab键用于demo应用程序中,一个tab键等于四个空格键。内核代码不再使用tab键。

5. 宏名称
大多数宏的名称由大写字母组成,前缀是小写字母,前缀暗示宏定义所在的文件。
前缀                                        定义文件位置                       
port (for example, portMAX_DELAY)          portable.h or portmacro.h
task (for example, taskENTER_CRITICAL())   task.h
pd (for example, pdTRUE)                   projdefs.h
config (for example, configUSE_PREEMPTION) FreeRTOSConfig.h
err (for example, errQUEUE_FULL)           projdefs.h
注意: 信号量(semaphore)函数APIs几乎全部以宏的形式编写的,但是遵循函数命名规则,而不是宏命名规则。
下表中的宏定义内核代码全文通用:
Macro   Value (通用宏定义表)
pdTRUE   1
pdFALSE  0
pdPASS   1
pdFAIL   0

2. 堆内存管理

堆,用于分配任务栈空间,变量等。
动态内存分配,是C语言编程中的概念,而不是FreeRTOS也不是多任务系统中的概念。FreeRTOS内核中对象,可以存放在动态分配的内存空间中。但是标准C库的 malloc() free() 并不适合嵌入式系统。

pvPortMalloc() and vPortFree() 是公用函数,可以被应用层代码调用。

3.1 Heap_1
适用于绝不删除任务和其他内核对象的场景。
适用于商业高安全性禁止动态内存分配的场景。
FreeRTOSConfig.h 中的常亮 configTOTAL_HEAP_SIZE 定义了堆内存的总字节数.
Heap_1的 pvPortMalloc()策略是,每调用一次,都会将8位长度类型的数组内存堆空间再分成更小的数据块。
每次分配任务空间,都需要调用两次 pvPortMalloc() ,一次分配任务的 TCB ,一次分配任务的堆栈。

3.2 Heap_2
Heap_2 已经被功能更多的 Heap_4 方案替代了。Heap_2之所以保存在代码中,只是提供向前的兼容性,并不推荐在新方案中使用。
Heap_2 的策略也是内存堆空间的再划分,但它使用了best-fit 算法去分配空间,而不像Heap_1实实在在地分配。 best-fit算法会寻找最接近需求空间的空闲数据块。

不像 Heap_4, Heap_2不会把临近的空闲空间整合为一整个更大的空间。所以官方强烈不推荐再使用Heap_2! 推荐使用 Heap_4方案。

4. 任务管理

4.7 Expanding the Not Running State
An application may consist of many tasks. If the processor running the application includes a single core, then only one task may be executing at any given time. This implies that a task may exist in one of two states: Running and Not Running. This simplistic model is considered first. Later in this chapter we describe the several sub-states of the Not Running state.
All task that are not currently Running are in the Not Running state. Only one task can be in the Running state at any one time in a single core MCU.
翻译:
应用程序可能包含许多任务。如果运行应用程序的处理器包含单个核心,则在任何给定时间只能执行一个任务。这意味着任务可能存在于两种状态之一:正在运行和未运行。首先考虑这个简单的模型。在本章后面,我们将描述未运行状态的几个子状态。
所有当前未运行的任务都处于未运行状态。在单核 MCU 中,任何时候只能有一个任务处于运行状态。非运行状态包括:阻塞状态和挂起状态;

4.7.1 The Blocked State
A task waiting for an event is said to be in the 'Blocked' state, a sub-state of the Not Running state.(Blocked: event-driven)
Tasks can enter the Blocked state to wait for two different types of events:
等待事件的任务被称为处于“阻塞”状态,这是“未运行”状态的子状态。(阻塞:事件驱动)
1. Temporal (time-related) events— these events occur either when a delay period expires or an absolute time is reached. For example, a task may enter the Blocked state to wait for 10 milliseconds to pass.
2. Synchronization events— these events originate from another task or interrupt. For example, a task may enter the Blocked state to wait for data to arrive on a queue. Synchronization events cover a broad range of event types.

FreeRTOS queues, binary semaphores, counting semaphores, mutexes, recursive mutexes, event groups, stream buffers, message buffers, and direct to task notifications can all create synchronization events. Later chapters cover most of these features.

A task can block on a synchronization event with a timeout, effectively blocking on both types of event simultaneously. For example, a task may choose to wait for a maximum of 10 milliseconds for data to arrive on a queue. The task will leave the Blocked state if data arrives within 10 milliseconds or if 10 milliseconds pass without data arriving.

4.7.2 The Suspended State
Suspended is also a sub-state of Not Running. Tasks in the Suspended state are not available to the scheduler. The only way to enter the Suspended state is through a call to the vTaskSuspend() API function, and the only way out is through a call to the vTaskResume() or xTaskResumeFromISR() API functions. Most applications do not use the Suspended state.

4.7.3 The Ready State
Tasks that are in the Not Running state and are not Blocked or Suspended are said to be in the Ready state. They can run, and are therefore 'ready' to run, but are not currently in the Running state.

4.7.4 Completing the State Transition Diagram 完整的任务状态转换逻辑图

Figure 4.7 expands on the simplified state diagram to include all of the Not Running sub-states described in this section.  下图就是完整的所有任务状态的切换:

Each task requires two blocks of RAM: one to hold its Task Control Block (TCB) and one to store its stack.

5 队列管理

“队列”提供了任务到任务、任务到中断以及中断到任务的通信机制。

5.1.1 本章涵盖:


如何创建队列。
队列如何管理其包含的数据。
如何将数据发送到队列。
如何从队列接收数据。
在队列上阻塞意味着什么。
如何在多个队列上阻塞。
如何覆盖队列中的数据。
如何清除队列。
在写入和读取队列时任务优先级的影响。
本章仅介绍任务到任务的通信。第 7 章介绍任务到中断和中断到任务的通信。

5.2 队列的特征


5.2.1 数据存储
队列可以容纳有限数量的固定大小的数据项[^8]。队列容纳数据项的数量称为其“长度”。每个数据项的长度和大小都是在创建队列时设置的。
[^8]: FreeRTOS 消息缓冲区(在 TBD 章节中描述)为保存可变长度消息的队列提供了一种更轻量的替代方案。

队列通常用作先进先出 (FIFO) 缓冲区,其中数据被写入队列的末尾(尾部)并从队列的前面(头部)移除。图 5.1 演示了将数据写入和读取用作 FIFO 的队列。也可以将数据写入队列的前面,并覆盖队列前面已有的数据。

Figure 5.1 An example sequence of writes to, and reads from a queue 

通常,有两种方法可以实现队列行为:
1. 数据复制方式 (复制队列)
复制方式,意味着发送到队列的数据被逐字节复制到队列中。
2. 数据指针方式 (指针队列)
数据指针方式,意味着队列仅保存指向发送数据的指针,而不是数据本身。

FreeRTOS 使用的是数据复制方式实现的队列。因为它比数据指针引用实现的队列更强大且更易于使用,因为:
-复制队列并不妨碍指针队列。无法执行复制队列时(比如数据无法复制时),则可以使用指针队列。
-任务栈区的局部变量,可直接发送到队列。即使任务被释放,意味着栈区也被释放,复制方式队列存的数据也不受影响。
-发送数据到队列,无需事先为数据分配存储空间。数据会被复制到队列内部的存储空间(pxQueue->pcWriteTo)。
-发送任务和接收任务彻底解耦:应用程序设计人员不需要关心哪个任务“拥有”数据,或者哪个任务负责发布数据。
-FreeRTOS 全权负责分配用于存储数据的内存。
-在具有内存保护的系统中,会限制对 RAM 的访问,只有当发送和接收任务都可以访问引用的数据时,才能使用指针队列。而复制队列则不存在这种问题。


5.2.2 队列的多任务访问
队列是内核中合法、独立存在的对象,可以被任何能够感知到队列存在的任务(any task)或者中断服务程序(ISR)访问。任何数量的任务都可以往同一个队列里写数据,并且任意数量的任务都可以从同一个队列里读取数据。在实际项目中,通常有相对较多的任务往同一个队列里写数据,且相对较少的任务从同一个队列里读取数据。

5.2.3 队列读取阻塞
任务从队列中读取数据时,可以设定一个阻塞时间。当任务读取到空队列时,这个时间用来阻塞任务,去等队列数据的到来。在限定时间内,若有其他任务或ISR将数据写入队列,则被阻塞的任务会自动切换成就绪态。如果超过规定的时间后,还没能获取队列数据,此任务也会自动由阻塞态切换为就绪态。

5.2.4 队列写入阻塞
跟队列读取阻塞情况类似,任务也可以在写入队列时,设定阻塞时间。若任务欲写入的队列中的数据已满,阻塞时间就是任务处于阻塞状态以等待队列可写入的最长时间。
队列允许多个任务写入,所以存在多个任务因队列空间已满而都被阻塞等待写入的情况。在这种情况下,当队列空间可写,只允许解除其中一个任务的阻塞状态(变为就绪态)。这个被解除阻塞态的任务是所有被阻塞任务中优先级最高的那个。如果阻塞任务中有多个同等最高优先级的任务,那么会选择等待时间最长的那个任务(被解除阻塞,进入就绪态)。

5.2.5 队列集的阻塞
多个队列可以组合成队列集,从而允许任务进入阻塞状态以等待集合中任何队列上的数据可用。第 5.6 节“从多个队列接收”将会详细介绍队列集。

5.2.6 创建队列: 队列存储空间的静态分配和动态分配
队列通过队列句柄来引用(使用)。队列句柄的数据类型是 QueueHandle_t. 在使用之前,必须先创建队列。
内核提供两个函数用于创建队列:xQueueCreate(), xQueueCreateStatic().
在创建时,每个队列都需要两块数据存储空间(RAM),其中一块存储空间用于存放队列数据结构体自身,另一块用于存放队列通信的消息数据。xQueueCreate() 从堆空间动态分配获取存储空间。 xQueueCreateStatic()则需要预先分配存储空间再以参数形式传入。

5.3 使用队列


5.3.1 The xQueueCreate() API 函数
函数原型:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); 
xQueueCreateStatic() 则需要两个额外的参数:预先分配的存储空间用来存放队列结构体自身数据和存储通信消息数据。
xQueueCreate() 参数与返回值:
-uxQueueLength: 队列长度,创建队列时指定的队列最多可容纳消息的个数。
-uxItemSize: 消息长度,队列单个消息的字节数。
-Return value: 
如果返回值=NULL,队列创建失败。内核可分配的空间不足导致。
如果返回值非空,队列创建成功。返回值是队列的句柄变量。
xQueueReset() 也是一个 API 函数,用来复位队列:把一个已经创建出来的队列恢复成它初始的“空”状态。

5.3.2 The xQueueSendToBack() and xQueueSendToFront() API Functions
可以猜的到, xQueueSendToBack() 发送数据到队列尾部, xQueueSendToFront() 发送数据到队列的头部.
xQueueSend() 与 xQueueSendToBack() 等效且完全相同。
注意:切勿从中断服务例程调用 xQueueSendToFront() 或 xQueueSendToBack()。应使用中断安全版本 xQueueSendToFrontFromISR() 和 xQueueSendToBackFromISR() 代替它们。这些内容在第 7 章中进行了描述。

BaseType_t xQueueSendToFront( QueueHandle_t xQueue, 
                              const void * pvItemToQueue, 
                              TickType_t xTicksToWait ); 
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, 
                             const void * pvItemToQueue, 
                             TickType_t xTicksToWait );
                             
xQueueSendToFront() 和 xQueueSendToBack() 函数参数及返回值:
-xQueue
数据被发送(写入)到的队列的句柄。队列句柄将从用于创建队列的 xQueueCreate() 或 xQueueCreateStatic() 调用中返回。
-pvItemToQueue
指向要复制到队列中的数据的指针。
队列的存储空间是在创建队列时设定的,因此许多字节会从 pvItemToQueue 复制到队列存储区域中。
-xTicksToWait
如果队列已满,则任务应保持阻塞状态以等待队列中有可用空间的最长时间。
如果 xTicksToWait 为零且队列已满,则 xQueueSendToFront() 和 xQueueSendToBack() 都将立即返回。
阻塞时间以内核的滴答周期为基本事件单位粒度,因此它所代表的绝对时间取决于滴答频率。宏 pdMS_TO_TICKS() 可用于将以毫秒为单位的时间转换为以滴答为单位的时间。
将 xTicksToWait 设置为 portMAX_DELAY 将导致任务无限期等待(不会超时),前提是 FreeRTOSConfig.h 中的 INCLUDE_vTaskSuspend 设置为 1。
-Return value
有两个可能的返回值:
pdPASS:
当数据成功发送到队列时,将返回 pdPASS。
如果指定了阻塞时间(xTicksToWait 不为零),则调用任务可能被置于阻塞状态,以等待队列中的空间在函数返回之前可用,且数据在阻塞时间到期之前已成功写入队列。
errQUEUE_FULL (same value as pdFAIL):
如果由于队列已满而无法将数据写入队列,则返回 errQUEUE_FULL。
任务由于等待队列的可用空间而切换为阻塞状态,但是在阻塞时间内仍然等不到队列的可用空间,也就是等待超时。

5.3.3 The xQueueReceive() API Function
xQueueReceive() 从队列中读取一个消息单元。 被读取的消息单元的队列存储空间将被释放,消息单元会从队列中删除。
注意:切勿从中断服务例程调用 xQueueReceive()。中断安全 xQueueReceiveFromISR() API 函数在第 7 章中描述。
BaseType_t xQueueReceive( QueueHandle_t xQueue, 
                          void * const pvBuffer, 
                          TickType_t xTicksToWait ); 
xQueueReceive() 函数的参数和返回值如下:
-xQueue
接收(读取)数据的队列句柄。队列句柄将从用于创建队列的 xQueueCreate() 或 xQueueCreateStatic() 调用中返回。
-pvBuffer
指向用于存放从队列读出数据的存储空间的指针。队列所保存的每个数据项的大小在创建队列时设置。
pvBuffer 指向的存储空间必须足够大以容纳从队列读出的数据。
-xTicksToWait
若队列无数据可读,xTicksToWait 就是任务因等待读取队列数据而被阻塞的最大时间。
如果 xTicksToWait 为零,则当队列已为空时 xQueueReceive() 将立即返回。
阻塞时间以内核的滴答周期为基本单位粒度,因此它所代表的绝对时间取决于滴答频率。宏 pdMS_TO_TICKS() 可用于将以毫秒为单位的时间转换为以滴答为单位的时间。
将 xTicksToWait 设置为 portMAX_DELAY 将导致任务无限期等待(不会超时),前提是 FreeRTOSConfig.h 中的 INCLUDE_vTaskSuspend 设置为 1。
-Return value
有两个可能的返回值:
pdPASS:
当成功从队列读取数据时,将返回 pdPASS。
在阻塞时间 (xTicksToWait 不为零)内, 由于等待队列消息属于而被阻塞的任务,获取到了队列消息数据.
errQUEUE_EMPTY (same value as pdFAIL):
如果由于队列已为空而无法从队列读取数据,则返回 errQUEUE_EMPTY。
如果指定了阻塞时间(xTicksToWait 不为零),则调用任务将处于阻塞状态以等待另一个任务或中断将数据发送到队列,但在阻塞时间内仍然等不到可用的队列信息数据,也就是等待超时。

5.3.4 The uxQueueMessagesWaiting() API Function
uxQueueMessagesWaiting() 查询队列中当下有多少个消息单元。
注意:切勿从中断服务例程调用 uxQueueMessagesWaiting()。应使用中断安全的 uxQueueMessagesWaitingFromISR() 代替它。
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue ); 
uxQueueMessagesWaiting() 函数的参数和返回值如下:
-xQueue
欲查询的队列的句柄。队列句柄将从用于创建队列的 xQueueCreate() 或 xQueueCreateStatic() 调用中返回。
-Return value
欲查询队列中的信息单元的个数。如果返回零,则队列为空。

---------------------------------------------------------------

5.4 从多个源头接收数据


在 FreeRTOS 设计中,一个任务从多个源头接收数据的情况很常见。接收任务需要知道数据来自哪里,才能确定如何处理它。一种易于实现的设计模式是使用单个队列传输包含数据值和数据源的结构,如图 5.4 所示。

typedef struct{
  ID_t eDateID;
  int32_t lDateValue;
} Date_t;

-队列消息单元的数据类型是 Date_t. 这个数据类型包含了要传递的真实有效数据的 ID_t 类型和消息种类定义的枚举类型。
-中央控制器任务执行主要的系统功能。它必须对队列中传送给它的输入和系统状态变化作出反应。
-CAN 总线任务用于封装 CAN 总线接口功能。当 CAN 总线任务已接收并解码消息时,它会将已解码的消息以Data_t 结构的形式发送到控制器任务。传输结构的 eDataID 成员告诉控制器任务数据是什么。在此处显示的是电机速度值。传输结构的 lDataValue 成员告诉控制器任务实际的电机速度值。
-人机界面 (HMI) 任务用于封装所有 HMI 功能。机器操作员可能可以通过多种方式输入命令和查询值,这些方式必须在 HMI 任务中检测和解释。输入新命令时,HMI 任务会将命令发送到 Data_t 结构中的控制器任务。传输结构的 eDataID 成员告诉控制器任务数据是什么。在此处显示的情况下,它是一个新的设定点值。传输结构的 lDataValue 成员告诉控制器任务实际的设定点值。

章节 (RB-TBD) 展示了如何扩展此设计模式,以便控制器任务可以直接回复排队结构的任务。

Example 5.2 Blocking when sending to a queue, and sending structures on a queue
【代码例子,省略】

---------------------------------------------------------------

5.5 处理大型或可变大小的数据


如果队列要存储的数据很大,则最好使用数据指针传输,而不采用在队列中复制大量数据的方式。传输指针在处理时间和创建队列所需的 RAM 量方面都更有效率。但是,在对指针进行排队时,必须格外小心以确保:
-严格划分数据RAM空间的访问权限
当通过指针在任务之间共享内存时,必须确保两个任务不会同时修改内存内容,或采取任何其他可能导致内存内容无效或不一致的操作。理想情况下,在将指针发送到队列之前,只应允许发送任务访问内存,在从队列接收到指针之后,只应允许接收任务访问内存。
-保证数据RAM空间的有效性
如果指向的内存是动态分配的,或从预分配的缓冲区池中获取的,则只有一个任务应该负责释放该内存。在内存被释放后,任何任务都不应尝试访问该内存。
绝不应使用指针访问任务栈上分配的数据。任务释放后,堆区也释放,数据将不再有效。

---------------------------------------------------------------

5.6 从多个队列接收消息


5.6.1 队列集
应用程序设计通常需要单个任务来接收不同大小、不同含义、以及不同来源的数据。上一节演示了如何使用接收结构的单个队列以简洁高效的方式执行此操作。但是,有时应用程序的设计人员会遇到限制其设计选择的约束,因此必须对某些数据源使用单独的队列。例如,集成到设计中的第三方代码可能假定存在专用队列。在这种情况下,可以使用“队列集”。

队列集允许任务从多个队列接收数据,而无需任务依次轮询每个队列来确定哪个队列(如果有)包含数据。

使用队列集从多个源接收数据的设计不如使用接收结构的单个队列实现相同功能的设计简洁且效率较低。因此,建议仅在设计约束使其使用绝对必要时才使用队列集。

接下来阐述如何使用队列集:
-创建队列集
-将多个队列加入队列集
信号量也能加入队列集。本书后面将介绍信号量。
-从队列集合中读取以确定集合中的哪些队列包含数据。
当任务从队列集中读取数据时,队列集会把接收到消息数据的那条队列的句柄返回给任务,若队列句柄有效,任务可以直接从该队列读取数据。
注意:每次从队列集接收队列句柄时,都必须读取队列数据,并且不能还没从队列集获取队列句柄,就直接读取队列。

通过在 FreeRTOSConfig.h 中将 configUSE_QUEUE_SETS 编译时配置常量设置为 1,可以启用队列集功能。

5.6.2 The xQueueCreateSet() API 函数
使用前须先创建队列集。在撰写本文时,xQueueCreateSetStatic() 尚未实现。但是队列集本身就是队列,因此可以使用预分配的内存创建一个集合,方法是使用专门设计的 xQueueCreateStatic() 调用。
队列集由句柄引用,句柄是 QueueSetHandle_t 类型的变量。 xQueueCreateSet() API 函数创建一个队列集并返回一个引用所创建队列集的 QueueSetHandle_t。
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength); 

xQueueCreateSet() 参数和返回值:
-uxEventQueueLength 队列集的事件队列消息长度
When a queue that is a member of a queue set receives data, the handle of the receiving queue is sent to the queue set. uxEventQueueLength defines the maximum number of queue handles that the queue set being created can hold at any one time.
当队列集中的一条队列接收到了数据,这条队列就会被发送到队列集中。
uxEventQueueLength 定义正在创建的队列集可以同时容纳的最大队列句柄数。

Queue handles are only sent to a queue set when a queue within the set receives data.
当队列集中的队列接收到数据后,接收到数据的队列句柄只会被发送到队列集中。
A queue cannot receive data if it is full, so no queue handles can be sent to the queue set if all the queues in the set are full. 
一条队列满了,就无法继续接收数据。所以,若队列集中每条队列都满,则没有队列句柄发送到队列集。
Therefore, the maximum number of items the queue set will ever have to hold at one time is the sum of the lengths of every queue in the set.
所以,队列集的消息容量就是每条队列集中的队列长度之和。

例如,如果集合中有三个空队列,每个队列的长度为 5,那么集合中的队列总共可以接收 15 个项目(三个队列乘以每个队列 5 个项目),然后集合中的所有队列才会满。在这个例子中,uxEventQueueLength 必须设置为 15,以保证队列集合可以接收发送给它的每条消息数据。

信号量也可以添加到队列集中。本书后面将介绍信号量。为了计算必要的 uxEventQueueLength,二进制信号量的长度为 1,互斥量的长度为 1,计数信号量的长度由信号量的最大计数值给出。

再举一个例子,如果一个队列集包含一个长度为 3 的队列和一个二进制信号量(长度为 1),则 uxEventQueueLength 必须设置为 4(3 加 1)。

-Return Value 返回值
如果返回 NULL,则无法创建队列集,因为 FreeRTOS 没有足够的堆内存来分配队列集数据结构和存储区域。第 3 章提供了有关 FreeRTOS 堆的更多信息。
如果返回非 NULL 值,则表示队列集创建成功,返回值为所创建队列集的句柄。

5.6.3 The xQueueAddToSet() API 函数
xQueueAddToSet() adds a queue or semaphore to a queue set. Semaphores are described later in this book.
xQueueAddToSet() 添加一个队列或信号量到队列集。信号量会在后续章节描述。
函数原型:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, 
                           QueueSetHandle_t xQueueSet ); 
                           
xQueueAddToSet() 参数和返回值:
-xQueueOrSemaphore
欲加入队列集的队列或信号量的句柄。
队列句柄或信号量句柄都可被转化为 QueueSetMemberHandle_t 类型。
-xQueueSet
队列集的句柄。
-Return Value
1. pdPASS:队列集创建成功。
2. pdFAIL:意味着队列或信号量,无法成功加入队列集。

Queues and binary semaphores can only be added to a set when they are empty. Counting semaphores can only be added to a set when their count is zero. Queues and semaphores can only be a member of one set at a time.
队列和二进制信号量只有当它们为空时才允许添加到集合中。计数信号量只有当它们的计数为零时才可以添加到集合中。队列和信号量一次只能成为一个集合的成员。一个队列或信号量“不能”同时成为多个队列集的成员。

5.6.4 The xQueueSelectFromSet() API 函数
xQueueSelectFromSet()用于从队列集中选取(读取)一个队列句柄。

当队列集中某条队列或信号量接收到数据以后,接收到数据的队列或信号量的句柄会被发送给队列集。
当任务调用  xQueueSelectFromSet() 后,接收到信息数据的队列句柄或信号量句柄便间接地会由队列集发送给调用的任务。
从 xQueueSelectFromSet() 函数中返回的队列或信号量句柄包含有效的消息数据,调用任务必须直接从相应的队列或信号量中读取数据。

Note: Do not read data from a queue or semaphore that is a member of a set unless the handle of the queue or semaphore has first been returned from a call to xQueueSelectFromSet(). Only read one item from a queue or semaphore each time the queue handle or semaphore handle is returned from a call to xQueueSelectFromSet().
注意: 严谨从队列集中的队列或信号量中读取数据,除非,队列或信号量句柄是第一次调用 xQueueSelectFromSet() 时返回的!
      每次调用 xQueueSelectFromSet() 取得的队列集的队列或信号量后,只能从相应的队列或信号量中读取一个消息单元。
      
函数原型:
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, 
                                            const TickType_t xTicksToWait ); 
                                            
xQueueSelectFromSet() 参与与返回值:
-xQueueSet
要接收消息单元的队列集的句柄。
-xTicksToWait
当队列集为空时,调用任务用于等待队列集中有效数据而被阻塞的最大时间。
若队列集为空且阻塞时间为0(xTicksToWait=0),那么 xQueueSelectFromSet() 将立即返回。
阻塞时间以内核滴答周期为基本时间单位。所以任务的阻塞时间只取决于内核的滴答频率。
宏 pdMS_TO_TICKS() 可用于将以毫秒为单位的时间转换为以内核滴答频率为单位的时间。
将 xTicksToWait 设置为 portMAX_DELAY 将导致任务无限期等待(不会超时),前提是 FreeRTOSConfig.h 中的 INCLUDE_vTaskSuspend 设置为 1。
-Return Value
非空的返回值是包含有效消息数据队列或信号量的句柄。队列集中队列或信号量的有效数据在阻塞时间内已成功接收,返回队列集中队列或信号量的句柄,任务由阻塞态切换为就绪态。一次 xQueueSelectFromSet 函数的调用,任务只能从队列或信号量中获取一个消息单元,且必须获取,不能放弃。句柄以 QueueSetMemberHandle_t 类型返回,可以转换为 QueueHandle_t 类型或 SemaphoreHandle_t 类型。
如果返回值为 NULL,则无法从队列集合中读取句柄。在阻塞时间内,调用任务将处于阻塞状态且一直等待,但最后还是等不来结果(消息数据),被辜负了哈哈哈。
Example 5.3 *Using a Queue Set(省略)
5.6.5 More Realistic Queue Set Use Cases(省略)

---------------------------------------------------------------

5.7 基于队列实现的邮箱

嵌入式社区对术语没有共识,并且“邮箱”在不同的 RTOS 中含义不同。在本书中,邮箱一词用于指长度为 1 的队列。队列可能被描述为邮箱,因为它在应用程序中的使用方式,而不是因为它与队列有功能差异:

A queue is used to send data from one task to another task, or from an interrupt service routine to a task. The sender places an item in the queue, and the receiver removes the item from the queue. The data passes through the queue from the sender to the receiver.
队列用于任务到任务,中断到任务的数据信息传递。发送者将信息数单元据插入队列,接收者从队列中取走数据消息单元。数据消息通过队列传递,移入、移出。

A mailbox is used to hold data that can be read by any task, or any interrupt service routine. The data does not pass through the mailbox, but instead remains in the mailbox until it is overwritten. The sender overwrites the value in the mailbox. The receiver reads the value from the mailbox, but does not remove the value from the mailbox.
邮箱也存储数据,但是任何任务或中断都可以读取邮箱里的信息。邮箱并不像队列那样传输消息数据,邮箱只是保存信息数据,直到此信息数据空间被复写。发送者复写邮箱中的消息存储空间,接受者只从邮箱中的数据存储空间读取数据,并不会移出数据。

本章节描述基于队列实现的邮箱。

Listing 5.28 shows how a queue is created for use as a mailbox.
代码如下:
typedef struct xExampleStructure 

    TickType_t xTimeStamp; 
    uint32_t ulValue; 
} Example_t; 
QueueHandle_t xMailbox; 
void vAFunction( void )

    xMailbox = xQueueCreate( 1, sizeof( Example_t ) ); 

//Listing 5.28 A queue being created for use as a mailbox

5.7.1 The xQueueOverwrite() API 函数
与 xQueueSendToBack() API 函数一样,xQueueOverwrite() API 函数将数据发送到队列。
与 xQueueSendToBack() 不同,如果队列已满,则 xQueueOverwrite() 将覆盖队列中已有的数据。

xQueueOverwrite() 只能用于长度为 1 的队列。覆盖模式将始终写入队列的前端并更新队列前端指针,但不会更新等待的消息。如果定义了 configASSERT,则在队列长度 > 1 时将发生断言。
注意:切勿从中断服务例程调用 xQueueOverwrite()。应使用中断安全版本 xQueueOverwriteFromISR() 代替。

函数原型:
BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue ); 

参数与返回值:
-xQueue
数据要发送(写入)到的队列的句柄。队列句柄将从用于创建队列的 xQueueCreate() 或 xQueueCreateStatic() 调用中返回。
-pvItemToQueue
指向要复制到队列中的数据的指针。
队列可以容纳的每个项目的大小是在创建队列时设置的,因此这么多的字节将从 pvItemToQueue 复制到队列存储区域中。
-Return value
即使队列已满,xQueueOverwrite() 也会写入队列,因此 pdPASS 是唯一可能的返回值。
Listing 5.30 shows how xQueueOverwrite() is used to write to the mailbox (queue) created in Listing 5.28.(省略)

5.7.2 The xQueuePeek() API 函数
xQueuePeek() 从队列中接收(读取)一个项目,但不从队列中删除该项目。
xQueuePeek() 从队列头部接收数据,但不修改存储在队列中的数据,也不修改数据在队列中的存储顺序。
注意:切勿从中断服务例程中调用 xQueuePeek()。应使用中断安全版本 xQueuePeekFromISR() 代替。

xQueuePeek()具有与xQueueReceive()相同的函数参数和返回值。
BaseType_t xQueuePeek( QueueHandle_t xQueue, 
                       void * const pvBuffer, 
                       TickType_t xTicksToWait ); 
                       
Listing 5.32 shows xQueuePeek() being used to receive the item posted to the mailbox (queue) in Listing 5.30.(略)

6. 任务同步与通信

二值信号量

计数信号量

通常计数信号量用于两种场合:1. 事件的计数(counting events) 2. 资源管理(resource management)。

7. 中断管理


7.1 介绍
7.1.1 事件
嵌入式实时系统必须响应来自环境中的事件。比如,以太网外设接收到数据时(外部事件),需要(CPU软件代码)将数据传入 TCP/IP 协议栈中进行处理(响应)。
复杂的系统必须为来自多个来源的事件提供服务,所有这些事件都有不同的处理开销和响应时间要求。在每种情况下,都必须判断最佳的事件处理实施策略:
-如何感知事件?通常使用中断,但也可以轮询感知事件的输入。
-若使用中断,应在中断服务程序 (ISR) 内部执行什么处理,在外部执行哪些处理?通常保持每个 ISR 尽可能短。
-事件如何传达给主(非 ISR)代码,以及如何构造此代码以最好地适应潜在异步事件的处理?
FreeRTOS 不会对应用程序设计人员强加任何特定的事件处理策略,但确实提供了允许以简单且可维护的方式实现所选策略的功能(内核功能 features for sych and communication)。

区分任务的优先级和中断的优先级非常重要:
任务是一种软件功能,与 FreeRTOS 运行的硬件无关。任务的优先级由应用程序编写者在软件中分配,软件算法(调度程序)决定将哪个任务置于运行状态。
虽然中断服务例程是用软件编写的,但它是一种硬件功能,因为硬件控制哪个中断服务例程将运行以及何时运行。任务仅在没有 ISR 运行时运行,因此最低优先级的中断将中断最高优先级的任务,并且任务无法抢占 ISR。

所有运行 FreeRTOS 的MCU架构都能够处理中断,但与中断进入和中断优先级分配相关的细节因MCU的架构而异。

7.1.2 范围
本章讨论:
-可以在中断服务例程中使用特定的 FreeRTOS API 函数。
-推迟中断处理到任务中的方法。
-如何创建和使用二值信号量和计数信号量。
-二值信号量与计数信号量的区别。
-如何使用队列将数据传入ISR或使用队列从ISR中从传出数据。
-某些特定MCU架构上可使用的中断嵌套模型功能。

7.2 可在ISR中使用的内核 APIs 接口函数
7.2.1 中断安全的 APIs 接口函数
通常在ISR中使用 FreeRTOS 内核提供的服务是十分必不可少且不可避免的,但是绝大多数的内核APIs是不能运行于 ISR 中,这是非法的操作,会导致不可预知的错误。这种在ISR中运行非法的内核接口函数,是不能运行在ISR中的,它们通常会将调用此类APIs接口的任务阻塞。若这种APIs函数在ISR中调用就没有可以用来阻塞的任务。FreeRTOS内核为了解决此类问题,于是推出了针对此类功能的两种版本的APIs接口函数,一种版本就是通常任务中调用的,另一种版本是专供ISR中使用的版本。ISR中使用的此类APIs函数名后缀会加上“FromISR”字符。
注意:在ISR中,严禁使用不带有“FromISR”名称后缀的此类内核APIs接口函数。

7.2.2 使用中断版本的内核APIs接口的优点
将那些不得不在ISR中使用的内核功能单独开发出只在ISR中使用的APIs分支版本,可以提高任务代码的执行效率,也可以提升ISR代码的执行效率,同时中断入口也变得更加简单。想知道为什么这么做,可以假设此类函数仅存在单一版本,那么:
-API 函数需要额外的逻辑来确定它们是从任务还是 ISR 调用的。额外的逻辑会引入函数中的新路径,使函数更长、更复杂、更难测试。
-当从任务调用该函数时,某些 API 函数参数将会过时,而当从 ISR 调用该函数时,其他 API 函数参数将会过时。
-Each FreeRTOS port would need to provide a mechanism for determining the execution context (task or ISR).
-每种MCU架构需要提供一个额外的机制去实现上下文切换。
-Architectures on which it is not easy to determine the execution context (task or ISR) would require additional, wasteful, more complex to use, and non-standard interrupt entry code that allowed the execution context to be provided by software.
-在不同的MCU架构上实现上下文切换并不容易,且使用起来需要额外的多余的操作,使得使用过程更加复杂,还引入了非标准的中断入口代码。

7.2.3 使用中断版本的内核APIs接口的缺点
引入准备特定内核两种版本的APIs接口函数,虽然使得任务代码和中断服务代码更加高效,但是同时也引入了一些新的问题; 有时需要从任务和 ISR 调用不属于 FreeRTOS API 但使用FreeRTOS API 的函数。
这通常只是在集成第三方代码时才会出现的问题,因为这是软件设计唯一不受应用程序编写者控制的情况。如果这确实成为一个问题,那么可以使用以下技术之一来克服该问题:
-将中断处理推迟到任务[^12],因此 API 函数只能从任务上下文中调用。
-如果您使用的是支持中断嵌套的 FreeRTOS 端口,则请使用以“FromISR”结尾的 API 函数版本,因为该版本可以从任务和 ISR 中调用。(反之则不然,不以“FromISR”结尾的 API 函数不能从 ISR 中调用。)
-第三方代码通常包含一个 RTOS 抽象层,可以实现该层来测试调用该函数的上下文(任务或中断),然后调用适合该上下文的 API 函数。
[^12]: 延迟中断处理将在本书的下一部分中介绍。

7.2.4 The xHigherPriorityTaskWoken 参数
这里介绍 xHigherPriorityTaskWoken 参数的概念. 如果不能完全理解这个参数,请不必困惑,因为后续会有实际代码分析。

如果上下文切换是由中断执行的,那么中断退出时运行的任务可能与进入中断时运行的任务不是同一个任务——中断将中断一个任务,但返回到另一个任务。 

一些 FreeRTOS API 函数可以将任务从阻塞状态移至就绪状态。这已经在 xQueueSendToBack() 等函数中得到体现,如果某个任务在阻塞状态下等待主题队列中的数据可用,该函数将解除对某个任务的阻塞。

如果由 FreeRTOS API 函数解除阻塞的任务的优先级高于处于运行状态的任务的优先级,则根据 FreeRTOS 调度策略,应该切换到优先级更高的任务。实际切换到优先级更高的任务的时刻取决于调用 API 函数的上下文环境:

-在任务中调用此API
If configUSE_PREEMPTION is set to 1 in FreeRTOSConfig.h then the switch to the higher priority task occurs autom

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值