系列文章目录
错误总结(没有什么各位有用的东西)跳转
关于嵌入式3/4作品展示(比较具体的描述了开机页面,灯控制页面的实现)跳转
心率和血压的测量稍微解释一下,有一个单个电源控制两个模块,两个模块相互影响怎么解决的一个问题。跳转
作品展示链接(B站平台内播放)
视频跳转
视频地址:
https://www.bilibili.com/video/BV1u44y1S7h5/?spm_id_from=333.999.0.0&vd_source=80dd998adf6419478f40a8a950edd07e
文章目录
前言
关于用单片机实现贪吃蛇,首先的第一个难点就是在屏幕上点的绘画。换句话来说就是对屏幕的应用得到达一定的水平。
第二个难点就是关于链表的理解。我使用C语言的方式写的,但是我熟悉链表这个词是在C++的容器里面。
仅此而已。所以说是一个比较初级的项目了。
源码在本文最下面。不想看我哔哔赖赖的直接怼代码去
一、项目前的需要攻破的难点
(1)、OLED屏幕的显示
对OLED的屏幕理解,就我目前的水平分一下理解等级
按数字越高理解越深的规则来说。1就是能够在OLED屏幕上显示英语。2是能够在OLED屏幕上显示汉字。3是能够精切的指导怎么擦除或者写入屏幕某一个位置的方法。4就是能够随意描点。
我在做这个的时候我在3这个理解层面。我在用3的理解层面写4层面的东西的时候出现了一个问题。在描绘同一页同一列的另一个点的时候,也就是说,第一步描绘同一列的第一个点,然后我还想点亮同一列的第二个点的时候,第一个点被我擦除了。遇到的问题再转换一个角度说就是,我还得提前知道屏幕里面的这一列的哪个灯亮。也就是说,在我想点亮这个列的别的灯的时候,我重新写入灯,我得把这一列之前亮的灯也带上,因为OLED屏幕给我们写入或者说控制OLED屏幕的方式是一列8位。这件事情我得怎么做到呢。
我借鉴到一个方法,就是在单片机里面直接有一张OLED屏幕的图。在想修改OLED屏幕的时候,把这个单片机里面的图改变,然后再真正的传进OLED。这个图的能力一定是需要一位一位的写入的,这样点亮一个灯的时候,就不会影响别的灯。这个图的实现方法是C语言里面定义一个和屏幕像素点一致的数组。
我的代码用的是数组是最简单易懂的map[128][64]。当然代码只有更好,思路只有更好。我了解到的其实可以用一维数组的办法,map[128*64],这样应该是更省空间的,这方面我不理解。甚至其实我调用的OLED库里面的那个定义也很巧妙的用map[128][8],他还是八位八位进去的,那怎么不打扰同一列的灯呢,用一个或。再换个说法,或不就是不打扰别人写入我这位吗,在OLED屏幕里面我用不了或,我在代码里面用或,最后在录屏幕上去不就行了吗。这段我想说的大概就是,数组的定义是可以很巧妙并且可以节省空间的。这样的定义map[128]64[]还是世界上最苯的定义方法,但是也是最直观的。
关于代码的实现:
uint8_t OLED_GRAM[144][8];
void OLED_DrawPoint(uint8_t x,uint8_t y)
{
uint8_t i,m,n;
i=y/8;
m=y%8;
n=1<<m;
OLED_GRAM[x][i]|=n;
}
//清除一个点
//x:0~127
//y:0~63
void OLED_ClearPoint(uint8_t x,uint8_t y)
{
uint8_t i,m,n;
i=y/8;
m=y%8;
n=1<<m;
OLED_GRAM[x][i]=~OLED_GRAM[x][i];
OLED_GRAM[x][i]|=n;
OLED_GRAM[x][i]=~OLED_GRAM[x][i];
}
//更新显存到OLED
void OLED_Refresh(void)
{
uint8_t i,n;
for(i=0;i<8;i++)
{
OLED_writeByte(0xb0+i,OLED_CMD); //设置行起始地址
OLED_writeByte(0x00,OLED_CMD); //设置低列起始地址
OLED_writeByte(0x10,OLED_CMD); //设置高列起始地址
for(n=0;n<128;n++)
OLED_writeByte(OLED_GRAM[n][i],OLED_DAT);
}
}
这里我展现的是我的OLED库的一些函数,贪吃蛇的函数实现方法我用更加浅显易懂的map[128][64]。每个元素我认为是布尔,只有1和0。
(1)、关于链表的相关知识
【1】.链表储存数据的形式
链表存储数据的方式先存自己的数据,再存前一个链表的数据,后一个链表的数据。虽然我觉得很多人应该都明白,但是我这里还是稍微描述一下为什么需要这样存。
为了能够让数据的插入和删除比数据这种容器简单,所以创作了链表这样的容器(关于链表的知识我是再C++中学到的)。用上图的构造创造出的数据存储方式,在添加数据的时候,可以通过改变插入数据位置的前后的指向,便完成数据的插入。这样做可以既不耗费同一块内存存储和一大堆数据,又简单的改变需要改变的少量值便可以完成,不需要遍历数组,重新写入整串数组就是为了插入一个数据。能理解的本来就理解,不理解的建议先到别的地方了解一下先。(特别说明尾部的后面一个链表的地址一般为空。)
创建链表的代码:
typedef struct snakeNode
{
uint8_t x;
uint8_t y;
struct snakeNode *next,*prev;
}SNAKE_T,*SNAKELIST;
【2】.双向链表对这个工程的意义
单向链表就是一个链表只指向后面的一个链表,而这里创建的双向链表需要在头部寻找前面的尾部。就是头需要又尾部信息的地址。为什么这样做呢主要有两大意义。
1.方便找到尾部
在这里我想先讲一个事情。创建链表的时候,一开始就是创建头,然后创建第二个链表,根据关系规定好第二个链表的内容。那么我们调用链表的时候,和数组是一样的,是需要一个地址去让我们找到这个数组。所以一开始我们就得记录一个全局变量的头,然后根据这个头把链表加长。也不知道这么绕一下能不能绕明白,绕不明白就记着。关于我们调用链表的话,我们只能通过头来找其他的数据。因为这样一个特性,所以才会有接下来的问题。 如果是单向链表,从头部找到尾部就需要一个想数组一样的轮询,一个一个的问,这样的效率一定不高。就像,我要你姐姐闺蜜的微信,我问你要,你还得问你姐姐,你姐姐还得问你闺蜜同不同意。很麻烦吧,等的心急如焚了。好嘞,皮一下,收回来。为了让系统的整个效率提高,或者说为了防止链表过长的时候卡死系统。所以我们让这个链表是双向的,让尾巴更容易找到。通过让记录的数据稍微庞大一个字节,来让系统的运行更加顺畅。 为什么就为了一个尾部这么大费周章,这样的操作应该就找到尾部的效率提高罢了,链表多起来,一个字节也是多啊。? 这个和接下来的第2点有很大的关系,我就目前先解释一下这个问题。为了方便清理尾部。再蛇移动的时候,无论你让蛇怎么动,你都需要清理尾巴,因为我们实现的方式就像在一张画里描点,蛇多长我们就在128*64的格子里面画点。第一次画图蛇占第一,第二,第三个格子。第二次画图,蛇站第二,第三,第四个格子。那我们留在图里面的蛇就是第一,第二,第三,第四个格子的长度。所以需要清理尾巴。清理尾巴得先找到尾巴吧,这么大费周章的为了更好的找到尾巴现有这方面的好处。 #### 2.更加简单,高效的让蛇移动 其实蛇的移动也算是这个工程的一个突破点,但为了讲明链表这里先涉及一下。 模仿蛇的移动。有两种办法可以模仿,或者说目前我知道的只有两种办法。第一种方法,所有的链表往前挪,简答粗暴。在蛇的移动的时候,所有的链表数据变成他自己前一个的链表的数据,在这样移动的时候瞬间,头链表的位置就有两个点也就是有两个自己数据一样,但是指向不一样的链表,头根据指示往你规定的方向移动,最后清楚蛇尾在图上的位置,这样便可以做到蛇的移动。没看明白,那就记着,第一种简单粗暴,所有的链表或者说蛇身给我往前挪。 第二种办法就高明的多得多。第一种方法,抽象点说,不就是清除蛇尾在图上的印记,移动蛇身吗。整个蛇的长度在没有别的情况下,一样长吧。在外面看来不就像,把蛇尾挪到蛇头吗。希望又能让你们豁然开朗,然后连说妙妙妙。快速的找到蛇尾,放在蛇头,这样蛇就动了,蛇身是什么东西,不认识。整个系统的效率不就哗啦啦的提高了。 **其实为什么用双向链表,五个字就可以表达的比较清楚了,为了蛇尾巴。**二、从头开始写贪吃蛇
1.描点函数开始。
画图你得先有笔吧,当然你咬手指用血画也行,那会很难受,方法不对,没有工具会很难受的,第一个很重要的工具就是描点函数。
代码展示:
点我跳转
2.制作一个二维数组作为贪吃蛇的整个地图。
笔有了还需要有图。如果觉得我说的抽象我这里再解释一下,这个图相当于一个你的秘书,你把想要改变的数据丢给你的秘书,让他差不多在下班之前给OLED屏幕刷新一下,这样不香嘛,省的你天天要麻烦OLED。能理解就理解,不理解但是想理解就看下我的经历或许对你有帮助。
代码显示:
uint8_t Map[128][64] = {0}; //为了更好的体验先保存最后更新BUF
以下为我的个人经历。
因为我知道画蛇是用链表,链表里面记录的数据有自己的位置,X,Y。所以我认为我自己在移动蛇,创造蛇的时候输入OLED屏幕数据不就好了嘛。我这么干了,过了一下午我直呼前辈牛逼。
首先我得画背景嘛,背景进去非常顺利,我甚至还在吐槽为什么要饶一个大弯来画背景,我直接进去不更快,更香。然后到创建蛇,也比较顺利,再到蛇移动的时候,我就觉得逻辑也来越难受,就像有只笔不知道往哪里画一样。我尝试咬破自己的血(比喻为硬上哈),然后再一堆血水上画,我发现,也许可以,但是整个代码的运行说不定会比较奇怪。首先,说一个用屏幕的一个很奇怪的其实也不奇怪的问题。你在屏幕的左边刷一些东西,想保留左边的东西,变化屏幕右边的东西的时候,你会发现非常奇怪,你不断变化右边的东西的时候,你左边的东西会乱码。为什么会出现这样的现象,我有一套不是很占的住脚的理论。因为你不断再刷新屏幕,你一段发送010101,第二段发送010101,当通道堵塞或者通道受到影响的时候,可能你屏幕收到就是111111。按着这样的理论我尝试调慢屏幕接受的速度,有一定的作用效果,但是最后还是会出现乱码。再回来,如果我不用图画的画,就创造蛇调用屏幕,创造食物调用屏幕,背景很可能会被我弄得七零八碎。就像再血水里用笔画画,你很难保证水不流回到你画的地方。(有那么一点点类似,但是不多。)首先我想到的问题就是这个,其实我想到一个问题就是我怕如果蛇长了,会明显的让客户玩游戏的体验差到要死。如果是轮询蛇尾巴,动动蛇人就可能分辨的出来循行的卡顿,动完蛇之后,食物又要有一定的时间才会出来,吃到食物蛇又有一定时间才能加长。就是说我如果不是画一张图,最后以整张图的方式上交给OLED的画,首先效果可能会产生分裂,体验也许会出现问题。所以我最后屈服了,我弄了一个数组来说图。因为这样不但解决了上面的问题,而且逻辑还清晰的要死。
3.为地图画上背景。
我们整个项目的逻辑是先画图,最后在把图放在OLED屏幕上,所以背景得先画在我们的图里面。把背景画完就可以直接放在OLED屏幕上先。
代码演示:
void Game_break(void)
{
int i,j;
//User = Create_Node();
//地图背景填充
for(i=0;i<128;i++)
{
for(j=0;j<64;j++)
{
if(i == 3||i==127||j==62||j==0)
{
Map[i][j] |= 1;//实际就是在Map BUF内写 1
}
}
}
//地图背景绘制
for(i=0;i<128;i++)
{
for(j=0;j<63;j++)
{
if(Map[i][j] == 1)
{
OLED_DrawPoint(i,j); //调用画点API
}
else if(Map[i][j] == 1)
{
OLED_ClearPoint(i,j); //调用画点API
}
}
}
}
4.代码是边写边改的,所以我建议先把显示部分搞定。
先把图放到OLED屏幕的相关操作作了比较好调试。
代码显示
//更新显示
void update_display(void)
{
uint8_t x,y;
for(x=0;x<128;x++)
{
for(y=0;y<64;y++)
{
if(Map[x][y] == 1)
{
OLED_DrawPoint(x,y); //填充点
}
if(Map[x][y] == 0)
{
OLED_ClearPoint(x,y); //清除点
}
}
}
}
下面是关于把蛇一个一个描点的操作,我放在游戏运行函数里面。但是按目前的步骤建议自己也命名为一个函数,逻辑上也好理解一点
while(n != NULL) //寻找表尾
{
Map[n->x][n->y] =1; //将表中的坐标写到Map中
n=n->next; //下一个节点
}
5.初始函数的配置
5.1 初始函数的配置
初始函数要做的事情就是画墙,创建蛇头,我们这里的创建是创建蛇头,然后创建了20个蛇身。
代码部分:
void GAME_init(void)
{
uint8_t i;
memset(Map,0,sizeof(Map));
Game_break();
User = Creat_Node();
User ->prev = User;
User ->next = NULL;
User ->x = 30;
User ->y = 30;
for(i=0;i<20;i++)
{
Add_Node(User);
}
memset()函数是用来让数组数据请0的,调用这个函数需要包含头文件#include “string.h”。
5.2 ADD_Node();函数详解
这里还封装了一个加蛇长的函数。
代码演示:
void Add_Node(SNAKELIST L)
{
SNAKELIST n = L;
//通过头来添加尾部
n = n->prev;
SNAKELIST NEW = Creat_Node();
NEW->prev = n;
NEW->next = n->next; //NULL
NEW->x = n->x;
NEW->y = n->y;
n->next =NEW;
L->prev = NEW;
}
具体就是加入输入链表的地址,根据蛇头找到蛇尾,加长蛇尾。
为什么叫做初始函数是因为初始函数只运行一次。把运行一次的东西都放在这里的操作叫做初始化。
6.有蛇,下一步就是蛇怎么运动。
蛇的运动在上面也稍微提了一下,这里给个跳转链接让你们先回去看下点我跳转
蛇的运动,第一步,先清除蛇的尾巴。
第二步,根据链表的结构改变蛇尾变成蛇头。
第三步,根据按钮的方向,改变头,其实是原来的尾巴的数据。
代码展示:
void Game_Mov(SNAKELIST L)
{
uint8_t event = last_dir;
SNAKELIST n,t;
t = L;
n = t->prev;
Map[n->x][n->y] = 0; //清除尾巴节点
// while(n != L) //从尾巴向前轮询
// {
// //轮询修改,蛇头先忽略,让蛇身先移动
// n->x = n->prev->x; //n的x 赋值为 上一个节点的x
// n->y = n->prev->y; //n的y 赋值为 上一个节点的y
// n = n->prev;
// }
n->prev->next = n->next;
n->next = t;
n->x = t->x;
n->y = t->y;
User = n;
//这是我按键读取的方法。
uint8_t key_snake = 0;
key_snake = get_KEY_number();
if(key_snake==1)
{
Input_event = 1;
}
else if(key_snake==2)
{
Input_event = 2;
}
else if(key_snake==3)
{
Input_event = 3;
}
else if(key_snake==4)
{
Input_event = 4;
}
if(Input_event != last_dir)
{
if(Input_event != NON)
{
last_dir = Input_event;
}
}
switch(event)
{
case UP:
n->y--; break;
case DOWN:
n->y++; break;
case LEFT:
n->x--; break;
case RIGHT:
n->x++; break;
}
}
7.你得有食物让蛇延长吧。
关于食物的产生有两个问题,第一个,随机数怎么产生。第二个,怎么判断地图上有没有食物。第三个,不让食物产生在墙上。
第一个问题的解决这里给出的方案是定时器,在轮询过程中,如果系统察觉到没有食物的时候,就会根据定时器的目前的CNT生成食物。这种随机数虽然也不是随机的,但是接近随机。
第二个问题这里给出的方案是设置一个标志位。在蛇吃到食物的时候就把标志位给去掉,那么就可以让整个系统察觉到没有食物,需要产生食物。
第三个问题,规定产生的位置就好嘛。
void GAME_NewFood(void)
{
uint32_t seed1;
uint8_t x,y;
if(food_flag == GAME_NO_FOOD)
{
while(1){
seed1 = TIM9->CNT;
//让随机值可以在屏幕打出杂点
x=seed1%120;
y=seed1%64;
//墙体以外不能放置食物
if(y > 0 && y < 63 && x > 3 && x < 126)
{
if(Map[x][y]==0) //判断食物的位置是否为不显示状态,不能让食物显示到蛇体或者墙体上
{
Map[x][y]=1; //填充地图内该点坐标为显示
food_x_y[0] = x; //记录食物位置
food_x_y[1] = y; //记录食物位置
food_flag = GAME_FOOD;
break;
}
}
}
}else{
return ;
}
}
为什么设置循环时防止食物长在蛇身上的时候,需要下一个轮询回来才生产食物,这样对用户的体验感就没那么好,不是吃完立刻就有食物产生。
food_X_Y是记录食物的位置。(不记录食物的位置就知道吃到的是墙还是食物。)
8.吃到食物,判断系统状态。
吃到食物是什么意思,撞到食物。和撞墙的原理很像。那么就先封装一个函数叫做状态函数,如果撞的是墙,那么就游戏结束,撞到的是食物,就加成蛇身。
代码展示:
uint8_t Game_Rules(SNAKELIST L)
{
//判断是否撞墙
if(L->x == 0 || L->x == 127 || L->y == 0 || L->y == 63)
{
return 1;
}
//判断是否吃到食物
if(L->x == food_x_y[0] && L->y == food_x_y[1])
{
food_flag = GAME_NO_FOOD; //吃到食物以后更新食物状态为无食物
Map[food_x_y[0]][food_x_y[1]]=0; //食物被吃掉则清理食物位置,当然蛇身一定会覆盖过蛇身,此语句有没有都行
return 2;
}
return 0;
}
这个函数都是返回数,所以预想的功能还未实现,只让系统有了一个分辨能力。
9.状态函数的预想的功能实现。
void Game_Run(void)
{
SNAKELIST p = User; //声明一个指针,指向User
SNAKELIST n = User; //声明一个指针,指向User
uint8_t ret = 0; //声明一个uint8_t 变量,用与检测状态
Game_Mov(User); //贪吃蛇的移动部分
while(n != NULL) //寻找表尾
{
Map[n->x][n->y] =1; //将表中的坐标写到Map中
n=n->next; //下一个节点
}
update_display();
GAME_NewFood(); //检测是否放新食物
ret = Game_Rules(User); //检测游戏状态
switch(ret)
{
case 0: //无障碍,直接跳出
break;
case 1: //撞墙 gameover
printf("Game over \r\n");
game_flag = GAME_RESET;
OLED_clear(); //清除屏幕显示
delete_Node();
break;
case 2: //吃到食物
Add_Node(p); //增加蛇身
break;
}
}
这件事情我放在一个叫运行游戏的函数里面。
三、最后的补充
代码还有些状态位,是为了拓宽功能的,比如暂停,加速这些。我这里就没有提及这些标志位的设置,命名也比较好理解。代码整理版的话,我稍微整理一下,但是整理版我是没有实物,验证代码是否可行的。所以没有整理的和整理的我都给你们。
总结
写太多了,我自己都受不了了。希望能对大家有帮助。贪吃蛇就核心来说只需要解决三个问题。第一个,蛇身的描绘,第二个蛇的移动,第三个蛇撞到了啥,整个系统该干啥。
(如有侵权,联系立删)
代码链接
链接:https://pan.baidu.com/s/1wcN4Nj1zp2BPCiOgnnFIfQ?pwd=8888
提取码:8888
–来自百度网盘超级会员V3的分享