基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(五)|| 队列 | 队列实验(打砖块游戏)

声明:参考韦东山老师网页知识点以及自己理解整理笔记。

一、数据传输方法对比(以下列举方法逐步改善)

在这里插入图片描述

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队列句柄,要读哪个队列
pvBufferbufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait如果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。

4、删除队列

只能删除使用动态方法创建的队列,它会释放内存。

void vQueueDelete( QueueHandle_t xQueue );

四、队列实验1–玩打砖块游戏(中断写队列 / 红外接收键值)

1、实验目标

通过遥控器按键发送红外信号,单片机使用队列读取红外遥控器按键,控制挡球板移动来玩下图所示打砖块游戏,同时音乐正常播放。
在这里插入图片描述

2、实验步骤:

  1. 创建队列
  2. IR、ISR(中断中)写队列
  3. 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、实验步骤

  1. 创建队列
  2. IR、ISR(旋转编码器中断)写队列B,唤醒 旋转编码器任务
  3. ** 创建旋转编码器任务;旋转编码器任务 读队列B,解析数据并处理后,写队列A**
  4. 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)

  1. 创建旋转编码器任务
  2. 在while(1)循环中使用 xQueueReceive(g_xQueueRotary, &rdata, portMAX_DELAY) 读队列
  3. 解析出数据 “速度” 和 “左右方向”
  4. 写队列,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、总结

最终实现红外遥控器和旋转编码器都能控制挡球板移动。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值