基于 Arduino Mega 2560 的全尺寸电动弹珠机设计与实现

摘要
本文介绍了一台以 Arduino Mega 2560 为核心控制器的全尺寸电动弹珠机(Pinball Machine)。整机采用木制机柜结构,配合市售弹珠机标准配件(弹板、弹簧、挡片、Pop Bumper 等),通过 Arduino 实现 得分逻辑、灯光控制、目标判定以及游戏流程管理。项目从机械结构、电子系统到软件逻辑均由作者自行设计与实现,历时约六个月间断完成,是一个集木工、机械设计、电力电子与嵌入式编程于一体的复杂系统工程。
文中详细说明机柜结构尺寸、可调节机脚设计、弹珠台倾角控制、24V 电源与 5V 逻辑电路隔离、Pop Bumper 高压检测的电压分压方案,以及基于 Arduino Mega 的得分与游戏逻辑实现方法,可为有意自制弹珠机的工程爱好者提供系统参考。
1 项目概述
作者在项目开始时,几乎找不到完整的“如何从零自制弹珠机”的资料,因此采用了如下策略:
使用 备用胶合板 作为原型试验平台:
每个机构(Flipper、Slingshot、Pop Bumper、目标靶等)都先在测试板上安装与调试,待可靠后才移植到正式机台。采用 标准弹珠机配件 + 自制木柜 的混合方式:
机柜、上盖、内部支撑结构自己木工制作;弹板、Pop Bumper、目标靶等则尽量使用成熟的替换件。逻辑控制全部交给 Arduino Mega 2560:
使用其丰富的数字与模拟 I/O,既负责采集各类开关信号,又负责控制灯光与得分显示。
结果是一台结构接近商用尺寸的弹珠机,但游戏规则与灯光逻辑完全可编程。
2 机械结构设计
2.1 机柜结构
机柜采用 橡木贴面胶合板 制作,基本尺寸(单位:英寸)如下:
前后面板:20 高 × 23 宽
侧板:20 高 × 47 长
台面(Playfield):22 宽 × 42 长
设计要点:
结构连接方式
板材采用斜接(miter)切割,使用饼干榫(biscuit joiner)与木胶拼接;
也可用暗扣螺丝 + 木胶组合,保证强度——弹珠机在游戏中会承受频繁冲击。
底部搁板与电源布置
不与侧板胶合,以适应木材膨胀收缩;
用于放置主电源、LED 灯电源等模块。
在底部上方 1 英寸位置开 ¾ 英寸宽的榫槽(dado),做一个“浮动”搁板:
台面与前部空腔
前方预留约 3 英寸空间,用于布线、安装弹簧拉杆(Plunger)和前部灯光等;
Playfield 长度 42 英寸,明显短于机柜 47 英寸:
台面通过侧板上的小木块支撑,距离上沿约 4 英寸;
维护时,只需打开上盖,将台面整体抬出并翻转即可检修底面机构与线束。
2.2 倾角与机脚设计
弹珠机的“节奏”很大程度取决于台面的倾角,通常在 1°–7° 之间。角度越大,球速越快,游戏越刺激。
为方便调节倾角,作者自制了可调机构的木质机脚:
使用市售木质桌腿,底部钻入约 12 英寸深孔;
在孔底用双组份环氧胶固定 3/8 英寸 Tee Nut(内螺母),注意避免胶体污染螺纹;
将 12 英寸长的 3/8 英寸螺纹杆旋入腿内,末端安装可调水平脚(Leveling Foot),中间加锁紧螺母;
橱柜底部通过金属腿板(Table Leg Mounting Plate)与木腿连接。
调节方法:
松开锁紧螺母 → 旋入 / 旋出螺杆以升降台面 → 锁紧螺母固定。
2.3 上盖与透明面板
上盖(Lid)与机柜同外形尺寸,材质为橡木框架:
上侧与两边框宽约 1.5 英寸;
底边加宽至 5 英寸,用于遮挡台面与机柜间的间隙;
内侧开槽嵌入 亚克力板(Plexiglas):
相比玻璃更轻、更安全,也更易切割;
采用暗扣螺丝(Pocket Screw)装配,便于今后更换亚克力板;
内侧安装一圈彩色 LED 灯带,用于装饰;
上盖与机柜通过 钢琴合页(Piano Hinge) 铰接,并在两侧开槽嵌入,提升外观与强度。
机柜背面还固定一条 电源插排,所有电源统一插入,便于通过一个总开关控制整机上电;同时预留一条 USB 延长线,方便在不拆台面的前提下更新 Arduino 程序。
3 控制与电子系统
3.1 Arduino 控制逻辑概述
整机由一块 Arduino Mega 2560 控制,主要承担四大任务:
采集开关与传感器状态
目标靶(Target Switch)
滚轮开关(Rollover Switch)
Pop Bumper 开关(通过电压分压)
弹球发射道开关
压力传感器(用于监测丢球)
控制灯光与效果
目标靶对应灯
滚轮灯
Pop Bumper 灯
Game Over 时全场闪烁效果
得分与规则管理
各类元件命中得分;
所有同类元件被依次命中后,触发灯光全闪与分值提升(倍乘);
记录当前球数、是否 Game Over 等。
LCD 显示
当前得分(Score)
当前球号(Ball)
使用一个简单串口 LCD 显示:
3.2 开关输入与上拉配置
大部分目标开关采用“输入 + 内部上拉”的方式接入 Arduino:
pinMode(pinNumber, INPUT_PULLUP);
接线方式:
一端接 Arduino 数字引脚;
另一端接 GND;
开关未触发时引脚为 HIGH(被上拉);
开关闭合(被弹珠撞击)时,引脚被拉低为 LOW。
在代码中,检测到输入由 HIGH → LOW,即认为该目标被击中。
3.3 Pop Bumper 与高压检测:电压分压
与普通目标与滚轮使用 Arduino 5V 供电不同,Pop Bumper 需要强劲的冲击力,因此采用独立的 25V–24V 电源 驱动线圈。Pop Bumper 的“触发开关”处于高压侧,不能直接接入 Arduino。
解决方案:
将 Pop Bumper 开关输出接入 电压分压电路;
通过适当的电阻比,将高压侧信号衰减到不超过 5V;
分压后的信号接入 Arduino 的 模拟输入端口(Analog Input)。
原因:
即使在未触发情况下,分压器也会漏少量电流,导致信号有一定电压噪声;
使用模拟口可以设置阈值,如
analogRead(i) > 500,更容易区分“真触发”与“背景泄漏”。
3.4 球的丢失检测:力传感器
系统还使用一个小型 力/压力传感器(Force Sensor),通常布置在“落球区”或回收通道。当球掉回底部时,会压到传感器:
用力传感器读数作为“丢球”判定依据;
同时配合发射道开关(上、下发射口),区分“球刚被射出”与“球已丢失”;
每次检测到丢球,球数(Ball)+1;
当 Ball 达到最大值(如 5 球),触发 Game Over 流程。
3.5 分数与效果逻辑(来自示例代码)
原文给出了完整的 Arduino 控制代码,核心思想如下:
变量设计
Score:当前得分Target,Pop,Roll:三类目标的基础得分值Targets[8],Rolls[3],Pops[4]:记录每个目标是否被命中Ball:当前球号(1–5)Flash:灯光闪烁延时Pressure:力传感器触发阈值目标靶(Targets)处理流程
所有目标灯闪烁若干次;
灯全部熄灭;
增加 Target 的分值(
Target = Target * 5),提高之后的得分奖励。循环扫描 8 个目标输入(数字引脚 2–9);
某个为 LOW → 标记该 Target 已命中、增加
Score += Target、点亮对应灯;若 8 个目标全部命中(
Sum == 8):Rollovers 处理
对应灯全闪;
分值倍乘并清零标记(代码中示例为
Score = Score * 2,Roll = Roll * 10)。类似逻辑,扫描 3 个 Rollover 开关;
全部命中后:
Pop Bumper 处理
所有 Pop 灯闪烁;
分值提升(
Pop = Pop * 2),形成累进奖励。标记该 Pop;
增加
Score += Pop;点亮对应灯;
使用
analogRead(i)读取 4 个模拟输入;若某一路超过阈值(>500),认为 Pop Bumper 被触发:
当 4 个 Pop 全部被触发后:
球数与 Game Over 判定
若在某次检测中读数持续高于
Pressure且Ball == 5,则:依次闪烁 Rollovers、Pop Bumpers 与 Targets 所有灯;
在 LCD 显示“Game Over!!!”。
若这是本球首次检测到,
Ball++,并设置Shot标记;超出设定最大球数(如
Ball == 6)时,清零分数与倍率参数重新开始。通过发射通道下方两个开关(下、上射出),当球通过时:
通过力传感器读数判断球是否落入最终回收区:
LCD 显示输出
清屏 → 打印当前得分 → 换行显示
Ball = x;通过 SoftwareSerial 驱动串口 LCD;
当
Score发生变化时:TxPin引脚用于向 LCD 发送串口数据。
4 电源与配线
4.1 24V 电源系统
弹珠机中的电磁组件(弹板、Slingshot、Pop Bumper 等)需较大的瞬时电流,故使用 24V 开关电源:
24V 电源主要供给:
弹板线圈(Flipper Assembly)
Slingshot 组件
Pop Bumper 线圈
24V 输出先接至安装在台面底部的 配电铜排(Bus Bar),再由铜排为各组件分配电源,这样布线更整齐、维护方便。
对于 Pop Bumper 等高压部分,需要特别注意:
若改用更高电压电源(>25V),则现成的电压分压模块可能不适用,需自行设计分压电路,确保 Arduino 端电压不超过 5V。
4.2 线径选择
根据原文经验:
接 24V 高压线圈类负载:约 16 AWG 线材(粗线,承载大电流);
接 Arduino I/O 与传感器信号:约 22 AWG 细线即可。
5 关键弹珠机构与配件
5.1 弹簧拉杆(Plunger)
Plunger 组件为标准件,从专业弹珠配件商处购入。安装流程:
在机柜前板上钻孔,使 Plunger 穿出前面板;
内部用螺丝固定;
高度对齐:
将直尺沿台面延伸至前内侧板;
在该点处标记 Plunger 的中心位置;
垂直方向上使 Plunger 中心略高于弹珠直径的一半(标准弹珠为 1 1/16 英寸)。
5.2 台面与装饰
台面使用 1/4 英寸的桦木贴面胶合板。为提升视觉效果,作者选取了一张 NASA 的太空照片作为背景:
在板面喷涂喷胶(Contact Adhesive);
将海报覆于板面,刮平气泡;
背面修剪多余边缘,使整体尺寸精确符合 22W × 42L。
5.3 Flippers(弹板)
弹板组件包括:
Flipper 机械总成
Flipper Bat(塑料挡板)
Flipper Switch(高压开关)
侧面按钮
购买后通常没有任何接线说明,需要参考弹珠维修资料中的典型接线方式(原文链接中给出示意图)。核心要点:
线圈通常包含高阻/低阻两组绕组,用于启动与保持;
开关结构在按钮与线圈之间,配合机械角度限制,实现可靠回弹。
5.4 Slingshots(侧击机构)
Slingshot 组件为弹珠经过时会被“侧推”的机构:
需要完整的 Slingshot Assembly、支撑销与专用橡胶圈;
安装在 Flipper 上方两侧,形成典型的下方区域拱形布局;
底部布线同样接至 24V 总线与 Arduino 触发信号。
作者还自己制作了 金属防护栏(Rails):
使用家居店购买的镀铬钢丝(原用途为草坪标记线);
按需切割、弯折成形;
在台面钻孔后,用环氧胶固定。
5.5 Pop Bumpers
Pop Bumper 由底座、线圈、顶盖、支撑杆与触发轴组成:
从上方看为一个白色圆盘;
当球撞入圆盘时,盘面被压下,带动中心轴下移;
轴底部触发叶片开关,进而驱动线圈通电;
线圈带动金属环向下拉,向上反弹时将球弹回。
安装时需在台面钻三孔:
两个用于支撑杆固定;
一个用于中心轴通过与触发叶片开关。
5.6 目标靶(Targets)、Rollovers 与灯具
目标靶:立式靶板,球撞击后触发后方微动开关;
Rollover Switch:嵌在台面上的小金属片或塑料结构,球滚过即触发;
LED Lamp:用于标记目标状态与装饰。
接线方式统一:
一端接 Arduino 输入(或输出控制灯);
一端接地或电源;
通过 Mega 2560 的数字口进行采集与控制。
6 总结与展望
本文系统介绍了一台 Arduino 控制的自制全尺寸弹珠机 的设计实现过程。项目的特点在于:
结构工程:
采用标准尺寸木柜结构,使用可调机脚控制倾角;
台面可整体抬出翻转,便于维护与调试。
电子与电源架构:
使用 24V 高压系统驱动线圈部件;
使用电压分压将 Pop Bumper 的高压信号安全地引入 Arduino;
将高压总线分发至配电铜排,提高布线整洁性。
逻辑控制与玩法扩展:
基于 Mega 2560 的多路数字与模拟输入输出,实现完整得分、倍分、全亮闪烁、Game Over 等玩法;
利用力传感器判断丢球,使游戏流程自动化;
LCD 显示当前得分与球号,增强交互性。
作者也指出,一旦开始搭建,你很可能会不断添加新元素(声效、更多灯效、奖励关卡等),但有了本文所述的结构与控制架构,后续扩展将更加容易。
代码:
const int TxPin = 17;
long Score = 0;
long OldScore = 0;
long Target = 1;
long Pop = 1;
long Roll = 10;
int Targets[8];
int Rolls[3];
int Pops[4];
int Milli = 10;
int Sum = 0;
int Flash = 100;
int Ball = 0;
int i=0;
int Shot = 0;
int Lost = 0;
int Pressure = 1024;
#include <SoftwareSerial.h>;
SoftwareSerial mySerial = SoftwareSerial(255, TxPin);
void setup() {
/* Words without an s are the value achieved by interacting with a device.
* Works with an s keep track of which individual ones were interacted with.
* The latter is needed to determine when all have been hit and the value needs upgrading
* and the lights need turning off.
*/
pinMode(TxPin, OUTPUT);
digitalWrite(TxPin, HIGH);
mySerial.begin(9600);
mySerial.write(12); // Clear
mySerial.write(17); // Turn backlight on
//target inputs
pinMode(2,INPUT_PULLUP);
pinMode(3,INPUT_PULLUP);
pinMode(4,INPUT_PULLUP);
pinMode(5,INPUT_PULLUP);
pinMode(6,INPUT_PULLUP);
pinMode(7,INPUT_PULLUP);
pinMode(8,INPUT_PULLUP);
pinMode(9,INPUT_PULLUP);
//rollover inputs
pinMode(10,INPUT_PULLUP);
pinMode(11,INPUT_PULLUP);
pinMode(12,INPUT_PULLUP);
//lower ball shot switch
pinMode(15,INPUT_PULLUP);
//upper ball shot switch
pinMode(16,INPUT_PULLUP);
//lcd output
pinMode(17,OUTPUT);
//target lights, respective
pinMode(32,OUTPUT);
pinMode(33,OUTPUT);
pinMode(34,OUTPUT);
pinMode(35,OUTPUT);
pinMode(36,OUTPUT);
pinMode(37,OUTPUT);
pinMode(38,OUTPUT);
pinMode(39,OUTPUT);
//rollover lights, respective
pinMode(40,OUTPUT);
pinMode(41,OUTPUT);
pinMode(42,OUTPUT);
//pop bumper lights
pinMode(50,OUTPUT);
pinMode(51,OUTPUT);
pinMode(52,OUTPUT);
pinMode(53,OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
//If a pull-down resistor is used, the input pin will be LOW when the switch is open and HIGH when the switch is closed.
//check if a target was hit
//****** Targets *****
for (int i=0; i<8; i++){
if (digitalRead(i+2) == LOW){
//Target activated
Targets[i]=1;
Score = Score + Target;
//turn on Target light
digitalWrite(i+32,HIGH);
//delay so as not get multiple points for one hit
delay(Milli);
break;
}
}
Sum = 0;
for (int i=0; i<8; i++){
Sum = Sum + Targets[i];
}
if (Sum == 8){
//all Targets lit, so flash and then turn off.
for (int j=0; j<3; j++){
for (int i=0; i<8; i++){
digitalWrite(i+32, LOW);
}
delay(Flash);
for (int i=0; i<8; i++){
digitalWrite(i+32, HIGH);
}
delay(Flash);
}
for (int i=0; i<8; i++){
digitalWrite(i+32, LOW);
Targets[i]=0;
}
delay(Flash);
//Multiply target value by 10
Target = Target * 5;
//goto Skip;
}
// *********** Rollovers *********
for (int i=0; i<3; i++){
if (digitalRead(i+10) == LOW){
//rollover activated
Rolls[i]=1;
Score = Score + Roll;
//turn on rollover light
digitalWrite(i+40,HIGH);
//delay so as not get multiple points for one hit
delay(Milli);
break;
}
}
Sum = 0;
for (int i=0; i<3; i++){
Sum = Sum + Rolls[i];
}
if (Sum == 3){
//all rollovers lit, so flash and then turn off.
for (int j=0; j<3; j++){
for (int i=0; i<3; i++){
digitalWrite(i+40, LOW);
}
delay(Flash);
for (int i=0; i<3; i++){
digitalWrite(i+40, HIGH);
}
delay(Flash);
}
for (int i=0; i<3; i++){
digitalWrite(i+40, LOW);
Rolls[i]=0;
}
delay(Flash);
//Multiply score by 2
Score = Score * 2;
Roll = Roll * 10;
//goto Skip;
}
//********** Pop Bumpers **********
for (int i=0; i<4; i++){
if (analogRead(i) > 500){
//pop activated
Pops[i]=1;
Score = Score + Pop;
//turn on pop bumper light
digitalWrite(i+50,HIGH);
//delay so as not get multiple points for one hit
//mySerial.print(analogRead(i));
//mySerial.print(" ");
delay(Milli);
break;
}
}
Sum = 0;
for (int i=0; i<4; i++){
Sum = Sum + Pops[i];
}
if (Sum == 4){
//all pop bumpers lit, so flash and then turn off.
for (int j=0; j<3; j++){
for (int i=0; i<4; i++){
digitalWrite(i+50, LOW);
}
delay(Flash);
for (int i=0; i<4; i++){
digitalWrite(i+50, HIGH);
}
delay(Flash);
}
for (int i=0; i<4; i++){
digitalWrite(i+50, LOW);
Pops[i]=0;
}
delay(Flash);
//Multiply target value by 10
Pop = Pop * 2;
//goto Skip;
}
Skip:
//Determine ball number
if (digitalRead(15) == LOW){
//ball hit lower alley switch
//if not already done so, increase Ball
if (Shot == 0){
//Set Lost = 0 since not on pressure pad
Lost = 0;
Pressure = analogRead(7) + 20;
//set OldScore so as to reprint ball value on LCD
OldScore =-1;
Ball = Ball + 1;
if (Ball == 6){
Ball = 1;
Score = 0;
Target = 1;
Roll = 1;
Pop = 1;
}
Shot = 1;
}
}
if (digitalRead(16) == LOW){
//ball hit lower alley switch
//if not already done so, increase Ball
if (Shot == 0){
//Set Lost = 0 since not on pressure pad
Lost = 0;
Pressure = analogRead(7) + 15;
//set OldScore so as to reprint ball value on LCD
OldScore =-1;
Ball = Ball + 1;
if (Ball == 6){
Ball = 1;
Score = 0;
Target = 1;
Roll = 1;
Pop = 1;
}
Shot = 1;
}
}
if (analogRead(7) > Pressure){
//ball on pressure pad
Shot = 0;
if (Lost == 0){
//mySerial.print(analogRead(7));
//Score = Score + 100;
Lost = 1;
if (Ball == 5){
//Game Over
//flash rollovers and then turn off.
for (int j=0; j<3; j++){
for (int i=0; i<3; i++){
digitalWrite(i+40, LOW);
}
delay(Flash);
for (int i=0; i<3; i++){
digitalWrite(i+40, HIGH);
}
delay(Flash);
}
for (int i=0; i<3; i++){
digitalWrite(i+40, LOW);
Rolls[i]=0;
}
// flash pop bumpers and then turn off
for (int j=0; j<3; j++){
for (int i=0; i<4; i++){
digitalWrite(i+50, LOW);
}
delay(Flash);
for (int i=0; i<4; i++){
digitalWrite(i+50, HIGH);
}
delay(Flash);
}
for (int i=0; i<4; i++){
digitalWrite(i+50, LOW);
Pops[i]=0;
}
//Flash Targets and then turn off.
for (int j=0; j<3; j++){
for (int i=0; i<8; i++){
digitalWrite(i+32, LOW);
}
delay(Flash);
for (int i=0; i<8; i++){
digitalWrite(i+32, HIGH);
}
delay(Flash);
}
for (int i=0; i<8; i++){
digitalWrite(i+32, LOW);
Targets[i]=0;
}
mySerial.write(12); // Clear
delay(5);
// Required delay
mySerial.print(Score); // First line
mySerial.write(13); // Form feed
mySerial.print("Game Over!!!"); // Second line
}
}
}
//print to LCD
if (Score != OldScore){
mySerial.write(12); // Clear
delay(5); // Required delay
//mySerial.print(analogRead(7));
mySerial.print(Score); // First line
mySerial.write(13); // Form feed
mySerial.print("Ball = "); // Second line
mySerial.print(Ball);
OldScore = Score;
}
}












评论