用Arduino与老式电话拨盘制作时间感知游戏机:嵌入式开发实战
1. 项目概述与核心思路
最近在整理工作室的旧物,翻出了一个上世纪的老式电话拨盘。看着它那充满机械美感的旋转结构和清脆的“咔哒”声,我就在想,除了怀旧,能不能用它做点更有趣的事情?一个想法冒了出来:我们人对时间的感知其实并不精确,闭眼默数几秒,再睁开眼睛,误差往往不小。那能不能用这个拨盘,做一个考验人对时间感知精确度的游戏呢?这就是“时间感知游戏机”项目的由来。
这个项目的核心,是制作一个独立的嵌入式交互设备。玩家需要按住一个“计时按钮”,心中默数一段时间(比如3.5秒),然后松开按钮,再通过旋转电话拨盘,输入自己认为刚才按了多久(比如拨数字“3”)。设备内部的核心——一块微控制器(我选择了兼容Arduino的Digispark)——会精确测量玩家按压按钮的实际时长,并与拨盘输入的数字进行比对,从而判断玩家的时间感知是否准确,并通过LED灯光和蜂鸣器播放的音乐给出“正确”或“错误”的反馈。
整个项目麻雀虽小,五脏俱全。它涵盖了嵌入式开发的几个关键环节:输入设备(按钮、旋转拨盘)的接口与信号读取、高精度时间间隔的测量、输出设备(LED、蜂鸣器)的驱动与控制,以及将这些环节串联起来的程序逻辑设计。对于刚接触Arduino或嵌入式开发的朋友来说,这是一个绝佳的练手项目,既能学到扎实的底层硬件交互知识,又能收获一个独一无二、充满复古蒸汽朋克风格的趣味作品。而对于有经验的开发者,如何优化时间测量精度、设计更富挑战性的游戏模式,也是一个值得深入探讨的话题。
2. 核心硬件选型与电路设计解析
2.1 主控芯片:为什么是Digispark?
在项目原型中,我选择了Digispark这款超迷你的Arduino兼容板。它的核心是一颗ATtiny85微控制器,体积只有大拇指指甲盖大小,但功能齐全:具备6个I/O口、8KB Flash、512B RAM,并且原生支持USB编程,无需额外的编程器。
注意:选择Digispark主要是出于作品小型化和趣味性的考虑。它的I/O资源非常紧张,在这个项目中几乎被用满。对于初次尝试或希望有更多扩展余地的朋友,Arduino Nano或Uno是更稳妥、更通用的选择。它们有更多的I/O口和内存,调试也更方便。本教程的代码和原理完全兼容这些更常见的板子,只需在Arduino IDE中正确选择板卡类型即可。
2.2 复古输入核心:电话拨盘的工作原理与接口
老式电话拨盘是整个项目的灵魂。它的工作原理是脉冲计数。当你把手指放入指孔,旋转到指挡处然后松开,拨盘在弹簧的作用下会自动回弹。在回弹过程中,一个内部的机械开关会随着旋转不断断开、闭合,每旋转一个数字位(如从“0”到“9”),就会产生对应次数的脉冲。例如,拨数字“5”,就会产生5个快速的开关脉冲;拨数字“0”,则会产生10个脉冲。
我们的目标就是让微控制器识别这些脉冲。通常,拨盘有两根线引出。在待机状态下,这两根线是导通的(开关闭合)。当拨盘开始回弹时,随着旋转,开关会快速断开、闭合,形成一系列电脉冲。我们需要将这个机械开关信号接入微控制器的一个数字输入引脚,并通过程序来计数。
为了确保信号稳定,防止机械开关抖动造成误计数,必须在硬件或软件上进行消抖处理。硬件上可以在开关两端并联一个0.1uF的电容;软件上则需要在检测到一次电平变化后,延时10-50毫秒再继续检测,避开抖动期。本项目将采用软件消抖。
2.3 输出反馈系统:LED与音频
反馈系统决定了游戏的体验。我设计了一个双色LED(共阴极RGB LED,仅使用红、绿两色)和一个微型扬声器(或蜂鸣器)。
- LED状态指示:
- 红色常亮:设备待机,等待玩家开始游戏。
- 绿色常亮:玩家正在按压“计时按钮”,计时进行中。
- 绿色闪烁 + 欢快音乐:玩家输入的时间正确,游戏胜利。
- 红色闪烁 + 低沉音乐:玩家输入的时间错误,游戏失败。
- 音频反馈:使用一个NPN三极管(如2N2222)来驱动小功率扬声器。微控制器的PWM引脚输出不同频率的方波信号,经过三极管放大后,驱动扬声器发出“嘀嘀”声甚至简单的旋律。通过编程改变频率和节奏,就能生成“正确”和“错误”提示音。
2.4 完整电路原理图与搭建要点
整个系统采用3V供电(两节AA电池),兼顾了便携性和对元器件的电压要求。下面是核心的连接方式:
| 组件 | 连接至 Digispark 引脚 | 说明 | 串联电阻/元件 |
|---|---|---|---|
| 电话拨盘 | P2 | 数字输入,检测脉冲 | 上拉电阻(内置或外接10kΩ) |
| 计时按钮 | P1 | 数字输入,开始/结束计时 | 上拉电阻(内置或外接10kΩ) |
| RGB LED (红) | P0 | 数字输出,控制红色灯 | 220Ω 限流电阻 |
| RGB LED (绿) | P4 | 数字输出,控制绿色灯 | 220Ω 限流电阻 |
| 扬声器 | P3 | PWM输出,驱动声音 | 2.2kΩ 基极电阻 -> 2N2222三极管 |
| 电源开关 | VCC/GND | 控制总电源 | - |
电路搭建实操心得:
- 面包板先行:强烈建议先在面包板上搭建整个电路并测试,确认所有功能正常后再进行焊接。这能避免因设计疏漏导致的反复拆焊。
- 注意LED极性:RGB LED有四个引脚,分别是共阳/共阴极和R、G、B。务必通过查阅资料或万用表测试确认引脚排列。本项目使用共阴极LED,阴极接GND,红色和绿色阳极分别通过限流电阻接单片机引脚。
- 三极管驱动:直接使用单片机引脚驱动扬声器声音会很小,且可能损坏引脚。使用三极管进行电流放大是标准做法。连接时确保三极管的发射极(E)接GND,集电极(C)接扬声器负极(扬声器正极接VCC),基极(B)通过一个2.2kΩ电阻接单片机PWM引脚。
- 电源去耦:在Digispark的VCC和GND之间,尽量靠近芯片的位置,焊接一个10uF的电解电容和一个0.1uF的瓷片电容,可以有效滤除电源噪声,提高系统稳定性,尤其是使用电池供电时。
3. 软件逻辑与代码实现详解
游戏的软件逻辑是项目的“大脑”。我们需要编写Arduino Sketch(程序)来完成时间测量、拨盘解码、逻辑判断和反馈控制。
3.1 程序整体框架与状态机
对于这类交互式项目,使用“状态机”模型来设计程序是最清晰的方式。设备在任何时刻都处于一个明确的状态,不同状态下检测不同的输入,并执行相应的操作。
我们可以定义以下几个核心状态:
- IDLE_STATE(待机状态):红色LED亮起。等待玩家按下“计时按钮”。
- TIMING_STATE(计时状态):玩家按下按钮,绿色LED亮起。系统开始高精度计时。
- INPUT_STATE(输入状态):玩家松开按钮,计时停止。绿色LED熄灭,红色LED亮起。等待玩家旋转拨盘输入数字。
- JUDGE_STATE(判断状态):拨盘输入完成。系统将测量的时间(秒)与输入的数字进行比对,控制LED和扬声器给出反馈,然后回到IDLE_STATE。
3.2 高精度时间测量的实现
Arduino的millis()函数返回自程序启动以来的毫秒数,它是一个不断递增的32位无符号整数。利用它进行时间间隔测量既��单又足够精确。
关键代码段与原理:
unsigned long startTime = 0; unsigned long elapsedTime = 0; // 单位:毫秒 bool timing = false; void loop() { if (digitalRead(TIMING_BUTTON_PIN) == LOW) { // 按钮被按下(假设低电平有效) if (!timing) { timing = true; startTime = millis(); // 记录开始时刻 setGreenLED(); // 点亮绿色LED } } else { // 按钮被松开 if (timing) { timing = false; elapsedTime = millis() - startTime; // 计算经过的毫秒数 // 将毫秒转换为秒,并保留一位小数(例如 3456 ms -> 3.4 s) float measuredSeconds = elapsedTime / 1000.0; // 接下来进入输入状态,等待拨盘输入... } } }注意事项:
millis()大约每50天会溢出归零,但对于我们这个最多测量10秒的游戏来说毫无影响。elapsedTime = millis() - startTime在溢出时也能正确计算时间差,因为无符号整数的减法运算在溢出情况下仍然是数学上正确的。
3.3 电话拨盘脉冲的读取与解码
读取拨盘的核心是检测脉冲下降沿(或上升沿)并计数。我们需要在INPUT_STATE下执行此任务。
关键代码段与原理:
int dialPin = 2; // 拨盘信号引脚 int pulseCount = 0; bool lastDialState = HIGH; bool numberEntered = false; void checkDial() { bool currentDialState = digitalRead(dialPin); // 检测下降沿:之前为高电平,现在为低电平 if (lastDialState == HIGH && currentDialState == LOW) { pulseCount++; delay(50); // 重要的软件消抖,避开机械抖动 } lastDialState = currentDialState; // 如何判断一次拨号完成?脉冲结束后会有一段较长的安静期。 // 简单方法:如果超过一定时间(如300ms)没有新脉冲,则认为输入完成。 static unsigned long lastPulseTime = 0; if (currentDialState == HIGH) { if (millis() - lastPulseTime > 300) { if (pulseCount > 0) { // 解码:脉冲数1-9对应数字1-9,10个脉冲对应数字0 int enteredNumber = pulseCount; if (pulseCount == 10) enteredNumber = 0; numberEntered = true; // 触发判断逻辑 pulseCount = 0; // 重置为下一次拨号准备 } } } else { lastPulseTime = millis(); // 更新最后一次脉冲时间 } }解码逻辑详解:拨盘产生的脉冲数等于你拨的数字,除了“0”对应10个脉冲。所以pulseCount为1到9时,数字就是其本身;为10时,数字是0。
3.4 判断逻辑与反馈生成
在获得measuredSeconds(测量的秒数,如3.456秒)和enteredNumber(输入的数字,如3)后,进行比对。游戏规则是:如果测量秒数的整数部分与输入数字相同,则判定正确。例如,3.0秒到3.999秒之间,输入数字3都算正确。
关键代码段:
if (numberEntered) { numberEntered = false; int integerPart = (int)measuredSeconds; // 取整数部分,例如 3.456 -> 3 if (integerPart == enteredNumber) { // 胜利反馈 playSuccessTune(); blinkGreenLED(); } else { // 失败反馈 playFailureTune(); blinkRedLED(); } delay(2000); // 反馈持续2秒 // 状态回归IDLE }音频生成技巧:tone(pin, frequency, duration)函数可以驱动蜂鸣器发出指定频率和时长的声音。通过组合不同频率和时长的tone()调用,并间以delay(),可以编写简单的旋律。例如,胜利音调可以用一组上升的欢快频率,失败音调用一组下降的低沉频率。
4. 机械结构制作与总装调试
4.1 外壳设计与制作
一个复古的木质外壳能为项目增色不少。我使用了一个带有合页和锁扣的小木盒。设计时需要考虑:
- 内部空间:要能容纳Digispark板、电池盒、扬声器,并留出布线的空间。
- 面板布局:在盒盖上面板化安装所有交互元件:电话拨盘、计时按钮、电源开关、LED显示窗。布局要符合人体工学,看起来美观。
- 装饰:为了营造蒸汽朋克风格,我将一些复古仪表盘、齿轮的图案打印在纸上,做旧处理后粘贴在面板上,最后整体涂了一层深色木器漆。
4.2 焊接与内部总装
在面包板测试无误后,就可以进行永久性的焊接了。建议使用一块洞洞板(万能板)作为内部承载板。
- 规划布局:在洞洞板上大致摆放主要元件(主控、电阻、三极管、接线端子),确保走线路径最短、最清晰。
- 先焊接低矮元件:如电阻、IC座,再焊接较高的元件,如电容、三极管。
- 电源走线要粗:VCC和GND的主干线可以使用更粗的导线,或者在洞洞板背面用焊锡铺设较宽的“电源轨”,以减少压降和噪声。
- 做好绝缘:所有裸露的焊点和导线,尤其是电池两极,务必使用热缩管或绝缘胶带包裹,防止短路。
- 固定元件:使用尼龙扎带或热熔胶将扬声器、电池盒等较大元件固定在盒子内,防止运输或移动时晃动脱落。
4.3 系统调试与校准
组装完成后首次上电,需按步骤调试:
- 电源检查:用万用表测量Digispark的VCC引脚,确认电压在3V左右。
- 输入测试:
- 按下“计时按钮”,绿色LED应点亮。
- 旋转电话拨盘,可以在串口监视器中打印出脉冲计数(需在代码中添加调试输出),确认能正确识别1-0的数字。
- 输出测试:
- 分别控制红色和绿色LED的引脚输出高/低电平,确认灯光正常。
- 运行一个简单的
tone()测试程序,确认扬声器能发声。
- 游戏逻辑联调:上传完整代码,进行端到端测试。按下按钮3秒后松开,拨数字“3”,观察是否得到胜利反馈。多测试几个时间点,特别是边界情况,如2.99秒(应判错)和3.00秒(应判对)。
5. 项目优化与扩展思路
基础版本完成后,这个项目还有巨大的优化和扩展空间,可以让游戏体验和可玩性更上一层楼。
5.1 提升时间测量精度与游戏公平性
基础版本使用millis()取整比较,对于3.0秒和3.9秒都算正确,区间较大。我们可以增加难度:
- 精确模式:不仅比较整数秒,还要求小数部分在一定误差范围内。例如,测量值为3.456秒,输入数字3,我们还可以判断
abs(3.456 - 3.5) < 0.1(即误差在0.1秒内)才算真正精确。这需要玩家有更强的时间感。 - 多轮挑战与评分系统:设计连续5轮或10轮游戏,每轮随机设定一个目标时间(通过LED闪烁次数等方式提示),玩家需要感知并输入这个时间。系统根据每轮的误差累计总分,误差越小得分越高。
5.2 丰富反馈机制与游戏模式
- 视觉反馈升级:使用一个WS2812B全彩LED灯环或矩阵。胜利时可以显示炫彩的旋转动画,失败时显示爆炸或“X”图案。
- 音频反馈升级:使用更高级的音频模块,如DFPlayer Mini,直接播放存储于TF卡中的MP3文件作为提示音,甚至播放一段鼓励或调侃的语音。
- 多种游戏模式:
- 经典模式:即当前模式。
- 极限模式:目标时间非常短(0.5秒)或非常长(9秒),挑战感知极限。
- 记忆模式:���让设备用灯光或声音展示一个时间间隔,玩家需要凭记忆复现这个间隔。
5.3 硬件升级与外观改造
- 主控升级:换用ESP32或Raspberry Pi Pico,可以利用其Wi-Fi/蓝牙功能,将游戏成绩上传到云端排行榜,或实现多人无线对战。
- 显示升级:增加一块小型OLED屏幕,用于显示目标时间、本轮成绩、历史记录、倒计时等丰富信息。
- 供电升级:改用锂电池和充电管理模块,实现可重复充电,并增加电量显示功能。
- 极致外观:加入真正的真空管(仅作装饰,内部用LED模拟灯丝发光),配合黄铜件、皮革、旧仪表盘,打造更浓厚的蒸汽朋克艺术装置感。
这个项目从一颗复古的零件出发,融合了硬件、软件和设计的思考。它最吸引我的地方在于,技术不再是冷冰冰的代码和电路,而是成为了连接人的感知与物理世界的一座有趣桥梁。当你看到朋友聚精会神地按住按钮,眉头紧锁地默数,然后小心翼翼拨动转盘,最后因为误差0.1秒而懊恼或因为精准命中而欢呼时,你会觉得所有的调试和打磨都是值得的。它不仅仅是一个“项目”,更是一个能带来笑声和思考的“玩具”。希望你在复现或改造它的过程中,也能享受到这种创造的乐趣。
