首页IT科技stm32贪吃蛇课程设计分工情况(使用STM32F103ZE开发贪吃蛇游戏)

stm32贪吃蛇课程设计分工情况(使用STM32F103ZE开发贪吃蛇游戏)

时间2025-09-16 21:16:45分类IT科技浏览5909
导读:目录...

目录

前言

一                、设置FreeROTS用户任务

        (1)事件event任务

        (2)按键输入方向控制任务

        (3)果实食物任务

        (4)显示任务函数

        (3)开始任务

二                        、主函数

三        、ADC采样

四                、效果展示

前言

        网络上贪吃蛇游戏的开源资料已经很丰富了                ,但是详细讲解代码的很少                        ,所以我打算取之开源        ,回馈于开源                ,帮助大家能够更好的完成这款很经典的游戏项目                。

        为了能够更好的实时处理贪吃蛇的各项任务                        ,如:贪吃蛇任务        ,果实任务        ,显示任务等;所以对原始代码上了FreeROTS操作系统                        。

        这里我就不详细介绍FreeROTS操作系统了                        ,以后我会单独出一期FreeROTS的文章                ,大家想了解的话        ,有一份《FreeROTS内核使用指南》可以详读        。 

         如果大家英语好的话                        ,推荐读英文版                ,会少一些翻译上的错误                。

        实验平台:STM32F103ZE开发板,5个独立按键

独立按键与开发板连接:

KEYUP→F0

KEYDOWN→F1

KEYLEFT→F2

KEYRIGHT→F3

STOP→F4

        贪吃蛇项目概述:

        贪吃蛇也叫“移动的链表                ”                        ,先将不同任务所需要的参数组成结构体                        ,在用指针不断调用,还得用TFTLCD进行显示                ,可以参考我以前写过的博客:

学习记录:调用TFTLCD液晶屏_lcd_shownum_Bitter tea seeds的博客-CSDN博客

        废话不多说                        ,代码(分析)来一波                        。

一                        、设置FreeROTS用户任务

        一个任务就是一个线程        ,由于操作系统管理不同的任务                ,不同的任务分配在不同的内存块中                        ,所以一开始要给不同的任务设置优先级并为他们分配堆栈空间        。被挂起的任务被送回堆栈        ,就绪任务和运行任务从栈中恢复被送入寄存器        。

#ifndef __MY_TASK_H #define __MY_TASK_H #include "FreeRTOS.h" #include "task.h" //用户任务 //任务优先级 #define EVENT_TASK_PRIO 7 //任务堆栈大小 #define EVENT_STK_SIZE 128 //任务句柄 TaskHandle_t EVENTTask_Handler; //任务函数 void event_task(void *pvParameters); //任务优先级 #define KEY_TASK_PRIO 6 //任务堆栈大小 #define KEY_STK_SIZE 128 //任务句柄 TaskHandle_t KEYTask_Handler; //任务函数 void key_task(void *pvParameters); //任务优先级 #define APPLE_TASK_PRIO 5 //任务堆栈大小 #define APPLE_STK_SIZE 128 //任务句柄 TaskHandle_t APPLETask_Handler; //任务函数 void apple_task(void *pvParameters); //任务优先级 #define SNAKE_TASK_PRIO 4 //任务堆栈大小 #define SNAKE_STK_SIZE 128 //任务句柄 TaskHandle_t SNAKETask_Handler; //任务函数 void snake_task(void *pvParameters); //任务优先级 #define DISPLAY_TASK_PRIO 3 //任务堆栈大小 #define DISPLAY_STK_SIZE 128 //任务句柄 TaskHandle_t DISPLAYTask_Handler; //任务函数 void display_task(void *pvParameters); //任务优先级 #define LED_TASK_PRIO 2 //任务堆栈大小 #define LED_STK_SIZE 128 //任务句柄 TaskHandle_t LEDTask_Handler; //任务函数 void led_task(void *pvParameters); //任务优先级 #define START_TASK_PRIO 1 //任务堆栈大小 #define START_STK_SIZE 128 //任务句柄 TaskHandle_t StartTask_Handler; //任务函数 void start_task(void *pvParameters); #endif

        (1)事件event任务

        这是最重要的任务        ,它负责数据处理                        ,所以得等其他任务完成之后                ,才轮到它来执行        ,它的优先级最小                        ,首先设置一个死循环                ,判断游戏是否正常运行,如果正常运行在判断游戏是否暂停                        ,都没有我们则对按键进行检测                        ,根据按键按下的情况对蛇头坐标进行更改,坐标根据TFTLCD分辨率进行设置                ,更改完蛇头坐标                        ,对蛇尾坐标进行保存        ,在进行判断                ,如果坐标和果实坐标相同的话                        ,蛇的长度加1        ,果实消失        ,使能食物函数生成食物                        ,使能LCD进行显示                ,如果游戏结束        ,则返回游戏结束函数                        。

        怎么让蛇的移速随着时间的变化越来越快?

我们可以初始化蛇的速度变量为一个定值                        ,然后通过除以蛇的移速设置延时函数                ,来控制事件任务执行时间的间隔,随着不断调用蛇的移速                        ,定值不断变大                        ,延时函数时间的间隔边长,任务处理的时间间隔变长                ,显示出来蛇的移速变快                。

void event_task(void *pvParameters) { while(1) { if(event.GameSta==ON)//如果游戏正常则继续 { if(event.Process==ON)//如果没有暂停则继续 { switch(event.Direction)//检测按键情况,根据方向调整蛇头坐标 { case UP: { snake.firsty-=1; if(snake.firsty>GAME_YPART-1) { snake.firsty=GAME_YPART-1; } }break; case DOWN: { snake.firsty+=1; if(snake.firsty>GAME_YPART-1) { snake.firsty=0; } }break; case LEFT: { snake.firstx-=1; if(snake.firstx>GAME_XPART-1) { snake.firstx=GAME_XPART-1; } }break; case RIGHT: { snake.firstx+=1; if(snake.firstx>GAME_XPART-1) { snake.firstx=0; } }break; } snake.lastx=snake_axis[0].x;//保存下蛇尾坐标 snake.lasty=snake_axis[0].y; if(snake.firstx==apple.x&&snake.firsty==apple.y)//如果此时的坐标与食物坐标相同 { event.AppleSta=OFF; //食物被吃掉 snake.energybuf+=apple.energy;//蛇的能量加一 vTaskResume(APPLETask_Handler); //使能生成食物函数 } vTaskResume(DISPLAYTask_Handler); //使能显示函数 } }else GameOver();//如果游戏为结束状态则游戏结束 delay_ms(1000/snake.speed); //按照蛇的速度调整此核心数据处理函数的时间间隔 } }

        (2)按键输入方向控制任务

        按键任务通过switch判断语句实现                        ,需要注意的是        ,我们按的方向如果是蛇移动的方向的反方向                ,是不能响应的                        ,因为蛇不能有两个脑袋吧?🐶然后就是从结构体中用指针调用参数使用        。

void key_task(void *pvParameters) { u8 key; while(1) { key=KEY_PLAY_Scan(0); switch(key) { case KEY_UP_PRES: { if(event.Direction!=DOWN) event.Direction=UP; }break; case KEY_DOWN_PRES: { if(event.Direction!=UP) event.Direction=DOWN; }break; case KEY_LEFT_PRES: { if(event.Direction!=RIGHT) event.Direction=LEFT; }break; case KEY_RIGHT_PRES: { if(event.Direction!=LEFT) event.Direction=RIGHT; }break; case KEY_PASS_PRES://按下切换暂停/继续状态 { event.Process=!event.Process; }break; } delay_ms(20);//每20ms响应一次 } }

        (3)果实食物任务

        首先        ,果实的分布是随机的        ,所以                        ,通过STM32F1自带的一个ADC采样随机获得ADC的值作为果实                ,将模拟量转换为数字量        ,如果在上位机上编写的话                        ,可以使用时间戳来作为随机值                        。

        得到随机果实的坐标之后                ,我们还要保证食物的坐标不能出现在蛇的身上                。蛇的坐标也是通过LCD分辨率来进行设置的。

void apple_task(void *pvParameters) { u16 flag,i; while(1) { flag=1; while(flag) { flag=0; apple.x=Get_Rand()%(u16)(GAME_XPART); apple.y=Get_Rand()%(u16)(GAME_YPART); for(i=0;i<snake.length;i++) { if(snake_axis[i].x==apple.x&&snake_axis[i].y==apple.y) { flag++; } } } Display(apple.x,apple.y,RED); vTaskSuspend(APPLETask_Handler); } }

        (4)显示任务函数

        对此任务,我们首先得知道自己的LCD型号id然后根据自己LCD的型号进行驱动程序的编写                        。它的任务是显示出来蛇的身子                        。

        Display显示出蛇头                        ,如果果实坐标与蛇头坐标相同                        ,蛇身长度+1,速度+1                ,然后更新蛇头坐标                        ,保存蛇尾坐标        ,期间检查蛇头有没有碰到自己                ,遍历蛇身坐标是否与蛇头坐标相同                        ,如果碰到了        ,游戏结束。

void display_task(void *pvParameters) { u16 i; while(1) { Display(snake.firstx,snake.firsty,RED);//显示蛇头 if(snake.energybuf==0) { Display(snake.lastx,snake.lasty,WHITE); for(i=0;i<snake.length-1;i++) { snake_axis[i].x=snake_axis[i+1].x; snake_axis[i].y=snake_axis[i+1].y; } }else //如果吃到了食物 { snake.energybuf--; snake.length++; // if(snake.length%2==0)snake.speed++; snake.speed++; } snake_axis[snake.length-1].x=snake.firstx; snake_axis[snake.length-1].y=snake.firsty; for(i=0;i<snake.length-1;i++) { if(snake_axis[i].x==snake.firstx&&snake_axis[i].y==snake.firsty) { event.GameSta=OFF; } } vTaskSuspend(DISPLAYTask_Handler); } }

        (3)开始任务

        使用操作系统        ,线程进入临界区                        ,为了处理临界区的代码                ,需要关闭线程中断        ,处理完毕后在开启中断                        ,这是为了避免同时有其他任务或中断服务ISR进入临界区代码                。

void start_task(void *pvParameters) { taskENTER_CRITICAL(); xTaskCreate((TaskFunction_t )snake_task, (const char* )"snake_task", (uint16_t )SNAKE_STK_SIZE, (void* )NULL, (UBaseType_t )SNAKE_TASK_PRIO, (TaskHandle_t* )&SNAKETask_Handler); //创建食物任务 xTaskCreate((TaskFunction_t )apple_task, (const char* )"apple_task", (uint16_t )APPLE_STK_SIZE, (void* )NULL, (UBaseType_t )APPLE_TASK_PRIO, (TaskHandle_t* )&APPLETask_Handler); //创建事件任务 xTaskCreate((TaskFunction_t )event_task, (const char* )"event_task", (uint16_t )EVENT_STK_SIZE, (void* )NULL, (UBaseType_t )EVENT_TASK_PRIO, (TaskHandle_t* )&EVENTTask_Handler); //创建显示任务 xTaskCreate((TaskFunction_t )display_task, (const char* )"display_task", (uint16_t )DISPLAY_STK_SIZE, (void* )NULL, (UBaseType_t )DISPLAY_TASK_PRIO, (TaskHandle_t* )&DISPLAYTask_Handler); //创建闪烁任务 xTaskCreate((TaskFunction_t )led_task, (const char* )"led_task", (uint16_t )LED_STK_SIZE, (void* )NULL, (UBaseType_t )LED_TASK_PRIO, (TaskHandle_t* )&LEDTask_Handler); //创建输入任务 xTaskCreate((TaskFunction_t )key_task, (const char* )"key_task", (uint16_t )KEY_STK_SIZE, (void* )NULL, (UBaseType_t )KEY_TASK_PRIO, (TaskHandle_t* )&KEYTask_Handler); vTaskDelete(StartTask_Handler); taskEXIT_CRITICAL(); }

二        、主函数

        操作系统与裸机开发的一个区别就是                ,少了那个while(1)死循环,改成了任务调度

int main(void) { delay_init(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); Rand_Adc_Init(); uart_init(115200); LED_Init(); KEY_PLAY_Init(); LCD_Init(); DisplayInit(); Snake_Init(&snake); Apple_Init(&apple); Event_Init(&event); xTaskCreate((TaskFunction_t )start_task, (const char* )"start_task", (uint16_t )START_STK_SIZE, (void* )NULL, (UBaseType_t )START_TASK_PRIO, (TaskHandle_t* )&StartTask_Handler); vTaskStartScheduler(); }

三        、ADC采样

ADC几个比较重要的参数:

(1)测量范围:测量范围对于 ADC 来说就好比尺子的量程                        ,ADC 测量范围决定了你外接的设备其信号输出电压范围                        ,不能超过 ADC 的测量范围(比如,STM32系列的 ADC 正常就不能超过3.3V)                        。

(2)分辨率:假如 ADC 的测量范围为 0-5V                ,分辨率设置为12位                        ,那么我们能测出来的最小电压就是 5V除以 2 的 12 次方        ,也就是 5/4096=0.00122V        。很明显                ,分辨率越高                        ,采集到的信号越精确        ,所以分辨率是衡量 ADC 的一个重要指标                。

(3)采样时间:当 ADC 在某时刻采集外部电压信号的时候        ,此时外部的信号应该保持不变                        ,但实际上外部的信号是不停变化的                        。所以在 ADC 内部有一个保持电路                ,保持某一时刻的外部信号        ,这样 ADC 就可以稳定采集了                        ,保持这个信号的时间就是采样时间        。

(4)采样率:也就是在一秒的时间内采集多少次        。很明显                ,采样率越高越好,当采样率不够的时候可能会丢失部分信息                        ,所以 ADC 采样率是衡量 ADC 性能的另一个重要指标

#include "rand.h" //使用ADC产生16位随机数 void Rand_Adc_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M //PA1 作为模拟通道输入引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚 GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目 ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器 ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1 ADC_ResetCalibration(ADC1); //使能复位校准 while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束 ADC_StartCalibration(ADC1); //开启AD校准 while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束 } //获得ADC值 //ch:通道值 0~3 u16 Get_Adc(u8 ch) { //设置指定ADC的规则组通道                        ,一个序列,采样时间 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_1Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期 ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束 return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果 } u16 Get_Rand(void) { u16 randnum=Get_Adc(ADC_Channel_1)&0x0001,i; for(i=0;i<15;i++) { randnum<<=1; randnum+=Get_Adc(ADC_Channel_1)&0x0001; } return randnum; }

四                        、效果展示

声明:本站所有文章                ,如无特殊说明或标注                        ,均为本站原创发布                        。任何个人或组织        ,在未征得本站同意时                ,禁止复制                、盗用        、采集                        、发布本站内容到任何网站                、书籍等各类媒体平台                。如若本站内容侵犯了原著者的合法权益                        ,可联系我们进行处理        。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
js分隔符截取(js分割字符串的方法)