材料:stm32开发板、0.96寸OLED模块(ssd1306 12864 SPI)、4×4矩阵键盘、杜邦线、st-link
实验原理:
1、 OLED通过SPI2显示游戏画面(使用了某商家驱动,包括oled.c、oled.h、oledfont.h)
2、 矩阵键盘实现按键事件判断(只用矩阵键盘行2、行3)S11(上:w) S12( 下:s) S8( 左:a) S16( 右:d) S4( 确认:o)
3、贪吃蛇游戏的原理是普通的贪吃蛇游戏的算法
实验设计:
PA4 PA5 PA6 PA7输出低电平
PC4 PC5为下降沿触发外部中断,并且设置为上拉
一行中有按键按下则触发中断,进行消抖,然后把一行中4个按键对应的PA依次置为高电平,检测4个按键中哪个按键被按下
具体按图连接
OLED按照下图连
关于游戏:
蛇的最大长度为36个方块
1、 标题界面(实现在play和help之间循环选择)
左边64×64部分为标题,右边64×64部分为菜单
2、 帮助界面
提供帮助信息
3、 游戏界面
左边64×64部分为游戏显示,右边64×64部分为分数显示
左边64×64部分留了宽度为4像素的空白,以及宽度为4像素的围墙
蛇由方块(8×8像素,填充部分为6×6像素)组成,撑满围墙内需要36块食物为一个方块(8×8像素,填充部分为2×2像素)
关于这个的贪吃蛇游戏的规则:
初始长度为3,方向为右每秒移动1个长度单位(匀速)
通过按键,方向可以改变为蛇前进方向的左边或右边(有bug,在往新方向移动之前)
随机生成食物
吃到食物长度+1
碰到墙壁或身体结束游戏(失败)
【长度达到最长长度结束游戏(成功?)
实验过程:
1、测试OLED基本功能
商家给的驱动没有图片显示,于是写了个简单粗暴的图片显示函数,定义在oled.c里,顺便在oled.h中声明(图片取模后就存在一个数组里。。。我放在了oledfont.h里)
//画图 void OLED_PIC1616(const unsigned char pic[][16]){ int i,j,k,temp; for(i=0;i<64;i++){ for(j=0;j<16;j++){ temp=pic[i][j]; for(k=0;k<8;k++){ OLED_DrawPoint(j*8+k,i,temp&0x80); //写好的设置单个像素是否填充的函数 temp=temp<<1; } } } }
图片取模方式如下(至于阴码、阳码,根据具体的效果来选):
原图(阳码):
显示效果:
2、测试矩阵键盘的功能
出现了按键时的抖动、释放可能多次触发了中断的问题
3、写代码测试
修复了一些bug
STM32CubeMX中的设置:
主要代码(以下只有USER CODE BEGIN里的代码):
/* USER CODE BEGIN Includes */ #include "oled.h" #include <stdlib.h> /* USER CODE END Includes */
/* USER CODE BEGIN PV */ /* Private variables ---------------------------------------------------------*/ extern const unsigned char pic1[][16]; //title图片 extern const unsigned char pic2[][16]; //lose图片 extern const unsigned char pic3[][16]; //win图片 const uint16_t gpio[]={GPIO_PIN_4,GPIO_PIN_5,GPIO_PIN_6,GPIO_PIN_7}; const char key5[]={'d','s','a','o'}; //d:right a:left w:up s:down o:ok uint8_t scene=0; //0:title 1:help 2:game uint8_t select=0; //0:run game 1:run help uint8_t length=3; //蛇的长度 //以下坐标都是方块左上角坐标 uint8_t head_x=32; //头部方块x坐标 uint8_t head_y=24; //头部方块y坐标 uint8_t snake_x[36]={32,24,16}; //身体方块x坐标 uint8_t snake_y[36]={24,24,24}; //身体方块y坐标 uint8_t direction='d'; //方向 uint8_t food=1;//判断食物存在 uint8_t food_x;//食物x坐标 uint8_t food_y;//食物y坐标 char score[3];//分数(长度)【用于打印】 /* USER CODE END PV */
/* USER CODE BEGIN PFP */ /* Private function prototypes -----------------------------------------------*/ void draw_food(void); void draw_score(void); void generateFood(void); void snake_init(void); uint8_t game(void); void init_title(void); /* USER CODE END PFP */
/* USER CODE BEGIN 2 */ __HAL_SPI_ENABLE(&hspi2); OLED_Init();//初始化OLED srand(1); init_title(); /* USER CODE END 2 */
/* USER CODE BEGIN 3 */ if(scene==2){ uint8_t flag; snake_init(); while(1){ //游戏循环 HAL_Delay(1000); flag=game(); if(flag==0){ OLED_PIC1616(pic2); HAL_Delay(5000); init_title(); scene=0; break; } else if(flag==2){ OLED_PIC1616(pic3); HAL_Delay(5000); init_title(); scene=0; break; } } } } /* USER CODE END 3 */
/* USER CODE BEGIN 4 */ //画出标题界面 void init_title(void){ OLED_Clear(); OLED_PIC1616(pic1); OLED_ShowString(64,4,"play",16); OLED_ShowString(64,24,"help",16); OLED_ShowString(104,20*select+4,"<-",16); } //画食物 void draw_food(void){ OLED_Fill(food_x+3,food_y+3,food_x+5,food_y+5,1); } //写分数 void draw_score(void){ sprintf(score,"%d",length); OLED_ShowString(64,24,score,16); } //生成食物坐标 void generateFood(void){ int i; do{ food_x=8*(rand()%6)+8; food_y=8*(rand()%6)+8; for(i=0;i<length;i++){ if(food_x==snake_x[i]&&food_y==snake_y[i]){ break; } } }while(i<length); } //进入游戏进行初始化 void snake_init(void){ int i; length=3; head_x=32; head_y=24; snake_x[0]=32; snake_x[1]=24; snake_x[2]=16; snake_y[0]=24; snake_y[1]=24; snake_y[2]=24; direction='d'; generateFood(); food=1; OLED_Clear(); OLED_Fill(4,4,60,60,1); OLED_Fill(8,8,56,56,0); for(i=0;i<length;i++){ OLED_Fill(snake_x[i]+1,snake_y[i]+1,snake_x[i]+6,snake_y[i]+6,1); } draw_food(); OLED_ShowString(64,4,"length:",16); draw_score(); } //游戏循环 uint8_t game(void){ int i; //更改头部坐标 switch(direction){ case 'w':head_y-=8;break; case 's':head_y+=8;break; case 'd':head_x+=8;break; case 'a':head_x-=8;break; } //撞墙判断 if(head_x==0||head_x==56||head_y==0||head_y==56){ return 0; } //更改身体坐标 for(i=length-1;i>=0;i--){ snake_x[i+1]=snake_x[i]; snake_y[i+1]=snake_y[i]; } snake_x[0]=head_x; snake_y[0]=head_y; //撞身判断 for(i=1;i<length;i++){ if(head_x==snake_x[i]&&head_y==snake_y[i]){ return 0; } } //吃判断 if(head_x==food_x&&head_y==food_y){ food=0; length++; if(length==36){ //长度最大,结束游戏 return 2; } draw_score(); } else{//若没有吃到,就消去最后一个方块 OLED_Fill(snake_x[length],snake_y[length],snake_x[length]+8,snake_y[length]+8,0); } OLED_Fill(head_x+1,head_y+1,head_x+6,head_y+6,1); //画头 //是否需要生成食物 if(food==0){ generateFood(); draw_food(); food=1; } return 1; } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){ UNUSED(GPIO_Pin); char key='n'; //确定键值 if(GPIO_Pin == GPIO_PIN_4){ HAL_Delay(50); if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_4) == 0){ HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET); if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_4) == 1){ key='w'; } HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET); while(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_4) == 0); } } else{ HAL_Delay(50); if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_5) == 0){ int i; for(i=0;i<4;i++){ HAL_GPIO_WritePin(GPIOA,gpio[i],GPIO_PIN_SET); if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_5) == 1){ key=key5[i]; HAL_GPIO_WritePin(GPIOA,gpio[i],GPIO_PIN_RESET); break; } HAL_GPIO_WritePin(GPIOA,gpio[i],GPIO_PIN_RESET); } while(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_5) == 0); } } //根据界面进行不同的操作 switch(scene){ case 0://标题界面 if(key=='w'||key=='s'){//选择 OLED_Fill(104,20*select+4,120,20*select+20,0); if(select){select=0;} else{select=1;} OLED_ShowString(104,20*select+4,"<-",16); } if(key=='o'){//确认 if(select){ scene=1; OLED_Clear(); OLED_ShowString(0,0,"normal play...",16); } else{ scene=2; } } break; case 1://帮助界面 if(key=='o'){//确认 scene=0; init_title(); } break; case 2://游戏界面 switch(direction){//改方向 case 'w': case 's': if(key=='a'||key=='d'){ direction=key; } break; case 'a': case 'd': if(key=='w'||key=='s'){ direction=key; } break; } break; } } /* USER CODE END 4 */
实验成果:
(↑还没赢过。。。)
基本实现了游戏功能,然而这个游戏还是有bug的(比如长按按键可以暂停、可以快速掉头进行自杀),暂时没改。