很多球友打到一定程度都会遇到同一个瓶颈没人陪练。花钱请教练不便宜找个水平相当的球友也不容易。我自己练球三年多从徒手挥拍到多球训练都试过后来实在不想每次都跑去球馆蹭人家的发球机就决定自己动手做一台。用Arduino做乒乓球发球机这个项目我在网上翻了很久发现大部分资料要么只讲机械结构要么只给一段控制代码真正把机械设计、电子接线、控制逻辑串起来讲透的非常少。所以这篇文章想把我从零开始做这台发球机的完整过程写出来包括我踩过的坑、反复调过的参数以及最后跑通的心得希望能帮到也想自己搞一台的朋友。这台发球机能做到什么程度简单说它能实现两种出球模式——固定频率出球和随机落点出球能通过按键调节出球间隔能通过舵机控制发射管俯仰角实现远近落点的变化还能用OLED显示屏实时显示当前模式、间隔和俯仰角数值。整机成本控制在300元以内核心主控用Arduino Uno执行机构用两个直流减速电机加两个舵机。无论是硬件接线、机械装配还是代码烧录全部开源可复现新手照着做也能两三个晚上搞定。如果你正准备入门Arduino或者打乒乓球想有个不占地方的陪练伙伴又或者单纯想看看“一个机电一体化小项目”到底怎么从图纸变成实物这篇文章都很适合你。接下来我会从整体设计思路开始一步步拆解机械结构、控制电路、核心代码和调试过程。1. 项目概述与总体设计思路1.1 这台发球机要解决什么问题先说清楚需求边界。市面上成品发球机从几百到几千都有贵的主要贵在出球精度、旋转模拟、遥控编程这些功能上。但对我来说最核心的需求就三个能把球稳定地发射出去、能控制发射频率、能改变落点范围。至于旋转球、弧线球、多球不规则落点属于进阶玩法可以先不做。再拆一下这三个需求分别对应三个子系统把球发射出去——需要一个“出球机构”常见方案有单轮摩擦、双轮摩擦、气动喷射等其中双轮摩擦结构最简单、成本最低、速度调节也最直观。控制发射频率——需要一个可调定时器用Arduino的定时器中断或非阻塞延时来实现配合按键步进调节。改变落点范围——通过舵机控制发射管俯仰角球出去之后的远近就靠这个角度决定如果条件允许还可以加一个水平摆动舵机控制左右落点。我最终选择的方案是双轮摩擦出球 舵机驱动发射管俯仰角。整机分为机械底座、出球结构、发射管、控制盒四部分全部用3D打印件加标准件拼装方便后期调整。1.2 整体架构与技术选型整个系统的架构可以用“输入—控制—输出”来衡量输入三个按键分别负责模式切换、间隔加、间隔减也可以扩展成更多按键。控制一块Arduino Uno负责读取按键状态、维护当前模式与参数、计算舵机目标角度、按时间控制出球电机启动。输出两个直流电机负责摩擦轮、两个舵机一个管俯仰角另一个预留管水平摆动、一个OLED显示屏。为什么主控选Arduino Uno而不是ESP32或STM32说实话这个项目对主控的算力要求极低不涉及网络、图像、音频处理纯粹是GPIO读写加定时控制Uno的ATmega328P完全够用。更重要的是Uno的生态最成熟接线教程、库文件、出问题排查方案都多适合新手第一次接触机电一体化项目。如果你的手里现成有ESP32开发板那也能做代码几乎不用改只是引脚号需要对应调整后面我会提一嘴差异。1.3 为什么用Arduino而不是纯硬件电路有人可能会问这种控制用555定时器加继电器不也能实现吗确实能纯硬件方案在很多工业设备里还在用但它有两个痛点一是参数调整要改电阻电容不够灵活二是后期想加功能比如随机落点、蓝牙遥控几乎要重新设计电路。Arduino方案把“控制逻辑”从硬件里抽离出来写进代码里这就带来了几个实实在在的好处改参数不用动硬件要调整出球间隔改一行代码里的时间变量就行不用拆机器换电阻。功能扩展只需要写代码从“固定间隔出球”升级到“随机落点”本质上是加一个随机数函数其他硬件不用动。调试友好通过串口监视器能实时看到当前模式、间隔值、舵机角度出问题能快速定位是硬件还是逻辑问题。我自己在调试阶段就深刻体会到了这个好处。有一次电机不转我先看了串口输出发现控制信号正常那问题就锁定在电机驱动模块和电源上直接拿万用表量五分钟就找到是驱动模块的GND没接好。要是纯硬件方案排查路径会复杂得多。2. 机械设计与零部件选型2.1 整机结构布局先画一下整机的布局方便理解后面每一部分的位置关系。我的设计分四层自上而下是顶层是乒乓球储球斗一个倾斜的料斗球靠重力自然滚落到出球轮位置。中间层是出球机构两个摩擦轮呈上下布局球从两个轮子之间穿过被挤压弹出轮子外壳有一个弧形导向槽限制球的运动轨迹。再往下是发射管支架一根直径略大于乒乓球的圆管可以绕水平轴旋转管口方向决定了球的出射角度。底层是底座固定所有部件同时安装Arduino、电机驱动模块、舵机和电源。这个布局的好处是重心低运行时稳定。我之前在图纸阶段试过把储球斗放侧面球容易卡在转角处后来改成顶部直落才解决。2.2 出球机构的核心设计——双轮摩擦双轮摩擦出球的原理很直观两个轮子同向高速旋转球进入两轮之间的间隙后轮缘表面与球面产生摩擦力把球加速到接近轮缘线速度然后从导向槽飞出。具体有四个参数需要重点计算轮距两轮之间的间隙。乒乓球直径40mm那么轮距应该略小于40mm通常在36-38mm之间。轮距越小球被挤压的形变越大摩擦力越大出球速度越快但太小会导致卡球电机负载急剧升高。我测试下来38mm是个比较舒服的折中值既能稳定出球又不会卡死。轮子直径。我用的轮子是直径60mm的橡胶轮配上减速比1:30、空载转速300RPM的直流减速电机轮缘线速度大约是 ( v \pi \times 0.06 \times (300/60) \approx 0.94 , m/s )这个速度对应球速不算快适合练习近台基本功。想提高球速可以把减速比调低到1:10或换用更大功率的电机。轮子表面材质。一定要用橡胶或硅胶材质的轮子纯塑料轮摩擦力不够球会打滑要么出不去要么出去的球转速不稳。我在淘宝上买过一套“轮式AGV小车橡胶轮”直径60mm轴孔6mm几块钱一个效果就很好。导向槽弧度。导向槽的作用是把球限定在两轮中间的通道里槽的横截面应该是圆弧形半径略大于球的半径约22mm。槽太浅球容易跳出来太深则增加摩擦阻力。出球速度的粗略估算公式是( v_{球} \omega_{轮} \times r_{轮} \times \eta )其中 (\eta) 是摩擦传递效率一般在0.7-0.85之间。比如轮子角速度 (\omega 31.4 , rad/s)300RPM轮半径 (r 0.03m)效率取0.8那么 ( v_{球} \approx 31.4 \times 0.03 \times 0.8 \approx 0.75 , m/s )。这个速度作为入门练习够用但肯定不如商用机动不动十几米每秒那么暴力。2.3 俯仰角调节机构落点的远近由发射管的俯仰角决定。我用了MG996R金属舵机扭矩标称9.4kg·cm实际6V供电下大概能到7kg·cm左右驱动一根发射管完全没有问题。结构上发射管尾端铰接在底座支架上舵机臂通过一根连杆连接发射管中部。舵机转动时连杆推拉发射管使其绕尾端铰点转动。这里要注意连杆的长度和舵机臂的角度要匹配否则会卡死。我建议在装配前先用SolidWorks或者FreeCAD简单仿真一下运动范围如果没有建模基础也可以直接用硬纸板裁个模型试角度我当初就是这么干的。俯仰角的控制精度其实不需要很高毕竟目标是把球打到桌面不同位置而不是精确到毫米。舵机输出约0-180度对应发射管仰角约-5度到40度这个范围覆盖了近台和半台远两种落点。需要注意的是发射管仰角越大球出去后落在桌面上的距离越近仰角越小甚至水平球飞得越远。原理很简单出射速度不变时抛射角的水平射程随仰角变化但在实际桌面上还要考虑球台高度和落点缓冲不能直接拿真空抛体公式硬套实机调试更重要。2.4 3D打印件与标准件清单整机外壳和支架我用PLA材料3D打印层高0.2mm填充率30%就够不需要太结实毕竟受力不大。没有3D打印机的朋友也可以用亚克力板、木板甚至硬纸板加热熔胶搭一个这个项目对结构刚度的要求真的不高。以下是完整物料清单部件型号/规格数量备注主控板Arduino Uno R31兼容板就行直流减速电机300RPM6V2配橡胶轮电机驱动模块L298N 或 TB6612FNG1L298N便宜TB6612效率高舵机MG996R1-2俯仰必用水平摆动可选橡胶轮直径60mm轴孔6mm2摩擦出球用电源12V 5A 开关电源1给电机驱动和舵机供电降压模块MP1584或LM2596112V转5V给Arduino供电OLED屏0.96寸 I2C SSD13061显示状态按键轻触按键3模式/加/减结构件3D打印外壳支架1套PLA即可发射管直径45mm PVC管长200mm1不要太长2.5 机械装配的坑提前说装配顺序建议是先装出球轮和电机再装发射管和舵机最后装储球斗和控制盒。千万先别急着把外壳完全合上因为后面调试时大概率还要拆开调整。我在第一次装配时犯过一个低级错误把舵机臂的角度装反了结果舵机一转发射管往桌子上砸而不是往天上抬幸好速度不快不然管子都断掉。还有一个细节直流电机转动方向的一致性。两个摩擦轮必须同向旋转球才能顺利往前送装电机前用万用表确认电机正负极接好线后点动测试确认两个轮子的转向一致再固定螺丝。我发现很多第一次做这个项目的人卡在“球老是吐不出来”原因往往就是两个轮子反向转了。3. 控制系统搭建与接线3.1 接线方案总览控制系统的接线是整个项目里最容易被忽视却又最关键的一步。接线错了轻则工作不正常重则烧板子所以我单独把这块拎出来好好讲讲。整机需要供电的设备有Arduino Uno、两个直流电机、两个舵机、OLED屏、按键。它们在电压和电流需求上差异很大设备工作电压典型工作电流供电来源Arduino Uno5V可通过Vin接7-12V50mA左右独立降压模块直流电机6V每个0.5-1.5A堵转时更大L298N 电机驱动输出MG996R舵机5-6V待机10mA堵转可达2A独立5V稳压模块OLED屏3.3-5V20mA左右Arduino的5V引脚按键5V几毫安Arduino的5V引脚注意上面两个重点舵机千万不要直接由Arduino的5V引脚供电。MG996R堵转电流能到2A而Uno板载AMS1117稳压芯片最大输出也就1A左右直接接会掉压重启甚至烧板子。直流电机更不要从Arduino引脚取电必须经电机驱动模块。我实际用的供电拓扑是总电源12V 5A开关电源接入L298N的12V输入端子。电机供电L298N的OUT1/OUT2接一个电机OUT3/OUT4接另一个电机驱动模块内部把12V降压到6V输出给电机如果觉得12V对6V电机太暴力可以在L298N电源输入端前面加一个DC-DC降压模块把电压先降到7V左右。Arduino供电从L298N的5V输出该输出来自板载稳压接到Arduino的5V引脚。这里有个坑——L298N板载5V是从12V线性稳压出来的电流不大带动Uno和OLED屏没问题但不要再去带舵机。舵机供电单独用一个MP1584降压模块把12V降到5.5V左右接到舵机电源线。MP1584输出电流能力大约3A带两个舵机够用。信号线Arduino的数字引脚给舵机、电机驱动模块提供PWM信号给OLED提供I2C信号给按键提供开关量输入。3.2 具体接线对照表以下是我用的接线引脚你可以根据自己的习惯修改只要在代码里同步更新就行Arduino引脚连接设备说明D3舵机1俯仰信号线D5舵机2水平摆动预留信号线D6L298N ENA电机A使能脚PWM调速D7L298N IN1电机A方向D8L298N IN2电机A方向D9L298N ENB电机B使能脚PWM调速D10L298N IN3电机B方向D11L298N IN4电机B方向A4OLED SDAI2C数据A5OLED SCLI2C时钟D2按键1模式切换接10K下拉电阻D4按键2间隔加接10K下拉电阻D12按键3间隔减接10K下拉电阻GNDL298N GND、电源模块GND全部共地必须的共地这件事多说一句所有模块的GND必须连在一起如果不共地信号线的参考电压不一样就会出现舵机乱抖、按键失灵之类的诡异问题。我一开始没给舵机电源模块和Arduino共地导致舵机偶尔自己抖一下找了半天才发现是共地没处理好。3.3 按键与OLED接线细节按键的接法有两种一种是用内部上拉一种是用下拉。我习惯用外部下拉电阻这样逻辑上按下为HIGH读起来直观。如果不想焊电阻也可以把按键一端接GND、另一端接引脚然后启用Arduino的INPUT_PULLUP代码里读LOW为按下。两种都可以只要你心里清楚逻辑就行。OLED是I2C接口只有四根线VCC、GND、SDA、SCL。把SCL接到A5、SDA接到A4再用Uno的5V供电。这里要注意如果OLED模块是3.3V版本最好接3.3V引脚而不是5V虽然大多数模块标称兼容5V但长期满压供电容易发热。我用的是5V版本就没这个问题。3.4 供电预算验证在通电之前先估算一下总电流避免电源模块过载。整机最耗电的是电机和舵机两个直流电机同时转电流约 (2 \times 1A 2A)。两个舵机同时动作峰值电流约 (2 \times 0.5A 1A)静态更低。Arduino加OLED和按键约100mA。总电流约3.1A。12V 5A的开关电源有足够余量但如果手头只有2A电源肯定会在电机启动瞬间掉压然后整机复位。很多DIY发球机的“一开电机就重启”问题基本都是电源余量不足。我的建议是电源规格至少按最大电流的1.5倍选也就是 ( 3.1A \times 1.5 \approx 4.6A )所以5A刚好合适。4. 控制逻辑设计与代码实现4.1 软件整体架构控制逻辑这部分是整个项目的大脑代码看起来不长但设计思路上有几个关键点值得展开讲。先明确需求三种工作模式待机模式电机不转、连续出球模式按设定间隔一直出球、随机落点模式出球间隔和俯仰角在范围内随机变化。按键调整模式切换键循环切换三种模式间隔加/减键在待机模式下调整出球间隔步进0.5秒范围1-5秒。舵机控制待机时舵机回到初始角度连续出球时舵机固定在当前角度随机模式时每出一次球舵机在25度和75度之间随机取一个值。状态显示OLED显示当前模式、出球间隔、舵机角度。软件架构上我用了最常见的“非阻塞状态机”思路——主循环里不断检查按键、刷新显示器、查询当前时间并判断是否该出球通过millis()函数来判断时间而不是用delay()卡住整个程序。这一点很重要因为如果用了delay()程序在等待期间做不了任何其他事按键会失灵OLED也不会刷新。4.2 出球频率控制的两种实现方式控制出球频率有两种常规做法定时器中断法利用ATmega328P的定时器1产生固定时基中断中断服务函数里翻转电机IO状态。优点是计时精准不占用主循环缺点是中断和Servo.h库存在资源冲突Servo库自己也用定时器1需要换其他定时器。非阻塞延时法millis()差量判断主循环里持续记录上一次出球的时间lastFireTime每次循环判断millis() - lastFireTime intervalMillis成立则执行一次出球动作并更新时间戳。优点是代码简单、不会阻塞其他任务计时精度对于发球机这种场景绰绰有余。我最终采用了第二种原因很简单出球间隔是秒级不需要微秒级精度的中断定时而且非阻塞写法让我能在同一个循环里同时处理按键、OLED刷新和舵机控制代码结构清爽得多。4.3 舵机控制与PWM原理Arduino用Servo.h库控制舵机本质是输出50Hz的PWM信号通过改变脉冲宽度通常1ms到2ms对应0到180度来设定舵机角度。这个库使用方便但你不需要在代码里手动计算PWM占空比直接myservo.write(angle)就行。有一点要留意Servo.h库和高频PWM引脚如D9、D10会有冲突。如果你把舵机接到D9/D10同时用analogWrite()在这些引脚上输出PWM给电机调速两者会互相干扰。我建议舵机信号线接D3/D5电机PWM接D6/D9这样避开冲突。具体原因涉及Arduino内部定时器的分配这里不展开记住结论即可。4.4 完整代码解析以下是我实际烧录的代码注释写得比较详细你直接复制到Arduino IDE里按照上面的接线表连接硬件烧录后就能跑起来。#include Wire.h #include Adafruit_SSD1306.h #include Servo.h // 引脚定义 #define BTN_MODE 2 #define BTN_ADD 4 #define BTN_SUB 12 #define SERVO_PITCH 3 #define MOTOR_ENA 6 #define MOTOR_IN1 7 #define MOTOR_IN2 8 #define MOTOR_ENB 9 #define MOTOR_IN3 10 #define MOTOR_IN4 11 Servo servoPitch; // OLED 0.96寸 I2C Adafruit_SSD1306 display(128, 64, Wire, -1); // 全局状态 int mode 0; // 0待机, 1连续出球, 2随机落点 int intervalSec 2; // 出球间隔默认2秒 int pitchAngle 60; // 俯仰角默认60度 unsigned long lastFireTime 0; bool motorRunning false; void setup() { Serial.begin(115200); pinMode(BTN_MODE, INPUT); pinMode(BTN_ADD, INPUT); pinMode(BTN_SUB, INPUT); pinMode(MOTOR_ENA, OUTPUT); pinMode(MOTOR_IN1, OUTPUT); pinMode(MOTOR_IN2, OUTPUT); pinMode(MOTOR_ENB, OUTPUT); pinMode(MOTOR_IN3, OUTPUT); pinMode(MOTOR_IN4, OUTPUT); // 电机初始停止 digitalWrite(MOTOR_IN1, LOW); digitalWrite(MOTOR_IN2, LOW); digitalWrite(MOTOR_IN3, LOW); digitalWrite(MOTOR_IN4, LOW); analogWrite(MOTOR_ENA, 0); analogWrite(MOTOR_ENB, 0); servoPitch.attach(SERVO_PITCH); servoPitch.write(pitchAngle); // OLED初始化 if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(OLED init failed); while(1); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); lastFireTime millis(); } void loop() { checkButtons(); updateDisplay(); updateMotorState(); } // 按键扫描与逻辑处理 void checkButtons() { static unsigned long lastDebounceTime 0; static int lastModeBtnState HIGH; static int lastAddBtnState HIGH; static int lastSubBtnState HIGH; int modeBtn digitalRead(BTN_MODE); int addBtn digitalRead(BTN_ADD); int subBtn digitalRead(BTN_SUB); // 防抖动处理只处理按下沿 if (modeBtn HIGH lastModeBtnState HIGH millis() - lastDebounceTime 200) { mode (mode 1) % 3; lastDebounceTime millis(); Serial.print(Mode changed to: ); Serial.println(mode); } if (addBtn HIGH lastAddBtnState HIGH millis() - lastDebounceTime 200) { if (mode 0) { intervalSec constrain(intervalSec 1, 1, 5); Serial.print(Interval: ); Serial.println(intervalSec); } lastDebounceTime millis(); } if (subBtn HIGH lastSubBtnState HIGH millis() - lastDebounceTime 200) { if (mode 0) { intervalSec constrain(intervalSec - 1, 1, 5); Serial.print(Interval: ); Serial.println(intervalSec); } lastDebounceTime millis(); } lastModeBtnState modeBtn; lastAddBtnState addBtn; lastSubBtnState subBtn; } // 电机与舵机状态机更新 void updateMotorState() { if (mode 0) { // 待机模式电机停舵机回初始角度 stopMotor(); if (pitchAngle ! 60) { pitchAngle 60; servoPitch.write(pitchAngle); } return; } if (mode 1) { // 连续出球模式按固定间隔触发 if (millis() - lastFireTime intervalSec * 1000UL) { fireBall(); lastFireTime millis(); } return; } if (mode 2) { // 随机落点模式每次出球时随机调整间隔和角度 if (millis() - lastFireTime intervalSec * 1000UL) { intervalSec random(1, 5); pitchAngle random(25, 76); servoPitch.write(pitchAngle); fireBall(); lastFireTime millis(); Serial.print(Random fire: interval); Serial.print(intervalSec); Serial.print( pitch); Serial.println(pitchAngle); } return; } } // 触发一次出球动作 void fireBall() { analogWrite(MOTOR_ENA, 200); analogWrite(MOTOR_ENB, 200); digitalWrite(MOTOR_IN1, HIGH); digitalWrite(MOTOR_IN2, LOW); digitalWrite(MOTOR_IN3, HIGH); digitalWrite(MOTOR_IN4, LOW); motorRunning true; // 这里有个重要设计电机不立即停而是持续转一段时间 // 因为球从储球斗滚到轮子之间并加速需要时间 // 我在实际测试中450ms比较合适太短球还没出来电机就停了 static unsigned long motorStopTime 0; motorStopTime millis() 450; while (millis() motorStopTime) { // 这里用while是为了保证出球动作完成但因为时间很短对系统影响可忽略 } stopMotor(); } void stopMotor() { digitalWrite(MOTOR_IN1, LOW); digitalWrite(MOTOR_IN2, LOW); digitalWrite(MOTOR_IN3, LOW); digitalWrite(MOTOR_IN4, LOW); analogWrite(MOTOR_ENA, 0); analogWrite(MOTOR_ENB, 0); motorRunning false; } // OLED显示刷新 void updateDisplay() { display.clearDisplay(); display.setCursor(0,0); display.print(Mode: ); switch(mode) { case 0: display.print(IDLE); break; case 1: display.print(CONTINUOUS); break; case 2: display.print(RANDOM); break; } display.setCursor(0,16); display.print(Interval: ); display.print(intervalSec); display.print(s); display.setCursor(0,32); display.print(Pitch: ); display.print(pitchAngle); display.print( deg); display.setCursor(0,48); display.print(State: ); display.print(motorRunning ? RUN : STOP); display.display(); }这段代码有几个地方值得特别说明fireBall()函数里我用了while (millis() motorStopTime)来让电机持续转450ms。严格来说这不是最好的做法因为它会阻塞主循环450ms。但考虑到这个动作发生在出球瞬间按下按键或切换模式不会在450ms里发生影响可以忽略。如果你写代码更追求优雅可以把这个“持续450ms”也改成非阻塞状态在updateMotorState()里再维护一个“电机开启状态”和“剩余运行时间”。我实际测试时先用了阻塞版确定450ms参数合适后才改成非阻塞版。建议你先用阻塞版把机器跑起来再考虑优化。checkButtons()里的防抖逻辑我做了简化没有用经典的滤波算法而是用“读取状态下沿200ms间隔”的方式。因为按键本身是机械结构按下瞬间会有抖动但如果两次有效操作之间至少间隔200ms抖动就被天然忽略了。对发球机这种使用场景没必要做太精细的消抖。4.5 参数调节的依据和思路代码里几个关键参数是我反复测试出来的现在把测试过程和思路写出来方便你根据自己的硬件情况调整。450ms的出球电机运行时间是怎么定的我先把电机设为常转然后手动把球一颗一颗放进储球斗观察球从进轮到飞出的时间。最终发现从球进入轮子空隙到被射出大约需要350-500ms取决于球的放置位置和轮子转速。取450ms作为电机持续运行时间能保证球有足够时间穿过轮子。出球间隔intervalSec的范围1-5秒是我拍脑袋定的上限吗其实我测试过更短的间隔比如0.5秒但发现两个问题一是球从储球斗滚到轮子之间的时间约0.8秒间隔太短会导致第二颗球还在下落途中就被第一颗球卡住二是电机频繁启停驱动器发热明显。所以我最终把下限设在1秒这刚好是“连续快攻”训练中比较快的节奏。如果你想让间隔更短可以考虑把储球斗改成螺旋下料器或者把电机改成常转用挡板机构逐颗放球——但那样机械结构会复杂不少。4.6 用Wokwi仿真验证控制逻辑写代码阶段如果你手头没有硬件可以用Wokwi这个在线仿真平台先验证逻辑。Wokwi支持Arduino Uno仿真也内置了Servo、OLED、按键这些虚拟器件不需要买任何硬件就能把代码跑起来。我装机的第一版代码就是在Wokwi里调试通过的尤其是按键防抖和OLED显示逻辑在仿真器里先验证再去接真机能少很多低级错误。Wokwi的使用很简单打开 wokwi.com选择Arduino Uno模板把代码复制进去然后在右侧添加Servo、OLED、Button等虚拟元件连线后点运行就能看到串口输出和舵机角度变化。这特别适合还在犹豫要不要买材料的朋友先用仿真清除代码层面的问题再下单硬件成功率会高很多。5. 组装调试与常见问题排查5.1 机械部分的组装调试机械部分的调试重点在出球机构。装好之后我先不接控制板直接给电机通电让它持续旋转然后手动把球放进储球斗观察球的路径是否顺畅。常见的三个问题球卡在储球斗出口说明出口尺寸太小或者角度太平。我做的储球斗出口宽42mm斜坡角度30度球能顺利滚落。如果球又卡住第一件事是量出口尺寸第二件事是确认斜坡角度是否大于摩擦角。球从导向槽侧边飞出去说明导向槽太浅或者槽壁不够高。我的导向槽深度做到20mm球正好嵌在里面没有跳出来的机会。出球速度太低先看两个轮子的转速是否一致再看轮距是不是太大。轮距可以从38mm逐步调小到36mm每次调0.5mm测试一次出球速度调到满意为止。但注意别低于35mm否则球会被过度挤压出球力量很大对电机和轮子寿命都有影响。5.2 电子部分的调试顺序电子部分不要一次性把所有模块都接上去调那样出了问题很难定位。我建议按这个顺序来先只接Arduino和电脑USB烧录一个LED闪烁程序确认板子正常。接OLED屏跑I2C扫描程序确认屏幕能点亮。接按键串口打印按键值测试消抖逻辑。接舵机让它从0到180度来回扫确认能正常转动。最后接电机驱动模块和电机先用小PWM值测试转向再逐步加大PWM。这样每一层都验证过再合起来整体跑出问题就能快速缩小范围到某个模块。5.3 常见问题速查表现象可能原因排查方法解决办法上电后Uno灯不亮电源没接对或功率不够万用表测Uno的5V引脚电压换成独立5V电源舵机静止时嗡嗡抖电源电压不足或没共地测舵机电源电压低于5V则不稳定用MP1584单独供电并共地电机不转但代码正常L298N使能脚接错或PWM太小量ENA/ENB电压确认PWM值100查代码PWM值并检查接线电机转但球吐不出来两轮转向不一致或轮距太大空转看轮子旋转方向调电机接线顺序或减小轮距球偶尔连续出两颗电机运行时间过长观察450ms时间是否合适缩短电机运行时间OLED屏幕花屏I2C地址不对或干扰跑I2C扫描程序找地址在代码中修改0x3C为实际地址按下按键没反应接线错或下拉没接万用表测按键两端电压确认按键接法和代码读取引脚一致5.4 我最想提醒的几个坑第一Arduino的5V引脚不要接电机和舵机。这句话我重复了无数遍因为真有人拿Uno直驱MG996R结果板子烧了。Uno的板载稳压器不是一个靠谱的功率器件驱动电机的活儿交给L298N和DC-DC模块Uno只负责发信号。第二出球电机的PWM值不是越大越好。我刚开始把PWM设成255满速结果球速太快落点直接飞出球台根本没法练。后来调成200出球速度比较温和落点也更容易控制。具体数值跟你的电机减速比、轮子直径、轮距都有关系建议从150开始慢慢往上加找到自己舒服的速度区间。第三随机落点模式的“随机”要限制范围不能完全放飞。我一开始让角度在0到180度之间随机结果球有时直接射上天有时砸在自己的脚边。后来改成25到75度之间才达到“大半张台面但可控”的效果。第四接线时所有线材尽量用颜色区分。电源正极用红色负极用黑色信号线用其他颜色后面排查会轻松很多。我去调试现场总是先拍照存档再动手改线不然装完之后发现接错了光是理线就能耗掉半小时。6. 后续扩展思路机器能跑通之后还可以往这几个方向做功能扩展。如果你手头有ESP32开发板可以直接把Uno换掉代码改动量很小主要是引脚映射不同。ESP32的好处是自带Wi-Fi和蓝牙能扩展出手机App控制、网页控制面板等功能。另一个很实用的扩展是把“声音识别”加进来。我注意到今年Arduino社区里ESP32加INMP441麦克风做声音识别的玩法很火思路是用ESP32读取麦克风数据通过边缘计算识别特定的语音指令或拍手声然后触发发球机启动或停止。比如你说一声“开球”或者拍一下手发球机就开始工作这对于一个人练球来说非常方便不用跑回控制台按按键。当然这需要增加麦克风模块和相应的音频处理代码复杂度比原来的按键方案高不少但作为进阶项目很有挑战性。如果你对运动控制有更高追求还可以在出球机构里增加轮子的独立调速——两个摩擦轮速度不同时球会带上旋或下旋。做法很简单用两个独立的PWM通道分别控制两个电机的转速然后在代码里增加一个“转速差”参数。我用这个方法试过左右轮速度差达到20%时球的旋转效果能明显感知弧线会有明显变化这对练习弧圈球防守非常有帮助。再往上走就是在发射管的水平方向加一个舵机通过左右摆动让球落在球台的左半台或右半台配合俯仰角控制基本就能模拟标准多球训练的内容。水平摆动的控制逻辑和俯仰角类似只是在随机模式里增加一个轴的随机变量而已难度不大。从我个人实际使用的体验来说这台发球机最大的价值不是省了买发球机的钱而是让我对机电一体化的整个流程有了完整的理解。从机械尺寸的验证到电机选型的功率计算再到Arduino的控制逻辑设计和参数调试每一步都踩过坑、填过坑。如果你照着这篇文章做下来遇到问题还能自己排查解决那这个项目带给你的收获绝对远超一台成品发球机。