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;elsecnt = 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);
#endifg_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);}
}