概要
通过freeRTOS创建多个任务。
- 旋转编码器任务
- 它的中断函数里面解析出旋转编码器的状态,写队列B
- 他的任务函数里面,读取队列B,构造好数据后写·
队列A
- 游戏任务
- 读取
队列A
获取控制信息,用来控制游戏
- 读取
- 红外遥控器驱动任务
- 中断函数里面解析出按键之后,写
队列A
- 中断函数里面解析出按键之后,写
关键代码
MX_FREERTOS_Init 任务初始化( 创建了游戏任务 ,音乐播放任务)
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(game1_task, "GameTask", 128, NULL, osPriorityNormal, NULL);
xTaskCreate(PlayMusic, "MusicTask", 128, NULL, osPriorityNormal, NULL);
}
1 . 游戏任务
这里我们创建了挡球平板任务: 任务就是一直读取队列A ( 读取红外遥控 )
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_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));
g_xQueueRotary = xQueueCreateStatic(10, sizeof(struct rotary_data), g_ucQueueRotaryBuf, &g_xQueueRotaryStaticStruct);
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();
//draw_end();
vTaskDelay(50);
}
}
1.0 挡球板任务函数:
[ 挡球板的任务就是控制它的左右移动 ,它需要一直读取队列A ]
【 写队列的是:红外遥控的中断服务程序】
QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
QueueHandle_t g_xQueueRotary; /* 旋转编码器队列 */
/* 挡球板任务 */
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))
xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY);
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. 红外中断服务程序写队列:
/**********************************************************************
* 函数名称: 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 data;
/* 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);
/* 写队列 */
data.dev = 0;
data.val = 0;
xQueueSendFromISR(g_xQueuePlatform, &data, NULL);
g_IRReceiverIRQ_Cnt = 0;
}
}
if (g_IRReceiverIRQ_Cnt == 68)
{
IRReceiver_IRQTimes_Parse();
g_IRReceiverIRQ_Cnt = 0;
}
}
程序架构
如果增加了一个mpu6050 六轴控制。那架构就变成如下:
缺点:
有一个设备就有一个任务,就需要用到栈,会比较消耗内存
架构优化
效果并不好
优化:使用队列集合
我们使用队列集来管理,各个硬件队列的数据。
比如队列A,写入队列A的时候,顺带吧队列A的句柄写入队列集S,任务B也是一样
架构变成如下所示:
分割线
并且非常容易得添加硬件
在InputTask 处理的时候,要判断是否是新添加的设备
队列集
在使用队列集的时候,需要先创建队列集。
然后吧需要加入队列集的队列通过下面函数设置:
xQueueAddToSet(g_xQueueIR, g_xQueueSetInput);
参数1:队列的句柄
参数2:队列集句柄
也就是说当前的队列集中收集了队列,后续队列集中对应的队列,只要有人写队列了,就会把当前队列的句柄写入队列集中。
InputTask 扫描队列集中的任务,取出队列句柄,处理数据,然后写档球板队列。
1. 创建队列集
队列集在创建的时候,使用如下函数
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
2. 把队列加入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
3 . 读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
代码架构分析
游戏game 任务
在游戏任务里面,主要是死循环里面是负责画 小球的位置。
而在游戏初始化里面,我们创建了挡球板任务(负责:读取挡球板队列)
而挡球板队列中( 写队列的人是:InputTask,它根据队列集中的队列句柄,扫描,然后取出队列句柄中的数据,[ 也就是要去对设备的队列获取数据
],转换成游戏控制数据后,写入挡球板队列中 )
void game1_task(void *params)
{
g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
draw_init();
draw_end();
/* 创建队列,队列集,创建输入任务InputTask */
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;
// 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);
}
}
InputTask 任务
/**********************************************************************
* 函数名称: InputTask
* 功能描述: 输入任务,检测多个输入设备并调用对应处理函数
* 输入参数: params - 未使用
* 输出参数: 无
* 返 回 值: 无
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/09/02 V1.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();
}
}
}
}
设备处理数据:红外IR数据
读取队列集,获得队列句柄,然后根据队列中的数据,写挡球板队列
读取IR红外 队列,然后 判断硬件数据,写挡球板队列
/**********************************************************************
* 函数名称: ProcessIRData
* 功能描述: 读取红外遥控器键值并转换为游戏控制键,写入挡球板队列
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/09/02 V1.0 韦东山 创建
***********************************************************************/
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);
}
设备处理数据:旋转编码数据
读取队列集,获得队列句柄,然后根据队列中的数据,写挡球板队列
读取旋转编码队列,然后 判断硬件数据,写挡球板队列
/**********************************************************************
* 函数名称: ProcessRotaryData
* 功能描述: 读取旋转编码器数据并转换为游戏控制键,写入挡球板队列
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/09/02 V1.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;
}
//cnt = rdata.speed / 10;
//if (!cnt)
// cnt = 1;
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);
}
}
设备处理数据:MPU6050数据
读取队列集,获得队列句柄,然后根据队列中的数据,写挡球板队列
读取MPU6050 队列,然后 判断硬件数据,写挡球板队列
/**********************************************************************
* 函数名称: ProcessMPU6050Data
* 功能描述: 读取MPU6050D的角度值并转换为游戏控制键,写入挡球板队列
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
* 修改日期: 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/09/05 V1.0 韦东山 创建
***********************************************************************/
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;
}
/* 写挡球板队列 */
idata.dev = 2;
xQueueSend(g_xQueuePlatform, &idata, 0);
}
游戏框架
游戏2
OLED 上有3辆小车,一开始在最左侧的位置。
每一辆汽车都有自己的汽车任务
:就是从最坐车跑到最右侧。
如果汽车想要进城、也就是发动汽车,那么此时需要信号量。
假设一开始的信号量有3个,那么3辆汽车分别同时重头的到位。
如果只有一个信号量,那么汽车1,发动之后,就消耗了信号量,此时汽车2、3 就无法发动汽车(阻塞了,永久等待),只能原地等待。
如果汽车1 到达终点之后,释放了信号量,那么任务2就获得了信号量,然后进城,到终点后,释放信号量。
此时,汽车3 获得信号量,发动汽车。
从上面的结果可以看出,信号量就是任务执行的一个资格,票据,没有票据就无法运行任务。