FreeRTOS_同步互斥与通信_环形buffer、队列_学习笔记
5.5 队列集
要支持多个输入设备时,我们需要实现一个“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 )
加入队列到队列集:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );
读取队列集:
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t const xTicksToWait);
5.5.1 多控制器实验
对上一篇笔记的代码进行修改,实现红外遥控和旋转编码器对游戏的共同控制。
在上一篇代码中,直接在中断响应函数IRReceiver_IRQ_Callback中写队列A,
在本例程中,先通过RotaryEncoder_IRQ_Callback(void)写队列B,再通过任务RotaryEncoder_Task读队列B,经过处理后再写队列A
在game1.c中做以下修改:
QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 */
static uint8_t g_ucQueueRotaryBuf[10*sizeof(struct rotary_data)]; //定义全局变量,保存队列buffer
static StaticQueue_t g_xQueueRotaryStaticStruct; //定义队列
/* 挡球板任务 */
static void platform_task(void *params)
{
...
while (1)
{
//if (0 == IRReceiver_Read(&dev, &data))
xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY);
...
}
...
}
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);
}
}
}
void game1_task(void *params)
{
...
/* 创建队列 */
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data)); //动态创建队列
g_xQueueRotary = xQueueCreateStatic(10, sizeof(struct rotary_data), g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct); //静态创建队列
// rotary_data{cnt,speed}和input_data{dev,val}这两个数据结构体在另一个文件中被定义
//g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct在全局变量区域被定义了
/*创建旋转编码器的任务,读队列B,解析数据,再写队列A*/
xTaskCreate(RotaryEncoderTask, "RotaryEncoderTask", 128, NULL, osPriorityNormal, NULL);
...
}
在旋转编码器的驱动程序中也需要进行读写队列的修改,driver_rotary_encoder.c如下:
extern QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 */
void RotaryEncoder_IRQ_Callback(void)
{
uint64_t time;
static uint64_t pre_time = 0;
struct rotary_data rdata;
/* 1. 记录中断发生的时刻 */
time = system_get_ns();
/* 上升沿触发: 必定是高电平
* 防抖
*/
mdelay(2);
if (!RotaryEncoder_Get_S1())
return;
/* S1上升沿触发中断
* S2为0表示逆时针转, 为1表示顺时针转
*/
g_speed = (uint64_t)1000000000/(time - pre_time);
if (RotaryEncoder_Get_S2())
{
g_count++;
}
else
{
g_count--;
g_speed = 0 - g_speed;
}
pre_time = time;
/* 写队列 */
rdata.cnt = g_count;
rdata.speed = g_speed;
xQueueSendFromISR(g_xQueueRotary, &rdata, NULL);
}
5.5.2 队列集改良实验
上一个实验中,红外写队列直接作用于任务队列,一般不推荐这么做,因为不方便代码移植。较好的做法是旋转编码器那边的做法,硬件部分管硬件,任务部分管任务。即,旋转编码器中断响应写队列(RotaryEncoder_IRQ_Callback)和旋转编码器任务读写队列(RotaryEncoderTask)分开,这样当旋转编码器要控制别的任务时,只需要修改后者即可,前者不需要修改。
现在将控制器增加为3个,每个控制器都有一个队列并各有一个任务写队列,并用一个Input_Task读取这三个队列并写挡球板队列。Input_Task读取三个队列的方法有两种:轮询和队列集。
如果采用轮询,依次进行读队列操作,那么将不可以设定超时时间(因为超时后才能读下一个队列),这种没有阻塞的轮询会极度浪费CPU资源。
因此使用队列集是合适的,队列集的创建与读写在本文开头说明,在此不再赘述。(队列集s是一个存放队列句柄的队列,写入队列a的时候会顺便把队列句柄写入其所在的队列集s)
因此本实验的改进思路就清晰了:三个控制器分别写三个队列abc,并把abc放到一个队列集s中。Iuput_Task任务读队列集s得到队列句柄,再读队列句柄得到队列abc,再处理数据写入挡球板队列。
在红外接收器驱动程序中做如下修改:
static QueueHandle_t g_xQueueIR; /* 红外队列 */
/*返回红外接收器驱动程序里的队列句柄*/
QueueHandle_t GetQueueIR(void)
{
return g_xQueueIR;
}
/*在初始化函数中添加创建队列的语句*/
void IRReceiver_Init(void)
{
#if 0
/*Configure GPIO pin : PB10 */
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_EVT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
#endif
g_xQueueIR = xQueueCreate(IR_QUEUE_LEN, sizeof(struct ir_data));
}
/* 在终端回调函数中写队列 */
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 ir_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;
}
/* 写队列 */
idata.dev = datas[0];
idata.val = datas[2];
xQueueSendFromISR(g_xQueueIR, &idata, NULL);
return 0;
}
在旋转编码器驱动程序中做如下修改:
static QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 */
static uint8_t g_ucQueueRotaryBuf[ROTARY_QUEUE_LEN*sizeof(struct rotary_data)];//用来静态分配的参数
static StaticQueue_t g_xQueueRotaryStaticStruct;
/*返回旋转编码器驱动程序里的队列句柄*/
QueueHandle_t GetQueueRotary(void)
{
return g_xQueueRotary;
}
/*在初始化函数中创建队列*/
void RotaryEncoder_Init(void)
{
g_xQueueRotary = xQueueCreateStatic(ROTARY_QUEUE_LEN, sizeof(struct rotary_data), g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct);
}
/*在中断回调函数中写队列*/
void RotaryEncoder_IRQ_Callback(void)
{
uint64_t time;
static uint64_t pre_time = 0;
struct rotary_data rdata;
/* 1. 记录中断发生的时刻 */
time = system_get_ns();
/* 上升沿触发: 必定是高电平
* 防抖
*/
mdelay(2);
if (!RotaryEncoder_Get_S1())
return;
/* S1上升沿触发中断
* S2为0表示逆时针转, 为1表示顺时针转
*/
g_speed = (uint64_t)1000000000/(time - pre_time);
if (RotaryEncoder_Get_S2())
{
g_count++;
}
else
{
g_count--;
g_speed = 0 - g_speed;
}
pre_time = time;
/* 写队列 */
rdata.cnt = g_count;
rdata.speed = g_speed;
xQueueSendFromISR(g_xQueueRotary, &rdata, NULL);
}
在MPU6050驱动程序中做如下修改:
#define MPU6050_QUEUE_LEN 10 /*定义队列长度*/
struct mpu6050_data{int32_t angle_x;}; /*定义陀螺仪数据*/
static QueueHandle_t g_xQueueMPU6050; /*陀螺仪队列*/
/*返回MPU6050驱动程序里的队列句柄*/
QueueHandle_t GetQueueMPU6050(void)
{
return g_xQueueMPU6050;
}
/*在初始化函数中添加创建队列的语句*/
int MPU6050_Init(void)
{
MPU6050_WriteRegister(MPU6050_PWR_MGMT_1, 0x00); //解除休眠状态
MPU6050_WriteRegister(MPU6050_PWR_MGMT_2, 0x00);
MPU6050_WriteRegister(MPU6050_SMPLRT_DIV, 0x09);
MPU6050_WriteRegister(MPU6050_CONFIG, 0x06);
MPU6050_WriteRegister(MPU6050_GYRO_CONFIG, 0x18);
MPU6050_WriteRegister(MPU6050_ACCEL_CONFIG, 0x18);
g_xQueueMPU6050 = xQueueCreate(MPU6050_QUEUE_LEN, sizeof(struct mpu6050_data));
return 0;
}
/*解析MPU6050数据,通过原生输入(加速度)得到x方向的角度*/
void MPU6050_ParseData(int16_t AccX, int16_t AccY, int16_t AccZ, int16_t GyroX, int16_t GyroY, int16_t GyroZ, struct mpu6050_data *result)
{
if (result)
{
result->angle_x = (int32_t)(acos((double)((double)(AccX + MPU6050_X_ACCEL_OFFSET) / 16384.0)) * 57.29577);
}
}
/*陀螺仪任务,将当前的角度处理后写队列*/
void MPU6050_Task(void *params)
{
//MPU6050_Init(); 把陀螺仪初始化函数放到freertos的入口执行
int16_t AccX; // 这里只关注x方向加速度
struct mpu6050_data result;
int ret;
extern volatile int bInUsed;// 这是判断LCD是否占用的全局变量
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程序中做如下修改:
static QueueSetHandle_t g_xQueueSetInput; /* 输入设备的队列集 */
static QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
static QueueHandle_t g_xQueueIR; /*红外接收器队列*/
static QueueHandle_t g_xQueueRotary; /*旋转编码器队列*/
static QueueHandle_t g_xQueueMPU6050; /*陀螺仪队列*/
/*挡球板任务*/
static void platform_task(void *params){...}
/*读取红外遥控器键值并转换为游戏控制键,写入挡球板队列*/
static void ProcessIRData(void)
{
struct ir_data idata; //创建红外接收数据
static struct input_data input; // 创建挡球板数据
xQueueReceive(g_xQueueIR, &idata, 0); // 读红外接收器队列
/*根据红外接收器数据修改挡球板数据*/
if (idata.val == IR_KEY_LEFT)
{
input.dev = idata.dev;
input.val = UPT_MOVE_LEFT;
}
else if (idata.val == IR_KEY_RIGHT)
{
input.dev = idata.dev;
input.val = UPT_MOVE_RIGHT;
}
else if (idata.val == IR_KEY_REPEAT)
{
/* 保持不变 */;
}
else
{
input.dev = idata.dev;
input.val = UPT_MOVE_NONE;
}
xQueueSend(g_xQueuePlatform, &input, 0); //写挡球板队列
}
/*读取旋转编码器数据并转换为游戏控制键,写入挡球板队列*/
static void ProcessRotaryData(void)
{
struct rotary_data rdata; // 创建旋转编码器数据
struct input_data idata; // 创建挡球板数据
int left;
int i, cnt;
xQueueReceive(g_xQueueRotary, &rdata, 0); // 读旋转编码器队列
/*根据旋转编码器数据修改挡球板数据*/
/* 判断速度: 负数表示向左转动, 正数表示向右转动 */
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); // 写挡球板队列
}
}
/*读取MPU6050D的角度值并转换为游戏控制键,写入挡球板队列*/
static void ProcessMPU6050Data(void)
{
struct mpu6050_data mdata; // 创建陀螺仪数据
struct input_data idata; // 创建挡球板数据
xQueueReceive(g_xQueueMPU6050, &mdata, 0); // 读陀螺仪队列
/*根据陀螺仪数据修改挡球板数据*/
/* 判断角度, 大于90度表示往左移动挡球板, 小于90度表示往右移动挡球板 */
if (mdata.angle_x > 90)
{
idata.val = UPT_MOVE_LEFT;
}
else if(mdata.angle_x < 90)
{
idata.val = UPT_MOVE_RIGHT;
}
else
{
idata.val = UPT_MOVE_NONE;
}
xQueueSend(g_xQueuePlatform, &idata, 0); // 写挡球板队列
}
/*输入任务,检测多个输入设备并调用对应处理函数*/
static void InputTask(void *params)
{
QueueSetMemberHandle_t xQueueHandle;
while (1)
{
/* 读队列集, 得到有数据的队列句柄 */
xQueueHandle = xQueueSelectFromSet(g_xQueueSetInput, portMAX_DELAY);
if (xQueueHandle)
{
/* 读队列句柄得到数据,处理数据 */
if (xQueueHandle == g_xQueueIR)
{
ProcessIRData();
}
else if (xQueueHandle == g_xQueueRotary)
{
ProcessRotaryData();
}
else if (xQueueHandle == g_xQueueMPU6050)
{
ProcessMPU6050Data();
}
}
}
}
void game1_task(void *params)
{
g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
draw_init();
draw_end();
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data)); // 创建挡球板队列
g_xQueueSetInput = xQueueCreateSet(IR_QUEUE_LEN + ROTARY_QUEUE_LEN + MPU6050_QUEUE_LEN); // 创建队列集
g_xQueueIR = GetQueueIR(); // 创建红外接收器队列
g_xQueueRotary = GetQueueRotary(); // 创建旋转编码器队列
g_xQueueMPU6050 = GetQueueMPU6050(); // 创建陀螺仪队列
/*将三个硬件的队列放入队列集*/
xQueueAddToSet(g_xQueueIR, g_xQueueSetInput);
xQueueAddToSet(g_xQueueRotary, g_xQueueSetInput);
xQueueAddToSet(g_xQueueMPU6050, g_xQueueSetInput);
xTaskCreate(MPU6050_Task, "MPU6050Task", 128, NULL, osPriorityNormal, NULL);
xTaskCreate(InputTask, "InputTask", 128, NULL, osPriorityNormal, NULL);
uptMove = UPT_MOVE_NONE;
ball.x = g_xres / 2;
ball.y = g_yres - 10;
ball.velX = -0.5;
ball.velY = -0.6;
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();
vTaskDelay(50);
}
}