1. 项目概述:一个父子共创的电子记忆游戏
几年前,我儿子对电脑游戏很着迷,但对屏幕背后的世界一无所知。为了让他理解“魔法”是如何发生的,我决定和他一起动手,做一个看得见、摸得着的电子玩具。我们的目标不是造一个多么复杂的设备,而是创造一个能让他亲手焊接、亲手编程,并且玩得开心的东西。最终,我们选定了一个经典游戏的复刻版——一个需要记忆颜色序列的“记忆游戏机”。
这个项目的核心非常简单:模仿上世纪七八十年代风靡一时的“Simon”游戏。游戏机会亮起一个颜色的LED,玩家需要按下对应颜色的按钮;接着,它会再随机亮起一个LED,玩家则需要按顺序重复整个越来越长的颜色序列。听起来简单,但序列变长后,对记忆力的挑战可不小。
我选择用Python作为编程语言,主要是因为它语法清晰、接近自然语言,对初学者非常友好。硬件上,我们选用了一块ESP32开发板,它功能强大、价格便宜,而且自带Wi-Fi和蓝牙(虽然我们这个项目用不上),更重要的是,它可以用一块锂电池供电,让我们的游戏机摆脱电线,真正“活”起来。整个项目只用了ESP32上的8个GPIO引脚:4个控制LED,4个读取按钮状态。为了增加点趣味性和信息反馈,我们还加了一块小小的128x64像素的OLED屏幕,用来显示分数和关卡。
从一堆散乱的元器件、面包板上的原型,到最后用3D打印外壳封装成一个可以揣在口袋里、带到任何地方玩的完整设备,这个过程充满了乐趣和挑战。这个游戏机陪伴我们度过了整个假期,它不仅是一个玩具,更是一堂生动的物理、电子和编程入门课。下面,我就把我们从零到一搭建这个“记忆大师”游戏机的完整过程、踩过的坑和收获的经验,毫无保留地分享出来。
2. 核心设计思路与方案选型
2.1 为什么选择“记忆游戏”作为入门项目?
对于电子和编程的初学者,尤其是孩子,第一个项目的选择至关重要。它需要满足几个条件:目标明确、反馈即时、有成就感,并且能拆解成清晰的步骤。记忆游戏完美契合这些要求。
它的游戏逻辑是线性的、可预测的:生成序列 -> 展示序列 -> 等待输入 -> 验证输入。这个循环对应到程序里,就是几个清晰的函数模块。孩子能很容易地理解“这里该写一个让灯闪起来的函数”,“那里该写一个检查按钮的函数”。当代码写完,下载到板子上,灯真的按写的逻辑亮起来,按钮真的能控制游戏时,那种“我创造了它”的成就感是无与伦比的。这比单纯在屏幕上打印“Hello World”要生动和有趣得多。
2.2 硬件平台选型:ESP32为何胜出?
在项目开始时,我们考虑过几种主流微控制器:Arduino Uno、Raspberry Pi Pico和ESP32。
- Arduino Uno:经典,资料极多。但它核心性能较弱,需要额外的硬件才能实现无线功能,而且原生开发环境(Arduino IDE)对Python支持不直接(需要通过Firmata等协议,增加了复杂度)。
- Raspberry Pi Pico:性价比高,原生支持MicroPython(一种针对微控制器的Python精简版本),非常不错。但在当时,其生态系统和社区资源相对于ESP32还是稍显年轻。
- ESP32:这是我们最终的选择。理由如下:
- 双核处理器与充足内存:比传统Arduino性能强得多,能轻松处理游戏逻辑、OLED显示,并为未来扩展(比如添加声音)留足余地。
- 对MicroPython支持成熟:有一个非常活跃的社区,固件稳定,库丰富。我们可以用纯Python来编程,学习路径一致。
- 集成Wi-Fi与蓝牙:虽然本项目未使用,但这意味着未来我们可以轻松升级项目,比如做成联网对战版,或者用手机蓝牙遥控,项目的“可成长性”很好。
- 广泛的电源管理特性:它支持从锂电池直接供电,并集成了充电管理电路。这意味着我们只需要一块常见的3.7V锂电池和一个USB口,就能实现充电、供电一体化,非常适合制作便携设备。
注意:市面上ESP32开发板型号繁多,推荐选择像“ESP32 DevKitC V4”或“NodeMCU-32S”这类引脚引出完善、自带USB转串口芯片的版本,会省去很多调试的麻烦。
2.3 软件与工具链的搭建
编程环境我们选择了Thonny。这是一个对初学者极其友好的Python IDE,特别适合MicroPython开发。
它的优势在于:
- 无缝连接硬件:安装好驱动后,可以像操作本地文件一样,直接在IDE里选择串口连接ESP32,上传、下载文件,甚至使用交互式命令行(REPL)实时调试,查看变量状态。
- 简洁直观的界面:没有复杂的配置,孩子可以快速聚焦于写代码本身。
- 内置包管理器:可以方便地为ESP32安装额外的MicroPython库,比如我们后面用到的OLED驱动库。
开发流程大致是:在Thonny中编写Python代码 -> 通过USB线连接ESP32 -> 将代码作为主脚本上传到ESP32的文件系统中 -> 复位或上电后ESP32自动运行。这种“写-传-跑”的循环非常快速直观。
3. 硬件电路设计与连接详解
3.1 元器件清单与作用
在画电路图之前,我们先明确需要哪些“演员”:
- 主角 ESP32开发板:1块,系统大脑。
- LED(发光二极管):4个(红、绿、蓝、黄各一)。用于显示颜色序列。注意要选择合适颜色的散光LED,视觉效果更柔和。
- 按钮(微动开关):4个。用于玩家输入。建议选用带帽的12x12mm四脚微动开关,手感清晰。
- 电阻:
- 限流电阻:4个,每个LED串联一个。阻值计算是关键。ESP32的GPIO输出电压约为3.3V,假设LED工作电流我们设定在10mA(足够亮且安全),不同颜色LED正向压降不同(通常红色约1.8V,蓝/绿/白约3.0V)。以红色LED为例,电阻值 R = (3.3V - 1.8V) / 0.01A = 150Ω。为方便采购,统一使用220Ω电阻,亮度稍减但完全可用,且更安全。
- 上拉电阻:4个,每个按钮接一个。ESP32的GPIO可以配置内部上拉电阻,但为了电路稳定性和教学直观,我们外部使用了10kΩ的电阻。它的作用是当按钮未按下时,将GPIO引脚稳定地拉到高电平(3.3V);按下时,引脚接地变为低电平。
- OLED显示屏(128x64):1块。常用型号是SSD1306驱动的I2C接口屏幕。它只有4个引脚(VCC, GND, SCL, SDA),接线非常简洁。
- 锂电池:1块,3.7V,容量建议在500mAh以上。用于便携供电。
- 电池充电/升压一体模块:1个(如TP4056+升压)。这是实现便携的关键。TP4056模块负责给锂电池安全充电,升压部分将电池的3.7V稳定升压到5V,供给ESP32。
- 面包板、杜邦线(公对公、公对母):用于原型搭建。
- 后续封装材料:如3D打印外壳、螺丝、导线等。
3.2 电路连接原理与实操接线图
电路的核心逻辑是“输出控制LED,输入读取按钮”。以下是接线明细表:
| 元件 | 引脚/端 | 连接到 ESP32 引脚 | 说明 |
|---|---|---|---|
| 红色LED | 阳极(长脚) | 通过220Ω电阻 -> GPIO 23 | 输出高电平时点亮 |
| 阴极(短脚) | GND | ||
| 绿色LED | 阳极 | 通过220Ω电阻 -> GPIO 22 | 输出高电平时点亮 |
| 阴极 | GND | ||
| 蓝色LED | 阳极 | 通过220Ω电阻 -> GPIO 21 | 输出高电平时点亮 |
| 阴极 | GND | ||
| 黄色LED | 阳极 | 通过220Ω电阻 -> GPIO 19 | 输出高电平时点亮 |
| 阴极 | GND | ||
| 红色按钮 | 一脚 | GPIO 15 | 配置为输入,内部/外部上拉 |
| 另一脚 | GND | 按下时,GPIO15接地 | |
| 绿色按钮 | 一脚 | GPIO 2 | 配置为输入,内部/外部上拉 |
| 另一脚 | GND | ||
| 蓝色按钮 | 一脚 | GPIO 4 | 配置为输入,内部/外部上拉 |
| 另一脚 | GND | ||
| 黄色按钮 | 一脚 | GPIO 18 | 配置为输入,内部/外部上拉 |
| 另一脚 | GND | ||
| OLED屏幕 | VCC | 3.3V | 切勿接5V,会烧毁! |
| GND | GND | ||
| SCL | GPIO 25 (I2C时钟线) | ||
| SDA | GPIO 26 (I2C数据线) | ||
| 电源 | 电池+ | 充电模块B+ | |
| 电池- | 充电模块B- | ||
| 充电模块OUT+ | ESP32 VIN (或5V引脚) | ||
| 充电模块OUT- | ESP32 GND | ||
| 充电模块USB口 | 用Micro USB线连接电脑或充电器 | 用于充电和初始编程 |
重要提示:在将任何元件连接到ESP32之前,务必断开USB或电池供电。接线时,优先完成GND(地线)的连接,建立一个共同的参考点。LED的长脚(阳极)必须通过电阻连接正极,短脚(阴极)接GND,接反了不会亮。按钮的两脚没有正反之分。
接线顺序建议:先在面包板上完成电源部分(ESP32、电池模块)的连接并测试通电正常。然后逐一连接LED及其电阻,每接好一个,就写一段简单的测试代码(如pin.value(1))验证是否能点亮。接着连接按钮,同样写测试代码读取引脚状态。最后连接OLED屏幕。这种“分模块测试”的方法,能让你在问题出现时快速定位,避免所有线都缠在一起后无从下手。
4. 软件编程:从零到一的逻辑实现
4.1 开发环境准备与固件烧录
首先,需要给ESP32“安装操作系统”,也就是MicroPython固件。
- 从MicroPython官网下载最新的ESP32通用固件(.bin文件)。
- 安装ESP32的刷机工具,如
esptool.py(可通过Python的pip安装)。 - 将ESP32通过USB连接电脑,进入下载模式:通常需要按住板上的“BOOT”按钮,再按一下“EN”复位按钮,然后松开“EN”,再松开“BOOT”。
- 使用命令行擦除原有固件并刷入新固件。命令类似:
(请将esptool.py --chip esp32 --port COM3 erase_flash esptool.py --chip esp32 --port COM3 --baud 460800 write_flash -z 0x1000 esp32-xxx.binCOM3替换为你的实际串口号,esp32-xxx.bin替换为你的固件文件名)。
刷写成功后,打开Thonny,在右下角选择解释器为“MicroPython (ESP32)”,并选择正确的串口端口。连接后,Shell交互窗口会出现>>>提示符,输入print(‘Hello, ESP32!’)测试,成功则环境搭建完成。
4.2 核心游戏逻辑的代码拆解
游戏的主程序可以分解为几个核心函数,我们采用面向过程的方式,让逻辑更清晰。
1. 硬件初始化 (init_hardware)这个函数负责配置所有用到的引脚。
from machine import Pin, I2C import ssd1306 import time # 定义LED和按钮对应的引脚 led_pins = [23, 22, 21, 19] button_pins = [15, 2, 4, 18] # 初始化LED对象列表(设置为输出模式) leds = [Pin(pin, Pin.OUT) for pin in led_pins] # 初始化按钮对象列表(设置为输入模式,并启用内部上拉电阻) buttons = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in button_pins] # 初始化OLED显示屏 (I2C) i2c = I2C(0, scl=Pin(25), sda=Pin(26), freq=400000) oled = ssd1306.SSD1306_I2C(128, 64, i2c) def init_hardware(): # 确保所有LED初始状态为熄灭(低电平) for led in leds: led.value(0) # 清空OLED屏幕并显示欢迎信息 oled.fill(0) oled.text("Memory Game!", 10, 10) oled.text("Press any key", 15, 30) oled.show()实操心得:
Pin.PULL_UP启用了芯片内部的上述电阻,这样我们就不必在外部为每个按钮焊接一个10k电阻了,极大地简化了电路。这是ESP32等现代微控制器的便利之处。
2. 游戏序列生成与展示 (generate_sequence,play_sequence)游戏的核心是生成一个随机序列,并以视觉(LED闪烁)的方式播放给玩家。
import urandom # 用于生成随机数 sequence = [] current_level = 1 MAX_LEVEL = 20 # 游戏最大关卡数 def generate_sequence(length): """生成指定长度的随机序列,每个元素是0-3的数字,对应4种颜色""" return [urandom.getrandbits(2) for _ in range(length)] # getrandbits(2)生成0-3的随机数 def play_sequence(seq, speed=0.5): """播放序列:点亮对应LED,延时,熄灭,间隔""" for color_index in seq: leds[color_index].value(1) # 点亮LED time.sleep(speed) # 保持点亮状态 leds[color_index].value(0) # 熄灭LED time.sleep(0.2) # 序列中每个颜色之间的间隔3. 玩家输入捕获与验证 (get_player_input,check_sequence)需要持续监听按钮,并对比玩家的输入和游戏序列。
def wait_for_button(): """等待并返回第一个被按下的按钮索引,同时提供视觉反馈""" pressed_index = None while pressed_index is None: for i, btn in enumerate(buttons): if btn.value() == 0: # 按钮按下时为低电平 leds[i].value(1) # 按下时对应LED亮起作为反馈 time.sleep(0.2) # 消抖兼反馈显示 leds[i].value(0) while btn.value() == 0: # 等待按钮释放 time.sleep(0.05) pressed_index = i break # 跳出for循环 time.sleep(0.01) # 短暂延时,防止循环空跑耗电 return pressed_index def check_sequence(seq): """让玩家重复整个序列,并逐项检查""" for i, correct_color in enumerate(seq): player_color = wait_for_button() if player_color != correct_color: return False # 按错,游戏结束 # 按对了,可以给一个简短的成功提示,比如快速闪烁一下对的灯 leds[player_color].value(1) time.sleep(0.1) leds[player_color].value(0) return True # 整个序列输入正确4. 游戏主循环与状态管理将以上函数串联起来,并管理游戏状态(开始、进行中、结束)。
def game_loop(): global sequence, current_level game_active = True sequence = generate_sequence(current_level) while game_active: # 1. 更新OLED显示当前关卡 oled.fill(0) oled.text("Level: {}".format(current_level), 20, 20) oled.show() # 2. 向玩家播放序列 play_sequence(sequence, speed=max(0.1, 0.6 - current_level*0.02)) # 速度随关卡递增 # 3. 获取并验证玩家输入 oled.text("Your turn!", 25, 40) oled.show() if check_sequence(sequence): # 玩家成功 oled.fill(0) oled.text("Correct!", 30, 25) oled.show() time.sleep(1) current_level += 1 if current_level > MAX_LEVEL: oled.fill(0) oled.text("You WIN!", 30, 25) oled.show() time.sleep(3) game_active = False break sequence = generate_sequence(current_level) # 生成更长的新序列 else: # 玩家失败 oled.fill(0) oled.text("Wrong! Score:{}".format(current_level-1), 10, 25) oled.show() # 失败动画:所有LED快速闪烁几次 for _ in range(5): for led in leds: led.value(1) time.sleep(0.1) for led in leds: led.value(0) time.sleep(0.1) time.sleep(3) game_active = False # 游戏结束,回到初始状态 current_level = 1 oled.fill(0) oled.text("Game Over", 25, 25) oled.text("Press to restart", 5, 45) oled.show() wait_for_button() # 等待任意按钮按下以重启5. 主程序入口最后,在main.py文件中调用初始化函数并启动游戏循环。确保将主循环代码文件命名为main.py,这样ESP32上电后会自动执行。
# main.py if __name__ == '__main__': init_hardware() while True: game_loop()将main.py和依赖的ssd1306.py库文件通过Thonny上传到ESP32的根目录,然后重启设备,游戏就应该跑起来了!
5. 从原型到产品:外壳设计与制作
当面包板上的电路稳定运行后,我们就开始考虑把它变成一个“产品”。这个过程能让孩子理解工程设计中的另一个重要环节——机械结构与外观设计。
5.1 3D建模与设计考量
我们使用免费的在线3D建模软件(如Tinkercad)进行设计。设计外壳时需要考虑以下几点:
- 固定与定位:外壳需要留出精确的孔位来固定ESP32开发板、OLED屏幕、4个按钮和4个LED。LED的孔需要稍微大一点,以便嵌入和扩散光线。
- 按钮手感:按钮帽要略高于外壳表面,并且周围留有间隙防止卡住。我们在按钮下方设计了支撑柱,确保按下时受力均匀。
- 电池仓与充电口:需要为锂电池设计一个容易放入取出的仓室,并为充电模块的Micro USB口开一个访问孔。
- 散热与组装:ESP32在运行时会有轻微发热,外壳顶部和底部我们设计了一些栅格状的通风孔。外壳设计为上下盖结构,用螺丝固定,方便后期维修和更换电池。
- 美学:和孩子一起选择颜色,并在外壳上刻上游戏的名字“Memory Master”和他的名字缩写,增加归属感。
5.2 焊接与内部走线
有了外壳,就需要将面包板上的临时连接变为永久性的可靠连接。
- 准备万用板:根据外壳内部空间,裁剪一块合适大小的洞洞板(万用板)。
- 规划布局:在板上大致摆放ESP32、按钮、LED、电阻等元件,规划一个整洁的布局,确保所有连接线最短,且不会互相干扰。
- 焊接:这是锻炼孩子精细操作和耐心的好机会。从低矮的元件(电阻)开始焊起,再到LED、按钮插座,最后是连接排针和飞线。
- 技巧:使用助焊剂可以让焊接更顺畅。焊锡不要太多,形成一个小圆锥形即可。每个焊点焊接时间不宜过长(2-3秒),以免烫坏元件或焊盘。
- 飞线:对于无法通过板子背面走线连接的引脚,使用不同颜色的细导线(如AWG30硅胶线)进行连接。用热熔胶或扎带固定线束,使其整洁牢固。
避坑指南:焊接LED和按钮时,务必再次确认极性(LED)和引脚对应关系(按钮)。最好在焊接前,用万用表的二极管档或通断档测试一下。一旦焊反,拆下来会很麻烦,可能损坏焊盘。
5.3 总装与测试
将所有模块组装进外壳:
- 先将焊好元件的万用板用螺丝固定在下壳内。
- 将OLED屏幕对准视窗卡好,并用少量热熔胶固定侧面。
- 将按钮从外壳内侧装入孔位,确保其能自由活动。
- 将LED从内侧插入孔位,可以使用一小段热缩管作为遮光罩,防止LED之间串光。
- 连接电池,并妥善放置电池和充电模块,用双面胶或尼龙扎带固定,防止晃动。
- 盖上上盖,拧紧螺丝。
总装完成后,不要急于庆祝。进行全面的功能测试:
- 测试每个按钮是否都能正常触发,且手感一致。
- 测试每个LED是否能正常点亮,亮度是否均匀。
- 测试充电功能是否正常(充电指示灯会亮)。
- 长时间运行游戏,检查是否有过热或程序崩溃的情况。
6. 项目总结与扩展思考
经过这个完整的项目,我儿子不仅学会了焊接、读电路图、3D设计基础,更重要的是,他理解了软件和硬件是如何协同工作的。他看到了一行行抽象的代码,如何转化为具体的灯光闪烁和屏幕显示,又如何通过物理按钮被控制。这种“创造-控制-反馈”的闭环体验,是任何理论课程都无法替代的。
项目过程中几个关键的收获点:
- 调试是常态:代码第一次就完美运行的概率极低。学会使用
print()输出变量状态,或者用LED闪烁不同次数来指示程序运行到哪一步,是嵌入式开发最基本的调试技能。 - 电源管理的重要性:当我们第一次用电池供电时,发现屏幕偶尔会闪烁。后来意识到是电池电量不足时,电压下降导致ESP32和屏幕工作不稳定。这引出了关于电压、电流和电池容量的一堂生动物理课。
- 用户体验(UX)细节:比如按钮按下时对应的LED要亮起给予反馈,游戏失败或成功要有明确的声光提示(我们后来加了一个有源蜂鸣器),关卡提升时序列播放速度可以适当加快以增加难度。这些细节让游戏变得“好玩”。
这个项目还有巨大的扩展潜力,可以作为一个持续学习的平台:
- 增加声音:加入一个无源蜂鸣器,为每种颜色分配一个不同的音调,实现真正的“Simon”游戏体验。
- 难度与模式:在OLED菜单上增加选择项,比如调整速度、开启“残酷模式”(错一次即结束)、或者改变序列生成规则(如禁止连续同色)。
- 数据记录:利用ESP32的Flash存储,保存最高分记录。
- 无线化:利用ESP32的Wi-Fi,将得分上传到简单的网络服务器,或者实现双人对战模式。
这个小小的记忆游戏机,最终远远超出了一个玩具的范畴。它是一把钥匙,打开了一扇通往创造世界的大门。看到孩子从“消费者”转变为“创造者”时眼中的光芒,我觉得所有投入的时间都无比值得。如果你也想为孩子或为自己开启这样一段旅程,不妨就从这样一个具体而微的项目开始吧。