同步与互斥
同步:两任务要协调
互斥:两任务要争用
举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
仅仅使用简单的全局变量的方法实现同步与互斥是有缺陷的
可以实现同步与互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
-
队列(Queue)
- 功能:用于在任务之间传递数据。队列是一种先进先出(FIFO)数据结构,可以存储一定数量的数据项。
- 用途:适用于需要传递数据或消息的场景,例如生产者-消费者模式。任务可以将数据发送到队列中,另一个任务可以从队列中接收数据。队列可以存储任意类型的数据,并且支持阻塞操作,以便任务可以在队列为空时等待。
-
事件组(Event Group)
- 功能:用于在多个任务之间传递事件或信号。这些事件可以是单个或多个标志位(bit),每个任务可以对这些标志位进行设置、清除或等待。
- 用途:适合处理需要多个任务之间协调的场景。例如,一个任务可以等待多个事件的发生(例如,一个任务等待两个条件同时满足),而另一个任务可以设置这些事件,从而通知等待的任务。事件组也可以实现任务的优先级控制。
-
信号量(Semaphore)
- 功能:用于控制对共享资源的访问。信号量主要有两种类型:二进制信号量和计数信号量。
- 用途:二进制信号量通常用于实现互斥,确保同一时间只有一个任务可以访问特定资源。计数信号量允许多个任务同时访问资源,直到达到预定的最大值。信号量可以帮助避免竞态条件和数据不一致问题。
-
任务通知(Task Notification)
- 功能:用于实现任务间的简单通知机制。在 FreeRTOS 中,每个任务都有一个相关的通知值,可以通过直接从一个任务通知另一个任务。
- 用途:适合需要简单、低开销的通知机制的场景。任务可以使用通知值来发送较小的信息(如指示一个事件发生),并且可以选择使用阻塞或非阻塞方式等待通知。这种机制比队列更加轻量级。
队列
创建队列(动态、静态)
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
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 |
读队列
使用 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:读取失败,因为队列空了 |
写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于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:写入失败,因为队列满了。 |
编码实例1
改造红外任务
之前:game1任务->挡球板任务->读取缓冲区,写缓冲区由红外中断完成。这种方式没有数据时会循环读取,导致很耗费cpu资源
改造:game1任务->挡球板任务->读队列,写队列在红外中断中完成。
编码实例2
使用队列实现多任务输入
系统框图
红外中断接收到按键值之后,会把按键值转换为游戏控制的键值写入挡球板队列。编码器中断接收到编码器旋转后,旋转数据写入编码器队列,编码器任务读到数据,进行数据转换(编码器数据转换为游戏数据),并把数据写入挡球板队列。
使用队列集改造系统结构
红外遥控器、旋转编码器,它们的驱动程序应该专注于“产生硬件数据”,不应该跟“业务有任何联系”。比如:红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。把红外遥控器的按键转换为游戏的控制键,应该在红外任务里实现。
但是,如果使用编码器的模式,也就是每个硬件中断都创建一个对应的任务,那么有多个硬件设备时,会很消耗CPU资源。
因此我们引入队列集这一概念,它可以读取多个队列。引入队列集之后,我们就可以只创建一个“inputTask”,来代替红外任务、编码器任务等多个硬件任务。在“inputTask”中,它读取各个设备的队列,得到数据后再分别转换为游戏的控制键。
队列集的本质也是队列,只不过里面存放的是“队列句柄”。使用过程如下:
-
- 创建队列A,它的长度是n1
- 创建队列B,它的长度是n2
- 创建队列集S,它的长度是“n1+n2”
- 把队列A、B加入队列集S
- 这样,写队列A的时候,会顺便把队列A的句柄写入队列集S
- 这样,写队列B的时候,会顺便把队列B的句柄写入队列集S
- InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了;然后InputTask再读取这个队列句柄得到数据。
创建队列集
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数 | 说明 |
---|---|
uxQueueLength | 队列集长度,最多能存放多少个数据(队列句柄) |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足 |
把队列放入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,QueueSetHandle_t xQueueSet );
参数 | 说明 |
---|---|
xQueueOrSemaphore | 队列句柄,这个队列要加入队列集 |
xQueueSet | 队列集句柄 |
返回值 | pdTRUE:成功pdFALSE:失败 |
读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,TickType_t const xTicksToWait );
参数 | 说明 |
---|---|
xQueueSet | 队列集句柄 |
xTicksToWait | 如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | NULL:失败,队列句柄:成功 |
改善后的系统框架
写队列的同时也会写队列集,Input任务会读取队列集得到队列句柄,然后根据句柄进行后续的数据处理,处理完毕后写入挡球板队列。
赛车游戏
红外遥控器,用不同的按键控制三辆汽车
现在有三辆汽车,创建三个汽车任务。在红外中断中将键值写队列,汽车任务要来读队列。因为是三个任务,所以要写三个队列,在这里为了代码的可移植性,我们使用一个队列注册函数以及一个队列分发函数。
void RegisterQueueHandle(QueueHandle_t queueHandle) {if (g_queue_cnt < 10){g_xQueues[g_queue_cnt] = queueHandle;g_queue_cnt++;} }
static void DispatchKey(struct ir_data *pidata) {int i;for (i = 0; i < g_queue_cnt; i++){xQueueSendFromISR(g_xQueues[i], pidata, NULL);} }
在开始时每个任务调用任务函数时,创建自己的队列,并注册队列,然后循环读取队列,读不到便阻塞。当有红外中断写队列后,阻塞被唤醒。
系统架构
信号量和互斥量
队列用于代替缓冲区,在任务之间或任务与中断之间传递数据。但有时我们只需要传递一个状态,这个状态值需要用一个数值表示。
信号量本质上起始就是队列,只不过不进行具体数据的传输。
队列: 信号量:
写:send(①拷贝数据②cnt++③唤醒) 写:give(①cnt++②唤醒)
读:receive(①拷贝数据②cnt--③唤醒) 读:take(①cnt--)