FreeRTOS - 队列

devtools/2024/10/18 11:36:21/

在学习FreeRTOS过程中,结合韦东山-FreeRTOS手册和视频、野火-FreeRTOS内核实现与应用开发、及网上查找的其他资源,整理了该篇文章。如有内容理解不正确之处,欢迎大家指出,共同进步。

1. 队列

1.1 队列基本概念

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

  • 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
  • 每个数据大小固定
  • 创建队列时就要指定长度、数据大小
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
  • 也可以强制写队列头部:覆盖头部数据

1.2 传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里
  • 引用:把数据、把变量的地址复制进队列里

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

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
  • 无需分配buffer来保存数据,队列中有buffer
  • 局部变量可以马上再次使用
  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
  • 如果数据实在太大,你还是可以使用队列传输它的地址
  • 队列的空间有FreeRTOS内核分配,无需任务操心
  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。
1.3 队列的核心
  • 队列的核心:关中断、环形缓冲区、链表。
    • 关中断:实现互斥
    • 环形缓冲区:保存数据
    • 链表:实现休眠和唤醒

  • 使用队列的好处:
    • 读时队列中无数据、写时队列满,则任务进入休眠(阻塞状态),不抢占CPU资源,不参与运行,可以把时间留出来给其他任务运行。
    • 使用队列可以保证数据在传输过程中不被破坏。
    • 支持休眠和唤醒,使程序运行的效率更高。
1.3.1 队列怎样实现互斥访问----关中断
  • 互斥的核心:关中断、开中断
    • 关中断:
      • 使时钟中断无法产生,任务就无法切换;
      • 关中断可以认为是关全部中断,也可以是部分中断,包括Tick中断。关了中断后,就相当于裸机程序

1.3.2 环形缓冲区

1.3.3 怎样休眠/唤醒–链表
  • API函数允许指定阻塞时间。
  • 如果多个任务阻塞在一个队列上,那么最高优先级别的任务会第一个解除阻塞。
  • 注:中断程序中必须使用“FromISR”结尾的API函数!

有两个链表:

  • 写队列不成功而挂起
    • 每当任务企图向一个满的队列写数据时,任务会进入阻塞状态,直到队列中出现有效空间或者阻塞时间到期。
  • 读队列不成功而挂起
    • 每当任务企图从一个空的队列读取数据时,任务会进入阻塞状态(这样任务不会消耗任何CPU时间并且另一个任务可以运行)直到队列中出现有效数据或者阻塞时间到期。

写队列:

  • 写队列不成功而阻塞 :
    • 将任务放入List_t xTasksWaitingToSend;从该链表中可以找到写此队列不成功的任务
    • 将任务从ReadList ——> DelayList;
  • 被唤醒 :别的任务读队列,有空间了可以写;超时时间到
    • List_t xTasksWaitingToSend;中移除
    • DelayList ——> ReadList ;

读队列:

  • 读队列不成功而阻塞:
    • 放入List_t xTasksWaitingToReceive;从该链表中可以找到读此队列不成功的任务
    • 将任务从ReadList ——> DelayList;
  • 被唤醒 :别的任务写数据了;超时时间到,
    • xTasksWaitingToReceive;中移除
    • DelayList ——> ReadList ;
1.4 队列的超时唤醒

1.5 读队列流程

  1. 当有多个任务来读该队列时,需要做一些保护:关中断
  2. 判断队列中有无数据
    • 无 Data
      • (1)不阻塞,直接返回,返回ERR
      • (2)休眠
        • 将任务放入:xTasksWaitingToReceive;
        • 将该任务从 ReadList ——> DelayList中
        • 如果有任务优先级 > 当前任务优先级
          • 任务切换
    • 有 Data
  3. 有Data:本来 队列 中就有数据,或 从休眠中被唤醒(写数据了;超时到了)
    • 任务从队列中读数据;
      • 拷贝方式;
      • 将队列中数据数减一;
    • 读数据后,队列中有空间了,如果有 任务 A / B 在等待写数据的话,就将其唤醒。唤醒:
      • xTasksWaitingToSend中的第 1 个任务移除
      • 将其从 DelayList——> ReadList 中
      • 如果恢复的任务优先级 > 当前任务
        • 任务切换
1.6 队列写流程

任务和中断都可以向队列发送消息。

如果队列未满或者允许覆盖入队,FreeRTOS会将消息拷贝到队列队尾,否则根据用户指定的阻塞超时时间进行阻塞。

当其他任务从其等待的队列中读入了数据(队列未满),该任务将自动由阻塞态转为就绪态。

当任务等待的时间,超过了指定的阻塞时间,计时队列中还不允许入队,任务也会自动从阻塞态转为就绪态,此时发送消息的任务或中断程序会收到一个错误码 errQUEUE_FULL。
在这里插入图片描述

  1. 关中断
  2. 判断队列中有无空间
    • 无 空间
      • (1)不阻塞,直接退出,返回ERR
      • (2)休眠(阻塞)
        • 将任务放入:xTasksWaitingToSend
        • 将该任务从 ReadList ——> DelayList中
    • 有 空间
  3. 有 空间:本来 队列 中就有空间,或 从休眠中被唤醒(读数据了有空间了;超时到了)
    • 任务往队列中写数据(拷贝方式);
    • 有任务在等待读队列,唤醒它。唤醒:
      • xTasksWaitingToReceive;中的第 1 个任务移除
      • 将其从 DelayList——> ReadList 中
      • 如果恢复任务的优先级 > 当前运行的任务优先级
        • 任务切换
    • 没有任务在等待读队列
      • 数据拷贝成功 ----> 任务切换

一个任务写队列时,如果队列已经满了,它会被挂起,何时被唤醒?

  • 超时:
    • 任务写队列不成功时,它会被挂起:从ready list移到delayed list中
    • 在delayed list中,按照"超时时间"排序
    • 系统Tick中断不断发生,在Tick中断里判断delayed list中的任务时间到没?时间到后就唤醒它

2. 队列函数

2.1 创建队列

  • 定义一个句柄变量,用于保存创建的队列:QueueHandle_t xQueue1;
  • 使用xQueueCreate()创建一个队列;
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, // 队列长度,最多能存放多少个数据UBaseType_t uxItemSize // 每个数据的大小:以字节为单位);

2.1.1 队列结构体

2.1.2 xQueueGenericCreate函数

xQueueGenericCreate内部会分配队列空间、初始化队列。

2.2 向队列发送消息函数(写队列)
  • 举例
// 举例
// 创建队列
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));// 写队列:将 input 中的数据写入到 g_xQueuePlatform
xQueueSend(g_xQueuePlatform,&input,0);
// 读队列:从 g_xQueuePlatform 中读数据,放入到 idata中
xQueueReceive(g_xQueuePlatform,&idata,portMAX_DELAY);
// 删除队列
vQueueDelete(g_xQueuePlatform);
2.2.1 写队列函数
  1. xQueueSend()函数

  • xQueueSend() 等 同 于 xQueueSendToBack()。
  • 用于向队列尾部发送一个队列消息。消息以拷贝的形式入队,而不是以 引用的形式。
  • 该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护 功能的 xQueueSendFromISR()来代替。

  1. xQueueSendFromISR()

xQueueSendFromISR(): 用于在中断服务程序中向队列尾部发送一个队列消息。

xQueueSendToBackFromISR 等同于 xQueueSendFromISR ()。
在这里插入图片描述

2.2.2 通用消息队列发送函数 xQueueGenericSend()(任务)
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, // 指针,指向要发送的消息TickType_t xTicksToWait, const BaseType_t xCopyPosition )
  • xQueue:消息队列句柄;
  • pvItemToQueue: 指针,指向要发送的消息
  • xTicksToWait:指定阻塞超时时间
  • xCopyPosition:发送数据到消息队列的位置:
    • queueSEND_TO_BACK:发送到队尾;
    • queueSEND_TO_FRONT:发送到队头;
    • queueOVERWRITE:以覆盖的方式发送

BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, // 指针,指向要发送的消息TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;for( ;; ){taskENTER_CRITICAL();{/* 队列未满,或,以覆盖的方式发送,无队列满或未满,都可以发送*/if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){traceQUEUE_SEND( pxQueue );// 队列未满,将消息拷贝到队列中xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 有任务在等待读数据 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 将任务从事件链表xTasksWaitingToReceive中删除,事件链表按由新阿基顺序排序,链表中的第一个优先级最高。放入到就绪链表。如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换*/queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else if( xYieldRequired != pdFALSE ){/* 没有等待的任务,拷贝成功也需要任务切换 */queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}taskEXIT_CRITICAL();return pdPASS;}else  /* 队列已满 */{if( xTicksToWait == ( TickType_t ) 0 ){/* 不指定阻塞时间,直接退出 */taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){/* 指定了超时时间,系统会初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount和溢出次数 xNumOfOverflows,为后面的阻塞任务做准备*/vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* Entry time was already set. */mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL();/* 挂起调度器 ,不会进行任务切换,但不能禁止中断的发生,所以还需要给队列上锁*/vTaskSuspendAll();prvLockQueue( pxQueue );/* 检查指定的超时时间是否已经过去了 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){  // 没过/* 队列是满的 */if( prvIsQueueFull( pxQueue ) != pdFALSE ){	/* 根据用户指定的超时时间来阻塞一下任务*/traceBLOCKING_ON_QUEUE_SEND( pxQueue );/* 将其放入到写队列不成功链表中,xTasksWaitingToSend*/vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );/* 队列解锁 */prvUnlockQueue( pxQueue );/* 恢复调度器,如果调度器挂起期间有任务解除阻塞,并且解除阻塞的任务优先级比当前任务高,就需要进行一次任务切换*/if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}}else // 队列没满,有空间{/* 重新发送消息 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else // 超时时间已过,返回一个errQUEUE_FULL错误代码,退出{/* The timeout has expired. */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}}
}
2.2.2.1 prvCopyDataToQueue

  1. 尾部入队

  1. 头部入队

2.2.2.2 xTaskRemoveFromEventList
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{
TCB_t *pxUnblockedTCB;
BaseType_t xReturn;/* 事件链表按优先级顺序排序,链表中第一个的优先级最高*/pxUnblockedTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxEventList );configASSERT( pxUnblockedTCB );( void ) uxListRemove( &( pxUnblockedTCB->xEventListItem ) );/*调度器未挂起 */if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){	/* 从阻塞链表移除 */( void ) uxListRemove( &( pxUnblockedTCB->xStateListItem ) );/* 添加到就绪链表 */prvAddTaskToReadyList( pxUnblockedTCB );}else{/* 调度器挂起,将 任务放入xPendingReadyList链表(任务已就绪,但调度器挂起)*/vListInsertEnd( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) );}if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority ){xReturn = pdTRUE;xYieldPending = pdTRUE;}else{xReturn = pdFALSE;}#if( configUSE_TICKLESS_IDLE != 0 ){prvResetNextTaskUnblockTime();}#endifreturn xReturn;
}
2.2.2.3 vTaskPlaceOnEventList
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )
{configASSERT( pxEventList );/* 按优先级大小插入到事件链表中,高优先级插入到链表头部,确保 最高优先级的任务先唤醒 */vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}
2.3 从队列中读取消息函数
2.3.1 读队列函数
  1. xQueueReceive()

xQueueReceive()用于从一个队列中接收消息并把消息从队列中删除。接收的消息是以拷贝 的形式进行的,所以我们必须提供一个足够大空间的缓冲区。 具体能够拷贝多少数据到缓 冲区,这个在队列创建的时候已经设定。

BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait )

  1. xQueuePeek()

接收到了消息但不删除

BaseType_t xQueuePeek( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait )
  1. xQueueReceiveFromISR()与 xQueuePeekFromISR()

xQueueReceiveFromISR()是 xQueueReceive ()的中断版本,用于在中断服务程序中接收 一个队列消息并把消息从队列中删除;

xQueuePeekFromISR()是 xQueuePeek()的中断版本, 用于在中断中从一个队列中接收消息,但并不会把消息从队列中移除。

2.3.2 从队列读取消息函数 xQueueReceive
BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait )
{BaseType_t xEntryTimeSet = pdFALSE;TimeOut_t xTimeOut;Queue_t * const pxQueue = xQueue;for( ; ; ){taskENTER_CRITICAL();{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;/* 看看队列中有没有消息 */if( uxMessagesWaiting > ( UBaseType_t ) 0 ){/* 拷贝消息到用户指定区域 */prvCopyDataFromQueue( pxQueue, pvBuffer );traceQUEUE_RECEIVE( pxQueue );/* 读取消息并且消息出队,当前队列中的消息个数减一*/pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting - ( UBaseType_t ) 1 );/* 是否有等待发送消息的任务 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){// 有,将其从xTasksWaitingToSend移除,放入就绪队列if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){// 如果恢复任务的优先级比当前任务高,进行任务切换queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}taskEXIT_CRITICAL();traceRETURN_xQueueReceive( pdPASS );return pdPASS;}else{   if( xTicksToWait == ( TickType_t ) 0 ){/* 不阻塞,直接返回 */taskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );traceRETURN_xQueueReceive( errQUEUE_EMPTY );return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){/* 阻塞一定的超时时间 */vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{/* Entry time was already set. */mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL();vTaskSuspendAll();prvLockQueue( pxQueue );/* 检查超时时间是否已经过去了 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){/* 时间未过,队列为空 */if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );/* 将其放入xTasksWaitingToReceive链表 */vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() == pdFALSE ){/* 如果有任务优先级比当前任务高,会进行一次任务切换 */taskYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}else{/*  如果队列有消息了,就再试一次获取消息 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 超时时间已过,退出 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();/* 如果队列还是空的,返回错误码errQUEUE_EMPTY */if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){traceQUEUE_RECEIVE_FAILED( pxQueue );traceRETURN_xQueueReceive( errQUEUE_EMPTY );return errQUEUE_EMPTY;}else{mtCOVERAGE_TEST_MARKER();}}}
}
2.3.2.1 prvCopyDataFromQueue

2.4 消息队列删除函数 vQueueDelete()
void vQueueDelete( QueueHandle_t xQueue )

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都

会被系统回收清空,而且不能再次使用这个消息队列了。

vPortFree( pxQueue ); // 释放消息队列的内存

3. 队列的应用

3.1 13_queue_game

本节代码为:13_queue_game。以前使用环形缓冲区传输红外遥控器的数据,本程序改为使用队列。

01_game_template使用轮询的方式从环形缓冲区读取红外遥控器的键值,13_queue_game把环形缓冲区改为队列

  • (1)创建队列
  • (2)红外中断IRReceiver_IRQ_Callback:解析出按键后,写队列
  • (3)挡球板任务platform_task:读队列,控制挡球板的运动

3.2 多设备玩游戏

本节代码为:14_queue_game_encoder。实现红外遥控器和旋转编码器控制挡球板的运动。

  1. 创建挡球板队列A:g_xQueuePlatform;
  2. 创建旋转编码器队列B:g_xQueueRotary;
  3. 挡球板任务platform_task:读队列A(g_xQueuePlatform),控制挡球板的运动
  4. 红外中断IRReceiver_IRQ_Callback:解析出按键后,写队列A(g_xQueuePlatform)
  5. 旋转编码器:
    1. 旋转编码器中断RotaryEncoder_IRQ_Callback:解析出编码器状态,写队列B(g_xQueueRotary)
    2. 旋转编码器任务RotaryEncoder_task:读队列B(g_xQueueRotary),处理数据,写挡球板队列A(g_xQueuePlatform)

3.3 队列集
3.3.1 队列集简述

在上一个代码中,

假设有2个输入设备:红外遥控器、旋转编码器,它们的驱动程序应该专注于“产生硬件数据”,不应该跟“业务有任何联系”。比如:红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。

把红外遥控器的按键转换为游戏的控制键,应该在游戏的任务里实现。

要支持多个输入设备时,我们需要实现一个“InputTask”,它读取各个设备的队列,得到数据后再分别转换为游戏的控制键。

InputTask如何及时读取到多个队列的数据?要使用队列集

队列集的本质也是队列,只不过里面存放的是“队列句柄”。使用过程如下:

  • 创建队列A,它的长度是n1
  • 创建队列B,它的长度是n2
  • 创建队列集S,它的长度是“n1+n2”
  • 把队列A、B加入队列集S
  • 这样,写队列A的时候,会顺便把队列A的句柄写入队列集S
  • 这样,写队列B的时候,会顺便把队列B的句柄写入队列集S
  • InputTask先读取队列集S,它的返回值是一个队列句柄,这样就可以知道哪个队列有有数据了;然后InputTask再读取这个队列句柄得到数据。

3.3.2 程序框架
  1. 修改“Core\Inc\FreeRTOSConfig.h“,增加:
/* USER CODE BEGIN Includes */
#define configUSE_QUEUE_SETS 1
/* Section where include file can be added */
/* USER CODE END Includes */
  1. 在STM32CubeMX里把堆TOTAL_HEAP_SIZE调大,比如8000:
  2. 红外遥控器代码:
  • 声明一个队列
  • 创建红外遥控器队列
  • 写队列
/*创建红外队列*/
static QueueHandle_t g_xQueueIR;/* 获取红外的队列句柄 */
QueueHandle_t GetQueueIR(void)
{return g_xQueueIR;
}/* 中断函数中写队列*/
void IRReceiver_IRQ_Callback(void)
{/* -----写队列----*/data.dev = 0;data.val = 0;xQueueSendToBackFromISR(g_xQueueIR,&data,NULL);
}
/* 红外接收器的初始化函数 */
void IRReceiver_Init(void)
{/* 创建队列 */g_xQueueIR = xQueueCreate(IR_QUEUE_LEN, sizeof(struct ir_data));
}
  1. 旋转编码器代码
  • 声明一个队列
  • 创建旋转编码器队列
  • 写队列
static QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 *//* 旋转编码器创建队列的第三个参数 */
static uint8_t g_ucQueueRotaryBuff[10*sizeof(struct rotary_data)];
/* 旋转编码器创建队列的第四个参数 :用来保存队列的数据结构*/
static StaticQueue_t g_xQueueRotaryStaticstruct;static int32_t g_count = 0;
static int32_t g_speed = 0; /* 速度(正数表示顺时针旋转,负数表示逆时针旋转,单位:每秒转动次数) *//* 获取旋转编码器的队列句柄 */
QueueHandle_t GetQueueRotary(void)
{return g_xQueueRotary;
}void RotaryEncoder_IRQ_Callback(void)
{/* 写队列 */ rotarydata.cnt = g_count;rotarydata.speed = g_speed;xQueueSendToBackFromISR(g_xQueueRotary,&rotarydata,NULL);
}void RotaryEncoder_Init(void)
{/* 创建旋转编码器队列,*/g_xQueueRotary = xQueueCreateStatic(ROTARY_QUEUE_LEN,sizeof(struct rotary_data),g_ucQueueRotaryBuff,&g_xQueueRotaryStaticstruct);  
}
  1. 游戏
  • 创建队列集,包含:
    • 红外遥控器队列
    • 旋转编码器队列
  • 创建输入任务InputTask:检测多个输入设备并调用对应处理函数
    • 读队列集,得到有数据的队列句柄
    • 读队列句柄得到数据,处理数据
  • 红外遥控器处理数据ProcessIRData
    • 读取红外遥控器键值并转换为游戏控制键,写入挡球板队列
  • 旋转编码器处理数据ProcessRotaryData
    • 读取旋转编码器数据并转换为游戏控制键,写入挡球板队列
  • 挡球板任务
    • 读挡球板队列
    • 移动挡球板
void game1_task(void *params)
{/* 创建队列,队列集,创建输入任务InputTask*/g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));g_xQueueSetInput = xQueueCreateSet(IR_QUEUE_LEN+ ROTARY_QUEUE_LEN);g_xQueueIR = GetQueueIR();g_xQueueRotary = GetQueueRotary();xQueueAddToSet(g_xQueueIR, g_xQueueSetInput);xQueueAddToSet(g_xQueueRotary, g_xQueueSetInput);xTaskCreate(InputTask, "InputTask", 128, NULL, osPriorityNormal, NULL);
}/* 输入任务* 检测多个输入设备并调用对应处理函数*/
static void InputTask(void *params)
{QueueSetMemberHandle_t xQueueSetHandle;while(1){/* 读队列集,得到有数据队列句柄*/xQueueSetHandle = xQueueSelectFromSet(g_xQueueSetInput, portMAX_DELAY);if (xQueueSetHandle){/* 读队列句柄得到数据,处理数据*/if (xQueueSetHandle == g_xQueueIR){ProcessIRData();}else if (xQueueSetHandle == g_xQueueRotary){ProcessRotaryData();}}}
}/* 读队列句柄得到数据,处理数据* 读取红外遥控器键值并转换为游戏控制键,写入挡球板队列
*/
static void ProcessIRData(void)
{struct ir_data irdata;static struct input_data input;/* 读红外队列 */xQueueReceive(g_xQueueIR, &irdata, 0);if (irdata.val == IR_KEY_LEFT){input.dev = irdata.dev;input.val = UPT_MOVE_LEFT;}else if (irdata.val == IR_KEY_RIGHT){input.dev = irdata.dev;input.val = UPT_MOVE_RIGHT;}else if (irdata.val == IR_KEY_REPEAT){/* 保持不变 */}else {input.dev = irdata.dev;input.val = UPT_MOVE_NONE;}/* 写挡球板队列 */xQueueSend(g_xQueuePlatform,&input,0);
}/* 读队列句柄得到数据,处理数据* 读取旋转编码器数据并转换为游戏控制键,写入挡球板队列
*/
static void ProcessRotaryData(void)
{struct rotary_data rdata;static struct input_data input;int left, cnt;/* 读旋转编码器队列 */xQueueReceive(g_xQueueRotary,&rdata,0);/* 处理数据 *//* 判断速度:负数表示向左转动,正数表示向右转动 */if (rdata.speed < 0){left = 1;rdata.speed = 0 - rdata.speed;}else{left = 0;}
//		cnt = rotarydata.speed / 10;
//		if (!cnt)
//			cnt = 1;if (rdata.speed > 100)cnt = 4;else if (rdata.speed > 50)cnt = 2;else cnt = 1;/* 写挡球板队列 */input.dev = 1;input.val = left ? UPT_MOVE_LEFT : UPT_MOVE_RIGHT;for (int i = 0; i < cnt; i++){xQueueSend(g_xQueuePlatform,&input,0);}
}/* 挡球板任务 */
static void platform_task(void *params)
{byte platformXtmp = platformX;    //uint8_t data;//uint8_t last_data;static struct input_data idata;// Draw platformdraw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);draw_flushArea(platformXtmp, g_yres - 8, 12, 8);while (1){/* 读取红外遥控器 */// 读环形缓冲区//if (0 == IRReceiver_Read(&dev, &data))/* 读队列 */xQueueReceive(g_xQueuePlatform,&idata,portMAX_DELAY);uptMove = idata.val;// Hide platformdraw_bitmap(platformXtmp, g_yres - 8, clearImg, 12, 8, NOINVERT, 0);draw_flushArea(platformXtmp, g_yres - 8, 12, 8);// Move platformif(uptMove == UPT_MOVE_RIGHT)platformXtmp += 3;else if(uptMove == UPT_MOVE_LEFT)platformXtmp -= 3;uptMove = UPT_MOVE_NONE;// Make sure platform stays on screenif(platformXtmp > 250)platformXtmp = 0;else if(platformXtmp > g_xres - PLATFORM_WIDTH)platformXtmp = g_xres - PLATFORM_WIDTH;// Draw platformdraw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);draw_flushArea(platformXtmp, g_yres - 8, 12, 8);platformX = platformXtmp;}
}
3.5 队列集-mpu6050

在上个代码的基础上,添加mpu6050控制挡球板运动

  • driver_mpu6050.c代码中:
    • 创建队列
    • 创建mpu6050任务
      • 读数据
      • 解析数据
      • 写队列g_xQueueMPU6050
  • game1.c代码:
    • 将mpu6050队列g_xQueueMPU6050加入队列集;
    • 读取MPU6050数据并转换为游戏控制键,写入挡球板队列

/* --------------在driver_mpu6050.c----------------*/
static QueueHandle_t g_xQueueMPU6050;/*MPU6050队列*//* 获取MPU6050的队列句柄 */
QueueHandle_t GetQueueMPU6050(void)
{return g_xQueueMPU6050;
}int MPU6050_Init(void)
{g_xQueueMPU6050 = xQueueCreate(MPU6050_QUEUE_LEN, sizeof(struct mpu6050_data));
}void MPU6050_Task(void *params)
{int16_t AccX;struct mpu6050_data result;int ret;extern volatile int bInUsed;while (1){   while (bInUsed);bInUsed = 1;ret = MPU6050_ReadData(&AccX, NULL, NULL, NULL, NULL, NULL);bInUsed = 0;/* 读数据 */if (0 == ret){/* 解析数据 */MPU6050_ParseData(AccX, 0, 0, 0, 0, 0, &result);/* 写队列 */xQueueSend(g_xQueueMPU6050,&result,0);}/* delay */vTaskDelay(50);}
}/* --------------game1.c----------------*/
/* 读队列句柄得到数据,处理数据* 读取MPU6050数据并转换为游戏控制键,写入挡球板队列
*/
static void ProcessMPU6050Data(void)
{struct mpu6050_data mpudata;static struct input_data input;/* 读MPU6050队列 */xQueueReceive(g_xQueueMPU6050,&mpudata,0);/* 处理数据 *//* 判断x轴的角度:大于90°则挡球板向左移动,小于90°则挡球板向右移动 */if (mpudata.angle_x > 90){input.val = UPT_MOVE_LEFT;}else if (mpudata.angle_x < 90){input.val = UPT_MOVE_RIGHT;}else{input.val = UPT_MOVE_NONE;}/* 写挡球板队列 */input.dev = 1;xQueueSend(g_xQueuePlatform,&input,0);
}
分发数据给多个任务(赛车游戏)


在这里插入图片描述
实现逻辑:

  • 红外遥控器的中断函数解析出按键值后,写入3个队列:3个赛车任务读取其中一个队列得到按键数据。
  • 采用game2.c代码
  1. 红外遥控器代码
static QueueHandle_t g_xQueueIR;/*红外队列*/static QueueHandle_t g_xQueues[10]; /* 队列数组 */
static int g_Queue_cnt = 0;/* 获取红外的队列句柄 */
QueueHandle_t GetQueueIR(void)
{return g_xQueueIR;
}/* 使用该函数写三个队列 */
void RegisterQueueHandle(QueueHandle_t g_xQueueHandle)
{if (g_Queue_cnt < 10){g_xQueues[g_Queue_cnt] = g_xQueueHandle;g_Queue_cnt++;}
}static void DispatchKey(struct ir_data *pidata)
{int i;/* 写三个队列 */for (i = 0; i < g_Queue_cnt; i++){xQueueSendToBackFromISR(g_xQueues[i],pidata,NULL);}
}void IRReceiver_IRQ_Callback(void)
{struct ir_data data;/* -----写队列----*/data.dev = 0;data.val = 0;DispatchKey(&data);
}void IRReceiver_Init(void)
{RegisterQueueHandle(g_xQueueIR);  
}
  1. game2代码
/* 1. 创建队列* 2. 注册队列* 3. 显示汽车* 4. 读取队列,控制汽车的移动
*/
static void CarTask(void *params)
{struct car *pcar = params;struct ir_data irdata;/* 创建自己的队列 */QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));/* 注册队列 */RegisterQueueHandle(xQueueIR);/* 显示汽车 */ShowCar(pcar);while(1){/* 读取按键值:读队列*/xQueueReceive(xQueueIR, &irdata, portMAX_DELAY);/* 控制汽车往右移动 */if (irdata.val == pcar->control_key){if (pcar->x < g_xres - CAR_LENGTH){/* 隐藏汽车 */HideCar(pcar);/* 调整位置 */pcar->x += 20;if (pcar->x > g_xres - CAR_LENGTH)pcar->x = g_xres - CAR_LENGTH;/* 重新显示汽车 */ShowCar(pcar);}}}
}/* 1. 画路标* 2. 创建3个汽车任务*/
void car_game(void)
{//int x;int i,j;g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);draw_init();draw_end();/* 画出路标 */for (i = 0; i < 3; i++){for (j = 0; j < 8; j++){draw_bitmap(16 * j, 16 + 17 * i, roadMarking, 8, 1, NOINVERT, 0);draw_flushArea(16 * j, 16 + 17 * i, 8, 1);}}xTaskCreate(CarTask, "car1", 128, &g_cars[0], osPriorityNormal, NULL);xTaskCreate(CarTask, "car2", 128, &g_cars[1], osPriorityNormal, NULL);xTaskCreate(CarTask, "car3", 128, &g_cars[2], osPriorityNormal, NULL);	}

http://www.ppmy.cn/devtools/126709.html

相关文章

UNIX网络编程-简介

概述 要编写通过计算机网络通信的程序&#xff0c;首先要确定这些程序相互通信所用的协议&#xff08;protocal&#xff09;。 大多数网络应用划分为客户端&#xff08;client&#xff09;和服务器&#xff08;server&#xff09;。在设计网络应用时&#xff0c;确定总是由客户…

线性代数 行列式

一、行列式 1、定义 一个数学概念&#xff0c;主要用于 线性代数中&#xff0c;它是一个可以从方阵&#xff08;即行数和列数相等的矩阵&#xff09;形成的一个标量&#xff08;即一个单一的数值&#xff09; 2、二阶行列式 &#xff0c;像这样将一个式子收缩称为一个 2*2 的…

2d 数字人实时语音聊天对话使用案例;支持asr、llm、tts实时语音交互

参考: https://github.com/lyz1810/live2dSpeek 下载live2dSpeek项目 ## 下载live2dSpeek git clone https://github.com/lyz1810/live2dSpeek cd live2dSpeek-main ## 运行live2dSpeek npm install -g http-server http-server .更改新的index.html页面 index.html

Android从上帝视角来看PackageManagerService

戳蓝字“牛晓伟”关注我哦&#xff01; 用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章&#xff0c;技术文章也可以有温度。 前言 阅读该篇之前&#xff0c;建议先阅读下面的系列文章&#xff1a; Android深入理解包管理–PackageManagerService和它的“小伙伴…

《拿下奇怪的前端报错》:1比特丢失导致的音视频播放时长无限增长-浅析http分片传输核心和一个坑点

问题背景 在一个使用MongoDB GridFS实现文件存储和分片读取的项目中&#xff0c;同事遇到了一个令人困惑的问题&#xff1a;音频文件总是丢失最后几秒&#xff0c;视频文件也出现类似情况。更奇怪的是&#xff0c;播放器显示的总时长为无限大。这个问题困扰了团队成员几天&…

【RS】GEE(Python):栅格计算

在遥感影像处理中&#xff0c;栅格计算是一项至关重要的操作。栅格数据代表了地球表面特定范围内的物理量信息&#xff0c;利用栅格计算可以进行多种分析操作&#xff0c;比如计算植被指数、分类、过滤、组合波段&#xff0c;甚至执行复杂的空间分析任务。本篇教程将详细介绍遥…

【学习笔记】MongoDB 概念

文章目录 MongoDB 概念MongoDb 的应用场景什么时候会选择MongoDB&#xff1f; MongoDB 概念 MongoDb 的应用场景 传统的关系型数据库(如MySQL)&#xff0c;在数据操作的三高需求以及应对Web2.0的网站需求面前&#xff0c;显得力不从心。 那什么是“三高”&#xff1f; 高血…

rollup.js 插件实现原理与自定义

Rollup.js 是一个JavaScript模块打包器&#xff0c;它主要用于将小块代码编译成大块复杂的库或应用程序。相较于Webpack&#xff0c;Rollup更专注于代码的ES模块转换和优化&#xff0c;特别适合构建库或者那些对代码体积、执行效率有严格要求的应用。Rollup的核心特性之一就是它…