1. 项目概述与核心价值作为一个常年混迹于创客圈和嵌入式开发领域的玩家我一直在寻找那些能将趣味性和实用性完美结合的DIY项目。最近我成功复现并深度优化了一个让我眼前一亮的作品一个基于Arduino的、可容纳8个独立骰子的电子套装。它的核心魅力在于你可以通过一个普通的红外遥控器将每个骰子的面数在2到999之间任意配置并且所有设置都会掉电保存。想象一下无论是需要经典六面骰的《大富翁》还是需要复杂二十面骰的《龙与地下城》甚至是需要自定义概率的独特桌游这一个巴掌大的设备就能全部搞定。这不仅仅是做一个电子骰子那么简单它涉及了微控制器的精准控制、红外遥控的解码与交互设计、EEPROM的非易失性存储应用以及如何在极小的OLED显示空间内优雅地呈现不同形态的信息。对于嵌入式入门者这是一个绝佳的综合性练手项目对于桌游爱好者这是一个独一无二的便携神器。接下来我将从设计思路、硬件选型、代码解析到避坑指南为你完整拆解这个“万能骰子盒”的实现过程。2. 整体系统设计与核心思路拆解2.1 需求分析与方案选型这个项目的核心需求很明确一个便携设备能模拟多个、面数可任意配置的骰子并具备直观的显示和自然的投掷交互。传统的多面体物理骰子携带不便且面数固定。采用电子方案是必然选择。为什么选择 Arduino Pro Mini 和 ATmega328Arduino Pro Mini 是基于 ATmega328P 的微控制器开发板其核心优势在于极小的物理尺寸约18x33mm和极低的功耗非常适合嵌入到这种便携设备中。ATmega328P 拥有足够的GPIO引脚来驱动显示器、传感器和红外接收头内置的EEPROM1KB正好用于存储8个骰子的面数配置。相比于功能更强大的ESP32或STM32ATmega328P在满足需求的前提下保持了电路的简洁和成本的极致控制对于这个项目来说是“刚好够用”的典范。显示方案的抉择为何是 SSD1306 OLED显示部分需要在一个42mm直径的圆形区域内清晰展示8个骰子的状态。点阵式LCD如1602尺寸过大且显示内容单调。SSD1306驱动的0.96英寸128x64 OLED屏以其高对比度、自发光在暗光环境下优势明显、极快的响应速度和I2C接口仅需两根数据线的优点脱颖而出。I2C通信节省了宝贵的GPIO资源让布线更简洁。交互与感知的设计逻辑投掷动作感知使用振动传感器或敲击传感器来检测“摇晃”动作模拟投掷骰子的物理行为。这比按键触发更加自然和富有仪式感。传感器输出数字信号连接到中断引脚可以即时响应。配置方式选择配置8个骰子、每个最多3位数如果使用按键组合将需要极其复杂的操作逻辑。红外遥控成为了最优解。它利用成熟的NEC编码协议一个遥控器上的方向键、数字键和确认键可以构建出非常直观的菜单操作逻辑用户体验远胜多个物理按键。数据持久化骰子面数配置需要被记住。ATmega328P内置的EEPROM提供了掉电不丢失的存储空间。我们将8个骰子的面数每个占用2字节可存储0-65535顺序存入EEPROM每次启动时读取配置后更新。2.2 系统架构与工作流程整个系统的工作流程可以概括为一个状态机初始化系统上电从EEPROM读取8个骰子的面数配置。初始化OLED显示绘制初始界面显示所有骰子及其当前面数或图标。主循环等待状态持续检测振动传感器。若无触发则循环检测红外信号处理可能的配置请求。投掷状态振动传感器被触发进入投掷动画状态。在屏幕上快速模拟骰子翻滚的视觉效果最后利用微控制器的内置随机数生成器以传感器触发时刻的微秒时间等作为种子为每个有效骰子生成一个随机结果并显示。配置状态在投掷动画过程中这是一个关键时机窗口如果接收到红外遥控的“OK”键信号则进入配置模式。屏幕上高亮一个待配置的骰子用户使用遥控器的左右键选择骰子上下键、数字键调整面数再次按下“OK”保存当前骰子配置并退出。新配置会立即写入EEPROM对应位置。注意将进入配置模式的时机放在“投掷动画过程中”是一个精妙的设计。这避免了需要单独设计一个“配置按钮”也防止了误触。你想修改设置时只需在摇动骰子的同时按下遥控器非常符合直觉。3. 硬件电路详解与组装实操要点3.1 核心元件清单与功能说明除了项目原文提到的这里补充一些选型细节和备选方案主控Arduino Pro Mini (5V/16MHz版本)。务必确认是5V版本以兼容其他模块电平。编程器FT232RL USB转串口模块。这是给Pro Mini烧录程序的必备工具。显示0.96英寸 I2C接口 SSD1306 OLED。注意有4针VCC, GND, SCL, SDA和7针多出RES, DC等两种本项目使用4针I2C版本以简化连接。振动传感器J34敲击开关模块。这是一个数字传感器当振动超过阈值时输出低电平。也可以使用SW-1801P等弹簧振动传感器但可能需要搭配上拉电阻。红外接收VS1838B或HS0038B红外接收头。它们对38kHz载波信号解调输出数字信号。电池3.7V 300mAh锂电池。选择带保护板的安全第一。充电管理TP4056微型充电模块。最大充电电流可通过模块上的电阻调节默认约1A对于300mAh电池建议更换电阻将电流调至300mA左右0.3C以延长电池寿命。开关小型拨动开关或贴片开关。3.2 电路连接图与布线心得虽然原文提供了步骤图但理解原理图至关重要。以下是各模块与Arduino Pro Mini的连接逻辑模块引脚连接至 Pro Mini说明SSD1306 OLEDVCCVCC (RAW)接5V电源正极GNDGND电源地SCLA5I2C时钟线SDAA4I2C数据线VS1838B IRVCCVCC (RAW)与OLED共电源GNDGND共地OUTD10红外数据信号可使用外部中断引脚J34 振动传感器DO (或OUT)D12数字输出触发时输出低电平GNDGNDVCCVCC (RAW)锂电池VCC (RAW)正极接开关后输入-GND负极直接接地拨动开关公共端电池控制总电源常开端VCC (RAW)开关打开时供电布线实操心得与避坑指南电源优先务必确保所有模块的VCC和GND连接牢固且无短路。Pro Mini的RAW引脚是未经稳压的输入引脚可接3.7V-12VVCC引脚是板载稳压器输出的5V。我们直接将电池3.7V接至RAW同时将OLED和IR的VCC也接到RAW或VCC因为电池电压在3.7V-4.2V仍在OLED和IR的工作范围内。更稳妥的做法是将电池接RAW利用Pro Mini的稳压器输出稳定的5V到VCC引脚再将其他模块接至VCC。信号线精简I2C总线SDA, SCL可以并联多个设备但本项目只有OLED所以直接连接即可。注意线长不宜过长。红外接收头处理如原文所述需要拆掉金属屏蔽壳否则信号会被严重遮挡。焊接时引脚剪短至2-3mm用热熔胶或AB胶固定在壳体开孔处确保接收窗对准外壳上的透光孔。振动传感器固定用胶水将其牢固粘贴在底壳内部中心位置确保设备的振动能有效传递到传感器。绝缘绝缘绝缘所有焊接点检查无误后用绝缘胶带或热缩管包裹尤其是电池正负极和Arduino背面可能短路的焊点。最后用绝缘胶带覆盖整个Arduino板子再合盖。4. 核心代码解析与编程实现项目提供了两个核心程序DiceEEPROM.ino和DiceIR.ino。我们来深入剖析其关键部分。4.1 初始化与EEPROM管理 (DiceEEPROM.ino)这个草图的作用是初始化EEPROM写入默认的骰子面数例如8个骰子都初始化为6面。#include EEPROM.h #define NUM_DICE 8 #define DEFAULT_FACES 6 void setup() { for (int i 0; i NUM_DICE; i) { int addr i * 2; // 每个骰子面数用2字节int存储 EEPROM.put(addr, DEFAULT_FACES); } // 初始化完成后可以通过串口提示但本项目无串口输出 } void loop() { // 空循环仅执行一次初始化 }关键点EEPROM.put(addr, value)可以方便地存储任意数据类型如int。每个骰子面数占用2字节8个共16字节远小于ATmega328P的1KB EEPROM容量。这个草图只需在上传主程序前上传一次。4.2 主程序逻辑架构 (DiceIR.ino)主程序集成了显示、传感器、红外和随机数生成。1. 库文件引入与全局变量#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #include EEPROM.h #include IRremote.h // 用于红外解码 // 定义引脚 #define VIBRATION_PIN 12 #define IR_RECEIVER_PIN 10 // 初始化OLED对象 Adafruit_SSD1306 display(128, 64, Wire, -1); IRrecv irrecv(IR_RECEIVER_PIN); decode_results results; int diceFaces[NUM_DICE]; // 存储当前骰子面数的数组 int currentDiceIndex 0; // 配置模式下当前选中的骰子索引 bool configMode false; bool rolling false;2.setup()函数关键操作void setup() { pinMode(VIBRATION_PIN, INPUT_PULLUP); // 振动传感器内部上拉 // 初始化OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while(1); // 初始化失败死循环 } display.clearDisplay(); // 初始化红外接收 irrecv.enableIRIn(); // 从EEPROM读取骰子配置 for(int i0; iNUM_DICE; i) { EEPROM.get(i*2, diceFaces[i]); // 简单校验面数应在合理范围 if(diceFaces[i] 2 || diceFaces[i] 999) diceFaces[i] 6; } drawHomeScreen(); // 绘制初始界面 }3. 核心状态机loop()函数void loop() { // 1. 检查红外信号在任何模式下 if (irrecv.decode(results)) { handleIR(results.value); irrecv.resume(); // 准备接收下一个信号 } // 2. 如果不是配置模式检查振动 if (!configMode digitalRead(VIBRATION_PIN) LOW) { // 防抖延时 delay(50); if(digitalRead(VIBRATION_PIN) LOW) { rollTheDice(); // 执行投掷函数 } } // 3. 如果是配置模式更新配置界面 if (configMode) { drawConfigScreen(); } }4. 红外处理函数handleIR(unsigned long value)这是交互的核心需要根据遥控器的实际编码值进行映射。void handleIR(unsigned long value) { switch(value) { case 0xFF38C7: // 假设“OK”键的编码 if (rolling) { // 仅在投掷过程中按OK进入配置 configMode !configMode; currentDiceIndex 0; if (!configMode) saveConfigToEEPROM(); // 退出配置时保存 } break; case 0xFF5AA5: // 左键 if(configMode) currentDiceIndex (currentDiceIndex - 1 NUM_DICE) % NUM_DICE; break; case 0xFF10EF: // 右键 if(configMode) currentDiceIndex (currentDiceIndex 1) % NUM_DICE; break; case 0xFF18E7: // 上键 (1) if(configMode diceFaces[currentDiceIndex] 999) diceFaces[currentDiceIndex]; break; case 0xFF4AB5: // 下键 (-1) if(configMode diceFaces[currentDiceIndex] 2) diceFaces[currentDiceIndex]--; break; case 0xFF6897: // 数字键1 (10) if(configMode) diceFaces[currentDiceIndex] constrain(diceFaces[currentDiceIndex] 10, 2, 999); break; // ... 其他数字键处理逻辑类似 } }实操心得红外编码获取你需要先运行一个简单的红外接收示例代码按下遥控器各键在串口监视器中查看并记录对应的十六进制编码值然后替换到上述case语句中。不同遥控器编码可能完全不同。5. 骰子绘制与投掷动画函数drawDiceFace(int index, int x, int y, int faceNum, bool selected)函数是图形显示的核心。它需要根据面数faceNum绘制不同的样式面数0: 不绘制。面数1: 绘制“石头剪刀布”图标。面数2: 绘制“拇指上下”图标。面数6: 绘制标准六点骰子图案计算点阵位置。面数12: 在五边形内绘制数字。面数20: 在三角形内绘制数字。其他: 在矩形框内绘制数字。rollTheDice()函数则负责设置rolling true。在屏幕上快速循环绘制随机数或图案产生动画效果例如循环20次每次延时50ms。动画结束后为每个面数大于0的骰子生成最终随机结果random(1, diceFaces[i] 1)。调用drawHomeScreen()显示最终结果。设置rolling false。随机数质量random()函数在未设置种子时每次上电后的序列是相同的。我们可以在setup()中读取一个未连接的模拟引脚如A0的“噪声”值作为种子randomSeed(analogRead(A0));。在rollTheDice()中可以用micros()时间作为二次种子使结果更不可预测。5. 3D打印外壳与充电底座制作5.1 外壳适配与修改建议原作者使用了现成的42mm直径表壳进行改装。如果你选择自己3D打印需要注意精度层高建议0.15mm-0.2mm确保OLED窗口、红外接收孔、充电触点孔位精准。支撑外壳内部可能有悬空结构如固定柱需要生成支撑。充电底座的卡槽部分也需要支撑。材料推荐PLA强度足够且易于打印。PETG则更耐用、耐温。装配公差设计时盖子与主体的配合建议采用过盈配合或加入卡扣结构。单纯靠胶水后期更换电池会非常困难。可以在侧壁设计一个小的螺丝孔用一颗细螺丝固定。5.2 充电底座电路连接充电底座的本质是一个将Micro USB口的5V电源通过TP4056模块传导到设备外壳上两个充电触点的“夹具”。TP4056模块连接BAT和BAT-分别连接两块小PCB板即充电触点。IN和IN-连接Micro USB接口的5V和GND。触点制作如原文所述用单面覆铜板切割成小块焊接导线。关键是要确保当设备放入底座时设备外壳上的两个弹簧触点或铜柱触点能准确、稳定地接触到这两块PCB板。极性确认务必用万用表确认设备外壳上两个触点的极性与底座上PCB的极性完全一致接反会损坏设备或充电模块。建议在设备外壳和底座内部用“”和“-”符号清晰标记。6. 常见问题排查与进阶优化6.1 问题速查表现象可能原因排查步骤OLED不显示1. 电源未接通或电压不足2. I2C地址错误3. 接线错误SDA/SCL接反4. 屏幕本身损坏1. 检查VCC和GND测量电压是否在3.3V-5V之间。2. 多数SSD1306地址是0x3C尝试改为0x3D。3. 确认SDA接A4SCL接A5。4. 运行一个简单的I2C扫描程序确认设备地址。红外遥控无反应1. 红外接收头引脚接错2. 接收头被遮挡或距离太远3. 遥控器电池没电4. 代码中红外编码值不匹配1. 确认VCC, GND, OUT三线对应。2. 确保接收头正对遥控器且中间无遮挡。3. 更换遥控器电池。4.最重要使用IRrecvDumpV2示例代码获取你遥控器的真实编码。振动不触发1. 传感器类型不对可能是模拟输出2. 灵敏度调节电位器未调好3. 引脚模式未设置上拉1. 确认使用的是数字开关型振动传感器如J34。2. 调节传感器上的蓝色电位器逆时针提高灵敏度。3. 代码中pinMode需设置为INPUT_PULLUP。骰子结果不随机随机数种子未设置或设置不当在setup()中加入randomSeed(analogRead(A0));并将A0引脚悬空不连接任何东西。EEPROM配置丢失1. EEPROM写入过于频繁2. 电源在写入过程中断开1. 仅在退出配置模式时一次性保存所有配置避免在循环中频繁写入。2. 确保电池接触良好。EEPROM每个单元有约10万次写入寿命正常使用完全足够。设备异常重启1. 电池电量不足导致电压跌落2. 短路或瞬间大电流1. 充电或更换电池。2. 仔细检查所有焊点排除短路可能。6.2 进阶优化与扩展思路低功耗优化当前方案中OLED和Arduino始终在工作耗电较大。可以修改代码在无操作一段时间后关闭OLED背光display.clearDisplay(); display.display();并将Arduino设置为休眠模式使用LowPower库仅通过振动传感器或红外中断唤醒。这可以极大延长待机时间。增加声音反馈加入一个微型无源蜂鸣器在投掷、配置切换、按键时发出短促音效提升交互体验。连接到另一个PWM引脚即可。无线升级与更多配置将主控更换为ESP8266或ESP32通过Wi-Fi接入网络可以开发一个简单的Web页面来配置骰子甚至实现多设备联机同步投掷结果用于远程桌游。改进显示效果为更多面数的骰子设计专属图标如4面体、8面体、10面体轮廓而不仅仅是数字加方框。利用Adafruit_GFX库的图形函数可以绘制简单多边形。电池电量指示通过分压电路读取电池电压在屏幕上显示电量图标提醒用户及时充电。这个项目从构思到实现充分体现了嵌入式系统设计的魅力在严格的资源尺寸、功耗、成本约束下通过巧妙的软硬件结合解决一个具体的实际问题。它不仅仅是一个玩具更是一个涵盖了输入、输出、存储、交互、电源管理的完整嵌入式系统教学案例。当你亲手把它做出来看着自己摇动设备屏幕上骰子翻滚并定格时那种成就感是无可替代的。希望这份详细的拆解能帮助你顺利复现并创造出属于自己的独特版本。