目录

前言

一、设置FreeROTS用户任务

        (1)事件event任务

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

        (3)果实食物任务

        (4)显示任务函数

        (3)开始任务

二、主函数

三、ADC采样

四、效果展示


前言

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

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

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

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

        实验平台: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;
} 	

四、效果展示

STM32精英开发板制作贪吃蛇游戏

发表回复