声明:参考韦东山老师网页知识点以及自己理解整理笔记。
一、数据传输方法对比(以下列举方法逐步改善)
1、全局变量
在FreeRTOS中,使用全局变量 进行数据传输可能被打断,导致传输错误,上节具体讲过原因。
2、环形缓冲区
2.1 写和读方法:
判断条件:当读=写时,buffer为空;当读的next=写时,buffer已满,这样的做法会让buffer少写一位;
3、队列
3.1 读取数据
本质:在环形缓冲区 的基础上增加了 互斥、阻塞-唤醒机制。
3.2 队列本质
- 环形buffer;
- 两个链表:Sender List; Receiver List;
3.3 就绪链表变化过程(重要!)
假设有两个任务Task_A和Task_B,Task_A为写任务,Task_B任务为读任务。
刚创建任务时,Task_B处于ready状态准备读数据,把自己放入ReayList链表中;如果队列中没有数据,会阻塞,将自己从ReadyList移入QueueReceiverList中(QueueReceiverList会存放一些等待读数据的任务);当然,DelayedList(阻塞链表)也会有Task_B。
如果此时有Task_A来写任务了,它会从QueueReceiverList中找出第一个读任务来唤醒,即Task_B会进入到ReadyList中准备Running读任务。
二、队列数据传输方法
- 拷贝:把数据、把变量的值复制进队列里
- 引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法:
- 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
- 无需分配buffer来保存数据,队列中有buffer
- 局部变量可以马上再次使用
- 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
- 如果数据实在太大,你还是可以使用队列传输它的地址
- 队列的空间有FreeRTOS内核分配,无需任务操心
- 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。
三、队列实验
程序部分使用游戏来体现
1、创建队列
队列的创建有两种方法:动态分配内存、静态分配内存。
1.1 动态分配内存
队列的内存在函数内部动态分配。
函数原型:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
1.2 静态分配内存
队列的内存要事先分配好。
函数原型:
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 |
2、写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR(中断函数)中使用。
写入队列尾部
2.1 在任务中写队列
xQueueSend 等同于 xQueueSendToBack
/* 等同于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
);
2.1 在中断函数中写队列
在中断函数中使用,不可阻塞。在中断函数不需要指定超时时间,无论是否成功都要即刻返回。
该处引用自文章
ISR要尽量快,否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得很卡顿
- 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
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
);
3、读队列
使用 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:读取失败,因为队列空了。 |
4、删除队列
只能删除使用动态方法创建的队列,它会释放内存。
void vQueueDelete( QueueHandle_t xQueue );
四、队列实验1–玩打砖块游戏(中断写队列 / 红外接收键值)
1、实验目标
通过遥控器按键发送红外信号,单片机使用队列读取红外遥控器按键,控制挡球板移动来玩下图所示打砖块游戏,同时音乐正常播放。
2、实验步骤:
- 创建队列
- IR、ISR(中断中)写队列
- platform_task(挡球板任务中)读队列
3、核心代码展示
项目代码获取
存放目录:DShanMCU-F103开发板资料\5_程序源码\02_FreeRTOS程序\02_视频配套的源码\13_queue_game.7z
(1)在 “ MX_FREERTOS_Init ” 函数中创建game1_task任务;
xTaskCreate(game1_task, "GameTask", 128, NULL, osPriorityNormal, NULL);
(2)在 “ game1_task函数 ” 实现 “ platform_task ”挡球板任务 和 while(1)循环 ,循环中实现球的移动;
xTaskCreate(platform_task, " xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
while (1)
{
game1_draw();
//draw_end();
vTaskDelay(50);
}
我们在该函数中创建队列(这里使用动态队列创建方法 xQueueCreate ),以便读取队列A获得控制信息,用来控制游戏。
QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
struct input_data {
uint32_t dev;
uint32_t val
};
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data)); // 长度随意设为10;
(3)修改 红外接收器中断回调函数:IRReceiver_IRQ_Callback;将原来 向环形缓冲区 写数据的代码换成 向队列写数据。
在终端中队列写数据函数:xQueueSendToBackFromISR
//PutKeyToBuf(0);
//PutKeyToBuf(0);
/* 写队列 */
idata.dev = 0;
idata.val = 0;
xQueueSendToBackFromISR(g_xQueuePlatform, &idata, NULL);
(4)“ platform_task ”挡球板任务函数中实现 使用队列读取红外遥控器 ;
(5)最终,在 MX_FREERTOS_Init 函数中,创建了两个任务:PlayMusic 和 game1_task 可同时运行,实现播放音乐的同时玩游戏。因为队列有互斥、阻塞-唤醒机制,以高效使用CPU。
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
LCD_Init();
LCD_Clear();
IRReceiver_Init();
LCD_PrintString(0, 0, "Starting");
extern void PlayMusic(void *params);
xTaskCreate(PlayMusic, "MusicTask", 128, NULL, osPriorityNormal, NULL);
xTaskCreate(game1_task, "GameTask", 128, NULL, osPriorityNormal, NULL);
}
4、总结
中断写队列仅适用于较简单的任务,复杂任务在任务中写队列。
五、队列实验2–玩打砖块游戏(任务写队列 / 旋转编码器)
当一个任务很花时间,我们就不能在中断中处理了,所以需要在任务去解析处理数据。
1、实验目标
在队列实验1的基础上,继续增加旋转编码器来控制挡球板移动,即既可以通过红外遥控器控制挡球板移动,也可以通过旋转编码器控制挡球板移动。
2、实验步骤
- 创建队列
- IR、ISR(旋转编码器中断)写队列B,唤醒 旋转编码器任务
- ** 创建旋转编码器任务;旋转编码器任务 读队列B,解析数据并处理后,写队列A**
- platform_task(挡球板任务中)读队列
3、核心代码展示
(1)在“ game1_task 函数中 ” 创建队列
QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 */
static uint8_t g_ucQueueRotaryBuf[10*sizeof(struct rotary_data)];
static StaticQueue_t g_xQueueRotaryStaticStruct;
struct rotary_data {
int32_t cnt;
int32_t speed;
};
//创建队列 这里使用静态xQueueCreateStatic方法来创建
g_xQueueRotary = xQueueCreateStatic(10, sizeof(struct rotary_data), g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct);
//创建旋转编码器任务
xTaskCreate(RotaryEncoderTask, "RotaryEncoderTask", 128, NULL, osPriorityNormal, NULL);
(2)旋转编码器 IRQ中断函数 中写队列
(3)
- 创建旋转编码器任务
- 在while(1)循环中使用 xQueueReceive(g_xQueueRotary, &rdata, portMAX_DELAY) 读队列
- 解析出数据 “速度” 和 “左右方向”
- 写队列,xQueueSend(g_xQueuePlatform, &idata, 0)
static void RotaryEncoderTask(void *params)
{
struct rotary_data rdata;
struct input_data idata;
int left;
int i, cnt;
while (1)
{
/* 读旋转编码器队列 */
xQueueReceive(g_xQueueRotary, &rdata, portMAX_DELAY);
/* 处理数据 */
/* 判断速度: 负数表示向左转动, 正数表示向右转动 */
if (rdata.speed < 0)
{
left = 1;
rdata.speed = 0 - rdata.speed;
}
else
{
left = 0;
}
if (rdata.speed > 100)
cnt = 4;
else if (rdata.speed > 50)
cnt = 2;
else
cnt = 1;
/* 写挡球板队列 */
idata.dev = 1;
idata.val = left ? UPT_MOVE_LEFT : UPT_MOVE_RIGHT;
for (i = 0; i < cnt; i++)
{
xQueueSend(g_xQueuePlatform, &idata, 0);
}
}
}
(4)挡球板任务 “ platform_task ” 读队列,并根据读到的指令移动挡球板
xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY);
4、总结
最终实现红外遥控器和旋转编码器都能控制挡球板移动。