FreeRTOS(操作与内部机制)

目录

一、概述

1.1 前缀的含义

2.1 目录结构

1.3 移植时涉及的文件

1.4 头文件相关

1.4.1 头文件目录

1.4.2 头文件

1.5 内存管理

二、任务的创建与删除

2.1、普通任务创建

2.2、创建静态任务

2.3、任务删除

三、任务优先级和TICK

3.1 任务优先级

3.2 修改优先级

3.3 Tick

3.4 任务状态

3.5 Delay函数

3.6 空闲任务及其钩子函数

3.6.1 为什么必须要有空闲任务?

3.6.2 钩子函数(Idle Task Hook Functions):空闲任务里的主函数,作用:

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

3.6.4 使用钩子函数的前提

3.7 调度算法

四、 同步互斥与通信

4.3 各类方法的对比

五、 队列(queue)

5.1.2  传输数据的两种方法

5.1.3 队列的阻塞访问

5.2 队列函数

5.2.1 创建

5.2.2 复位

5.2.3 删除

5.2.4 写队列

5.2.5 读队列

 5.2.6 查询 

5.2.7 覆盖 / 窥视

第六章 信号量(semaphore)

6.1 信号量的特性

6.2 信号量函数

6.2.1 创建

6.2.2 删除

6.2.3 give / take

第七章 互斥量(mutex)

7.1 互斥量的使用场合 

7.2 互斥量函数

7.2.1 创建

7.2.2 其他函数

7.3 示例15: 互斥量基本使用

7.4 示例16: 谁上锁就由谁解锁?

7.5 示例17: 优先级反转

7.6 示例18: 优先级继承

7.7 递归锁

7.7.1 死锁的概念

7.7.2 自我死锁

7.7.3 函数

7.7.4 示例19: 递归锁

第八章 事件组(event group)

8.1 事件组概念与操作

8.1.1 事件组的概念

8.1.2 事件组的操作

8.2 事件组函数

8.2.1 创建

8.2.2 删除

8.2.3 设置事件

8.2.4 等待事件

8.2.5 同步点

第九章 任务通知(Task Notifications)

9.1 任务通知的特性

9.1.1 优势及限制

9.1.2 通知状态和通知值

第十章 软件定时器(software timer)

第十一章 中断管理(Interrupt Management)

第十二章 资源管理(Resource Management)



一、概述

1.1 前缀的含义

变量名前缀含义
cchar
sint16_t,short
lint32_t,long
xBaseType_t,
其他非标准的类型:结构体、task handle、queue handle等
uunsigned
p指针
ucuint8_t,unsigned char
pcchar指针
函数名前缀含义
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
通用宏的定义
pdTRUE1
pdFALSE0
pdPASS1
pdFAIL0

栈的作用:保护现场(调用关系、局部变量、函数返回地址、任务现场等)

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.hFreeRTOS的配置文件,比如选择调度算法: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),如下:
  1. 当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
  2. 其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点

可抢占的前提下,同优先级的任务是否轮流执行(配置项: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释放资源而被唤醒

各类操作方法的区别

内核对
生产者消费 者数据/状态说明
队列ALLALL数据:若干个数据
谁都可以往队列里扔数据,
谁都可以从队列里读数据
用来传递数据,
发送者、接收者无限制,
一个数据只能唤醒一个接收者
事件组ALLALL多个位:或、与
谁都可以设置(生产)多个位,
谁都可以等待某个位、若干个位

用来传递事件,

可以是N个事件,
发送者、接受者无限制,
可以唤醒多个接收者:像广播

信号量ALLALL数量: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队列句柄,要读哪个队列
pvBufferbufer指针,队列的数据会被复制到这个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中使用
givexSemaphoreGivexSemaphoreGiveFromISR
takexSemaphoreTakexSemaphoreTakeFromISR

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次,这个锁才会被释放
     

递归锁的函数根一般互斥量的函数名不一样,参数类型一样,列表如下:

递归锁一般互斥量
创建xSemaphoreCreateRecursiveMutexxSemaphoreCreateMutex
获得xSemaphoreTakeRecursivexSemaphoreTake
释放xSemaphoreGiveRecursivexSemaphoreGive

/* 创建一个递归锁,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非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的,怎么会影响事件组?这只是基于效率来考虑
  1. 若configUSE_16_BIT_TICKS是1,表示该处理器使用16位更高效,所以事件组也使用16位
  2. 若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"?
pdTRUE: 等待的位,全部为1;
pdFALSE: 等待的位,某一个为1即可

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()
函数内部完成,这是一个原子操作。
 

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() 给队列发送数据时,任务可以进入阻塞状态等待
发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

9.1.2 通知状态和通知值


 

第十章 软件定时器(software timer)

第十一章 中断管理(Interrupt Management)

第十二章 资源管理(Resource Management)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值