基于Arduino Uno与OLED的PONG游戏开发实战

发布时间:2026/6/4 14:56:59

基于Arduino Uno与OLED的PONG游戏开发实战 1. 项目概述与核心思路想用一块小小的Arduino Uno开发板自己动手做一个能玩的游戏这听起来可能有点不可思议但今天我们要做的就是把经典的PONG游戏从70年代的街机厅搬到一块0.96英寸的OLED屏幕上。PONG游戏规则极简一个球两块挡板但正是这种纯粹让它成为了学习嵌入式交互系统开发的绝佳入门项目。这个项目不只是一个简单的代码复制粘贴它完整地串联了硬件电路搭建、外设驱动、实时逻辑处理和人机交互设计是理解微控制器如何“感知”世界并“做出反应”的生动一课。整个项目的核心思路非常清晰Arduino Uno作为大脑负责运行游戏逻辑SSD1306 OLED显示屏作为眼睛负责渲染游戏画面两个轻触开关作为双手负责接收玩家的操作指令。我们需要做的就是正确地连接这三者并编写程序让它们协同工作。在这个过程中你会接触到I2C通信协议如何驱动显示屏、如何通过数字引脚读取按键状态、如何用代码模拟物理运动球的反弹、挡板的移动以及如何将这些元素整合成一个流畅、可玩的游戏循环。无论你是刚接触Arduino的新手还是想寻找一个综合性练手项目的爱好者这个项目都能让你在动手的乐趣中扎实地掌握嵌入式开发的基本功。2. 硬件清单与核心元件解析动手之前清点并理解你手中的每一个元件至关重要。这不仅是为了备齐材料更是为了在后续连接和编程时心里有底。2.1 核心控制器Arduino Uno我们选用Arduino Uno R3作为主控板这是Arduino家族中最经典、资源最丰富的一款。它基于ATmega328P微控制器拥有14个数字输入/输出引脚其中6个可用于PWM输出、6个模拟输入引脚、16 MHz的晶振时钟。对于本项目而言它的数字I/O引脚足够我们连接两个按键其内置的5V稳压电路可以直接为OLED显示屏供电而硬件I2C引脚A4-SDA A5-SCL的存在使得驱动SSD1306显示屏变得异常简单。选择Uno的另一个原因是其庞大的社区支持任何你遇到的问题几乎都能找到解答。2.2 显示核心0.96英寸 SSD1306 OLEDI2C接口显示屏是本项目的视觉输出核心。我们选用的是0.96英寸、分辨率通常为128x64像素的SSD1306驱动芯片的OLED屏并且必须是I2C通信接口的版本。这一点非常重要因为市面上还有SPI接口的版本接线和库函数调用方式不同。OLED优势相较于LCDOLED是自发光器件每个像素点独立发光因此具有极高的对比度、更快的响应速度和更广的视角。在显示PONG游戏这种对比强烈的图形时效果非常出色。SSD1306驱动芯片它负责管理屏幕上的每一个像素点。我们通过I2C协议向它发送指令和数据它再将其转化为屏幕上的亮暗。I2C接口这是一种两线制串行通信总线SDA-数据线 SCL-时钟线允许多个设备共享同一条总线只需通过不同的设备地址Address来区分。我们这款屏的默认地址通常是0x3C或0x3D。引脚说明通常这类模块会有4个引脚VCC电源正极、GND电源负极、SDA数据线、SCL时钟线。有些模块可能还带有RESET复位引脚但通过I2C控制时通常可以悬空或接高电平。2.3 输入设备轻触开关Push Button我们使用两个最普通的4脚轻触开关。其内部结构很简单未按下时两组引脚断开按下时两组引脚导通。在电路中我们通常会配合上拉或下拉电阻来形成一个确定的电平信号。Arduino的数字引脚内部可以配置为上拉模式这样我们就可以省去外部电阻简化电路。2.4 辅助材料清单面包板一块用于免焊接搭建和测试电路是原型开发的神器。杜邦线若干建议使用公对公杜邦线用于连接Arduino、面包板和各个元件。可选10kΩ电阻两个如果你不打算使用Arduino内部上拉电阻则需要为每个按键配备一个外部下拉电阻。但本项目采用内部上拉方案因此可以省略。注意元件采购提示购买OLED屏时务必确认是“SSD1306 0.96 I2C”版本。你可以向卖家询问或查看产品描述I2C版本通常只有4个引脚VCC, GND, SDA, SCL而SPI版本通常有7个或更多引脚包括DC, RES, CS等。3. 电路连接详解与原理正确的电路连接是项目成功的物理基础。我们将分步完成并解释每一步背后的电气原理。3.1 OLED显示屏与Arduino的连接这是整个项目中最标准化的部分。找到你OLED模块上的4个引脚按以下方式连接OLED VCC-Arduino 5V为OLED模块提供5V工作电压。SSD1306模块通常集成了3.3V稳压所以可以直接接5V。OLED GND-Arduino GND共地为电流提供回路是所有电路连接的基准点。OLED SDA-Arduino A4在Arduino Uno上A4引脚除了是模拟输入其复用功能就是I2C的数据线SDA。OLED SCL-Arduino A5同理A5是I2C的时钟线SCL。连接原理I2C总线通过SDA和SCL这两根线在Arduino主机和OLED从机之间建立通信。时钟线SCL由主机Arduino产生用于同步数据传输的节奏。数据线SDA则在时钟的节拍下一位一位地传输数据。这种连接方式非常简洁只需要两根信号线和电源线就能驱动复杂的显示设备。3.2 按键与Arduino的连接我们使用两个独立按键分别控制左右挡板的上下移动。为了简化电路并利用Arduino的内部功能我们采用“内部上拉电阻按键接地”的方案。以控制“右挡板向上移动”的按键为例连接至数字引脚2按键的一个引脚连接到Arduino 数字引脚 2 (D2)。按键的另一个引脚连接到Arduino GND。另一个控制“右挡板向下移动”的按键则连接至数字引脚3同样另一端接GND。电路原理与内部上拉在Arduino程序中我们将D2和D3引脚模式设置为INPUT_PULLUP。这会启用芯片内部的一个上拉电阻约20kΩ-50kΩ将该引脚通过电阻连接到VCC5V。当按键未按下时D2引脚通过内部上拉电阻连接到5V因此我们通过digitalRead(D2)读取到的是高电平HIGH或1。当按键按下时D2引脚通过按键的导线直接连接到GND0V。由于导线的电阻远小于内部上拉电阻此时D2引脚被“拉低”到GND电位digitalRead(D2)读取到低电平LOW或0。这样我们就通过检测引脚电平从HIGH到LOW的变化来判断按键是否被按下。这种方案省去了外部电阻使电路更简洁。实操心得按键消抖机械按键在按下和弹起的瞬间内部的金属触点会发生物理抖动导致电平在极短时间内快速变化多次。如果不处理程序可能会误判为多次按下。我们会在软件部分通过“延时检测”或“状态机”的方法来解决这个经典的“按键抖动”问题。3.3 整体电路布局建议在面包板上搭建时建议将Arduino放在一侧OLED模块插在面包板中部两个按键放在便于操作的位置。电源5V和GND可以从Arduino引出在面包板两侧的电源轨上分布这样所有元件需要电源或接地时直接用短线连接到电源轨即可使布线清晰有序便于检查和调试。4. 软件开发环境与库配置硬件连接好后我们需要让Arduino“认识”这些设备并具备驱动它们的能力。这需要通过Arduino IDE安装相应的库文件。4.1 Arduino IDE基础设置首先确保你已安装最新版的Arduino IDE。将Arduino Uno通过USB线连接到电脑在IDE的“工具”菜单中选择正确的板卡工具-开发板-Arduino AVR Boards-Arduino Uno。选择对应的端口工具-端口选择识别出的Arduino Uno所在端口如COM3, /dev/cu.usbmodem14101等。4.2 安装必需的库文件Arduino的强大之处在于其丰富的开源库。对于SSD1306 OLED最常用的是Adafruit SSD1306库和它的依赖库Adafruit GFX Library。这两个库提供了高度封装的函数让我们可以用几条简单的命令就能在屏幕上画点、线、矩形、圆形和文字。安装方法通过库管理器推荐打开Arduino IDE点击项目-加载库-管理库...。在库管理器的搜索框中输入“Adafruit SSD1306”。在搜索结果中找到由“Adafruit”发布的“Adafruit SSD1306”库点击“安装”。通常它会提示你安装所有依赖库点击“全部安装”即可。这将会自动安装Adafruit GFX Library和Adafruit BusIO库。为了确保你可以再搜索“Adafruit GFX”确认其已安装状态会显示“已安装”。安装方法手动安装备选 如果网络原因无法使用库管理器你可以从GitHubAdafruit的仓库下载这两个库的ZIP文件。然后在Arduino IDE中点击项目-加载库-添加.ZIP库...选择下载的ZIP文件即可。注意先安装Adafruit GFX再安装Adafruit SSD1306。4.3 库的测试与验证库安装成功后我们可以用一个简单的示例程序来测试硬件连接和库是否工作正常。在Arduino IDE中点击文件-示例-Adafruit SSD1306-ssd1306_128x64_i2c。这个示例程序会尝试与你的OLED屏幕通信并显示Adafruit的Logo和一些测试图形。将代码上传到Arduino Uno。如果一切正常你应该能看到OLED屏幕亮起并显示图案。如果屏幕没有显示请按以下步骤排查检查接线确保VCC、GND、SDA、SCL四根线连接牢固没有接错。检查I2C地址有些屏幕的I2C地址是0x3D而不是默认的0x3C。你可以在示例代码中找到display.begin(SSD1306_SWITCHCAPVCC, 0x3C)这一行尝试将0x3C改为0x3D。使用I2C扫描工具上传一个I2C扫描程序可在示例中找到查看Arduino检测到的设备地址以确定屏幕地址。5. 游戏代码深度解析与编写理解了硬件和库之后我们来深入剖析PONG游戏的代码。我们将分模块构建游戏而不是直接上传一个完整的、难以理解的代码块。5.1 程序框架与全局变量定义任何游戏的核心都是一个循环在Arduino中就是loop()函数。在循环开始前我们需要在setup()中初始化硬件并定义游戏所需的所有状态变量。// 包含必要的库 #include SPI.h #include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // 定义OLED屏幕对象参数宽度(128), 高度(64) I2C通信对象 复位引脚-1表示无 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); // 定义按键引脚 #define BUTTON_UP_RIGHT 2 // 控制右挡板上的按键 #define BUTTON_DOWN_RIGHT 3 // 控制右挡板下的按键 // 左挡板控制单人模式可先由电脑控制或增加两个按键 #define BUTTON_UP_LEFT 4 // 预留 #define BUTTON_DOWN_LEFT 5 // 预留 // 游戏对象参数 int ballX SCREEN_WIDTH / 2; // 球初始X坐标 int ballY SCREEN_HEIGHT / 2; // 球初始Y坐标 int ballSpeedX 2; // 球X方向速度像素/帧正数向右 int ballSpeedY 1; // 球Y方向速度像素/帧正数向下 int paddleHeight 10; // 挡板高度 int paddleWidth 3; // 挡板宽度 int paddleRightY SCREEN_HEIGHT / 2 - paddleHeight / 2; // 右挡板Y坐标上边缘 int paddleLeftY SCREEN_HEIGHT / 2 - paddleHeight / 2; // 左挡板Y坐标 int scoreRight 0; // 右玩家得分 int scoreLeft 0; // 左玩家得分 // 按键状态跟踪用于消抖和状态判断 int buttonStateUpRight HIGH; int lastButtonStateUpRight HIGH; unsigned long lastDebounceTimeUpRight 0; unsigned long debounceDelay 50; // 消抖延时单位毫秒 // 其他按键状态变量定义类似...5.2 初始化函数 setup()setup()函数在Arduino上电或复位后只运行一次用于初始化串口、屏幕、引脚模式等。void setup() { // 初始化串口通信用于调试输出可选 Serial.begin(9600); // 初始化OLED显示如果失败则停止程序 if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // 地址0x3C或0x3D Serial.println(F(SSD1306 allocation failed)); for(;;); // 死循环阻止程序继续执行 } // 清空屏幕缓冲区 display.clearDisplay(); display.display(); // 将缓冲区内容发送到屏幕显示 delay(2000); // 等待2秒 // 设置按键引脚为输入上拉模式 pinMode(BUTTON_UP_RIGHT, INPUT_PULLUP); pinMode(BUTTON_DOWN_RIGHT, INPUT_PULLUP); // pinMode(BUTTON_UP_LEFT, INPUT_PULLUP); // 预留引脚 // pinMode(BUTTON_DOWN_LEFT, INPUT_PULLUP); // 显示游戏开始界面 display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 20); display.println(PONG GAME); display.setCursor(15, 40); display.println(Press any button); display.display(); // 等待任意按键按下开始游戏简易实现 while(digitalRead(BUTTON_UP_RIGHT) HIGH digitalRead(BUTTON_DOWN_RIGHT) HIGH) { // 空循环等待按键 } delay(500); // 去抖延时 }5.3 核心游戏逻辑函数 loop()loop()函数会无限循环执行每一轮循环就是一帧游戏画面。我们需要在其中完成读取输入、更新游戏状态、绘制画面。void loop() { // 1. 读取并处理输入以右挡板上键为例带消抖 int readingUpRight digitalRead(BUTTON_UP_RIGHT); if (readingUpRight ! lastButtonStateUpRight) { // 按键状态发生变化重置消抖计时器 lastDebounceTimeUpRight millis(); } if ((millis() - lastDebounceTimeUpRight) debounceDelay) { // 经过消抖延时后状态稳定 if (readingUpRight ! buttonStateUpRight) { buttonStateUpRight readingUpRight; // 只有当按键被稳定地按下LOW时才执行动作 if (buttonStateUpRight LOW) { movePaddleRightUp(); // 调用移动挡板的函数 } } } lastButtonStateUpRight readingUpRight; // 对BUTTON_DOWN_RIGHT进行同样的消抖处理... // 2. 更新游戏状态 updateBallPosition(); // 更新球的位置 checkBallCollision(); // 检测球与边界、挡板的碰撞 updateAIPaddle(); // 更新电脑控制的左挡板简易AI // 3. 绘制游戏画面 drawGameScene(); // 控制游戏帧率避免运行过快 delay(10); // 约100 FPS }5.4 关键子函数实现上面loop()中调用的函数需要我们自己实现。5.4.1 移动挡板函数void movePaddleRightUp() { // 右挡板上移但要确保不超出屏幕顶部 paddleRightY - 4; // 每次移动4像素 if (paddleRightY 0) { paddleRightY 0; } } void movePaddleRightDown() { // 右挡板下移确保不超出屏幕底部 paddleRightY 4; if (paddleRightY SCREEN_HEIGHT - paddleHeight) { paddleRightY SCREEN_HEIGHT - paddleHeight; } }5.4.2 更新球的位置与碰撞检测这是游戏逻辑的核心。void updateBallPosition() { // 根据速度更新位置 ballX ballSpeedX; ballY ballSpeedY; // 检测与上下边界的碰撞 if (ballY 0 || ballY SCREEN_HEIGHT - 1) { // -1是因为球有大小 ballSpeedY -ballSpeedY; // Y方向速度反向实现反弹 // 可以在这里加入一个简单的“哔”声如果有蜂鸣器 } // 检测与右挡板的碰撞 if (ballX SCREEN_WIDTH - paddleWidth - 2 ballX SCREEN_WIDTH - 1) { if (ballY paddleRightY ballY paddleRightY paddleHeight) { ballSpeedX -ballSpeedX; // X方向速度反向 // 增加一点随机性到Y方向速度使游戏更有趣 ballSpeedY random(-1, 2); // 确保速度不会太大或太小 ballSpeedY constrain(ballSpeedY, -3, 3); } } // 检测与左挡板的碰撞类似右挡板 if (ballX paddleWidth 2 ballX 0) { if (ballY paddleLeftY ballY paddleLeftY paddleHeight) { ballSpeedX -ballSpeedX; ballSpeedY random(-1, 2); ballSpeedY constrain(ballSpeedY, -3, 3); } } // 检测得分球出左右边界 if (ballX SCREEN_WIDTH) { // 球从右侧出界左玩家得分 scoreLeft; resetBall(); // 重置球到中心 } if (ballX 0) { // 球从左侧出界右玩家得分 scoreRight; resetBall(); } } void resetBall() { ballX SCREEN_WIDTH / 2; ballY SCREEN_HEIGHT / 2; // 随机决定发球方向 ballSpeedX random(0, 2) 0 ? -2 : 2; ballSpeedY random(-1, 2); }5.4.3 简易AI控制左挡板为了让单人游戏可玩我们需要一个简单的AI来控制左挡板。void updateAIPaddle() { // 最简单的AI让左挡板的中心跟随球的Y坐标 int paddleCenter paddleLeftY paddleHeight / 2; if (paddleCenter ballY - 2) { paddleLeftY 2; // 球在下方挡板下移 } else if (paddleCenter ballY 2) { paddleLeftY - 2; // 球在上方挡板上移 } // 确保挡板不超出屏幕 paddleLeftY constrain(paddleLeftY, 0, SCREEN_HEIGHT - paddleHeight); }5.4.4 绘制游戏场景使用Adafruit GFX库的函数进行绘制。void drawGameScene() { // 清空上一帧的显示缓冲区 display.clearDisplay(); // 绘制中间虚线 for (int i 0; i SCREEN_HEIGHT; i 4) { display.drawPixel(SCREEN_WIDTH / 2, i, SSD1306_WHITE); } // 绘制球一个实心圆 display.fillCircle(ballX, ballY, 2, SSD1306_WHITE); // 半径为2像素的球 // 绘制右挡板一个填充矩形 display.fillRect(SCREEN_WIDTH - paddleWidth, paddleRightY, paddleWidth, paddleHeight, SSD1306_WHITE); // 绘制左挡板 display.fillRect(0, paddleLeftY, paddleWidth, paddleHeight, SSD1306_WHITE); // 绘制比分 display.setTextSize(1); display.setCursor(SCREEN_WIDTH / 4, 0); display.println(scoreLeft); display.setCursor(3 * SCREEN_WIDTH / 4, 0); display.println(scoreRight); // 将缓冲区内容发送到屏幕完成一帧的显示 display.display(); }6. 系统调试与功能优化代码编写完成后上传到Arduino并观察现象。很可能第一次不会完美运行这就需要调试和优化。6.1 常见问题与硬件排查屏幕不亮或白屏检查电源用万用表测量OLED模块VCC和GND之间是否有5V电压。检查I2C地址这是最常见的问题。运行一个I2C扫描程序可在File-Examples-Wire-scanner找到确认你的屏幕地址是0x3C还是0x3D并修改代码中的begin()函数参数。检查接线确保SDA、SCL没有接反。有时杜邦线接触不良可以重新插拔或更换。按键无反应检查引脚模式确认代码中使用了INPUT_PULLUP模式。检查接线确认按键一端接信号引脚如D2另一端接的是GND而不是5V。检查消抖逻辑将消抖延时debounceDelay调大如100ms看是否改善。也可以在loop()中直接读取并打印引脚状态到串口监视器观察按键按下时电平变化是否正常。游戏运行卡顿或闪烁帧率控制loop()末尾的delay(10)决定了帧率。太小的延迟会导致Arduino处理不过来出现卡顿太大的延迟会导致游戏不跟手。可以尝试调整这个值。绘制优化display.clearDisplay()和display.display()是比较耗时的操作。确保只在需要更新画面时调用它们。在我们的设计中每帧都清屏重绘是合理的。6.2 软件功能优化建议基础版本运行稳定后你可以尝试以下优化让游戏体验更佳增加声音反馈连接一个无源蜂鸣器到另一个数字引脚。在球碰到挡板或边界时用tone()函数发出一个短促的声音在得分时发出不同的音调。改进AI难度当前的AI是“完美”跟随太难了。可以加入反应延迟、随机失误或者设置不同的难度等级通过改变跟随速度。// 带反应延迟和随机失误的AI void updateAIPaddleAdvanced() { int reactionDelay 5; // 反应延迟帧数 int mistakeChance 20; // 百分之一的失误概率 static int delayCounter 0; static int targetY SCREEN_HEIGHT / 2; delayCounter; if (delayCounter reactionDelay) { delayCounter 0; if (random(100) mistakeChance) { // 大部分时间正确判断 targetY ballY; } else { // 偶尔判断错误 targetY random(SCREEN_HEIGHT); } } int paddleCenter paddleLeftY paddleHeight / 2; if (paddleCenter targetY - 1) { paddleLeftY 1; // 更慢的移动速度 } else if (paddleCenter targetY 1) { paddleLeftY - 1; } paddleLeftY constrain(paddleLeftY, 0, SCREEN_HEIGHT - paddleHeight); }增加游戏状态引入“开始菜单”、“游戏进行中”、“得分结算”、“暂停”等状态用状态机来管理使游戏逻辑更清晰。双人模式再增加两个按键连接到预留的D4和D5引脚修改代码让左挡板也由玩家控制实现真正的双人对战。球速变化每成功击球一次球的X方向速度绝对值增加一点点随着回合进行游戏节奏会越来越快增加紧张感。6.3 性能考量与资源管理Arduino Uno的ATmega328P只有2KB的SRAM和32KB的Flash。我们的代码和库占用需要留意Adafruit库Adafruit_SSD1306和Adafruit_GFX库会占用不少程序空间Flash。如果未来增加更多功能如复杂菜单、多种音效可能会接近存储上限。可以使用工具-导出已编译的二进制文件来查看生成的.hex文件大小。全局变量所有全局变量如球坐标、速度、挡板位置、分数都存放在SRAM中。我们的变量不多完全在可控范围内。但如果定义大型数组如存储多帧动画就需要谨慎了。优化技巧对于不变的数据如字体、位图可以使用PROGMEM关键字将其存储在Flash中以节省宝贵的SRAM。7. 项目总结与扩展思考完成这个PONG游戏项目你走过的路正是一个典型的嵌入式产品开发流程需求分析做一个游戏- 元件选型Arduino, OLED, 按键- 电路设计连接图- 软件开发游戏逻辑与驱动- 调试优化解决问题提升体验。每一个环节都蕴含着重要的工程思维。这个项目的价值远不止于游戏本身。你实际掌握了一套方法论I2C设备驱动你学会了如何通过I2C总线与一个复杂的数字芯片SSD1306通信这套方法可以迁移到其他I2C传感器如温湿度计、气压计、陀螺仪上。数字输入与消抖你掌握了读取机械开关状态的标准方法包括硬件连接上拉电阻和软件处理消抖算法这是所有交互设备的基础。实时系统编程在loop()函数中你实践了如何在一个非操作系统、单线程的环境下管理多个并发的“任务”读取输入、更新逻辑、渲染输出并通过延时来控制节奏这是嵌入式实时编程的雏形。状态管理与碰撞检测你实现了一个简单的物理系统匀速直线运动、反弹和碰撞检测逻辑边界、矩形挡板这是许多游戏和模拟程序的核心。基于这个项目你的扩展之路可以非常广阔硬件扩展加入一个蜂鸣器做音效加入一个旋钮电位器来调节游戏难度或挡板速度甚至加入一个蓝牙模块用手机来控制挡板。软件复杂化将游戏升级为“打砖块”Breakout增加多个砖块、不同的球速、道具系统等。显示进阶尝试在OLED上显示更复杂的图形、动画或者绘制游戏开始、结束的炫酷界面。框架迁移尝试用同样的硬件Arduino Uno OLED 按键制作一个简单的音乐播放器界面、一个环境数据监视器或者一个贪吃蛇游戏。你会发现核心的驱动和输入处理部分是完全通用的。我个人在多次制作和教学这个项目中发现最能让初学者获得成就感的时刻往往不是第一次成功运行而是在他们根据自己的想法修改了某个参数比如让球速更快、让挡板变长并立刻在硬件上看到效果的时候。这种“代码改变物理世界”的即时反馈正是嵌入式开发最迷人的地方。从这个小游戏开始你已经拿到了进入这个广阔世界的第一把钥匙。

相关新闻