ESP32实战:AI生成嵌入式代码开发生存小游戏全记录
把“用 AI 写代码”这件事和嵌入式硬件结合起来一直是我很感兴趣的一个方向。ESP32 这块芯片性能不弱、外设丰富、开发门槛也不算高很适合拿来验证一个问题AI 生成的嵌入式代码到底能不能直接跑通。于是就有了这次实验。目标很简单用 AI 辅助写代码做一款基于 ESP32 的生存小游戏第一章先跑通整体流程从硬件选型、环境搭建、AI 协作开发到最终烧录运行完整走一遍。这篇文章就是 EP01 的实战记录里面会给出可复制的完整代码、调试思路和常见坑点无论是想了解 AI 写代码实战价值还是想在 ESP32 上做小游戏开发都可以参考。1. 项目背景为什么选 ESP32 做生存游戏1.1 AI 写代码的准确边界在哪里很多人在讨论 AI 写代码时习惯拿 Web 项目、后端接口、算法题来举例。这些场景里 AI 表现确实不错因为输入输出边界清晰、库生态成熟、错误信息丰富。但嵌入式开发不一样。它涉及到引脚复用、电路电平、内存限制、外设驱动时序以及串口监视器里那一堆看似莫名其妙的寄存器输出。AI 生成代码时按“通用编程思维”来写很容易忽略真实硬件的物理限制。用 ESP32 做小游戏刚好能暴露这类问题引脚能不能用、I2C 地址是多少、OLED 能不能正常点亮、按键上拉是否生效、游戏刷新率和屏幕刷新率是否匹配。每一个问题背后都是 AI 写代码时容易忽略的“硬件上下文”。所以这次实验比单纯用 AI 写“todo list”更有参考价值。1.2 生存游戏的功能定位这次做的不是大型 3D 生存游戏而是基于 OLED 屏幕的极简 2D 生存小游戏。玩法参考经典躲避游戏玩家控制一个小方块在屏幕左侧上下移动。敌人方块从右侧不断生成向左移动。玩家需要躲避敌人直到 HP 归零。每躲过一个敌人得分增加。HP 初始为 3碰到敌人则减 1。之所以选这个玩法是因为它逻辑足够简单AI 容易理解但又不至于简化为“LED 闪烁”。它涉及屏幕绘制、非阻塞循环、按键输入、碰撞检测、游戏状态管理这些是嵌入式游戏开发中最核心的基础能力。1.3 硬件方案的取舍生存游戏需要“显示 输入 主控”ESP32 可以全部搞定模块型号/方案说明主控ESP32 DevKitC V4经典开发板引脚多资料丰富屏幕0.96 寸 SSD1306 OLEDI2C128x64 分辨率占引脚少按键轻触开关 x2控制角色上、下移动供电电脑 USB / 充电宝5V 供电板载稳压到 3.3VOLED 是小游戏显示的不错选择。它相比 TFT 彩屏功耗更低、驱动更简单而且 Adafruit 和 U8g2 库都很成熟AI 训练数据里也有大量相关示例。2. 项目设计与引脚规划2.1 功能模块拆分写代码之前先拆功能。这是 AI 写代码最容易出问题的地方如果需求不明确AI 会按它理解的“游戏”自由发挥生成的内容可能不符合你的硬件。我在这里把需求拆成四个模块显示模块初始化 OLED绘制玩家、敌人、HP、分数。输入模块读取两个按键控制玩家上下移动带消抖和节流。游戏逻辑模块生成敌人、更新坐标、检测碰撞、管理游戏状态。主循环模块用非阻塞方式调度游戏更新和画面刷新。2.2 引脚分配根据 ESP32 DevKitC 经典引脚布局结合 OLED I2C 和按键需求分配如下外设引脚说明OLED SCLGPIO22I2C 时钟OLED SDAGPIO21I2C 数据按键上移GPIO13内部上拉按下为 LOW按键下移GPIO15内部上拉按下为 LOW说明一点这里特意避开了 GPIO0、GPIO2、GPIO12 等特殊引脚。GPIO0 是启动模式选择引脚GPIO2 是开发板上的板载 LED 引脚GPIO12 在部分模组上会影响 Flash 电压。AI 不一定知道这些细节但硬件工程师必须知道。2.3 I2C 地址确认SSD1306 的 I2C 地址通常是 0x3C个别屏幕是 0x3D。代码里默认使用 0x3C。如果屏幕不亮优先用 I2C 扫描程序确认地址不要一上来就改代码。3. 开发环境搭建3.1 Arduino IDE 安装 ESP32 支持开发环境我使用的是 Arduino IDE 2.x配合 ESP32 官方板卡支持包。打开 Arduino IDE 后进入文件 - 首选项在“附加开发板管理器网址”中填写https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json然后进入开发板管理器搜索esp32安装esp32 by Espressif Systems即可。需要注意版本问题。较早版本的开发板包在安装时可能看到类似这样的报错Failed to install platform: esp32:3.3.11. 13: internal: download failed: co...这个大概率是工具链下载超时或被网络策略拦截。解决方案有几种多试几次中断后重新安装。从 GitHub Releases 手动下载 esp32 开发板包手动解压到 Arduino 的 hardware 目录。更换更稳定的网络环境。如果公司或学校网络有限制可以联系管理员放行下载域名或者使用离线安装包。3.2 安装 OLED 显示库在库管理器中搜索并安装Adafruit SSD1306Adafruit GFX LibraryAdafruit SSD1306 库负责驱动屏幕底层Adafruit GFX 库提供画点、画线、画矩形、显示文字等绘图 API。两个库需要配套安装否则编译会报错。如果你更习惯 U8g2 库也可以用它。本文代码基于 Adafruit 库整体思路不变。3.3 项目结构Arduino 的项目结构比较扁平基本就是.ino文件。为了可读性我们保持单文件工程即可esp32_survival/ └── esp32_survival.ino如果你使用 PlatformIO则对应是esp32_survival/ ├── platformio.ini └── src/ └── main.cpp下面代码基于 Arduino IDE 编写PlatformIO 用户需要略微调整头文件包含方式核心逻辑一致。4. 用 AI 生成代码三轮回合实战4.1 第一轮让 AI 搭出基础框架我给 AI 的提示词很具体直接描述硬件环境、引脚和功能需求请用 Arduino 框架为 ESP32 写一个生存躲避游戏硬件如下 - 0.96 寸 SSD1306 OLEDI2C 地址 0x3CSCL 接 GPIO22SDA 接 GPIO21 - 两个按键GPIO13 控制角色向上移动GPIO15 控制角色向下移动均使用内部上拉 - 游戏规则玩家是屏幕左侧一个 6x8 的小方块向上/向下移动敌人方块 6x8 从右侧随机生成并向左移动玩家碰到敌人HP 减 1HP 为 0 时游戏结束敌人移出屏幕左侧时得分加 10 - 要求使用 Adafruit_SSD1306 和 Adafruit_GFX 库不使用 delay 阻塞游戏逻辑带按键消抖游戏结束时按任意按键重新开始AI 收到这样的需求后能够直接生成一个可运行版本。不过 AI 生成的代码存在几个通病它倾向于把所有逻辑写进 loop结构不清晰。敌人数组大小和随机生成逻辑可能会超出 OLED 显示范围。按键处理常直接使用 delay 消抖导致画面卡顿。4.2 第二轮补充游戏逻辑细节AI 第一版的核心逻辑基本正确但我在检查时发现两个问题敌人只会“一次性生成很多个”没有按时间间隔生成。玩家碰到敌人后敌人没有立即消失导致连续多次扣血。于是继续追问 AI敌人需要按间隔生成不能一次全出现玩家碰到敌人后该敌人立即消失并且 1 秒内不能再次扣血增加一个 gameStartTime用毫秒级别计时显示游戏存活时间。这轮迭代后AI 生成了 spawnEnemy、updateEnemies、checkCollision 等拆分函数。可以看到只要把需求量化清楚AI 是能较好理解游戏逻辑的。4.3 第三轮处理硬件相关的坑第三轮主要处理嵌入式特有的问题OLED 刷新率太高会闪烁需要调整 loop 里的刷新间隔。按键按下瞬间检测和持续检测的区别。ESP32 引脚内部上拉的可靠性。AI 在通用场景下生成的按键扫描代码往往是“按下一次触发一次”。但躲避游戏里玩家希望“按住持续向上移动”。这个细微差别如果不说明AI 很容易生成体验很差的控制逻辑。给 AI 的追问是我希望按住按键时角色持续移动而不是按一次动一次移动间隔控制在 80msOLED 刷新频率控制在 30FPS 左右不要使用 delay(33) 阻塞整个循环改用距离上次刷新的时间判断。经过这三轮对话代码从“能跑”变成了“好操作、不闪烁、逻辑稳定”的可玩版本。5. 完整代码与关键逻辑解读5.1 最终完整代码下面给出本次实验最终使用的完整代码可以直接在 Arduino IDE 中编译烧录。// 文件路径esp32_survival/esp32_survival.ino #include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // 屏幕配置 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); // 按键引脚 #define BTN_UP 13 #define BTN_DOWN 15 // 玩家参数 #define PLAYER_X 12 #define PLAYER_W 6 #define PLAYER_H 8 #define PLAYER_Y_MIN 10 #define PLAYER_Y_MAX (SCREEN_HEIGHT - PLAYER_H - 2) #define PLAYER_STEP 4 // 敌人参数 #define ENEMY_W 6 #define ENEMY_H 8 #define ENEMY_SPEED_MIN 1 #define ENEMY_SPEED_MAX 3 #define MAX_ENEMIES 6 #define SPAWN_INTERVAL_MS 1200 // 游戏全局变量 struct Enemy { int x; int y; int speed; bool active; }; Enemy enemies[MAX_ENEMIES]; int playerY 28; int health 3; long score 0; bool gameOver false; unsigned long lastSpawnTime 0; unsigned long lastFrameTime 0; unsigned long lastMoveTime 0; unsigned long gameStartTime 0; // 游戏初始化 void setup() { Serial.begin(115200); Serial.println(ESP32 Survival Game Starting...); pinMode(BTN_UP, INPUT_PULLUP); pinMode(BTN_DOWN, INPUT_PULLUP); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(SSD1306 allocation failed); while (1); } display.clearDisplay(); display.display(); resetGame(); } // 主循环 void loop() { unsigned long now millis(); handleInput(now); if (gameOver) { renderGameOver(); if (digitalRead(BTN_UP) LOW || digitalRead(BTN_DOWN) LOW) { delay(200); resetGame(); } return; } updateGame(now); // 控制画面刷新率约 30 FPS if (now - lastFrameTime 33) { render(); lastFrameTime now; } } // 按键处理 void handleInput(unsigned long now) { if (gameOver) return; // 按 80ms 间隔持续移动兼具按键消抖与移动节流 if (now - lastMoveTime 80) return; if (digitalRead(BTN_UP) LOW) { playerY - PLAYER_STEP; if (playerY PLAYER_Y_MIN) playerY PLAYER_Y_MIN; lastMoveTime now; } if (digitalRead(BTN_DOWN) LOW) { playerY PLAYER_STEP; if (playerY PLAYER_Y_MAX) playerY PLAYER_Y_MAX; lastMoveTime now; } } // 游戏更新 void updateGame(unsigned long now) { spawnEnemy(now); updateEnemies(); // 碰撞检测 for (int i 0; i MAX_ENEMIES; i) { if (!enemies[i].active) continue; if (checkCollision(PLAYER_X, playerY, PLAYER_W, PLAYER_H, enemies[i].x, enemies[i].y, ENEMY_W, ENEMY_H)) { enemies[i].active false; health--; if (health 0) { gameOver true; } } } } // 敌人生成 void spawnEnemy(unsigned long now) { if (now - lastSpawnTime SPAWN_INTERVAL_MS) return; for (int i 0; i MAX_ENEMIES; i) { if (!enemies[i].active) { enemies[i].x SCREEN_WIDTH; enemies[i].y random(PLAYER_Y_MIN, PLAYER_Y_MAX); enemies[i].speed random(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX 1); enemies[i].active true; lastSpawnTime now; return; } } } // 敌人移动 void updateEnemies() { for (int i 0; i MAX_ENEMIES; i) { if (!enemies[i].active) continue; enemies[i].x - enemies[i].speed; if (enemies[i].x ENEMY_W 0) { enemies[i].active false; score 10; } } } // 碰撞检测 bool checkCollision(int px, int py, int pw, int ph, int ex, int ey, int ew, int eh) { return (px ex ew px pw ex py ey eh py ph ey); } // 绘制界面 void render() { display.clearDisplay(); // 玩家 display.fillRect(PLAYER_X, playerY, PLAYER_W, PLAYER_H, SSD1306_WHITE); // 敌人 for (int i 0; i MAX_ENEMIES; i) { if (enemies[i].active) { display.fillRect(enemies[i].x, enemies[i].y, ENEMY_W, ENEMY_H, SSD1306_WHITE); } } // 顶部信息 display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.print(HP:); display.print(health); display.setCursor(SCREEN_WIDTH - 55, 0); display.print(Score:); display.print(score); display.display(); } // 游戏结束界面 void renderGameOver() { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(12, 20); display.print(GAME OVER); display.setTextSize(1); display.setCursor(24, 44); display.print(Score: ); display.print(score); display.display(); } // 重置游戏 void resetGame() { playerY 28; health 3; score 0; gameOver false; for (int i 0; i MAX_ENEMIES; i) { enemies[i].active false; enemies[i].x 0; enemies[i].y 0; enemies[i].speed ENEMY_SPEED_MIN; } lastSpawnTime millis(); lastFrameTime millis(); lastMoveTime millis(); gameStartTime millis(); }5.2 关键代码逻辑说明这里挑几个比较重要的点展开。非阻塞主循环游戏主循环中不使用delay()控制节奏而是用millis()记录时间差。OLED 刷新间隔为 33ms约 30FPS。这个刷新率在 128x64 的小屏上足够流畅也不会因为刷新太快导致屏幕闪烁。if (now - lastFrameTime 33) { render(); lastFrameTime now; }按键持续移动普通的按键检测一般只在“按下瞬间”执行一次。但躲避游戏中需要“按住持续移动”所以这里通过 80ms 间隔节流if (now - lastMoveTime 80) return;这个间隔同时起到了软件消抖的作用。物理按键按下时会产生抖动如果每次抖动都被读取到玩家会感觉角色“跳了好几下”。节流后同一时间段内只响应一次移动指令手感更稳。随机敌人与速度敌人从屏幕右侧生成纵向位置和移动速度都使用random()。这样每局的体验都有差异。enemies[i].y random(PLAYER_Y_MIN, PLAYER_Y_MAX); enemies[i].speed random(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX 1);这里有一个细节random(a, b)的范围是[a, b)所以如果希望速度最大为 3需要写成random(1, 4)。AI 第一次生成的代码使用的是random(1, 3)导致永远不会出现最大速度。这种问题在人工审查时很容易漏掉必须在功能测试中观察敌人速度变化。碰撞检测碰撞检测用矩形相交判定bool checkCollision(int px, int py, int pw, int ph, int ex, int ey, int ew, int eh) { return (px ex ew px pw ex py ey eh py ph ey); }这个公式的含义是矩形 A 的左边在矩形 B 右边的左边同时矩形 A 的右边在矩形 B 左边的右边纵向同理。四个条件同时成立则两个矩形重叠。5.3 编译与烧录在 Arduino IDE 中选择正确的开发板开发板ESP32 Dev Module或你的板子具体型号端口选择实际识别的 COM 口编译前确认库已经安装然后点击上传。烧录成功后OLED 会先显示空屏随后出现游戏画面。如果烧录时出现A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header多半是芯片没有进入下载模式。按住开发板上的 BOOT 键点击上传待日志提示连接时松开 BOOT 键即可。6. 常见问题与排查思路6.1 ESP32 板卡安装失败问题现象常见原因解决思路Failed to install platform: esp32:3.3.11工具链下载超时、网络限制重试手动离线安装更换网络环境开发板列表里找不到 ESP32开发板地址未添加或格式错误检查附加开发板管理器网址是否正确编译时报错找不到esp32头文件开发板包安装不完整重新安装 esp32 支持包手动安装离线包时需要把下载的esp32-arduino包解压到 Arduino 的hardware/espressif/esp32目录具体目录结构以官方 Release 说明为准。6.2 OLED 屏幕不亮问题现象常见原因解决思路屏幕完全黑屏I2C 地址不对、接线错误使用 I2C Scanner 扫描地址常见地址为 0x3C屏幕亮但无内容初始化失败、对比度问题检查display.begin(SSD1306_SWITCHCAPVCC, 0x3C)返回值显示有残影/闪烁刷新率太低或 VCC 供电不稳提高刷新率检查供电电压避免 USB 线过长排查 OLED 问题时最好先跑一个最简单的“显示 Hello World”程序确认屏幕硬件和库没有问题再进行游戏逻辑调试。6.3 按键反应异常问题现象常见原因解决思路按键按下没反应引脚接错、未启用上拉检查引脚号确认使用INPUT_PULLUP移动一次后需要松开才能再动代码用了边沿触发改用持续检测 时间节流多个按键同时按下时逻辑混乱未做多键处理分别判断两个按键独立节流如果按键引脚带电异常可以通过万用表测量按键两端的电压。正常未按下时引脚应为高电平3.3V按下后变为低电平。6.4 AI 生成代码的常见坑点除了运行时报错AI 生成的代码还会出现一些“逻辑正确但体验不对”的问题不考虑引脚复用AI 可能建议使用 GPIO12但实际上部分模组上用 GPIO12 会影响启动。默认屏幕刷新率过高AI 生成的代码可能每帧都调用display.display()没有帧率控制导致画面闪烁或卡顿。按键逻辑不符合硬件交互AI 默认按键是“点按触发”但游戏需要“长按持续移动”。内存分配问题AI 可能建议动态创建对象在内存有限的单片机上容易碎片化。这些问题不一定会导致编译失败但会直接影响玩家体验。因此在“AI 生成代码”这个工作流中人工审查和真机测试是绝对不能省略的环节。7. 用 AI 写嵌入式代码的工程建议7.1 提示词要写清楚硬件上下文嵌入式 AI 编程和普通编程最核心的区别在于提示词必须包含硬件约束。我给出的提示词是使用 Arduino 框架ESP32 开发板SSD1306 屏幕 I2C 地址 0x3C SCLGPIO22SDAGPIO21按键GPIO13/GPIO15内部上拉。这样 AI 才不会“自由发挥”选择其他引脚或驱动库。很多第一次尝试 AI 写嵌入式代码的开发者只告诉 AI“写一个游戏”AI 返回的代码可能是用 SDL 写的或者需要图形库完全无法使用。7.2 让 AI 解释代码而不是只给代码拿到 AI 生成的代码后至少应该追问几个问题这个函数的作用是什么这段代码哪里是阻塞的如果不使用delay()应该怎么改引脚 GPIO13 和 GPIO15 在你的方案里有什么特殊含义吗让 AI 解释代码一方面可以帮你判断 AI 的思路是否正确另一方面也能让你更快掌握代码结构。更重要的是嵌入式项目一旦出问题你必须比 AI 更清楚代码在做什么否则无法排查真机上的异常。7.3 分步生成不要一次要完整游戏一次让 AI 生成完整游戏然后复制进 IDE 编译通过的概率很低。即使编译过了物理世界的问题也会让程序跑不过去。推荐的节奏是先让 AI 生成 OLED 显示 Hello World。再让 AI 生成“玩家方块 按键移动”。再让 AI 添加“敌人生成与移动”。最后让 AI 添加碰撞检测与游戏结束状态。每个阶段都编译烧录一次确保硬件链路没有断裂再叠加逻辑复杂度。AI 在每一步修改时都能基于已有代码继续工作不容易把前面的代码搞乱。7.4 保持手动审查代码的习惯AI 可以帮你生成 90% 的逻辑代码但引脚定义、电源约束、外设时序这类硬件相关细节必须由人来确认。我这次就在审查中发现AI 第一版代码里把敌人数组大小设置为 20而 ESP32 的 SRAM 虽然够用但游戏同时出现的敌人根本不需要这么多白白浪费内存。像这样的代码AI 不会报错但会降低效率、增加维护成本。建议在 AI 生成代码后逐行检查全局变量、数组大小、引脚定义、串口打印这四类内容。7.5 善用 git 管理 AI 迭代版本AI 生成代码迭代速度很快但也容易“改坏”。建议项目一开始就初始化 git 仓库每个阶段生成并验证通过的代码都提交一次。这样 AI 下一次改崩了可以快速回滚到上一个正常版本。git init git add esp32_survival.ino git commit -m feat: 第一版游戏框架在 EP01 的实际开发中这个习惯帮我省了很多时间。AI 在第三轮优化时改动了 spawnEnemy 函数导致敌人生成间隔异常我通过git diff快速定位到了问题。8. 总结与后续计划这次实验证明了一件很重要的事用 AI 写嵌入式代码不是“把需求丢给 AI然后复制粘贴运行”这么简单而是“AI 生成基础逻辑 人工补充硬件上下文 真机测试反馈修正”的协作过程。EP01 的最终结果是ESP32 SSD1306 OLED 两个按键成功运行了一个 30FPS 的生存躲避小游戏。玩家可以上下移动躲避敌人碰撞扣血游戏结束可重启。完整代码已在上文给出可以直接编译烧录。下一步可以考虑的方向有加入 OLED 动画效果比如玩家的三角箭头造型。增加敌人子弹或道具系统。使用 PlatformIO 重构工程方便扩展模块。把游戏状态通过串口或蓝牙发送到手机 App 显示。如果你也在尝试用 AI 写嵌入式代码建议先从小游戏、传感器采集这种闭环项目入手。这类项目硬件链路短、问题容易定位AI 代码有明确的运行结果可验证非常适合用来找到“AI 写代码”在嵌入式领域的边界。如果这篇 EP01 对你有帮助可以收藏备用后续 EP02 会继续更新游戏优化和新增功能的实战过程。动手跑一遍代码比看十篇教程都有用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →