基于Arduino与LCD的贪吃蛇游戏开发:嵌入式系统与状态机实战

张开发
2026/6/5 15:24:29 15 分钟阅读

分享文章

基于Arduino与LCD的贪吃蛇游戏开发:嵌入式系统与状态机实战
1. 项目概述与核心价值几年前当我第一次把一块Arduino UNO从盒子里拿出来时我就在想除了让LED灯闪烁这个小板子到底能做什么更有趣的事情很快我意识到嵌入式开发的魅力就在于用代码赋予硬件“生命”去实现那些我们习以为常的软件功能。今天我想分享的就是这样一个项目用Arduino、一块普通的LCD屏幕和几个按键亲手打造一个属于你自己的贪吃蛇游戏机。这个项目绝不仅仅是一个简单的代码复制粘贴。它是一扇门通往嵌入式系统开发、实时游戏逻辑和底层硬件交互的实践世界。对于初学者你将完整走一遍从电路搭建、库文件配置到C/C编程的全流程理解信号是如何从你的手指按下按键经过去抖动处理最终转化为屏幕上小蛇的转向指令。对于有一定经验的开发者项目中涉及的状态机设计、基于内存的自定义图形渲染以及游戏速度的动态调整算法都是非常值得深入研究的实战技巧。整个项目的核心在于“系统思维”。你需要同时考虑硬件电路连接、引脚分配、软件游戏逻辑、显示驱动和人机交互按键响应、画面刷新三个层面。最终当小蛇在你自己搭建的“世界”里游走、成长时那种成就感是单纯在电脑上玩游戏无法比拟的。下面我们就从零开始一步步拆解这个有趣的工程。2. 硬件清单与电路设计解析动手之前清点并理解每一件物料是成功的第一步。这个项目的硬件需求非常基础大部分都能在入门套件中找到。2.1 核心组件详解Arduino UNO R3 (x1): 项目的大脑。选择UNO是因为其引脚布局经典社区资源丰富USB编程方便。它负责运行我们编写的游戏逻辑处理按键输入并向LCD屏幕发送显示指令。16x2 LCD 屏幕 (I2C接口) (x1): 项目的眼睛。这里强烈建议使用I2C接口的LCD屏而非传统的并行接口屏。原因很简单I2C屏只需要4根线VCC, GND, SDA, SCL就能驱动极大地节省了宝贵的IO口并简化了布线。传统的并行屏需要至少6个IO口会让电路显得非常杂乱。轻触开关/按键 (x2): 项目的控制手柄。我们只需要两个按键分别控制小蛇向左转和向右转。这种控制方式模拟了经典贪吃蛇游戏“顺时针/逆时针”转向的直觉操作比上下左右四个键更简洁也更适合在有限的IO资源下实现。10kΩ 电阻 (x2): 用于为按键配置上拉电阻。虽然Arduino的INPUT_PULLUP模式可以利用内部上拉电阻但使用外部物理电阻是更规范、抗干扰能力更强的做法能确保按键信号的稳定。面包板 (x1) 与 杜邦线 (若干): 项目的试验田和血管。面包板让我们可以无需焊接快速、灵活地搭建和修改电路。准备足够多的公对公杜邦线用于连接。注意购买LCD屏时务必确认其I2C地址。最常见的地址是0x27或0x3F。我们的代码中默认使用0x27如果你的屏幕不亮第一件事就是尝试修改这个地址。2.2 电路连接原理与实操电路连接的核心思想是“分区域理清电源和信号”。下面我们按步骤分解第一步建立电源骨架将面包板两侧的电源轨通常标有红色“”和蓝色“-”利用起来。用一根杜邦线将Arduino UNO的5V引脚连接到面包板的红色正极轨再用另一根线将Arduino的GND引脚连接到面包板的蓝色负极轨。这样整个面包板就都有了稳定的5V电源和地线。第二步连接LCD屏幕 (I2C)找到你的I2C LCD屏背面的小模块它通常有4个引脚GND: 连接到面包板的蓝色负极轨。VCC: 连接到面包板的红色正极轨。SDA: 这是I2C的数据线连接到Arduino UNO的A4引脚。在UNO上A4就是固定的SDA功能引脚。SCL: 这是I2C的时钟线连接到Arduino UNO的A5引脚。同理A5是固定的SCL功能引脚。 连接好后给Arduino上电LCD屏幕的背光应该会亮起这证明电源和背光驱动是好的。第三步搭建按键电路这是硬件部分最容易出错的地方关键在于理解“上拉电阻”和“下拉检测”。将两个轻触开关跨接在面包板的中缝两侧确保每个按键的四个引脚分属两个不同的电气行。对于每个按键进行如下连接按键的一端例如左侧引脚用一根杜邦线连接到面包板的蓝色负极轨GND。按键的另一端右侧引脚需要连接两样东西首先连接一个10kΩ电阻的另一端该电阻的另一端则连接到红色正极轨5V。这就是“上拉电阻”它在按键未按下时将该点电压“拉”到高电平5V。同时从按键的这一端与上拉电阻相连的同一点引出一根信号线连接到Arduino的数字引脚。我们假设左键接D6右键接D7。 这样当按键未按下时信号线通过上拉电阻保持高电平当按键按下时信号线直接与GND接通变为低电平。Arduino通过检测引脚从高到低的跳变来判断按键动作。完整的连接示意如下表所示组件引脚/端连接目标作用与原理LCD (I2C)VCC面包板 5V 轨提供工作电压GND面包板 GND 轨提供参考地SDAArduinoA4I2C 数据通信SCLArduinoA5I2C 时钟同步按键 Left引脚1面包板 GND 轨按下时提供低电平通路引脚210kΩ电阻 - 5V 轨上拉电阻维持默认高电平引脚2ArduinoD6信号输入检测低电平按键 Right引脚1面包板 GND 轨按下时提供低电平通路引脚210kΩ电阻 - 5V 轨上拉电阻维持默认高电平引脚2ArduinoD7信号输入检测低电平实操心得连接时养成“先电源后信号”的习惯。先确保所有元件的VCC和GND都正确连接到电源轨再连接数据线。上电前务必再次检查防止VCC和GND短路这是烧毁元件的头号杀手。3. 软件环境配置与核心库剖析硬件搭建完毕接下来是让硬件“活”起来的软件部分。Arduino开发的优势在于其丰富的库生态系统能让我们避免重复造轮子。3.1 开发环境与库安装首先确保你已安装最新版的Arduino IDE。打开IDE后我们需要安装驱动LCD屏幕的关键库LiquidCrystal_I2C。点击菜单栏的工具-管理库...打开库管理器。在搜索框中输入 “LiquidCrystal I2C”你会找到多个相关库。这里推荐使用由fmalpartida或johnrickman维护的版本它们比较稳定且功能完整。点击“安装”即可。安装完成后你可以在文件-示例中找到该库的示例程序可以用来测试你的LCD屏幕是否正常工作。除了这个库我们代码中还包含了stdlib.h和limits.h。这两个是C语言的标准库Arduino环境默认已包含无需额外安装。它们分别提供了随机数生成rand()和整数范围定义ULONG_MAX等功能。3.2 核心代码结构总览在开始逐行分析代码前我们先从顶层理解整个程序的架构。这是一个典型的事件驱动加状态机的嵌入式游戏程序。事件驱动主循环loop()不断检测两个事件1) 按键是否被按下输入事件2) 是否到达预定的游戏更新间隔定时事件。一旦事件发生就执行相应的处理函数。状态机游戏有多个状态如GAME_MENU菜单、GAME_PLAY游戏中、GAME_LOSE失败、GAME_WIN胜利。程序在任何时刻只处于一种状态不同状态下对同一事件如按键的反应完全不同。这比用一堆if-else判断要清晰得多。整个代码的骨架可以概括为初始化 (setup): 配置LCD、按键引脚、创建自定义字符、设置初始游戏状态。主循环 (loop): 不间断地 a. 扫描按键进行去抖动处理并更新小蛇方向或开始游戏。 b. 检查游戏更新定时器时间到了就计算一次游戏逻辑移动、碰撞检测、吃苹果并刷新屏幕。游戏引擎: 包含game_init初始化、game_calculate_logic逻辑计算、game_calculate_display显示计算等核心函数。图形引擎: 包含graphic_generate_characters创建字符、graphic_add_item绘图、graphic_flush刷屏等函数负责将抽象的游戏对象坐标转换成LCD能显示的字符。理解了这个架构再看具体的代码就会清晰很多。接下来我们将深入几个最关键的代码模块。4. 核心代码模块深度解析我们将跳出代码顺序聚焦于实现游戏功能最核心、也最容易产生困惑的几个部分进行拆解。4.1 去抖动处理让按键响应干净利落机械按键在按下和弹起的瞬间金属触点会发生物理抖动导致在几毫秒内产生一连串不稳定的高低电平信号。如果不处理一次按键可能会被误判为多次按下。我们的代码实现了一个简洁高效的边沿检测去抖动算法。#define DEBOUNCE_DURATION 20 // 去抖动时间单位毫秒 bool debounce_activate_edge(unsigned long* debounceStart) { if(*debounceStart ULONG_MAX){ return false; // 已触发过等待释放 }else if(*debounceStart 0){ *debounceStart millis(); // 首次检测到按下记录时间戳 }else if(millis()-*debounceStart DEBOUNCE_DURATION){ *debounceStart ULONG_MAX; // 标记为已触发 return true; // 抖动期已过返回“有效触发” } return false; // 仍在抖动期内忽略 }工作原理当digitalRead首次检测到按键为高电平注意我们的电路是低电平有效代码中检测HIGH是因为使用了内部上拉模式这里需要核对原电路使用外部上拉未按下时引脚为高按下时为低。但原代码中检测的是HIGH这似乎矛盾。这是一个关键点实际上原代码的按键检测逻辑可能基于不同的电路设计。在我们的硬件设计中按键按下拉低代码应检测LOW。为了兼容原代码逻辑我们可能需要修改电路为内部上拉模式即不接外部上拉电阻并将按键一端接GND另一端接IO口然后在setup()中使用pinMode(pin, INPUT_PULLUP)。这样未按下时引脚被内部电阻拉高读取为HIGH按下时接地读取为LOW。我们需要调整代码中的检测逻辑。为了教学清晰我们假设采用内部上拉模式检测LOW作为按键按下时debounceStart从0变为当前时间。在接下来的DEBOUNCE_DURATION20ms内即使信号因抖动在高低之间跳动函数也只会返回false。20ms后抖动通常已经停止。此时函数返回true表示一个干净、有效的按键动作被确认并将debounceStart设为ULONG_MAX防止在按键持续按住期间重复触发。只有当按键释放digitalRead为低在loop的另一部分代码中调用debounce_deactivate将debounceStart重置为0后才能准备下一次检测。注意事项DEBOUNCE_DURATION的值20ms是一个经验值对于大多数轻触开关是足够的。如果发现按键仍有连击现象可以适当增加到30-50ms。但不宜过长否则会影响按键响应的灵敏性。4.2 自定义图形渲染在字符型LCD上“绘图”标准16x2 LCD本质上是字符显示器每个位置只能显示一个预定义的字符。如何显示一条连续的小蛇和一个苹果技巧在于自定义字符和内存映射。第一步定义图形点阵代码中定义了两种图形的3行点阵数据因为LCD自定义字符是5x8像素我们只用了上半部分3行来让图形更紧凑byte block[3] { B01110, B01110, B01110 }; // 小蛇身体方块 byte apple[3] { B00100, B01010, B00100 }; // 苹果B01110是二进制表示对应每一行哪些像素点亮1哪些熄灭0。01110即左右两边像素灭中间三个像素亮形成一个粗点。第二步合成自定义字符LCD允许用户定义8个自定义字符编号0-7。我们的graphic_generate_characters函数巧妙地合成了这些字符。它利用一个循环生成8种不同的字符每种字符的上下两部分可以是空白、方块或苹果。例如编号i的字符其上半部分图形由(i1)/3决定下半部分由(i1)%3决定。这样我们就有了能表示“上蛇下空”、“上蛇下苹果”、“上空下蛇”等各种组合的字符集。第三步建立图形内存与刷新graphicRam是一个二维数组它在逻辑上对应屏幕的每一个“像素块”因为一个字符包含上下两个图形元素。graphic_add_item函数根据物体的坐标(x, y)和类型蛇或苹果计算出这个“像素块”应该显示哪种组合图形并将信息编码后存入graphicRam。 最后graphic_flush函数遍历屏幕每个位置从graphicRam中读出该位置需要的字符编号并调用lcd.write将其显示出来。这个过程就像先在内存里画好一整帧画面然后一次性刷到屏幕上避免了频繁操作LCD导致的闪烁。4.3 游戏逻辑与状态机实现游戏的核心逻辑集中在game_calculate_logic函数中它每隔一段时间gameUpdateInterval被调用一次。1. 移动逻辑 小蛇的移动通过一个位置历史数组snakePosHistory实现。数组的第0个元素是蛇头。移动时我们先将所有身体段的位置向后移动一格覆盖掉旧的蛇尾然后根据当前方向snakeDirection计算出新的蛇头位置。这种实现方式非常高效避免了移动整个数组。2. 碰撞检测撞墙检查新的蛇头坐标x和y是否超出了屏幕边界GRAPHIC_WIDTH和GRAPHIC_HEIGHT。撞自己遍历蛇身所有段从第1段开始检查是否有任何一段的位置与蛇头新位置重合。吃苹果检查蛇头新位置是否与苹果位置重合。如果重合则蛇长snakeLength加1并且游戏更新间隔gameUpdateInterval缩短为原来的90%*9/10实现加速效果增加游戏难度。3. 状态迁移 游戏状态gameState是一个枚举变量。在loop函数中根据不同的状态程序行为不同GAME_MENU: 等待按键按下以开始游戏调用game_init。GAME_PLAY: 正常游戏流程处理转向和逻辑更新。GAME_LOSE/GAME_WIN: 游戏结束状态在屏幕上显示结果和最终长度并等待虽然代码中未实现重启逻辑但可以扩展为长按按键重启。4. 方向控制 两个按键左、右的控制逻辑非常巧妙。它并非直接设置方向而是在当前方向的基础上进行顺时针或逆时针90度旋转。例如无论当前小蛇朝向哪里按下“右”键都会让蛇头向右转即上-右-下-左-上循环。这种设计用两个键实现了四个方向的操控简化了硬件需求也符合直觉。5. 完整代码集成与烧录调试在深入理解了各个模块之后我们现在需要将完整的代码整合、烧录并进行实际调试。以下是基于我们之前讨论的硬件连接内部上拉模式按键按下为低电平调整后的关键代码部分以及完整的操作流程。5.1 代码适配与关键修改点首先根据我们的硬件连接方案使用Arduino内部上拉电阻需要对原代码的引脚模式和检测逻辑进行调整。引脚定义与模式设置 在setup()函数中我们需要将按键引脚设置为输入上拉模式并移除原代码中可能矛盾的外部上拉电阻连接方式。#define BUTTON_LEFT 6 // 左键接D6 #define BUTTON_RIGHT 7 // 右键接D7 void setup() { lcd.init(); lcd.backlight(); lcd.print( Snake Game); lcd.setCursor(0, 1); // 关键修改设置为输入上拉模式引脚默认被内部电阻拉高 pinMode(BUTTON_LEFT, INPUT_PULLUP); pinMode(BUTTON_RIGHT, INPUT_PULLUP); graphic_generate_characters(); gameState GAME_MENU; }按键检测逻辑修改 在loop()函数中由于使用了内部上拉按键未按下时读数为HIGH按下时读数为LOW。因此我们需要检测LOW电平来判定按键按下。void loop() { // ... 其他代码 ... // 检测左键按下时为LOW if(digitalRead(BUTTON_LEFT) LOW){ if(debounce_activate_edge(debounceCounterButtonLeft) !thisFrameControlUpdated){ // ... 处理转向或开始游戏 ... } } else { debounce_deactivate(debounceCounterButtonLeft); } // 检测右键按下时为LOW if(digitalRead(BUTTON_RIGHT) LOW){ if(debounce_activate_edge(debounceCounterButtonRight) !thisFrameControlUpdated){ // ... 处理转向或开始游戏 ... } } else { debounce_deactivate(debounceCounterButtonRight); } // ... 游戏更新逻辑 ... }注意debounce_activate_edge函数内部的逻辑是通用的它只关心时间差不关心电平高低。我们只需要在电平符合条件时调用它即可。5.2 完整代码整合与烧录步骤新建项目打开Arduino IDE创建一个新的项目。粘贴代码将我们分析并修改后的完整代码包含所有函数、变量定义粘贴到新建的.ino文件中。确保#include LiquidCrystal_I2C.h等库引用正确。选择板卡与端口在工具菜单中选择开发板为Arduino Uno并选择正确的串行端口当你插入Arduino后会出现对应的端口号如COM3或/dev/ttyUSB0。编译验证点击左上角的“验证”对勾图标。IDE会检查代码语法错误并编译。如果遇到关于LiquidCrystal_I2C库的错误请确认库已正确安装。上传烧录点击“上传”向右箭头图标。等待进度条完成看到“上传成功”的提示。观察结果上传完成后Arduino会自动复位运行。你的LCD屏幕应该会显示“Snake Game”字样按下任意按键左或右游戏即可开始。5.3 调试与功能验证如果屏幕没有显示或者游戏逻辑不正常请按以下步骤排查问题1LCD屏幕无任何显示背光也不亮检查电源用万用表测量LCD的VCC和GND之间是否有5V电压。检查面包板电源轨连接是否牢固。检查I2C地址这是最常见的问题。尝试将代码中LiquidCrystal_I2C lcd(0x27,16,2);的地址0x27改为0x3F重新上传测试。有些屏幕的地址可能是0x20或0x38。你可以运行一个I2C扫描程序来查找地址。检查接线确认SDA、SCL是否分别接在了Arduino Uno的A4和A5引脚上且没有接反。问题2屏幕有背光但无字符或显示乱码对比度调节大多数I2C LCD模块上都有一个蓝色的电位器。用螺丝刀缓慢旋转它调节屏幕对比度直到字符清晰显示。初始化顺序确保在setup()中先执行lcd.init()再执行lcd.backlight()。问题3按键无反应或反应异常连击检查接线确认按键的信号线是否确实连接到了代码中定义的D6和D7引脚并且另一端可靠接地。检查上拉模式确认代码中使用了INPUT_PULLUP并且硬件上没有再接外部上拉电阻到5V否则会造成冲突。调整去抖动时间如果按键偶尔出现一次按下多次响应可以尝试将DEBOUNCE_DURATION从20增加到30或50。问题4小蛇移动或显示异常检查图形内存逻辑确保GRAPHIC_WIDTH和GRAPHIC_HEIGHT的定义16和4与你的LCD屏幕尺寸16x2以及图形渲染逻辑匹配。注意我们的渲染逻辑将2行字符屏幕虚拟成了4行像素行每行字符分上下两部分。检查坐标范围在game_calculate_logic中碰撞检测的边界条件GRAPHIC_WIDTH和GRAPHIC_HEIGHT必须与图形渲染使用的坐标系统一致。使用串口调试在怀疑的位置添加Serial.print语句输出蛇头坐标、蛇长、游戏状态等变量到串口监视器观察其变化是否符合预期。这是嵌入式调试最强大的工具。6. 项目优化与扩展思路一个基础版本运行起来后我们可以从多个角度对它进行优化和功能扩展这不仅能提升游戏体验也是深入学习嵌入式开发的好方法。6.1 性能与体验优化更流畅的动画目前的游戏更新是基于固定时间间隔的。可以引入更精细的时间管理将逻辑更新如移动、碰撞与画面渲染的帧率解耦。例如使用millis()管理一个稳定的30Hz或60Hz的渲染循环而游戏逻辑的更新速度即蛇的移动速度可以独立控制并逐渐加快。更丰富的视觉反馈吃苹果特效在蛇吃到苹果的瞬间可以让苹果所在位置的字符快速闪烁几次。死亡动画游戏失败时让小蛇的身体分段逐渐消失而不是直接显示“You lose!”。分数显示在游戏进行时在屏幕的角落例如右上角实时显示当前长度或分数。更人性化的交互开始菜单实现一个简单的菜单可以选择游戏难度初始速度。暂停功能增加一个暂停按键或者在游戏过程中快速双击某个键实现暂停/继续。声音反馈连接一个无源蜂鸣器为吃苹果、撞墙、游戏结束等事件添加简单的提示音。这需要用到PWM或tone()函数。6.2 硬件与功能扩展更换显示设备将字符型LCD升级为OLED显示屏通常也是I2C接口。OLED是像素级控制的可以显示更平滑的曲线、更小的像素点实现真正流畅的蛇身移动彻底摆脱字符块的限制。你需要将LiquidCrystal_I2C库替换为Adafruit_SSD1306等OLED驱动库并重写图形渲染部分。多样化输入方式用摇杆模块替代两个按键。摇杆可以提供更直观、更连续的方向控制虽然贪吃蛇只需要四个方向。你需要读取摇杆的X、Y轴模拟值并将其量化为上、下、左、右四个方向。增加游戏模式障碍物模式在游戏初始化时在场地中随机生成一些固定的障碍物。双人模式增加另一条由不同按键控制的小蛇实现竞争或合作。传送门模式屏幕边缘不再是墙而是传送到对侧。数据持久化加入EEPROM存储用来保存历史最高分。每次游戏结束后与最高分比较如果打破记录则更新并显示。6.3 代码结构重构建议随着功能增加最初的代码可能会变得臃肿。可以考虑进行面向对象式的重构创建Snake类封装蛇的位置数组、长度、方向、移动、生长等方法。创建Game类封装游戏状态、分数、更新间隔、碰撞检测、苹果生成等逻辑。创建Renderer类封装所有与显示相关的操作针对不同的显示设备LCD, OLED可以派生出子类。 这样重构后主程序loop()会变得非常清晰读取输入 - 更新游戏对象 - 渲染画面。模块之间的耦合度降低更易于维护和扩展。从一块简单的开发板到一款可玩的游戏这个项目完整地展示了嵌入式开发中硬件交互、实时逻辑和资源受限编程的核心思想。最重要的不是复现了这个游戏而是在解决每一个具体问题如按键抖动、LCD绘图、状态管理的过程中积累的经验和形成的思维模式。当你下次面对一个需要读取传感器、控制电机、并显示信息的项目时你会发现很多难题的解决思路早已在这个小小的贪吃蛇游戏中埋下了种子。

更多文章