本文最后更新于 <span id="expire-date"></span> 天前,文中部分描述可能已经过时。

大部分内容都为摘抄
其实已经看过很多遍,但一直没用过。

内存管理

task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API函数的涉及,甚至可以减少内存的使用。

FreeRTOS的5种内存管理方法

文件优点缺点
heap_1.c分配简单,时间确定只分配、不回收
heap_2.c动态分配、最佳匹配碎片、时间不定
heap_3.c调用标准库函数速度慢、时间不定
heap_4.c相邻空闲内存可合并可解决碎片问题、时间不定
heap_5.c在heap_4基础上支持分隔的内存块可解决碎片问题、时间不定

heap_1.c

  • 实现最简单

  • 没有碎片问题

  • 一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1

heap_2.c 弃用

Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。

heap_3.c

Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。

C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。

heap_4.c

跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。

Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。

heap_5.c

Heap_5分配内存、释放内存的算法跟Heap_4是一样的。

相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。

在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。

既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:

在使用pvPortMalloc之前,必须先指定内存块的信息

使用vPortDefineHeapRegions来指定这些信息

Heap相关的函数

函数说明

void *pvPortMalloc( size_t xWantedSize ) | 分配内存
void vPortFree( void *pv ) | 释放内存
size_t xPortGetFreeHeapSize( void ) | 获取剩余内存
size_t xPortGetMinimumEverFreeHeapSize( void ) | 获取最小剩余内存

malloc失败的钩子函数

在pvPortMalloc函数内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void * pvPortMalloc( size_t xWantedSize )
{
......
#if ( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif

return pvReturn;
}

所以,如果想使用这个钩子函数:

在FreeRTOSConfig.h中,把configUSE_MALLOC_FAILED_HOOK定义为1

提供vApplicationMallocFailedHook函数

pvPortMalloc失败时,才会调用此函数

任务管理

在FreeRTOS中,任务就是一个函数,原型如下:

1
void ATaskFunction( void *pvParameters );

要注意的是:

  • 这个函数不能返回
  • 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
  • 函数内部,尽量使用局部变量:
    • 每个任务都有自己的栈
    • 每个任务运行这个函数时
      • 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
      • 不同任务的局部变量,有自己的副本
    • 函数使用全局变量、静态变量的话
      • 只有一个副本:多个任务使用的是同一个副本
      • 要防止冲突(后续会讲)

创建任务

创建任务时使用的函数如下:

1
2
3
4
5
6
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

参数说明:

参数描述
pvTaskCode函数指针,可以简单地认为任务就是一个C函数。
它稍微特殊一点:永远不退出,或者退出时要调用”vTaskDelete(NULL)”
pcName任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。
长度为:configMAX_TASK_NAME_LEN
usStackDepth每个任务都有自己的栈,这里指定栈大小。
单位是word,比如传入100,表示栈大小为100 word,也就是400字节。
最大值为uint16_t的最大值。
怎么确定栈的大小,并不容易,很多时候是估计。
精确的办法是看反汇编码。
pvParameters调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
uxPriority优先级范围:0~(configMAX_PRIORITIES – 1)
数值越小优先级越低,
如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask用来保存xTaskCreate的输出结果:task handle。
以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。
如果不想使用该handle,可以传入NULL。
返回值成功:pdPASS;
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
注意:文档里都说失败时返回值是pdFAIL,这不对。
pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。

任务的删除

删除任务时使用的函数如下:

1
void vTaskDelete( TaskHandle_t xTaskToDelete );

参数说明:

参数描述
pvTaskCode任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。
也可传入NULL,这表示删除自己。

怎么删除任务?举个不好的例子:

  • 自杀:vTaskDelete(NULL)
  • 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
  • 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄

任务优先级和Tick

任务优先级

在上个示例中我们体验过优先级的使用:高优先级的任务先运行。

优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。

  • 通用方法
    使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。
    configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。
  • 架构相关的优化的方法
    架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。
    使用这种方法时,configMAX_PRIORITIES的取值不能超过32。
    configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。

在学习调度方法之前,你只要初略地知道:

  • FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
  • 对于相同优先级的、可运行的任务,轮流执行

这无需记忆,就像我们举的例子:

  • 厨房着火了,当然优先灭火
  • 喂饭、回复信息同样重要,轮流做

Tick

对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。

任务状态

以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。

对于非运行的状态,还可以继续细分,比如前面的FreeRTOS_04_task_priority中:

  • Task3执行vTaskDelay后:处于非运行状态,要过3秒种才能再次运行
  • Task3运行期间,Task1、Task2也处于非运行状态,但是它们随时可以运行
  • 这两种”非运行”状态就不一样,可以细分为:
    • 阻塞状态(Blocked)
    • 暂停状态(Suspended)
    • 就绪状态(Ready)

Delay函数

有两个Delay函数:

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
  • vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。

空闲任务及其钩子函数

为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:

空闲任务优先级为0:它不能阻碍用户任务运行

空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务”抢占”(pre-empt)了空闲任务,这是由调度器实现的。

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

执行一些低优先级的、后台的、需要连续执行的函数

测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。

让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

空闲任务的钩子函数的限制:

不能导致空闲任务进入阻塞状态、暂停状态

如果你会使用vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

调度算法

配置项ABCDE
configUSE_PREEMPTION11110
configUSE_TIME_SLICING1100x
configIDLE_SHOULD_YIELD1010x
说明常用很少用很少用很少用几乎不用

注:

  • A:可抢占+时间片轮转+空闲任务让步

  • B:可抢占+时间片轮转+空闲任务不让步

  • C:可抢占+非时间片轮转+空闲任务让步

  • D:可抢占+非时间片轮转+空闲任务不让步

  • E:合作调度

同步互斥与通信

队列:

里面可以放任意数据,可以放多个数据

任务、ISR都可以放入数据;任务、ISR都可以从中读出数据

事件组:

一个事件用一bit表示,1表示事件发生了,0表示事件没发生

可以用来表示事件、事件的组合发生了,不能传递数据

有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒

信号量:

核心是”计数值”

任务、ISR释放信号量时让计数值加1

任务、ISR获得信号量时,让计数值减1

任务通知:

核心是任务的TCB里的数值

会被覆盖

发通知给谁?必须指定接收任务

只能由接收任务本身获取该通知

互斥量:

数值只有0或1

谁获得互斥量,就必须由谁释放同一个互斥量

队列

队列(queue)可以用于”任务到任务”、”任务到中断”、”中断到任务”直接传输信息。

  • 队列可以包含若干个数据:队列中有若干项,这被称为”长度”(length)

  • 每个数据大小固定

  • 创建队列时就要指定长度、数据大小

  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读

  • 也可以强制写队列头部:覆盖头部数据

队列的特性

传输数据的两种方法

FreeRTOS使用拷贝值的方法,这更简单:

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据

  • 无需分配buffer来保存数据,队列中有buffer

  • 局部变量可以马上再次使用

  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据

  • 如果数据实在太大,你还是可以使用队列传输它的地址

  • 队列的空间有FreeRTOS内核分配,无需任务操心

  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

队列阻塞访问

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。

任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

优先级最高的任务

如果大家的优先级相同,那等待时间最久的任务会进入就绪态

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写”满队列”时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?

优先级最高的任务

如果大家的优先级相同,那等待时间最久的任务会进入就绪态

队列函数

使用队列的流程:创建队列、写队列、读队列、删除队列。

创建

队列的创建有两种方法:动态分配内存、静态分配内存,

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配

函数原型如下:

1
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
返回值非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好

函数原型如下:

1
2
3
4
5
6
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,
此数组大小至少为”uxQueueLength * uxItemSize”
pxQueueBuffer必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为pxQueueBuffer为NULL

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )

// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;

// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];

void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;

// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorage,
&xQueueBuffer );
}

复位

队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()把队列恢复为初始状态,此函数原型为:

1
2
3
4
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

删除

删除队列的函数为vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。原型如下:

1
void vQueueDelete( QueueHandle_t xQueue );

写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);

/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);


/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);

/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

这些函数用到的参数是类似的,统一说明如下:

参数说明
xQueue队列句柄,要写哪个队列
pvItemToQueue数据指针,这个数据的值会被复制进队列,
复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait如果队列满则无法写入新数据,可以让任务进入阻塞状态,
xTicksToWait表示阻塞的最大时间(Tick Count)。
如果被设为0,无法写入数据时函数会立刻返回;
如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
返回值pdPASS:数据成功写入了队列
errQUEUE_FULL:写入失败,因为队列满了。

读队列

使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

1
2
3
4
5
6
7
8
9
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );

BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);

参数说明如下:

参数说明
xQueue队列句柄,要读哪个队列
pvBufferbufer指针,队列的数据会被复制到这个buffer
复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait果队列空则无法读出数据,可以让任务进入阻塞状态,
xTicksToWait表示阻塞的最大时间(Tick Count)。
如果被设为0,无法读出数据时函数会立刻返回;
如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值pdPASS:从队列读出数据入
errQUEUE_EMPTY:读取失败,因为队列空了。

查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

1
2
3
4
5
6
7
8
9
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

覆盖/偷看

当队列长度为1时,可以使用xQueueOverwrite()xQueueOverwriteFromISR()来覆盖数据。
注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。
函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);

BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用”窥视”,也就是xQueuePeek()xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么”偷看”时会导致阻塞;一旦队列中有数据,以后每次”偷看”都会成功。
函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);

BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);

信号量

前面介绍的队列(queue)可以用于传输数据:在任务之间、任务和中断之间。

有时候我们只需要传递状态,并不需要传递具体的信息,比如:

我的事做完了,通知一下你

卖包子了、卖包子了,做好了1个包子!做好了2个包子!做好了3个包子!

这个停车位我占了,你们只能等着

在这种情况下我们可以使用信号量(semaphore),它更节省内存。

信号量的常规操作

两种信号量的对比

信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。

差别列表如下:

二进制信号量技术型信号量
被创建时初始值为0被创建时初始值可以设定
其他操作是一样的其他操作是一样的

信号量函数

创建

使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。

对于二进制信号量、计数型信号量,它们的创建函数不一样:

二进制信号量计数型信号量
动态创建xSemaphoreCreateBinary
计数值初始值为0
xSemaphoreCreateCounting
vSemaphoreCreateBinary(过时了)
计数值初始值为1
静态创建xSemaphoreCreateBinaryStaticxSemaphoreCreateCountingStatic

创建二进制信号量的函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );

/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

创建计数型信号量的函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

/* 创建一个计数型信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* pxSemaphoreBuffer: StaticSemaphore_t结构体指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer );

删除

对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。

vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下:

1
2
3
4
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

give/take

二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:

在任务中使用在ISR中使用
givexSemaphoreGivexSemaphoreGiveFromISR
takexSemaphoreTakexSemaphoreTakeFromISR

xSemaphoreGive的函数原型如下:

1
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

xSemaphoreGive函数的参数与返回值列表如下:

参数说明
xSemaphore信号量句柄,释放哪个信号量
返回值pdTRUE表示成功,
如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败

pxHigherPriorityTaskWoken的函数原型如下:

1
2
3
4
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);

xSemaphoreGiveFromISR函数的参数与返回值列表如下:

参数说明
xSemaphore信号量句柄,释放哪个信号量
pxHigherPriorityTaskWoken如果释放信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
返回值pdTRUE表示成功,
如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败

xSemaphoreTake的函数原型如下:

1
2
3
4
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);

xSemaphoreTake函数的参数与返回值列表如下:

参数说明
xSemaphore信号量句柄,获取哪个信号量
xTicksToWait如果无法马上获得信号量,阻塞一会:
0:不阻塞,马上返回
portMAX_DELAY: 一直阻塞直到成功
其他值: 阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
返回值pdTRUE表示成功

xSemaphoreTakeFromISR的函数原型如下:

1
2
3
4
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);

xSemaphoreTakeFromISR函数的参数与返回值列表如下:

参数说明
xSemaphore信号量句柄,获取哪个信号量
pxHigherPriorityTaskWoken如果获取信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
返回值pdTRUE表示成功

互斥量

在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。

比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。

上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。

互斥量也被称为互斥锁,使用过程如下:

互斥量初始值为1

任务A想访问临界资源,先获得并占有互斥量,然后开始访问

任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞

任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源

任务B使用完毕,释放互斥量

正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。

但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。

函数

创建

互斥量是一种特殊的二进制信号量。

使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。

创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );

/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );

要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:

1
#define configUSE_MUTEXES 1

其他函数

要注意的是,互斥量不能在ISR中使用。

各类操作函数,比如删除、give/take,跟一般是信号量是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

/* 释放(ISR版本) */
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);

/* 获得 */
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
/* 获得(ISR版本) */
xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);

事件组

事件组可以简单地认为就是一个整数:

  • 每一位表示一个事件
  • 每一位事件的含义由程序员决定,比如:Bit0表示用来串口是否就绪,Bit1表示按键是否被按下
  • 这些位,值为1表示事件发生了,值为0表示事件没发生
  • 一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
  • 可以等待某一位、某些位中的任意一个,也可以等待多位

事件组用一个整数来表示,其中的高8位留给内核使用,只能用其他的位来表示事件。那么这个整数是多少位的?

  • 如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
  • 如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件
  • configUSE_16_BIT_TICKS是用来表示Tick Count的,怎么会影响事件组?这只是基于效率来考虑
    • 如果configUSE_16_BIT_TICKS是1,就表示该处理器使用16位更高效,所以事件组也使用16位
    • 如果configUSE_16_BIT_TICKS是0,就表示该处理器使用32位更高效,所以事件组也使用32位

事件组的操作

事件组和队列、信号量等不太一样,主要集中在2个地方:

  • 唤醒谁?
    • 队列、信号量:事件发生时,只会唤醒一个任务
    • 事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有”广播”的作用
  • 是否清除事件?
    • 队列、信号量:是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
    • 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件

以上图为列,事件组的常规操作如下:

  • 先创建事件组

  • 任务C、D等待事件:

    • 等待什么事件?可以等待某一位、某些位中的任意一个,也可以等待多位。简单地说就是”或”、”与”的关系。
    • 得到事件时,要不要清除?可选择清除、不清除。
  • 任务A、B产生事件:设置事件组里的某一位、某些位

事件组函数

创建

使用事件组之前,要先创建,得到一个句柄;使用事件组时,要使用句柄来表明使用哪个事件组。

有两种创建方法:动态分配内存、静态分配内存。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );

/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );

删除

对于动态创建的事件组,不再需要它们时,可以删除它们以回收内存。

vEventGroupDelete可以用来删除事件组,函数原型如下:

1
2
3
4
/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )

设置事件

可以设置事件组的某个位、某些位,使用的函数有2个:

  • 在任务中使用xEventGroupSetBits()
  • 在ISR中使用xEventGroupSetBitsFromISR()

有一个或多个任务在等待事件,如果这些事件符合这些任务的期望,那么任务还会被唤醒。

函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* 返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );


/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有, pdFALSE-没有
* 返回值: pdPASS-成功, pdFALSE-失败
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );

值得注意的是,ISR中的函数,比如队列函数xQueueSendToBackFromISR、信号量函数xSemaphoreGiveFromISR,它们会唤醒某个任务,最多只会唤醒1个任务。

但是设置事件组时,有可能导致多个任务被唤醒,这会带来很大的不确定性。所以xEventGroupSetBitsFromISR函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon task)发送队列数据,由这个任务来设置事件组。

如果后台任务的优先级比当前被中断的任务优先级高,xEventGroupSetBitsFromISR会设置*pxHigherPriorityTaskWoken为pdTRUE。

如果daemon task成功地把队列数据发送给了后台任务,那么xEventGroupSetBitsFromISR的返回值就是pdPASS。

等待事件

使用xEventGroupWaitBits来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。

函数原型如下:

1
2
3
4
5
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

先引入一个概念:unblock condition。一个任务在等待事件发生时,它处于阻塞状态;当期望的时间发生时,这个状态就叫”unblock condition”,非阻塞条件,或称为”非阻塞条件成立”;当”非阻塞条件成立”后,该任务就可以变为就绪态。

函数参数说明列表如下:

参数说明
xEventGroup等待哪个事件组?
uxBitsToWaitFor等待哪些位?哪些位要被测试?
xWaitForAllBits怎么测试?是”AND”还是”OR”?
pdTRUE: 等待的位,全部为1;
pdFALSE: 等待的位,某一个为1即可
xClearOnExit函数提出前是否要清除事件?
pdTRUE: 清除uxBitsToWaitFor指定的位
pdFALSE: 不清除
xTicksToWait如果期待的事件未发生,阻塞多久。
可以设置为0:判断后即刻返回;
可设置为portMAX_DELAY:一定等到成功才返回;
可以设置为期望的Tick Count,一般用pdMS_TO_TICKS()把ms转换为Tick Count
返回值返回的是事件值,
如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值;
如果是超时退出,返回的是超时时刻的事件值。

举例如下:

事件组的值uxBitsToWaitForxWaitForAllBits说明
01000101pdTRUE任务期望bit0,bit2都为1,
当前值只有bit2满足,任务进入阻塞态;
当事件组中bit0,bit2都为1时退出阻塞态
01000110pdFALSE任务期望bit0,bit2某一个为1,
当前值满足,所以任务成功退出
01000110pdTRUE任务期望bit1,bit2都为1,
当前值不满足,任务进入阻塞态;
当事件组中bit1,bit2都为1时退出阻塞态

你可以使用xEventGroupWaitBits()等待期望的事件,它发生之后再使用xEventGroupClearBits()来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。

可以使用设置xClearOnExit为pdTRUE,使得对事件组的测试、清零都在xEventGroupWaitBits()函数内部完成,这是一个原子操作。

同步点

有一个事情需要多个任务协同,比如:

  • 任务A:炒菜
  • 任务B:买酒
  • 任务C:摆台
  • A、B、C做好自己的事后,还要等别人做完;大家一起做完,才可开饭

使用xEventGroupSync()函数可以同步多个任务:

  • 可以设置某位、某些位,表示自己做了什么事
  • 可以等待某位、某些位,表示要等等其他任务
  • 期望的时间发生后,xEventGroupSync()才会成功返回。
  • xEventGroupSync成功返回后,会清除事件

xEventGroupSync函数原型如下:

1
2
3
4
EventBits_t xEventGroupSync(    EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );

参数列表如下:

参数说明
xEventGroup哪个事件组?
uxBitsToSet要设置哪些事件?我完成了哪些事件?
比如0x05(二进制为0101)会导致事件组的bit0,bit2被设置为1
uxBitsToWaitFor等待那个位、哪些位?
比如0x15(二级制10101),表示要等待bit0,bit2,bit4都为1
xTicksToWait如果期待的事件未发生,阻塞多久。
可以设置为0:判断后即刻返回;
可设置为portMAX_DELAY:一定等到成功才返回;
可以设置为期望的Tick Count,一般用pdMS_TO_TICKS()把ms转换为Tick Count
返回值返回的是事件值,
如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值;
如果是超时退出,返回的是超时时刻的事件值。

任务通知

所谓”任务通知”,你可以反过来读”通知任务”。

使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的”通知”:

优势及限制

任务通知的优势:

  • 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
  • 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

任务通知的限制:

  • 不能发送数据给ISR:
    ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
  • 数据只能给该任务独享
    使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
    在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
  • 无法缓冲数据
    使用队列时,假设队列深度为N,那么它可以保持N个数据。
    使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
  • 无法广播给多个任务
    使用事件组可以同时给多个任务发送事件。
    使用任务通知,只能发个一个任务。
  • 如果发送受阻,发送方无法进入阻塞状态等待
    假设队列已经满了,使用xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。
    使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

通知状态和通知值

每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:

  • 一个是uint8_t类型,用来表示通知状态
  • 一个是uint32_t类型,用来表示通知值
1
2
3
4
5
6
7
8
typedef struct tskTaskControlBlock
{
......
/* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
......
} tskTCB;

通知状态有3种取值:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
1
2
3
#define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )  /* 也是初始状态 */
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )

通知值可以有很多种类型:

  • 计数值
  • 位(类似事件组)
  • 任意数值

任务通知的使用

使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。

两类函数

任务通知有2套函数,简化版、专业版,列表如下:

  • 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
  • 专业版函数支持很多参数,可以实现很多功能
简化版专业版
发出通知xTaskNotifyGive
vTaskNotifyGiveFromISR
xTaskNotify
xTaskNotifyFromISR
取出通知ulTaskNotifyTakexTaskNotifyWait

xTaskNotifyGive/ulTaskNotifyTake

在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:

  • 使得通知值加一
  • 并使得通知状态变为”pending”,也就是taskNOTIFICATION_RECEIVED,表示有数据了、待处理

可以使用ulTaskNotifyTake函数来取出通知值:

  • 如果通知值等于0,则阻塞(可以指定超时时间)
  • 当通知值大于0时,任务从阻塞态进入就绪态
  • 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零

使用ulTaskNotifyTake函数可以实现轻量级的、高效的二进制信号量、计数型信号量。

这几个函数的原型如下:

1
2
3
4
5
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

xTaskNotifyGive函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
返回值必定返回pdPASS

vTaskNotifyGiveFromISR函数的参数说明如下:

参数说明
xTaskHandle任务句柄(创建任务时得到),给哪个任务发通知
pxHigherPriorityTaskWoken被通知的任务,可能正处于阻塞状态。
此函数发出通知后,会把它从阻塞状态切换为就绪态。
如果被唤醒的任务的优先级,高于当前任务的优先级,
则”*pxHigherPriorityTaskWoken”被设置为pdTRUE,
这表示在中断返回之前要进行任务切换。

ulTaskNotifyTake函数的参数说明如下:

参数说明
xClearCountOnExit函数返回前是否清零:
pdTRUE:把通知值清零
pdFALSE:如果通知值大于0,则把通知值减一
xTicksToWait任务进入阻塞态的超时时间,它在等待通知值大于0。
0:不等待,即刻返回;
portMAX_DELAY:一直等待,直到通知值大于0;
其他值:Tick Count,可以用pdMS_TO_TICKS()把ms转换为Tick Count
返回值函数返回之前,在清零或减一之前的通知值。
如果xTicksToWait非0,则返回值有2种情况:
1. 大于0:在超时前,通知值被增加了
2. 等于0:一直没有其他任务增加通知值,最后超时返回0

xTaskNotify/xTaskNotifyWait

xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:

  • 让接收任务的通知值加一:这时xTaskNotify()等同于xTaskNotifyGive()
  • 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
  • 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
  • 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似xQueueOverwrite()函数,这就是轻量级的邮箱。

xTaskNotify()xTaskNotifyGive()更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR()是它对应的ISR版本。

这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?

使用xTaskNotifyWait()函数!它比ulTaskNotifyTake()更复杂:

  • 可以让任务等待(可以加上超时时间),等到任务状态为”pending”(也就是有数据)
  • 还可以在函数进入、退出时,清除通知值的指定位

这几个函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );

xTaskNotify函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
ulValue怎么使用ulValue,由eAction参数决定
eAction见下表
返回值pdPASS:成功,大部分调用都会成功
pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite,
并且通知状态为”pending”(表示有新数据未读),这时就会失败。

eNotifyAction参数说明:

eNotifyAction取值说明
eNoAction仅仅是更新通知状态为”pending”,未使用ulValue。
这个选项相当于轻量级的、更高效的二进制信号量。
eSetBits通知值 = 原来的通知值 | ulValue,按位或。
相当于轻量级的、更高效的事件组。
eIncrement通知值 = 原来的通知值 + 1,未使用ulValue。
相当于轻量级的、更高效的二进制信号量、计数型信号量。
相当于xTaskNotifyGive()函数。
eSetValueWithoutOverwrite不覆盖。
如果通知状态为”pending”(表示有数据未读),
则此次调用xTaskNotify不做任何事,返回pdFAIL。
如果通知状态不是”pending”(表示没有新数据),
则:通知值 = ulValue。
eSetValueWithOverwrite覆盖。
无论如何,不管通知状态是否为”pendng”,
通知值 = ulValue。

xTaskNotifyFromISR函数跟xTaskNotify很类似,就多了最后一个参数pxHigherPriorityTaskWoken。在很多ISR函数中,这个参数的作用都是类似的,使用场景如下:

  • 被通知的任务,可能正处于阻塞状态
  • xTaskNotifyFromISR函数发出通知后,会把接收任务从阻塞状态切换为就绪态
  • 如果被唤醒的任务的优先级,高于当前任务的优先级,则”*pxHigherPriorityTaskWoken”被设置为pdTRUE,这表示在中断返回之前要进行任务切换。

xTaskNotifyWait函数列表如下:

参数说明
ulBitsToClearOnEntry在xTaskNotifyWait入口处,要清除通知值的哪些位?
通知状态不是”pending”的情况下,才会清除。
它的本意是:我想等待某些事件发生,所以先把”旧数据”的某些位清零。
能清零的话:通知值 = 通知值 & ~(ulBitsToClearOnEntry)。
比如传入0x01,表示清除通知值的bit0;
传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
ulBitsToClearOnExit在xTaskNotifyWait出口处,如果不是因为超时推出,而是因为得到了数据而退出时:
通知值 = 通知值 & ~(ulBitsToClearOnExit)。
在清除某些位之前,通知值先被赋给”*pulNotificationValue”。
比如入0x03,表示清除通知值的bit0、bit1;
传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
pulNotificationValue用来取出通知值。
在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给”*pulNotificationValue”。
如果不需要取出通知值,可以设为NULL。
xTicksToWait任务进入阻塞态的超时时间,它在等待通知状态变为”pending”。
0:不等待,即刻返回;
portMAX_DELAY:一直等待,直到通知状态变为”pending”;
其他值:Tick Count,可以用pdMS_TO_TICKS()把ms转换为Tick Count
返回值1. pdPASS:成功
这表示xTaskNotifyWait成功获得了通知:
可能是调用函数之前,通知状态就是”pending”;
也可能是在阻塞期间,通知状态变为了”pending”。
2. pdFAIL:没有得到通知。

软件定时器

软件定时器的特性

我们在手机上添加闹钟时,需要指定时间、指定类型(一次性的,还是周期性的)、指定做什么事;还有一些过时的、不再使用的闹钟。

使用定时器跟使用手机闹钟是类似的:

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
  • 指定类型,定时器有两种类型:
    • 一次性(One-shot timers):
      这类定时器启动后,它的回调函数只会被调用一次;
      可以手工再次启动它,但是不会自动启动它。
    • 自动加载定时器(Auto-reload timers ):
      这类定时器启动后,时间到之后它会自动启动它;
      这使得回调函数被周期性地调用。
  • 指定要做什么事,就是指定回调函数

实际的闹钟分为:有效、无效两类。软件定时器也是类似的,它由两种状态:

  • 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
  • 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用

定时器运行情况示例如下:

  • Timer1:它是一次性的定时器,在t1启动,周期是6个Tick。经过6个tick后,在t7执行回调函数。它的回调函数只会被执行一次,然后该定时器进入冬眠状态。
  • Timer2:它是自动加载的定时器,在t1启动,周期是5个Tick。每经过5个tick它的回调函数都被执行,比如在t6、t11、t16都会执行。

软件定时器的上下文

守护任务

要理解软件定时器API函数的参数,特别是里面的xTicksToWait,需要知道定时器执行的过程。

FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。在哪里执行定时器函数?第一印象就是在Tick中断里执行:

  • 在Tick中断中判断定时器是否超时
  • 如果超时了,调用它的回调函数

FreeRTOS是RTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。

所以,FreeRTOS中,不在Tick中断中执行定时器函数。

在哪里执行?在某个任务里执行,这个任务就是:RTOS Damemon Task,RTOS守护任务。以前被称为”Timer server”,但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task。

当FreeRTOS的配置项configUSE_TIMERS被设置为1时,在启动调度器时,会自动创建RTOS Damemon Task。

我们自己编写的任务函数要使用定时器时,是通过”定时器命令队列”(timer command queue)和守护任务交互,如下图所示:

image-20210809193524596

守护任务的优先级为:configTIMER_TASK_PRIORITY;定时器命令队列的长度为configTIMER_QUEUE_LENGTH。

守护任务的调度

守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:

  • 处理命令:从命令队列里取出命令、处理
  • 执行定时器的回调函数

能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。下面使用2个例子来演示。

例子1:守护任务的优先性级较低

  • t1:Task1处于运行态,守护任务处于阻塞态。
    守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
    至于守护任务能否马上执行,取决于它的优先级。

  • t2:Task1调用xTimerStart()
    要注意的是,xTimerStart()只是把”start timer”的命令发给”定时器命令队列”,使得守护任务退出阻塞态。
    在本例中,Task1的优先级高于守护任务,所以守护任务无法抢占Task1。

  • t3:Task1执行完xTimerStart()
    但是定时器的启动工作由守护任务来实现,所以xTimerStart()返回并不表示定时器已经被启动了。

  • t4:Task1由于某些原因进入阻塞态,现在轮到守护任务运行。
    守护任务从队列中取出”start timer”命令,启动定时器。

  • t5:守护任务处理完队列中所有的命令,再次进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。

  • 注意:假设定时器在后续某个时刻tX超时了,超时时间是”tX-t2”,而非”tX-t4”,从xTimerStart()函数被调用时算起。

例子2:守护任务的优先性级较高

  • t1:Task1处于运行态,守护任务处于阻塞态。
    守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
    至于守护任务能否马上执行,取决于它的优先级。

  • t2:Task1调用xTimerStart()
    要注意的是,xTimerStart()只是把”start timer”的命令发给”定时器命令队列”,使得守护任务退出阻塞态。
    在本例中,守护任务的优先级高于Task1,所以守护任务抢占Task1,守护任务开始处理命令队列。
    Task1在执行xTimerStart()的过程中被抢占,这时它无法完成此函数。

  • t3:守护任务处理完命令队列中所有的命令,再次进入阻塞态。
    此时Task1是优先级最高的就绪态任务,它开始执行。

  • t4:Task1之前被守护任务抢占,对xTimerStart()的调用尚未返回。现在开始继续运行次函数、返回。

  • t5:Task1由于某些原因进入阻塞态,进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。

image-20210809161518141

注意,定时器的超时时间是基于调用xTimerStart()的时刻tX,而不是基于守护任务处理命令的时刻tY。假设超时时间是10个Tick,超时时间是”tX+10”,而非”tY+10”。

回调函数

定时器的回调函数的原型如下:

1
void ATimerCallback( TimerHandle_t xTimer );

定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。

所以,定时器的回调函数不要影响其他人:

  • 回调函数要尽快实行,不能进入阻塞状态

  • 不要调用会导致阻塞的API函数,比如vTaskDelay()

  • 可以调用xQueueReceive()之类的函数,但是超时时间要设为0:即刻返回,不可阻塞

软件定时器的函数

根据定时器的状态转换图,就可以知道所涉及的函数:

创建

要使用定时器,需要先创建它,得到它的句柄。

有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 使用动态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以Tick为单位
* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* 返回值: 成功则返回TimerHandle_t, 否则返回NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );

/* 使用静态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以Tick为单位
* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* pxTimerBuffer: 传入一个StaticTimer_t结构体, 将在上面构造定时器
* 返回值: 成功则返回TimerHandle_t, 否则返回NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer );

回调函数的类型是:

1
2
3
void ATimerCallback( TimerHandle_t xTimer );

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

删除

动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:

1
2
3
4
5
6
7
/* 删除定时器
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

定时器的很多API函数,都是通过发送”命令”到命令队列,由守护任务来实现。

如果队列满了,”命令”就无法即刻写入队列。我们可以指定一个超时时间xTicksToWait,等待一会。

启动/停止

启动定时器就是设置它的状态为运行态(Running、Active)。

停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。

涉及的函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 启动定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 启动定时器(ISR版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL表示"启动命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );

/* 停止定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"停止命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 停止定时器(ISR版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL表示"停止命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );

注意,这些函数的xTicksToWait表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。

xTicksToWait不是定时器本身的超时时间,不是定时器本身的”周期”。

创建定时器时,设置了它的周期(period)。xTimerStart()函数是用来启动定时器。假设调用xTimerStart()的时刻是tX,定时器的周期是n,那么在tX+n时刻定时器的回调函数被调用。

如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当于执行xTimerReset(),重新设定它的启动时间。

复位

从定时器的状态转换图可以知道,使用xTimerReset()函数可以让定时器的状态从冬眠态转换为运行态,相当于使用xTimerStart()函数。

如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。假设调用xTimerReset()的时刻是tX,定时器的周期是n,那么tX+n就是重新确定的超时时间。

复位函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 复位定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"复位命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 复位定时器(ISR版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL表示"停止命令"无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );

修改周期

从定时器的状态转换图可以知道,使用xTimerChangePeriod()函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。

修改定时器的周期时,会使用新的周期重新计算它的超时时间。假设调用xTimerChangePeriod()函数的时间tX,新的周期是n,则tX+n就是新的超时时间。

相关函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* xTicksToWait: 超时时间, 命令写入队列的超时时间
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );

/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken );

定时器ID

定时器的结构体如下,里面有一项pvTimerID,它就是定时器ID:

怎么使用定时器ID,完全由程序来决定:

  • 可以用来标记定时器,表示自己是什么定时器
  • 可以用来保存参数,给回调函数使用

它的初始值在创建定时器时由xTimerCreate()这类函数传入,后续可以使用这些函数来操作:

  • 更新ID:使用vTimerSetTimerID()函数
  • 查询ID:查询pvTimerGetTimerID()函数

这两个函数不涉及命令队列,它们是直接操作定时器结构体。

函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* 获得定时器的ID
* xTimer: 哪个定时器
* 返回值: 定时器的ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );

/* 设置定时器的ID
* xTimer: 哪个定时器
* pvNewID: 新ID
* 返回值: 无
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );

中断管理

两套API函数

为什么需要两套API

在任务函数中,我们可以调用各类API函数,比如队列操作函数:xQueueSendToBack。但是在ISR中使用这个函数会导致问题,应该使用另一个函数:xQueueSendToBackFromISR,它的函数名含有后缀”FromISR”,表示”从ISR中给队列发送数据”。

FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有”FromISR”后缀。

为什么要引入两套API函数?

  • 很多API函数会导致任务计入阻塞状态:
    • 运行这个函数的任务进入阻塞状态
    • 比如写队列时,如果队列已满,可以进入阻塞状态等待一会
  • ISR调用API函数时,ISR不是”任务”,ISR不能进入阻塞状态
  • 所以,在任务中、在ISR中,这些函数的功能是有差别的

为什么不使用同一套函数,比如在函数里面分辨当前调用者是任务还是ISR呢?示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BaseType_t xQueueSend(...)
{
if (is_in_isr())
{
/* 把数据放入队列 */

/* 不管是否成功都直接返回 */
}
else /* 在任务中 */
{
/* 把数据放入队列 */
/* 不成功就等待一会再重试 */
}
}

FreeRTOS使用两套函数,而不是使用一套函数,是因为有如下好处:

  • 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,是的函数更长、更复杂、难以测试

  • 在任务、ISR中调用时,需要的参数不一样,比如:

    • 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会
    • 在ISR中调用:不需要指定超时时间,无论是否成功都要即刻返回
    • 如果强行把两套函数揉在一起,会导致参数臃肿、无效
  • 移植FreeRTOS时,还需要提供监测上下文的函数,比如is_in_isr()

  • 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于ISR中,就需要额外添加更多、更复杂的代码

使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时,即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数,你无法修改库函数。这个问题可以解决:

  • 把中断的处理推迟到任务中进行(Defer interrupt processing),在任务中调用库函数
  • 尝试在库函数中使用”FromISR”函数:
    • 在任务中、在ISR中都可以调用”FromISR”函数
    • 反过来就不行,非FromISR函数无法在ISR中使用
  • 第三方库函数也许会提供OS抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数

两套API函数列表

类型在任务中在ISR中
队列(queue)xQueueSendToBackxQueueSendToBackFromISR
xQueueSendToFrontxQueueSendToFrontFromISR
xQueueReceivexQueueReceiveFromISR
xQueueOverwritexQueueOverwriteFromISR
xQueuePeekxQueuePeekFromISR
信号量(semaphore)xSemaphoreGivexSemaphoreGiveFromISR
xSemaphoreTakexSemaphoreTakeFromISR
事件组(event group)xEventGroupSetBitsxEventGroupSetBitsFromISR
xEventGroupGetBitsxEventGroupGetBitsFromISR
任务通知(task notification)xTaskNotifyGivevTaskNotifyGiveFromISR
xTaskNotifyxTaskNotifyFromISR
软件定时器(software timer)xTimerStartxTimerStartFromISR
xTimerStopxTimerStopFromISR
xTimerResetxTimerResetFromISR
xTimerChangePeriodxTimerChangePeriodFromISR

xHigherPriorityTaskWoken参数

xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。

还是以写队列为例。

任务A调用xQueueSendToBack()写队列,有几种情况发生:

  • 队列满了,任务A阻塞等待,另一个任务B运行
  • 队列没满,任务A成功写入队列,但是它导致另一个任务B被唤醒,任务B的优先级更高:任务B先运行
  • 队列没满,任务A成功写入队列,即刻返回

可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做”context switch”,上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。

xQueueSendToBackFromISR()函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

/* 用法示例 */

BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);

if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}

pxHigherPriorityTaskWoken参数,就是用来保存函数的结果:是否需要切换

  • *pxHigherPriorityTaskWoken等于pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR应该进行任务切换
  • *pxHigherPriorityTaskWoken等于pdFALSE:没有进行任务切换的必要

为什么不在”FromISR”函数内部进行任务切换,而只是标记一下而已呢?为了效率!示例代码如下:

1
2
3
4
5
6
7
8
void XXX_ISR()
{
int i;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(...); /* 被多次调用 */
}
}

ISR中有可能多次调用”FromISR”函数,如果在”FromISR”内部进行任务切换,会浪费时间。解决方法是:

  • 在”FromISR”中标记是否需要切换
  • 在ISR返回之前再进行任务切换
  • 示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(.. &xHigherPriorityTaskWoken); /* 被多次调用 */
}

/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
}

上述的例子很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。

在ISR中调用API时不进行任务切换,而只是在”xHigherPriorityTaskWoken”中标记一下,除了效率,还有多种好处:

  • 效率高:避免不必要的任务切换
  • 让ISR更可控:中断随机产生,在API中进行任务切换的话,可能导致问题更复杂
  • 可移植性
  • 在Tick中断中,调用vApplicationTickHook():它运行与ISR,只能使用”FromISR”的函数

使用”FromISR”函数时,如果不想使用xHigherPriorityTaskWoken参数,可以设置为NULL。

怎么切换任务

FreeRTOS的ISR函数中,使用两个宏进行任务切换:

1
2
3
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );

portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

这两个宏做的事情是完全一样的,在老版本的FreeRTOS中,

  • portEND_SWITCHING_ISR使用汇编实现
  • portYIELD_FROM_ISR使用C语言实现

新版本都统一使用portYIELD_FROM_ISR

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(.. &xHigherPriorityTaskWoken); /* 被多次调用 */
}

/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken为pdTRUE时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

中断的延迟处理

前面讲过,ISR要尽量快,否则:

  • 其他低优先级的中断无法被处理:实时性无法保证
  • 用户任务无法被执行:系统显得很卡顿
  • 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套

如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:

  • ISR:尽快做些清理、记录工作,然后触发某个任务
  • 任务:更复杂的事情放在任务中处理

这种处理方式叫”中断的延迟处理”(Deferring interrupt processing),处理流程如下图所示:

  • t1:任务1运行,任务2阻塞
  • t2:发生中断,
    • 该中断的ISR函数被执行,任务1被打断
    • ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
  • t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是任务2,它要完成中断的处理。任务2就被称为”deferred processing task”,中断的延迟处理任务。
  • t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行

中断与任务间的通信

前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。

要注意的是,在ISR中使用的函数要有”FromISR”后缀。

资源管理

屏蔽中断

屏蔽中断有两套宏:任务中使用、ISR中使用:

  • 任务中使用:taskENTER_CRITICA()/taskEXIT_CRITICAL()
  • ISR中使用:taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()

在任务中屏蔽中断

在任务中屏蔽中断的示例代码如下:

1
2
3
4
5
6
7
8
9
/* 在任务中,当前时刻中断是使能的
* 执行这句代码后,屏蔽中断
*/
taskENTER_CRITICAL();

/* 访问临界资源 */

/* 重新使能中断 */
taskEXIT_CRITICAL();

taskENTER_CRITICA()/taskEXIT_CRITICAL()之间:

  • 低优先级的中断被屏蔽了:优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
    • 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
  • 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生

这套taskENTER_CRITICA()/taskEXIT_CRITICAL()宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为0时,调用taskEXIT_CRITICAL()才会重新使能中断。

使用taskENTER_CRITICA()/taskEXIT_CRITICAL()来访问临界资源是很粗鲁的方法:

  • 中断无法正常运行
  • 任务调度无法进行
  • 所以,之间的代码要尽可能快速地执行

在ISR中屏蔽中断

要使用含有”FROM_ISR”后缀的宏,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void vAnInterruptServiceRoutine( void )
{
## 用来记录当前中断是否使能 */
UBaseType_t uxSavedInterruptStatus;

## 在ISR中,当前时刻中断可能是使能的,也可能是禁止的
## * 所以要记录当前状态, 后面要恢复为原先的状态
## * 执行这句代码后,屏蔽中断
## */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

## 访问临界资源 */

## 恢复中断状态 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
## 现在,当前ISR可以被更高优先级的中断打断了 */
}

taskENTER_CRITICA_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()之间:

  • 低优先级的中断被屏蔽了:优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
    • 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
  • 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生

暂停调度器

如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。

如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。

使用这2个函数来暂停、恢复调度器:

1
2
3
4
5
6
7
8
/* 暂停调度器 */
void vTaskSuspendAll( void );

/* 恢复调度器
* 返回值: pdTRUE表示在暂定期间有更高优先级的任务就绪了
*## 可以不理会这个返回值
*/
BaseType_t xTaskResumeAll( void );

示例代码如下:

1
2
3
4
5
vTaskSuspendScheduler();

/* 访问临界资源 */

xTaskResumeScheduler();

这套vTaskSuspendScheduler()/xTaskResumeScheduler()宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为0时,调用taskEXIT_CRITICAL()才会重新使能中断。

参考原文地址

本文作者:wxy

本文链接: https://c.undf.top/posts/1rvt88k/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

本站不设评论,网站底部邮件图标可以联系博主

评论

如评论失效,请点击最下方的邮件图标联系作者。