目录
3.6.2 钩子函数(Idle Task Hook Functions):空闲任务里的主函数,作用:
第十一章 中断管理(Interrupt Management)
第十二章 资源管理(Resource Management)
一、概述
1.1 前缀的含义
变量名前缀 | 含义 |
c | char |
s | int16_t,short |
l | int32_t,long |
x | BaseType_t, 其他非标准的类型:结构体、task handle、queue handle等 |
u | unsigned |
p | 指针 |
uc | uint8_t,unsigned char |
pc | char指针 |
函数名前缀 | 含义 |
vTaskPrioritySet | 返回值类型:void 在task.c中定义 |
xQueueReceive | 返回值类型:BaseType_t 在queue.c中定义 |
pvTimerGetTimerID | 返回值类型:pointer to void 在tmer.c中定义 |
宏的前缀 | 含义:在哪个文件里定义 |
port (比如portMAX_DELAY) | portable.h或portmacro.h |
task (比如taskENTER_CRITICAL()) | task.h |
pd (比如pdTRUE) | projdefs.h |
config (比如configUSE_PREEMPTION) | FreeRTOSConfig.h |
err (比如errQUEUE_FULL) | projdefs.h |
通用宏的定义 | 值 |
pdTRUE | 1 |
pdFALSE | 0 |
pdPASS | 1 |
pdFAIL | 0 |
栈的作用:保护现场(调用关系、局部变量、函数返回地址、任务现场等)
2.1 目录结构
FreeRTOS的最核心文件只有2个:
- FreeRTOS/Source/tasks.c
- FreeRTOS/Source/list.c
其他文件的作用也一起列表如下:
FreeRTOS/Source/下的文件 | 作用 |
tasks.c | 必需,任务操作 |
list.c | 必须,列表 |
queue.c | 基本必需,提供队列操作、信号量(semaphore)操作 |
timer.c | 可选,software timer |
event_groups.c | 可选,提供event group功能 |
croutine.c | 可选,过时了 |
1.3 移植时涉及的文件
移植FreeRTOS时涉及的文件放在 FreeRTOS/Source/portable/[compiler]/[architecture] 目录
下。 比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。
里面有2个文件:
- port.c
- portmacro.h
1.4 头文件相关
1.4.1 头文件目录
FreeRTOS需要3个头文件目录:
- FreeRTOS本身的头文件:FreeRTOS/Source/include
- 移植时用到的头文件:FreeRTOS/Source/portable/[compiler]/[architecture]
- 含有配置文件FreeRTOSConfig.h的目录
1.4.2 头文件
头文件 | 作用 |
FreeRTOSConfig.h | FreeRTOS的配置文件,比如选择调度算法:configUSE_PREEMPTION 每个demo都必定含有FreeRTOSConfig.h 建议去修改demo中的FreeRTOSConfig.h,而不是从头写一个 |
FreeRTOS.h | 使用FreeRTOS API函数时,必须包含此文件。 在FreeRTOS.h之后,再去包含其他头文件,比如: task.h、queue.h、semphr.h、event_group.h |
1.5 内存管理
文件 | 优点 | 缺点 |
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
二、任务的创建与删除
2.1、普通任务创建
任务的参数:pvParameters,用在函数参数里: void vTask1( void *pvParameters )
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //函数(任务)
const char * const pcName, //任务名(不重要)
const configSTACK_DEPTH_TYPE usStackDepth, //栈大小
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。 |
2.2、创建静态任务
使用 xTaskCreateStatic() 函数静态创建任务。该函数与 xTaskCreate() 类似,但它使用静态分配的任务控制块和堆栈空间,而不是在运行时动态分配。函数原型如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer );
参数 | 描述 |
puxStackBuffer | 静态分配的栈 |
举例:静态创建任务
#define taskSensorScan_Priority (5)
#define taskSensorScan_StackDepth (128)
static TaskHandle_t taskSensorScan_Handle = NULL; //任务返回值
static StackType_t taskSensorScan_Stack[taskSensorScan_StackDepth]; //任务内存
static void taskSensorScanHanlder(void* pvParameters);
static StaticTask_t taskSensorScan_TCB;
taskSensorScan_Handle = xTaskCreateStatic((TaskFunction_t )taskSensorScanHanlder,
(const char* )"taskMoudleScan",
(uint32_t )taskSensorScan_StackDepth,
(void* )NULL,
(UBaseType_t )taskSensorScan_Priority,
(StackType_t* )taskSensorScan_Stack,
(StaticTask_t* )&taskSensorScan_TCB);
// if( NULL != taskSensorScan_Handle )
// printf("taskSensorScan_Handle Create success!\n");
2.3、任务删除
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数 | 描述 |
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行 vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄
- 杀人:执行 vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
三、任务优先级和TICK
3.1 任务优先级
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
3.2 修改优先级
使用uxTaskPriorityGet来获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用vTaskPrioritySet 来设置任务的优先级,
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
优先级的一些宏定义
可抢占:高优先级的任务先运行 若不抢占,则空闲任务永远礼让,任务也不会轮转
时间片轮转:同优先级的任务可轮流执行
空闲任务礼让:如果有同优先级0的其他就绪任务,空闲任务主动礼让一次运行机会
3.3 Tick
- 对于同优先级的任务,它们“轮流”执行,使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每1ms发生一次时钟中断。
- 两次中断之间的时间被称为时间片(time slice、tick period)。
- 时间片的长度由 configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms。
有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100,Tick周期时10ms,等待20ms
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。
这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
3.4 任务状态
freeRTOS任务的状态有四种:运行、就绪、阻塞、挂起。
运行态(runnnig):当任务正在运行,此时的状态被称为运行态,即CPU的使用权被这个任务占用;
就绪态(ready):任务已经具备了运行条件(没有被挂起或阻塞),但是又更高优先级或同优先级的任务正在运行,所以需要等待的状态。
挂起态(suspended):任务被暂时停止,通过调用挂起函数(vTaskSuspend())可以把指定任务挂起,任务挂起后暂时不会运行,只有调用恢复函数(xTaskResume())才可以退出挂起状态;
阻塞态(blocked):任务在等待信号量、消息队列、事件标准组、系统延时时,被称为阻塞态,如果等待的事件到了,就会自动退出阻塞态,准备运行;
3.5 Delay函数
有两个Delay函数:
- vTaskDelay (n): 至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil (&Pre, n): 等待到指定的绝对时刻,才能变为就绪态
void vTaskDelay( const TickType_t xTicksToDelay ); //xTicksToDelay:等待多少给Tick
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
vTaskDelay:指定的是阻塞的时间
vTaskDelayUntil:指定的是任务执行的间隔、周期
3.6 空闲任务及其钩子函数
3.6.1 为什么必须要有空闲任务?
一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务。需要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
3.6.2 钩子函数(Idle Task Hook Functions):空闲任务里的主函数,作用:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着无重要的事情要做,当然可以进入省电模式
3.6.3 空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用 vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存
3.6.4 使用钩子函数的前提
在 FreeRTOS\Source\tasks.c 中,可以看到如下代码,所以前提就是:
- 把这个宏定义为1:configUSE_IDLE_HOOK
- 实现 vApplicationIdleHook 函数
3.7 调度算法
所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。
通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、
configUSE_TIME_SLICING
可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
- 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
- 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling),如下:
- 当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
- 其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
- 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片
- 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占
在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:
configIDLE_SHOULD_YIELD)
- 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
- 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
四、 同步互斥与通信
理解同步与互斥: 举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
4.3 各类方法的对比
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
- A获取资源,用完后A释放资源
- A获取不到资源则阻塞,B释放资源并把A唤醒
- A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为B释放资源而被唤醒
各类操作方法的区别
内核对 象 | 生产者 | 消费 者 | 数据/状态 | 说明 |
队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据, 谁都可以从队列里读数据 | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位 | 用来传递事件, 可以是N个事件, |
信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量 | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者 |
任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者 | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
互斥量 | 只能A开锁 | A上锁 | 位:0、1 我上锁:1变为0, 只能由我开锁:0变为1 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
五、 队列(queue)
- 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
- 每个数据大小固定
- 创建队列时就要指定长度、数据大小
- 数据的操作采用先进先出(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
- 也可以强制写队列头部:覆盖头部数据
正常读写队列:先进先出,先写10,再写20。 读队列时,先写的先读 ,读出一个数据。
5.1.2 传输数据的两种方法
使用队列传输数据时有两种方法:
- 拷贝:把数据、把变量的值复制进队列里
- 引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法,这更简单:
- 局部变量的值发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
- 无需分配buffer来保存数据,队列中有buffer
- 局部变量可以马上再次使用
- 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
- 如果数据实在太大,你还是可以使用队列传输它的地址
- 队列的空间有FreeRTOS内核分配,无需任务操心
- 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列
5.1.3 队列的阻塞访问
只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可多个任务读写队列。
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态(队列来数据时,会将等数据的任务从阻塞态链表放置就绪态链表)。如果一直都没有数据,则时间到之后它也会进入就绪态。
既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
读队列、写队列函数里做的事情:
5.2 队列函数
5.2.1 创建
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
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 |
5.2.2 复位
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset( ) 把队列恢复为初始状态,此函数原型为:
BaseType_t xQueueReset ( QueueHandle_t pxQueue);
/* pxQueue : 复位哪个队列; 返回值 : pdPASS(必定成功) */
5.2.3 删除
删除队列的函数为 vQueueDelete( ) ,只能删除使用动态方法创建的队列,它会释放内存。
void vQueueDelete( QueueHandle_t xQueue );
5.2.4 写队列
可以把数据写到队列头部,也可写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。
参数 | 说明 |
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为 xTicksToWait */
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t 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 );
5.2.5 读队列
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken );
参数 | 说明 |
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
5.2.6 查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/* 返回队列中可用数据的个数 */
UBaseType_t uxQueueMessagesWaiting ( const QueueHandle_t xQueue );
/* 返回队列中可用空间的个数 */
UBaseType_t uxQueueSpacesAvailable ( const QueueHandle_t xQueue );
5.2.7 覆盖 / 窥视
当队列长度为1时,可以使用 xQueueOverwrite( ) 或 xQueueOverwriteFromISR() 来覆盖数据。
注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,意味着函数不会被阻塞。
BaseType_t xQueueOverwrite( QueueHandle_t xQueue,
const void * pvItemToQueue );
BaseType_t xQueueOverwriteFromISR( QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken );
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是 xQueuePeek( ) 或 xQueuePeekFromISR( ) 。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
void *pvBuffer, );
第六章 信号量(semaphore)
6.1 信号量的特性
- 信号:起通知作用
- 量:还可以用来表示资源的数量
当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores),被创建时初始值可以设定。
当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores),被创建时初始值为0。
计数型信号量的典型场景是:
- 计数:事件产生时"give"信号量,让计数值加1;处理事件时要先"take"信号量,就是获得信号量,让计数值减1。
- 资源管理:要想访问资源需要先"take"信号量,让计数值减1;用完资源后"give"信号量,让计数值加1
信号量是特殊的队列
队列 | 信号量 |
可以容纳多个数据, 创建队列时有2部分内存: 队列结构体、存储数 据的空间 | 只有计数值,无法容纳其他数据。 创建信号量时,只需要分配信号量结构体 |
生产者:没有空间存入数据时可以阻塞 | 生产者:用于不阻塞,计数值已经达到最大时 返回失败 |
消费者:没有数据时可以阻塞 | 消费者:没有资源时可以阻塞 |
6.2 信号量函数
使用信号量时,先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量。
6.2.1 创建
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。
- 创建 二进制信号量 的函数原型如下:(二进制过时了)
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功 */
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功 */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t
*pxSemaphoreBuffer )
- 创建 计数型信号量 的函数原型如下
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 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 );
6.2.2 删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下
/* xSemaphore: 信号量句柄,你要删除哪个信号量 */
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
6.2.3 give / take
二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:
在任务中使用 | 在ISR中使用 | |
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
xSemaphoreGive的函数原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
参数 | 说明 |
xSemaphore | 信号量句柄,释放哪个信号量 |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
xSemaphoreGiveFromISR的函数原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );
参数 | 说明 |
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
xSemaphoreTake的函数原型如下
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
参数 | 说明 |
xSemaphore | 信号量句柄,获取哪个信号量 |
xTicksToWait | 如果无法马上获得信号量,阻塞一会: 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick个数,可以使用 pdMS_TO_TICKS() 来指定阻塞时间为若干ms |
返回值 | pdTRUE表示成功 |
xSemaphoreTakeFromISR的函数原型如下:
BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
参数 | 说明 |
xSemaphore | 信号量句柄,获取哪个信号量 |
pxHigherPriorityTaskWoken | 如果获取信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功 |
第七章 互斥量(mutex)
使用互斥量可以解决这个问题,互斥量的名字取得很好:
量:值为0、1 互斥:用来实现互斥访问 核心在于:谁上锁,就只能由谁开锁。
很奇怪的是,FreeRTOS的互斥锁,并没有在代码上实现这点:
- 即使任务A获得了互斥锁,任务B竟然也可以释放互斥锁。
- 谁上锁、谁释放:只是约定。
7.1 互斥量的使用场合
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。
任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。
互斥量也被称为互斥锁,使用过程如下:
- 互斥量初始值为1
- 任务A想访问临界资源,先获得并占有互斥量,然后开始访问
- 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
- 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
- 任务B使用完毕,释放互斥量
正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。
但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量
7.2 互斥量函数
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
##define configUSE_MUTEXES 1
7.2.1 创建
互斥量是一种特殊的二进制信号量。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer
);
7.2.2 其他函数
要注意的是,互斥量不能在ISR中使用
各类操作函数,比如删除、give/take,跟一般是信号量是一样的。
删除:
/* 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);
7.3 示例15: 互斥量基本使用
使用互斥量时有如下特点:
- 刚创建的互斥量可以被成功"take"
- "take"互斥量成功的任务,被称为"holder",只能由它"give"互斥量;别的任务"give"不成功
- 在ISR中不能使用互斥量
7.4 示例16: 谁上锁就由谁解锁?
互斥量、互斥锁,本来的概念确实是:谁上锁就得由谁解锁。
但是FreeRTOS并没有实现这点,只是要求程序员按照这样的惯例写代码。
A任务获取互斥量(上锁),可由B任务释放互斥量(解锁)。
7.5 示例17: 优先级反转
假设任务A、B都想使用串口,A优先级比较低:
任务A获得了串口的互斥量
任务B也想使用串口,它将会阻塞、等待A释放互斥量
高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)
如果涉及3个任务,可以让"优先级反转"的后果更加恶劣。
互斥量可以通过"优先级继承",可以很大程度解决"优先级反转"的问题,这也是FreeRTOS中互斥量和二级制信号量的差别。
7.6 示例18: 优先级继承
实现了:提升低优先级任务的优先级,让它能尽快运行、释放锁,之后恢复其优先级。降低优先级反转的影响。
/* 创建互斥量 / 二进制信号量 */
//xLock = xSemaphoreCreateBinary( ); //会有优先级反转
xLock = xSemaphoreCreateMutex( ); //有优先级继承,不会出现优先级反转
二进制信号量会出现 "优先级反转” 的现象。
举例:低优先级任务A先获取了二进制信号量,还没来得及释放信号量就轮到高优先级的任务B,任务B也去获取信号量,由于任务A来不及释放,所有任务B会获取不了
互斥量有 优先级继承 功能,不会优先级反转。
举例同上,任务A先获取互斥量,还未释放任务B就运行。此时让任务A继承任务B的优先级,任务A得以运行释放互斥量,释放后恢复原来的优先级。之后任务B再去获取就不会阻塞了。
7.7 递归锁
7.7.1 死锁的概念
- A获得了互斥量M1
- B获得了互斥量M2
- A还要获得互斥量M2才能运行,结果A阻塞
- B还要获得互斥量M1才能运行,结果B阻塞
- A、B都阻塞,再无法释放它们持有的互斥量
- 死锁发生
7.7.2 自我死锁
- 任务A获得了互斥锁M
- 它调用一个库函数
- 库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!
- 死锁发生!
7.7.3 函数
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
- 任务A获得递归锁M后,它还可以多次去获得这个锁
- "take"了N次,要"give"N次,这个锁才会被释放
递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下:
递归锁 | 一般互斥量 | |
创建 | xSemaphoreCreateRecursiveMutex | xSemaphoreCreateMutex |
获得 | xSemaphoreTakeRecursive | xSemaphoreTake |
释放 | xSemaphoreGiveRecursive | xSemaphoreGive |
/* 创建一个递归锁,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait);
7.7.4 示例19: 递归锁
递归锁实现了:谁上锁就由谁解锁
第八章 事件组(event group)
8.1 事件组概念与操作
8.1.1 事件组的概念
事件组可以简单地认为就是一个整数:
- 整数的每一位表示一个事件
- 每一位事件的含义由程序员决定,如: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位
8.1.2 事件组的操作
事件组和队列、信号量等不太一样,主要集中在2个地方:
唤醒谁?
- 队列、信号量:事件发生时,只会唤醒一个任务
- 事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有"广播"的作用
是否清除事件?
- 队列、信号量:是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
- 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件
8.2 事件组函数
8.2.1 创建
使用事件组之前,要先创建,得到一个句柄;使用事件组时,要使用句柄来表明使用哪个事件组。
有两种创建方法:动态分配内存、静态分配内存。函数原型如下:
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功 */
EventGroupHandle_t xEventGroupCreate( void );
/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功 */
EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer);
8.2.2 删除
对于动态创建的事件组,不再需要它们时,可以删除它们以回收内存。
vEventGroupDelete可以用来删除事件组,函数原型如下:
/* xEventGroup: 事件组句柄,你要删除哪个事件组*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
8.2.3 设置事件
可以设置事件组的某个位、某些位,使用的函数有2个:
- 在任务中使用 xEventGroupSetBits()
- 在ISR中使用 xEventGroupSetBitsFromISR()
/* 设置事件组中的位
* 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。
8.2.4 等待事件
使用 xEventGroupWaitBits 来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位; 等到期望的事件后,还可以清除某些位。
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 | 等待哪些位?哪些位要被测试? |
xClearOnExit | 函数提出前是否要清除事件? pdTRUE: 清除uxBitsToWaitFor指定的位 pdFALSE: 不清除 |
xWaitForAllBits | 怎么测试?是"AND"还是"OR"? |
xTicksToWait | 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用 pdMS_TO_TICKS() 把ms转换为Tick Count |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
举例如下:
事件组的 值 | uxBitsToWaitFor | xWaitForAllBits | 说明 |
0100 | 0101 | pdTRUE | 任务期望bit0,bit2都为1, 当前值只有bit2满足,任务进入阻塞态; 当事件组中bit0,bit2都为1时退出阻塞态 |
0100 | 0110 | pdFALSE | 任务期望bit0,bit2某一个为1, 当前值满足,所以任务成功退出 |
0100 | 0110 | pdTRUE | 任务期望bit1,bit2都为1, 当前值不满足,任务进入阻塞态; 当事件组中bit1,bit2都为1时退出阻塞态 |
你可以使用 xEventGroupWaitBits() 等待期望的事件,它发生之后再使用 xEventGroupClearBits()
来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。
可以使用设置 xClearOnExit 为pdTRUE,使得对事件组的测试、清零都在 xEventGroupWaitBits()
函数内部完成,这是一个原子操作。
8.2.5 同步点
有一个事情需要多个任务协同,比如:
- 任务A:炒菜
- 任务B:买酒
- 任务C:摆台
A、B、C做好自己的事后,还要等别人做完;大家一起做完,才可开饭
使用 xEventGroupSync() 函数可以同步多个任务:
- 可以设置某位、某些位,表示自己做了什么事
- 可以等待某位、某些位,表示要等等其他任务
- 期望的时间发生后, xEventGroupSync() 才会成功返回。
- xEventGroupSync 成功返回后,会清除事件
xEventGroupSync 函数原型如下:
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 |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值 |
第九章 任务通知(Task Notifications)
我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:
使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知":
9.1 任务通知的特性
9.1.1 优势及限制
任务通知的优势:
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
- 更节省内存:使用其他方法都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的限制:
- 不能发送数据给ISR:
ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知
的功能,发数据给任务。
- 数据只能给该任务独享
使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。
使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把
一个数据源的数据发给多个任务。
- 无法缓冲数据
使用队列时,假设队列深度为N,那么它可以保持N个数据。
使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
- 无法广播给多个任务
使用事件组可以同时给多个任务发送事件。
使用任务通知,只能发个一个任务。
- 如果发送受阻,发送方无法进入阻塞状态等待
假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待
发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。