一次更新不完,有错误请指正,每次会更新一部分,在学习过程中有不合适的我也会改过来
1.环形缓冲区
有缺陷的环形缓冲区
2.队列
队列的结构体中包含队列头(数据读位置)队列尾(数据写位置),队列中数据个数,队列读写的本质就是环形缓冲区,在环形缓冲区的基础上增加了互斥措施,阻塞-唤醒机制。
如果这个队列不传输数据只调整“数据个数(num)”,它就是信号量(semaphore)。
如果在信号量的基础上只限定数据个数最大为1,那它就是互斥量(mutex)。
所以信号量和互斥量的本质都是队列!
2.1队列内部机制
我们使用队列无非就是进行三种操作:创建队列,写队列,读队列
我只写了一对一的情况多对多同样的道理,我们要注意的是不是什么杂七杂八的任务都可以用一个队列,比如挡球板游戏,我可以用编码器,遥控器去写队列因为他们都可以来控制挡球板,我不能把一个光敏常感器的值写入队列来控制挡球板这不就乱套了。
我们假设任务A写数据,任务B从队列读数据将会出现三种情况:
情况1:队列里有数据,任务B读数据,任务A写数据正常进行
情况2:队列数据是满的,A写数据无法写,那么A就要么直接返回写失败,要么就阻塞一会,等任务B去读队列的时候去唤醒任务A任务A写数据成功,如果任务B一直都没去读队列,直到任务A等待结束返回写失败。
任务A写队列时进入等待会做两件事:
1、把自己从Ready链表中删除,2、把自己放入阻塞链表和写链表。
情况3:队列是空的,任务B去读队列,要么直接返回读失败,要么就等待,在等待中有可能等到任务A在队列中写数据唤醒任务B任务B读数据成功,如果任务A一直不写队列,直到任务B等待结束返回读失败。
任务B读队列时进入等待会做两件事:
1、把自己从Ready链表中删除,2、把自己放入阻塞链表和读链表。
我们根据等待我们知道它有两种情况:要么等待结束前被其他任务唤醒,要么等待结束,这两种情况的过程是怎样的呢?内部机制是什么?我们下面来讲:
2.1.1任务在等待过程被唤醒
在任务A等待队列不为满的过程中如果任务B读队列,在任务B在队列读数据后,任务B会做一个唤醒动作,任务B并不知道它要唤醒哪个任务,它会去遍历写队列链表(Sender_list,在任务A写链表阻塞的时候,任务A把自己放入了这个链表),先唤醒写队列链表优先级最高的任务,同级优先级任务等待时间最长的先唤醒(将这个任务从阻塞链表和写队列链表中删除放入就绪链表)
在任务B等待的过程中,任务A在队列写数据后,任务A会做一个唤醒动作,任务A并不知道它要唤醒哪个任务,它会去遍历读队列链表(QueueReceiver_list,在任务B读链表阻塞的时候,任务B把自己放入了这个链表),先唤醒写队列链表优先级最高的任务,同级优先级任务等待时间最长的先唤醒(将这个任务从阻塞链表和写队列链表中删除放入就绪链表)
2.1.2任务等待结束
每次Tick中断会去判断任务A的阻塞时间是否已到(去遍历这个阻塞链表里的任务),如果任务A阻塞时间到,那就把任务A从阻塞链表和写链表删除,放入就绪链表,返回写失败。
每次Tick中断会去判断任务B的阻塞时间是否已到(去遍历这个阻塞链表里的任务),如果任务B阻塞时间到,那就把任务B从阻塞链表和读链表删除,放入就绪链表,返回读失败。
所以队列里有三样东西:环形缓冲区,读队列链表和写队列链表。
2.2.3源码分析
我们通过一个读队列源代码,大家可以阅读一下我翻译的文字来感受一下这个过程:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
/* Check the pointer is not NULL. */
/*检查指针是否为NULL*/
configASSERT( ( pxQueue ) );
/* The buffer into which data is received can only be NULL if the data size
is zero (so no data is copied into the buffer. */
/*只有当数据大小为零时,接收数据的缓冲区才能为NULL(因此没有数据复制到缓冲区中)*/
configASSERT( !( ( ( pvBuffer ) == NULL ) && ( ( pxQueue )->uxItemSize != ( UBaseType_t ) 0U ) ) );
/* Cannot block if the scheduler is suspended. */
/*如果计划程序已挂起,则无法阻止*/
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
{
configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
}
#endif
/* This function relaxes the coding standard somewhat to allow return
statements within the function itself. This is done in the interest
of execution time efficiency. */
/*此函数在一定程度上放宽了编码标准,允许在函数本身中使用返回语句。
这样做是为了提高执行时间效率*/
for( ;; )
{
taskENTER_CRITICAL();
{
const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;
/* Is there data in the queue now? To be running the calling task
must be the highest priority task wanting to access the queue. */
/*队列中现在有数据吗?要运行,调用任务必须是要访问队列的最高优先级任务*/
if( uxMessagesWaiting > ( UBaseType_t ) 0 )
{
/* Data available, remove one item. */
/*数据可用,删除一项*/
prvCopyDataFromQueue( pxQueue, pvBuffer );
traceQUEUE_RECEIVE( pxQueue );
pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1;
/* There is now space in the queue, were any tasks waiting to
post to the queue? If so, unblock the highest priority waiting
task. */
/*队列中现在有空间,是否有任务等待发布到队列?如果是,请取消阻止优先级最高的等待任务*/
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
taskEXIT_CRITICAL();
return pdPASS;
}
else
{
if( xTicksToWait == ( TickType_t ) 0 )
{
/* The queue was empty and no block time is specified (or
the block time has expired) so leave now. */
/*队列为空,未指定块时间(或块时间已过期),请立即离开*/
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else if( xEntryTimeSet == pdFALSE )
{
/* The queue was empty and a block time was specified so
configure the timeout structure. */
/*队列为空,并且指定了块时间,因此配置超时结构*/
vTaskInternalSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/* Entry time was already set. */
/*已设置进入时间*/
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL();
/* Interrupts and other tasks can send to and receive from the queue
now the critical section has been exited. */
/*中断和其他任务可以发送到队列,也可以从队列接收,现在关键部分已经退出*/
vTaskSuspendAll();
prvLockQueue( pxQueue );
/* Update the timeout state to see if it has expired yet. */
/*更新超时状态以查看它是否已过期*/
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{
/* The timeout has not expired. If the queue is still empty place
the task on the list of tasks waiting to receive from the queue. */
/*超时尚未过期。如果队列仍然为空,则将该任务放在等待从队列接收的任务列表中*/
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
prvUnlockQueue( pxQueue );
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* The queue contains data again. Loop back to try and read the
data. */
/*队列再次包含数据。循环以尝试读取数据*/
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else
{
/* Timed out. If there is no data in the queue exit, otherwise loop
back and attempt to read the data. */
/*超时。如果队列出口中没有数据,则返回并尝试读取数据*/
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
/*-----------------------------------------------------------*/
2.2队列相关API函数
2.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 |
示例代码:
// 示例代码
#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 );
}
2.2.2复位队列
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
2.2.3删除
删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
2.2.4 写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在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:写入失败,因为队列满了。 |
2.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:读取失败,因为队列空了。 |
2.2.6 查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
2.2.7覆盖/偷看
当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。
注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。
函数原型如下:
/* 覆盖队列
* 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()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。
函数原型如下:
/* 偷看队列
* 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,
);
2.3队列的使用
我将在中断和任务中写队列,中断执行时间不能太长否则会影响CPU性能导致整个系统效率降低。
2.3.1红外遥控单独控制
在游戏任务里创建队列,在遥控器中断里写入队列,游戏任务读队列
部分代码展示:
任务里创建队列
QueueHandle_t g_xQueuPlatform;/*挡球班队列*/
void game1_task(void *params)
{
uint8_t dev, data, last_data;
g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
draw_init();
draw_end();
/*创建队列:平台任务从里面读到红外数据,.....*/
g_xQueuPlatform=xQueueCreate(10,sizeof(struct input_data));
uptMove = UPT_MOVE_NONE;
ball.x = g_xres / 2;
ball.y = g_yres - 10;
ball.velX = -0.5;
ball.velY = -0.6;
// ball.velX = -1;
// ball.velY = -1.1;
blocks = pvPortMalloc(BLOCK_COUNT);
memset(blocks, 0, BLOCK_COUNT);
lives = lives_origin = 3;
score = 0;
platformX = (g_xres / 2) - (PLATFORM_WIDTH / 2);
//创建一个挡球板任务,在这个任务里我们去读队列
xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
while (1)
{
game1_draw();
//draw_end();
vTaskDelay(50);
}
}
红外中断里写队列
struct input_data{
uint32_t dev;//设备
uint32_t val;//数值
};
/**********************************************************************
* 函数名称: IRReceiver_IRQ_Callback
* 功能描述: 红外接收器的中断回调函数,记录中断时刻
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/08/04 V1.0 韦东山 创建
***********************************************************************/
void IRReceiver_IRQ_Callback(void)
{
uint64_t time;
static uint64_t pre_time = 0;
struct input_data idata;//创建写队列参数XQueue
/* 1. 记录中断发生的时刻 */
time = system_get_ns();
/* 一次按键的最长数据 = 引导码 + 32个数据"1" = 9+4.5+2.25*32 = 85.5ms
* 如果当前中断的时刻, 举例上次中断的时刻超过这个时间, 以前的数据就抛弃
*/
if (time - pre_time > 100000000)
{
g_IRReceiverIRQ_Cnt = 0;
}
pre_time = time;
g_IRReceiverIRQ_Timers[g_IRReceiverIRQ_Cnt] = time;
/* 2. 累计中断次数 */
g_IRReceiverIRQ_Cnt++;
/* 3. 次数达标后, 解析数据, 放入buffer */
if (g_IRReceiverIRQ_Cnt == 4)
{
/* 是否重复码 */
if (isRepeatedKey())
{
// /* device: 0, val: 0, 表示重复码 */
// PutKeyToBuf(0);
// PutKeyToBuf(0);
//
idata.dev=0;
idata.val=0;
xQueueSendToBackFromISR( g_xQueuPlatform,&idata,NULL);//0不阻塞,中断中不允许阻塞
g_IRReceiverIRQ_Cnt = 0;
}
}
if (g_IRReceiverIRQ_Cnt == 68)
{
IRReceiver_IRQTimes_Parse();
g_IRReceiverIRQ_Cnt = 0;
}
}
/**********************************************************************
* 函数名称: IRReceiver_IRQTimes_Parse
* 功能描述: 解析中断回调函数里记录的时间序列,得到的device和key放入环形缓冲区
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 0 - 成功, (-1) - 失败
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/08/04 V1.0 韦东山 创建
***********************************************************************/
static int IRReceiver_IRQTimes_Parse(void)
{
uint64_t time;
int i;
int m, n;
unsigned char datas[4];
unsigned char data = 0;
int bits = 0;
int byte = 0;
struct input_data idata;
/* 1. 判断前导码 : 9ms的低脉冲, 4.5ms高脉冲 */
time = g_IRReceiverIRQ_Timers[1] - g_IRReceiverIRQ_Timers[0];
if (time < 8000000 || time > 10000000)
{
return -1;
}
time = g_IRReceiverIRQ_Timers[2] - g_IRReceiverIRQ_Timers[1];
if (time < 3500000 || time > 55000000)
{
return -1;
}
/* 2. 解析数据 */
for (i = 0; i < 32; i++)
{
m = 3 + i*2;
n = m+1;
time = g_IRReceiverIRQ_Timers[n] - g_IRReceiverIRQ_Timers[m];
data <<= 1;
bits++;
if (time > 1000000)
{
/* 得到了数据1 */
data |= 1;
}
if (bits == 8)
{
datas[byte] = data;
byte++;
data = 0;
bits = 0;
}
}
/* 判断数据正误 */
datas[1] = ~datas[1];
datas[3] = ~datas[3];
if ((datas[0] != datas[1]) || (datas[2] != datas[3]))
{
g_IRReceiverIRQ_Cnt = 0;
return -1;
}
// PutKeyToBuf(datas[0]);
// PutKeyToBuf(datas[2]);
idata.dev=datas[0];
idata.val=datas[2];
xQueueSendToBackFromISR( g_xQueuPlatform,&idata,NULL);//0不阻塞,中断中不允许阻塞
return 0;
}
挡球板任务里读队列
extern QueueHandle_t g_xQueuPlatform;/*挡球班队列*/
/* 挡球板任务 */
static void platform_task(void *params)
{
byte platformXtmp = platformX;
uint8_t dev, data, last_data;
struct input_data idata;
// Draw platform
draw_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))//IRReceiver_Read()会读取环形缓冲区改为读队列
if(pdPASS == xQueueReceive(g_xQueuPlatform,&idata,0))//读到数据不等待
{
data=idata.val;
if (data == 0x00)
{
data = last_data;
}
if (data == 0xe0) /* Left */
{
btnLeft();
}
if (data == 0x90) /* Right */
{
btnRight();
}
last_data = data;
// Hide platform
draw_bitmap(platformXtmp, g_yres - 8, clearImg, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
// Move platform
if(uptMove == UPT_MOVE_RIGHT)
platformXtmp += 3;
else if(uptMove == UPT_MOVE_LEFT)
platformXtmp -= 3;
uptMove = UPT_MOVE_NONE;
// Make sure platform stays on screen
if(platformXtmp > 250)
platformXtmp = 0;
else if(platformXtmp > g_xres - PLATFORM_WIDTH)
platformXtmp = g_xres - PLATFORM_WIDTH;
// Draw platform
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
platformX = platformXtmp;
}
}
}
2.3.2红外遥控,编码器共同控制
实现思路创建队列1->写队列1->处理队列1数据->写入队列2->读取队列2控制挡球板
编码器在中断中写队列1,在编码器任务里进行数据处理(比如获取编码器的旋转速度,根据编码器的旋转速度,根据速度快慢决定写队列的次数),编码器任务将数据处理成挡球板任务所需要的数据写入队列2,挡球板任从队列2中读数据来控制挡球板。
我们看到我们用编码器控制挡球板需要再编码器驱动中断里写队列1,还要创建一个编码器任务处理数据在写入队列2,如果一个设备需要多个设备控制那我们就要创建好多任务来处理底层的数据,创建多个处理数据的任务会对我们单片机资源造成很大的浪费因此我们引入队列集!
2.3.3使用同一个输入设备控制多个任务
怎么做呢?我们使用注册表,有一个游戏三辆小车,我们使用遥控器的三个键值来分别控制这三辆小车
注册表的实现:
我们创建一个数组,和一个记录句柄数。数组来保存每个任务队列的句柄,每存一个任务队列句柄计数值 加1,在写队列的时候我们就可以通过一个循环来将同一个数据分发给多个队列。
//队列注册函数
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)
{
#if 0
extern QueueHandle_t g_xQueueCar1;
extern QueueHandle_t g_xQueueCar2;
extern QueueHandle_t g_xQueueCar3;
xQueueSendFromISR(g_xQueueCar1, pidata, NULL);
xQueueSendFromISR(g_xQueueCar2, pidata, NULL);
xQueueSendFromISR(g_xQueueCar3, pidata, NULL);
#else
int i;
for (i = 0; i < g_queue_cnt; i++)
{
xQueueSendFromISR(g_xQueues[i], pidata, NULL);
}
#endif
}
怎么使用这个注册表呢?
1、我们在创建任务函数里创建队列后,将队列句柄传进注册表,这样注册表就有我们第一个任务的句柄,我们每创建一个,注册表中就多一个队列句柄。上面代码例程的注册表最多能下发到是个队列。
2、怎么将数值一次下发到所有队列,就是根据这个g_queue_cnt的值循环将同一个值写到所有注册表的队列里。
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.2队列集相关API函数
3.2.1 创建队列集
函数原型如下:
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数 | 说明 |
---|---|
uxQueueLength | 队列集长度,最多能存放多少个数据(队列句柄) |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足 |
3.2.2 把队列加入队列集
函数原型如下:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
参数 | 说明 |
---|---|
xQueueOrSemaphore | 队列句柄,这个队列要加入队列集 |
xQueueSet | 队列集句柄 |
返回值 | pdTRUE:成功 ,pdFALSE:失败 |
3.2.3 读取队列集
函数原型如下:
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
参数 | 说明 |
---|---|
xQueueSet | 队列集句柄 |
xTicksToWait | 如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | NULL:失败,队列句柄:成功 |
3.3使用队列集注意事项
一、我们想使用队列集需要对FreeRTOS进行配置才可以使用
二、如果有些任务创建没有成功那我们就要考虑堆的大小是不是不够了
三、创建队列集一定要在写队列之前
为什么呢?
如果先写队列这个时候队列并不知道自己在队列集里,就不会把自己的队列句柄放入队列集,队列如果在创建队列集时已经写满,那么任务读队列集解析就会得不到数据,队列一直是满的写队列的任务也就写不了队列。