51单片机学习笔记——OLED贪吃蛇
一、成果展示
功能:
1.贪吃蛇的基本游戏规则
2.有开始和结束界面
3.实现计分功能
4.游戏有无墙和有墙两种模式
5.游戏有简单和困难两种难度
6.在开始和结束时有声音提示


源码链接,提取码:o93u
二、软件部分
1.OLED模块与SSD1306使用
- OLED引脚图
OLED的引脚与SSD1306和单片机的通信方式有关。常见的有SPI,I^2^C串行通信和并行通信等。博主购买的是7针的OLED模块,引脚详情如下。
| 引脚 | 名称 | 解释 |
|---|---|---|
| 1 | GND | 接地端 |
| 2 | VCC | 电源端 |
| 3 | D0 | 在 SPI 和 I^2^C 通信中为时钟管脚(SCLK) |
| 4 | D1 | 在 SPI 和 I^2^C 通信中为数据管脚(MOSI) |
| 5 | RES | 复位管脚,低电平有效 |
| 6 | DC | 数据和命令控制管脚,0写命令,1写数据 |
| 7 | CS | 片选信号输入端,当输入低电平,表明OLED被选中,若只有OLED通信可直接接地 |
- OLED指令
与LCD1602类似,DC管脚置0之后,向OLED中写入命令,下表为部分指令。
| 指令 | 解释 |
|---|---|
| 00H-0FH | 页地址模式下设置列起始地址低位 |
| 10H-1FH | 页地址模式下设置列起始地址高位 |
| 20H | 设置寻址模式,00H/01H/02H为水平/垂直/页地址模式 |
| 26H/27H | 设置水平滚动的起始页,终止页和滚动速度 |
| 29H/2AH | 设置垂直和水平滚动的起始页,终止页,滚动速度,垂直滚动偏移 |
| 2EH | 禁用滚动,调用后RAM数据需要重写 |
| 2FH | 启用滚动,在26H/27H/29H/2AH设置好后调用 |
| 40H-7FH | 设置屏幕(地址)起始行,取值范围为[0,63],一般从头显示 |
| 81H | 设置对比度,共有256级对比度 |
| A0H/A1H | 设置段重映射,A0H左右反置,A1H正常 |
| A3H | 设置滚动垂直区 |
| A4H/A5H | 设置全屏点亮,A5H无视GDDRAM点亮全屏,A4H正常 |
| A6H/A7H | 设置反转显示,A7H反转(0表示点亮),A6H正常(1表示点亮) |
| A8H | 设置复用率,默认63 |
| AEH/AFH | 设置屏幕开启/关闭,AEH关闭屏幕,AFH开启屏幕 |
| B0H-B7H | 页地址模式下设置目标显示位置页起始地址 |
| C0H/C8H | 设置列输出扫描方向,C0H左右反置,C8H正常 |
| D3H | 设置显示偏移 |
| D5H | 设置显示时钟震荡频率 |
| D9H | 设置预充电周期 |
| DAH | 设置列引脚硬件配置 |
| DBH | 设置VCOMH反压值 |
| E3H | 空指令,不产生作用 |
- OLED与贪吃蛇
第一,由于OLED模块不能发送数据,所以不能读取OLED显存(GDDRAM)中的值。第二,OLED是对页地址进行整体赋值,贪吃蛇在显示蛇,食物等方面都要求对像素点进行操作。所以在OLED上显示像素点就会有困难。比如说某一页上的数据是 (0100 0000) 。如果想让其他像素点显示,会对这一页重新赋值,比如(0010 0000)。赋值过后,先前的数据就会被覆盖。也就是说一页只能打一个点,因此为了避免这种情况,要记录之前显示过的值,必须要在单片机内部申请一块内存区域充当OLED的显示缓存区,每次打点的时候读取先前的数据进行运算后再给OLED传送数据。
- 在OLED上打印像素点
博主购买的OLED附带中景园电子的部分源码,商家可能考虑到内存问题,就给打点函数删除掉了,博主花了好几天时间,查阅了相关资料,以及学长的帮助下,才完成了下面打印像素点的两个函数。
1 | |
以上代码需要注意的几个地方:
1 | |
在查阅相关资料发现,博主购买的单片机是STC公司的89C54RD+和12C5A60S2,其内存大小为均1280字节。OLED显示屏共128列8页,若在单片机中申请内存则是128×8×1=1024个字节。博主试过一次,IO口电平全部被拉低了…也就说申请的内存占用了IO口的寄存器。所以申请64×8的内存,节省一点,让蛇在左半屏活动就好了~
1 | |
没有这两句,OLED的显示过程中会出现这种诡异的情况,最后一页乱码了。博主猜测是由于使用的12单片机,送数据和送命令之间切换过快导致的。
- 在OLED上打印汉字和数字
在中景园电子提供的代码中,下面两个函数用于实现汉字的打印和数字的打印,非常方便。用于打印菜单和得分。
1 | |
2.贪吃蛇的基本算法
- 打印
1 | |
以上是部分打印函数。这几个函数都使用了打点函数,也就是左半屏,用于显示游戏画面。博主采用的方式是使用了一个定时器0。在定时器中断内进行显存的更新,以此来控制贪食蛇运动速度。在每次打印蛇和食物坐标之前,会使用一次清屏函数,清除上一帧蛇,否则会留下长长的尾巴。以下是部分定时器0中断内的代码。
1 | |
- 蛇
蛇类是贪吃蛇的核心算法。包括蛇身体坐标的储存,蛇的初始化,蛇的运动,蛇是否撞墙,是否吃到自己。
1 | |
1.蛇结构体:蛇身体坐标的储存采用了一个结构体数组,仅有x,y两个坐标的成员变量,同时也能储存食物的坐标。
2.蛇的初始化:首先要给蛇身体的结构体数组开辟一块内存空间,并用length变量储存蛇的长度。蛇头坐标为snake[0].x和snake[0].y;然后依次向后储存到第length个。C语言基础较好的朋友可能会用链表,malloc()函数去动态分配内存。在查阅相关资料发现由于51单片机内存太小,malloc()函数申请的内存很容易申请不到,返回NULL,也是因为博主学艺不精,放弃了这个想法。妥协之下,在蛇到达最大长度的时候直接退出游戏。最大长度为MAX,这里我设置的是30。(在控制台写贪吃蛇的时候博主很暴力的把max设置成地图长乘地图宽)第二,要给蛇头赋初值,博主在初始化函数中初始化了两节坐标。
3.蛇的移动:蛇的移动其实很好理解,蛇头先上下左右移动,然后把前一节的坐标赋给后一节。如果是无墙模式,先把头坐标直接赋值到另一侧,再进行后一节赋给前一节。
1 | |
但如果按照上述算法,有一个很致命的问题,在头坐标赋给第二节身体的时候,第二节身体再赋值给第三节,那第三节的元素不也是第一节的吗?因此采用的是从后向前赋值。从把倒数第二节坐标赋给尾巴,倒数第三节坐标赋给倒数第二节,以此类推。最后再根据蛇的运动方向使蛇头移动。至于为什么保留尾巴的坐标,与吃食物有关,后面再解释。
4.是否撞墙,是否吃到自己
撞墙和吃到自己的函数返回值都是bit类型(0和1),类似bool类型,如果撞墙/吃到自己返回1,否则返回0,表示无事发生。算法也很简单,撞墙就判断蛇头坐标是否和墙重合,吃自己函数就遍历所有身体坐标,看是否与头重合。
- 食物
食物类分为蛇的坐标,食物的刷新,食物被吃的函数。
1 | |
1.食物生成/更新:在游戏开始和食物被吃的时候,才调用这个函数,重新生成食物坐标。采用了定时器0和定时器1的TL值取模来生成x和y坐标(1-62之内的随机数)。如果和蛇身体有重合,则重新生成。
2.判断食物是否被吃:算法也很简单,先判断蛇头坐标是否和食物坐标重合。如果重合则蛇变长一节,把之前储存的蛇尾后面一节坐标赋给最新一节身体。然后重新生成一次食物。
3.按键检测
除正常的独立按键检测以外,还要储存前一次按键的值,用于蛇每次自动移动,用于表示蛇移动的方向;其次,由于贪吃蛇特性,譬如在向上走的同时,只能向左向右拐。因此每次给方向赋值的时候要先判断一次,防止其转向冲突。
4.开始界面
开始界面的实现首先要感谢好朋友的指点,给了我思路,虽然有些简陋,但基本功能可以实现。
代码部分较为冗长,这里只介绍一下基本思路。
首先要定义两个变量,一个是行数line,用于记录选中的行,一个是按键key,用于记录输入的按键。玩家只有上、下、确定键可以按。在按上下键的时候更改选中的行数,并重新打印小箭头;在按确定键的时候根据选中的行数去实现相应需求。开始游戏则跳出循环,更改难度和模式则更改相应变量的值,并把UI中的汉字更改掉。
5.得分面板,结束界面,无源蜂鸣器驱动函数
这部分就比较好实现了,得分为蛇长度-1;结束界面在相应位置打印汉字,关闭定时器0停止显存刷新;蜂鸣器使用不同频率的脉冲给蜂鸣器的IO口送电就好。如果想实现音调do re mi,由于Hz是每秒钟周期的次数,而我们的计时器和延时函数是按毫秒统计,所以需要进行频率的换算,比如C4是261Hz。
$$ \frac{1}{261}=\frac{x}{1000×2} $$
可以得到x=7.66,也就是约每7.66毫秒电平变化一次,可以发出C4(中央C)的音调。延时函数并不会很准确,如果想精确实现可以使用定时器。
三、硬件部分
1.元件清单
以下元件是博主自己使用到的,仅供参考。
| 元件名 | 数量 | 注释 |
|---|---|---|
| 洞洞板8*8 | 1个 | 也可以采用7×9,6×8,40pin锁紧座6.5cm左右 |
| 40针IC锁紧座 | 1个 | 也可以40针IC座,价格便宜但拔插困难 |
| 10K电阻 | 3个 | P0口使用了1个,蜂鸣器和复位电路1个,P0也可以买排阻 |
| 471(470Ω)电阻 | 1个 | 电源指示灯电路会用到 |
| 2K电阻 | 1个 | 蜂鸣器电路会用到 |
| 15Ω电阻 | 1个 | 蜂鸣器电路会用到 |
| 33μF电容 | 2个 | 晶振两端的负载电容 |
| 10μF电容 | 1个 | 复位电路会用到 |
| 104(0.1μF)电容 | 2个 | 电源的滤波电容和蜂鸣器旁路电容 |
| 独立按键 | 6个 | 上下左右确定和复位电路按键 |
| 排针、排母 | 若干 | 用于IO口和OLED的连接 |
| 杜邦线 | 若干 | 用于IO口和OLED的连接 |
| OLED模块 | 1个 | 显示屏幕 |
| STC89C54RD+ | 1个 | 单片机 |
| 12MHz晶振 | 1个 | 12MHz便于定时器定时 |
| 无源蜂鸣器 | 1个 | 发出提示音 |
| 三极管8550 | 1个 | 蜂鸣器电路使用,放大信号 |
| CH340模块 | 1个 | 用于供电,下载程序 |
2.最小系统的焊接
- 晶振电路
采用普中开发板的接法,如图所示。

- 复位电路
采用普中开发板的接法,如图所示。
- 电源指示灯

IO口给低电平二极管发光,用于测试单片机是否正常运行。
3.蜂鸣器电路的焊接
蜂鸣器电路采用这篇博客的接法,这里就不搬运了。区别是三极管集电极C端并联的33R电阻替换成了15Ω。(因为板子空间不是很够且没有买到33R)
4.独立按键的焊接
博主购买的是4脚的微动按键开关。
四脚分别两两导通。不想检测可以焊接对角线,对角线必然不导通。如果想检测可以使用万用表的二极管挡,如果万用表蜂鸣器响则代表导通(短路)。独立按键的焊接非常简单,一端连接IO口一端共地即可。
四、错误示范汇总
1.软件部分
- 关于51单片机选择问题
博主考虑到内存大小和flash空间问题,购买了STC89C54RD+和STC12C560S2,事实证明,12单片机总是会出现一些无法理解的错误,最后程序是在89C54上完成的。而常用的52单片机8K字节flash,512字节RAM更加捉襟见肘。 - 关于IIC和SPI通信选择问题
IIC通信速度较SPI通信速度慢,因此不适合使用IIC通信。 - 关于片选CS的问题
在阅读oled.c源码时,由于只有oled与单片机通信,CS端接地就万事大吉,以为CS没什么用,就手贱删掉了CS拉低的代码,事后想想也好蠢…后来又不接CS管脚,OLED也不会亮,还是老老实实的接在IO口上吧… - 关于地图大小的选择问题
之前非常天真的把地图设置成60×60,但是在给显存赋值,让蛇穿墙的时候,生成食物坐标时会造成麻烦,遂改成64×64。 - 定时器0与定时器1中断冲突问题
定时器1用于随机生成食物,但是千万不要开定时器1的中断!!!不要手欠写ET1=1;两个中断会冲突,这个错误排查了好久。
2.硬件部分
- 洞洞板的选择问题
为了图省事在线下购买的洞洞板,他下面都是连着的!!!表示第一次见这种洞洞板~第一次焊的时候,焊好之后才发现IO口都并上了….白闻了一下午焊锡….但是合理利用的话会省很多事。

(错误示范图) - 关于31管脚的上拉电阻问题
在普中科技的单片机开发板原理图中,31管脚有接一个4.7k的上拉电阻,但STC公司的单片机内部上拉,也可以不接。

- 元件引脚焊接问题
在第一块板子焊费之后,40脚的IC紧锁座根本拿不下来。(我的2块钱!!!)出于保险,在第二块板子上,我直接把IC座卡在板子上,没有点焊锡,结果电路连接的不是很顺畅,又检查了好几天。
3.排查电路问题的方法分享
- 检查电路是否导通
万用表真的很好用。调到二极管挡,测试有没有虚焊,哪里有没有导通,如果导通万用表蜂鸣器会响。就是用这个方法测出来IC座虚焊的。 - 检查晶振是否工作
晶振正常工作的时候,程序会执行,如果晶振不工作,程序会卡在初始状态。这个时候的现象一般是单片机管脚有电压,但是程序不运行。 - 检查单片机是否正常运行
可以通过电源指示灯来判断。我写了一个很简单的闪烁的灯的程序,如果灯正常闪烁,代表单片机正常运行。
部分图片和资料源自网络,如有侵权请联系删除。