十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32迷你保龄球计分系统:传感器、舵机与Arduino实战

ESP32迷你保龄球计分系统:传感器、舵机与Arduino实战 平时做 ESP32 开发时大多都是点灯、传感器采集、联网上报这类基础玩法。这次想尝试一个偏机械结构和逻辑规则的项目用 ESP32 做一套迷你保龄球计分系统。简单来说就是在桌面尺寸的球道上通过传感器判断球是否滚出、球瓶是否被击中再用 ESP32 驱动舵机完成摆瓶和推球复位同时把每次投球的得分实时显示在 OLED 屏幕上并可以通过蓝牙把分数同步到手机 App。这篇文章会完整拆解这套系统的技术方案涵盖硬件选型、传感器布局、舵机控制、保龄球十格计分规则、Arduino 代码实现以及蓝牙扩展。整个项目不需要复杂加工设备3D 打印加少量螺丝就能搭建。适合有一定 ESP32 开发基础、想尝试小型机电一体化项目的读者也适合想理解保龄球计分规则在嵌入式里如何落地的朋友。1. 背景与系统概述1.1 这是一个什么项目这套 ESP32 Bowling System本质上是一个桌面级保龄球计分装置。用户在迷你球道上掷球球滚过球道后撞击瓶区的球瓶传感器检测到球瓶被击倒的数量ESP32 根据保龄球规则累计分数并自动把球瓶摆回原位把球从球道末端推回发球区。听起来简单实际做起来涉及三个层面的问题物理层面怎么设计球道和球瓶结构怎么让球瓶稳定竖立、又能被球碰倒。传感器层面怎么区分“球已经滚过”和“瓶倒了几只”并且不能误判。逻辑层面保龄球有十格计分规则Strike全中和 Spare补中的加分规则比较绕必须在代码里正确处理。用 ESP32 做主控的好处很明显芯片本身带有 Wi-Fi 和蓝牙IO 资源丰富支持 Arduino、ESP-IDF、PlatformIO 多种开发方式电机舵机控制、传感器读取、OLED 显示、手机通信都能在一块板子上解决不需要额外加 MCU。1.2 常见应用场景这类系统常见的落地场景包括桌游吧、团建空间的互动娱乐设备体积小、玩法直观。学校创客实验室或工程实训项目用来演示传感器融合、闭环控制和状态机设计。产品原型机的前期验证验证计分逻辑和机械结构之后再改造成商用版本。个人 DIY 娱乐项目放在家里当桌面摆件配合手机蓝牙看分数。1.3 功能规划这套系统的完整功能规划如下模块功能说明球瓶检测检测球瓶被击倒数量支持 0-10 只判定滚动检测检测球是否已经从发球区滚出机械复位舵机驱动摆瓶机构复位球瓶推球机构把球送回发球区计分逻辑按标准保龄球十格规则计算得分包含 Strike、Spare、第 10 格特殊规则屏幕显示OLED 显示当前格、当前球权、本次倒瓶数、累计总分蓝牙扩展ESP32 通过低功耗蓝牙广播分数手机端接收并展示下面从系统架构开始逐步讲解各个环节的实现方案。2. 系统架构与硬件选型2.1 整体架构这套系统的数据流向可以用下面这张简图说明球手发球 - 红外传感器A检测到球滚过球道 - 球撞击瓶区 - 碰撞传感器/对射传感器检测倒瓶 - ESP32 读取传感器数据 - 运行十格计分逻辑 - OLED 显示本次得分与总分 - 舵机驱动摆瓶机构复位 - 舵机/电机驱动送球机构把球送回发球区 - 等待下一轮整个系统是一个典型的“传感器-控制器-执行器”闭环模型。ESP32 在这里承担两个角色实时响应传感器事件的硬实时控制器以及运行计分规则和应用逻辑的应用处理器。2.2 主控选型关于主控直接选用 ESP32 DevKitC 或 NodeMCU-32S 这类通用开发板即可。选择 ESP32 而非 Arduino Uno 的原因主要有三个IO 数量足够。传感器、舵机、OLED 加起来需要 7-8 个 GPIOESP32 完全够用。内置蓝牙。不需要外接 HC-05 模块直接用 ESP32 原生 BLE 就能实现手机端分数同步。主频和 Flash 更充裕。后续如果需要增加语音提示、网络排行榜、微信小程序也跑得动。需要注意的是不同厂家的 ESP32 开发板引脚定义不完全一致。不同板子默认的 I2C 引脚、板载 LED 引脚都可能不同。建议先查自己板子的 Pinout 图再对接线。2.3 传感器选型传感器是整个系统最容易翻车的地方。这里给出两种方案方案一对射式红外传感器推荐在球道两侧安装红外对射模块也就是发射管和接收管相对安装。球滚过时挡住红外线接收端电平跳变。检测通道 1球道入口处检测球是否已经发出。检测通道 2瓶区后方检测是否有球通过。对射式传感器抗干扰能力强不容易受环境光影响反应速度快适合检测球这种快速移动的物体。方案二反射式红外传感器把红外反射模块安装在球道底部球滚过时反射红外线模块输出电平变化。优点是安装简单只需要单侧安装缺点是容易受球道表面反光影响检测阈值需要反复调整。球瓶倒伏检测部分可以采用薄片式碰撞传感器或微动开关阵列。在瓶区底部铺设一排微动开关每只球瓶倒下后压住对应的开关ESP32 通过 GPIO 读取开关组合算出倒瓶数量。也可以用 10 个独立的倾斜开关或振动传感器但这样会占用太多 GPIO且容易受球撞击瞬间的振动干扰。实际项目中微动开关阵列是最稳定、最容易调试的方案。2.4 执行器选型机械复位部分需要两个执行器摆瓶舵机用舵机带动一根横杆把倒下的球瓶扫回竖立状态。选择 SG90 或 MG996R 均可。SG90 扭矩较小适合轻量球瓶如果球瓶较重建议用 MG996R扭矩更大不容易在扫瓶时堵转。送球舵机用舵机带动推板把滚到球道末端的球推回发球区。这个舵机受力相对较小SG90 就可以。舵机供电注意SG90 在启动和堵转瞬间电流可达 500mA 甚至更高如果直接用 ESP32 开发板的 3.3V 或 5V 引脚供电容易出现电压跌落、系统重启的问题。建议ESP32 开发板 - 通过 USB 或 5V 电源供电 舵机电源 - 单独使用 5V/2A 稳压模块供电 共地连接 - ESP32 的 GND 与舵机电源的 GND 必须连接2.5 OLED 显示显示部分选用 0.96 寸 I2C 接口 OLED分辨率 128x64。I2C 只需要两根线SDA、SCL接线简单而且 SSD1306 驱动库在 Arduino 生态下非常成熟显示分数足够清晰。2.6 硬件接线总表模块ESP32 引脚说明红外对射模块 1 输出GPIO 26球道入口检测检测球发出红外对射模块 2 输出GPIO 27球道末端检测检测球到达瓶区微动开关阵列信号GPIO 32-39 等读取倒瓶数量按需分配摆瓶舵机信号GPIO 13控制摆瓶机构送球舵机信号GPIO 12控制送球机构OLED SDAGPIO 21I2C 数据线部分板载默认引脚OLED SCLGPIO 22I2C 时钟线部分板载默认引脚舵机电源外部 5V 电源不直接接 ESP32 5V 引脚共地所有 GND 相连必须共地否则信号不稳定以上引脚可以根据你自己的板子调整。重点是传感器信号引脚必须支持输入模式舵机信号引脚不能和 I2C 引脚冲突。3. 环境准备与开发方式选择3.1 开发环境ESP32 的开发方式目前主要有两条主流路线Arduino IDE上手快库生态丰富适合快速验证硬件逻辑。本文的示例代码基于 Arduino 框架。PlatformIOVSCode 插件工程化管理更规范适合多文件项目、依赖管理、自动化构建长期迭代更推荐。如果你之前已经在用 Arduino IDE 做 ESP32 开发直接沿用即可。如果还没有配置过需要先在 Arduino IDE 中安装 ESP32 开发板支持。在 Arduino IDE 中打开“文件 - 首选项 - 附加开发板管理器网址”添加以下地址然后通过“工具 - 开发板 - 开发板管理器”搜索 ESP32 安装https://espressif.github.io/arduino-esp32/package_esp32_index.json受网络环境影响国内用户下载 ESP32 开发包经常失败。遇到这种情况可以手动下载离线安装包或者切换使用 PlatformIOPlatformIO 会自动管理工具链依赖但对网络也有一定要求。版本方面建议选择较新的稳定版本库在 Arduino IDE 2.x 或 VSCode 上都能正常使用。环境配置过程中常见的报错“Failed to install platform espressif32”基本是网络下载超时导致可以多试几次或者配置代理镜像加速。3.2 需要安装的第三方库在 Arduino IDE 的库管理器中搜索并安装库名称用途U8g2OLED 显示驱动支持中英文和数字混排Esp32ServoESP32 舵机控制基于 LEDC 定时器实现BLEDevice内置ESP32 低功耗蓝牙用于分数发送注意不要混用 Servo 库和 Esp32Servo 库。ESP32 原生的 Arduino Servo 库兼容性不好建议直接用 Esp32Servo。3.3 示例项目结构本文示例采用 Arduino IDE 单文件方式方便直接复制测试。正式项目中建议切换到 PlatformIO目录结构类似bowling-system/ ├── include/ │ └── config.h ├── src/ │ ├── main.cpp │ ├── scoring.h │ ├── scoring.cpp │ ├── hardware.h │ └── hardware.cpp ├── platformio.ini └── data/4. 核心原理与代码拆解4.1 保龄球计分规则保龄球一局共 10 格Frame。每格最多投两次球目标是把 10 只球瓶全部击倒。Strike全中第一球就击倒全部 10 只球瓶。该格分数为 10 分加上后两次投球的击倒瓶数。屏幕显示“X”。Spare补中两次投球合计击倒全部 10 只球瓶。该格分数为 10 分加上后一次投球的击倒瓶数。屏幕显示“/”。普通命中两次投球未全倒该格分数为两球击倒瓶数之和。第 10 格特殊规则如果第 10 格投出 Strike 或 Spare需要额外奖励投球机会。Strike 时可以再投两次Spare 时可以再投一次。第 10 格最多可以投三次球。举例说明第 1 格10Strike 第 2 格第一球 7第二球 2 第 1 格得分 10 7 2 19 第 2 格得分 7 2 9 前两格累计总分 28这个例子能直观说明为什么 Strike 的加分要等后续两球结束后才能计算。4.2 代码架构设计代码按模块拆分为三个部分ScoringEngine计分引擎维护格数、球权、倒瓶数、每格分数和总分。HardwareController硬件控制层负责读取传感器、控制舵机。setup()/loop()初始化各模块在主循环中调度状态机。状态机是这套程序的核心。整个游戏过程可以拆成以下状态IDLE - BALL_DETECTED - BALL_REACHED_PINS - READ_PINS_FALLEN - SCORE_UPDATE - RESET_PINS - RESET_BALL - NEXT_ROUND这里不采用复杂多线程而是用简单的顺序状态机。优点是逻辑清晰、便于调试也避免了舵机动作和传感器读取之间的竞争问题。4.3 完整代码实现下面给出一份可以在常见 ESP32 DevKit 上运行的示例代码。其中传感器和舵机引脚为示例配置请根据你的实际接线修改。文件项目根目录/bowling_system.ino#include Arduino.h #include U8g2lib.h #include Esp32Servo.h // 引脚定义 #define PIN_LAUNCH_SENSOR 26 // 球道入口红外对射输出 #define PIN_PIN_SENSOR 27 // 球道末端红外对射输出 #define PIN_PIN_SWITCH_1 32 // 微动开关用于检测倒瓶数量 #define PIN_PIN_SWITCH_2 33 #define PIN_PIN_SWITCH_3 34 #define PIN_PIN_SWITCH_4 35 #define PIN_PIN_SWITCH_5 36 #define PIN_SERVO_PIN_RESET 13 // 摆瓶舵机 #define PIN_SERVO_PIN_BALL 12 // 送球舵机 // OLED 初始化 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 21, 22); // 舵机对象 Servo pinResetServo; Servo ballReturnServo; // 保龄球计分引擎 class ScoringEngine { private: static const int MAX_FRAMES 10; int rolls[21]; // 最多可存储 21 次投球 int rollCount 0; int currentFrame 0; // 0~9 int currentBall 0; // 当前格第几次投球0 或 1 public: void reset() { memset(rolls, 0, sizeof(rolls)); rollCount 0; currentFrame 0; currentBall 0; } void addRoll(int pins) { rolls[rollCount] pins; if (currentBall 0) { if (pins 10) { // 第一球全中 - Strike if (currentFrame 9) { // 第10格仍要记录本次 roll继续允许额外投球 } else { currentFrame; currentBall 0; } } else { currentBall 1; } } else { // 第二球 currentBall 0; currentFrame; } if (currentFrame MAX_FRAMES) { currentFrame MAX_FRAMES; } } // 根据 10 格规则计算当前累计总分 int getTotalScore() { int score 0; int rollIndex 0; for (int frame 0; frame MAX_FRAMES; frame) { if (rollIndex rollCount) break; if (rolls[rollIndex] 10) { // Strike int nextOne (rollIndex 1 rollCount) ? rolls[rollIndex 1] : 0; int nextTwo (rollIndex 2 rollCount) ? rolls[rollIndex 2] : 0; score 10 nextOne nextTwo; rollIndex; } else { int first rolls[rollIndex]; int second (rollIndex 1 rollCount) ? rolls[rollIndex 1] : 0; int frameScore first second; if (frameScore 10) { // Spare int nextOne (rollIndex 2 rollCount) ? rolls[rollIndex 2] : 0; score 10 nextOne; } else { score frameScore; } rollIndex 2; } } return score; } int getCurrentFrame() { return currentFrame 1; } int getCurrentBall() { return currentBall; } bool isGameOver() { return currentFrame MAX_FRAMES; } }; ScoringEngine scorer; // 硬件控制层 bool readLaunchSensor() { return digitalRead(PIN_LAUNCH_SENSOR) LOW; // 对射被遮挡为 LOW根据模块调整 } bool readPinSensor() { return digitalRead(PIN_PIN_SENSOR) LOW; } int readFallenPinCount() { int count 0; count digitalRead(PIN_PIN_SWITCH_1) HIGH ? 1 : 0; count digitalRead(PIN_PIN_SWITCH_2) HIGH ? 1 : 0; count digitalRead(PIN_PIN_SWITCH_3) HIGH ? 1 : 0; count digitalRead(PIN_PIN_SWITCH_4) HIGH ? 1 : 0; count digitalRead(PIN_PIN_SWITCH_5) HIGH ? 1 : 0; return count; } void resetPins() { // 摆瓶舵机正转抬起横杆把球瓶扫回竖立位置 pinResetServo.write(120); delay(800); pinResetServo.write(60); delay(300); } void returnBall() { // 送球舵机把球推回发球区 ballReturnServo.write(100); delay(600); ballReturnServo.write(80); delay(200); } // 显示函数 void displayScore(int frame, int ball, int lastPins, int totalScore) { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_ncenB08_tr); char line1[32]; snprintf(line1, sizeof(line1), Frame:%d Ball:%d, frame, ball); u8g2.drawStr(0, 12, line1); char line2[32]; snprintf(line2, sizeof(line2), Pins:%d, lastPins); u8g2.drawStr(0, 30, line2); char line3[32]; snprintf(line3, sizeof(line3), Score:%d, totalScore); u8g2.drawStr(0, 48, line3); u8g2.sendBuffer(); } // 游戏状态机 enum GameState { WAIT_BALL, WAIT_PIN_AREA, COUNT_PINS, UPDATE_SCORE, RESET_MECH, GAME_OVER }; GameState state WAIT_BALL; int lastFallenPins 0; void setup() { Serial.begin(115200); pinMode(PIN_LAUNCH_SENSOR, INPUT_PULLUP); pinMode(PIN_PIN_SENSOR, INPUT_PULLUP); pinMode(PIN_PIN_SWITCH_1, INPUT_PULLDOWN); pinMode(PIN_PIN_SWITCH_2, INPUT_PULLDOWN); pinMode(PIN_PIN_SWITCH_3, INPUT_PULLDOWN); pinMode(PIN_PIN_SWITCH_4, INPUT_PULLDOWN); pinMode(PIN_PIN_SWITCH_5, INPUT_PULLDOWN); pinResetServo.attach(PIN_SERVO_PIN_RESET); ballReturnServo.attach(PIN_SERVO_PIN_BALL); u8g2.begin(); scorer.reset(); displayScore(1, 1, 0, 0); Serial.println(ESP32 Bowling System Ready); } void loop() { switch (state) { case WAIT_BALL: if (readLaunchSensor()) { // 球已发出等待球到达瓶区 state WAIT_PIN_AREA; Serial.println(Ball launched!); delay(100); } break; case WAIT_PIN_AREA: if (readPinSensor()) { // 球到达瓶区等待球瓶倒下 state COUNT_PINS; Serial.println(Ball reached pin area); delay(200); } break; case COUNT_PINS: // 延时等待球瓶稳定避免瞬间振动误判 delay(300); lastFallenPins readFallenPinCount(); Serial.print(Fallen pins: ); Serial.println(lastFallenPins); state UPDATE_SCORE; break; case UPDATE_SCORE: scorer.addRoll(lastFallenPins); displayScore( scorer.getCurrentFrame(), scorer.getCurrentBall() 1, lastFallenPins, scorer.getTotalScore() ); if (scorer.isGameOver()) { state GAME_OVER; } else { state RESET_MECH; } break; case RESET_MECH: resetPins(); returnBall(); state WAIT_BALL; break; case GAME_OVER: displayScore(10, 1, lastFallenPins, scorer.getTotalScore()); Serial.println(Game Over); delay(2000); // 重新开始 scorer.reset(); state WAIT_BALL; break; } }4.4 程序关键点解释以上代码中有几个地方值得展开说明。第一计分引擎的数据结构。rolls[21]数组用于保存每一次投球的倒瓶数。为什么是 21因为 10 格中前 9 格最多各投 2 次共 18 次第 10 格在 Strike 或 Spare 时最多投 3 次合计最多 21 次。这是一个非常经典的保龄球朴素记录方式。addRoll()方法负责把一次新的投球写入数组同时推进当前格和球权。需要注意第 10 格的特殊处理如果第 10 格第一球 Strike当前格数不再增加但需要继续允许后续两球进入数组。第二状态机的时序控制。COUNT_PINS状态中故意加了 300ms 的延时。一开始我没有加这个延时球撞击球瓶的瞬间振动会导致微动开关反复跳动倒瓶数量读数忽大忽小。加上延时后球瓶稳定后再读取准确率高很多。这里可以采用更严谨的软件消抖不过对于这个项目简单的固定延时已经可以把误判率降到很低的水平。第三传感器电平逻辑必须按模块调整。文中代码注释已经标注红外对射被遮挡时输出为 LOW这个逻辑因模块而异。如果你的模块是遮挡输出 HIGH需要把readLaunchSensor()里的比较逻辑反转。建议正式接线之前先用串口监视器打印传感器的实时电平确认逻辑再编写代码。5. 传感器布局与机械结构设计5.1 球道结构与尺寸桌面级球道建议总长度控制在 80-100cm宽度 20-25cm。球道可以用木板、亚克力板或 3D 打印件拼接。两侧需要加挡板防止球滚出球道。球道分为三个区域发球区长 15-20cm表面可以做一小段粗糙纹理方便球手控制出手力道。滚球区长 50-60cm表面要尽量平滑减少摩擦阻力。瓶区长 15-20cm放置 10 只球瓶。5.2 传感器安装位置红外对射模块 1 安装在发球区末端也就是球离开发球区的位置。球滚过时遮挡红外线触发readLaunchSensor()。红外对射模块 2 安装在瓶区后方。两对对射模块之间的距离需要大于球的直径避免同一颗球同时遮挡两个传感器导致状态机误判。微动开关阵列布置在瓶区底部。每只球瓶底部压在一个小翘板上球瓶倒下后翘板触发开关。这里最理想的方案是每只球瓶对应一个独立的机械触发结构但 10 路开关会占用较多 GPIO。我的方案是只采集 5 路开关通过二极管的组合逻辑扩展为 10 路或者直接用 ADC 分压方案用一路模拟信号分辨 0-10 种组合。如果只是为了练手也可以简化为 5 瓶模式规则不变瓶数改为 5 只。5.3 舵机支架设计摆瓶舵机的关键动作是扫瓶。舵机摇臂上固定一根横杆横杆高度正好位于球瓶重心偏上位置。舵机旋转时横杆从瓶区一侧扫到另一侧把倒下的球瓶全部扶正。球瓶底部可以用磁铁吸在铁片上这样既能保证球瓶被碰倒又能让扫瓶杆轻松把它们复位。送球舵机安装在球道末端侧面。当球滚到球道末端后舵机带动推板把球推向一侧的回收槽槽内带坡度球自动滚回发球区。5.4 电源设计整个系统的功耗集中在两个舵机上。摆瓶舵机扫瓶时瞬时电流大如果 USB 供电很容易掉电重启。建议采用下面的供电思路USB 5V 电源进入 5V/2A 稳压模块 稳压模块输出分成两路 一路给 ESP32 开发板供电Vin 或 5V 引脚 另一路直接给两个舵机供电 所有设备的 GND 连接在一起如果你手头有 7.4V 锂电池也可以先降压到 5V再按上述方式分配。原则只有一条舵机不要直接从 ESP32 的 3.3V 引脚取电。6. 蓝牙扩展手机实时查看分数6.1 为什么选择 BLEESP32 自带蓝牙 4.2支持经典蓝牙和 BLE。对于分数同步这种低频、小数据量的应用BLE 是更好的选择功耗低、连接快、手机端不需要配对流程服务端可以直接扫描读取。BLE 的数据交互方式有两种广播模式ESP32 周期性把分数放在广播包里手机端扫描即可读取不需要连接。GATT 模式ESP32 作为 Server手机作为 Client连接后通过自定义 Characteristic 读取分数。广播模式实现简单断开也不影响数据显示适合展示类场景。GATT 模式更稳定适合需要双向控制的场景。下面以 GATT 模式为例。6.2 BLE 服务设计设计一个自定义服务项目值Service UUID0000FFF0-0000-1000-8000-00805F9B34FBCharacteristic UUID0000FFF1-0000-1000-8000-00805F9B34FB属性Read / Notify数据格式字符串例如F:3 B:1 P:8 S:47数据含义Frame 3Ball 1Pins 8Score 47。6.3 在 Arduino 中集成 BLE首先包含 BLEDevice 库#include BLEDevice.h #include BLEUtils.h #include BLEServer.h #define SERVICE_UUID 0000FFF0-0000-1000-8000-00805F9B34FB #define CHARACTERISTIC_UUID 0000FFF1-0000-1000-8000-00805F9B34FB BLECharacteristic *pCharacteristic NULL; void setupBLE() { BLEDevice::init(ESP32-Bowling); BLEServer *pServer BLEDevice::createServer(); BLEService *pService pServer-createService(SERVICE_UUID); pCharacteristic pService-createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); pService-start(); BLEAdvertising *pAdvertising pServer-getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-start(); } void sendScoreBLE(int frame, int ball, int pins, int score) { if (pCharacteristic NULL) return; char buf[32]; snprintf(buf, sizeof(buf), F:%d B:%d P:%d S:%d, frame, ball, pins, score); pCharacteristic-setValue(buf); pCharacteristic-notify(); }调用时机在UPDATE_SCORE状态中每次分数更新后调用sendScoreBLE(...)即可。手机端可以使用任意通用 BLE 调试 App 连接并接收 Notify 数据。如果需要做成品级小程序可以基于 uni-app 或微信小程序蓝牙 API 开发扫码连接界面。7. 常见问题与排查思路7.1 常见故障一览问题现象常见原因解决思路传感器触发后状态机不跳转传感器电平逻辑反了串口打印实时电平调整LOW/HIGH判断球瓶倒瓶数量不准微动开关抖动加延时或编写软件消抖逻辑舵机抖动或无力供电不足舵机单独 5V 供电注意共地ESP32 自动重启舵机启动电流拉低电压用外部稳压模块避免 USB 直接供电OLED 不显示I2C 地址不对或接线错误用 I2C 扫描代码确认地址为 0x3C 或 0x3DBLE 手机扫描不到设备广播未启动或手机缓存重启开发板清除手机蓝牙缓存分数计算与人工不一致第 10 格逻辑错误用固定投球序列做单元测试逐格验证7.2 传感器电平确认方法如果代码跑起来状态机一直停在某个状态第一步就是在loop()中打印各传感器值void loop() { Serial.print(L:); Serial.print(readLaunchSensor()); Serial.print( P:); Serial.print(readPinSensor()); Serial.print( C:); Serial.println(readFallenPinCount()); delay(100); }用手模拟遮挡传感器观察串口输出是否变化。如果遮挡前后输出完全相同说明接线或者引脚定义有问题。这一步能快速定位 80% 的硬件接线错误。7.3 计分逻辑验证方法保龄球计分逻辑是整段代码最容易出错的地方。建议先在电脑上编写一个独立的小程序用固定序列做验证。比如下面这组数据投球序列(10) (10) (10) (10) (10) (10) (10) (10) (10) (10) (10) (10) 前 9 格全 Strike第 10 格三连 Strike满分为 300。再比如投球序列(8,2) (5,4) (10) (0,0) ...第一格补中第二格 9 分第三格 Strike逐格手算对比代码输出。我的经验是先用最长序列 21 球验证边界再随机生成 1000 组随机投球序列对比人工计算和代码计算结果是否一致。8. 最佳实践与工程建议8.1 代码分层设计在 Arduino 单文件里写完整逻辑没问题但是代码量超过 300 行之后可读性会明显下降。建议切换到 PlatformIO把代码拆分成scoring、display、ble、hardware四个模块。这样做的好处每个模块可以独立测试。比如scoring模块不依赖硬件可以直接在电脑上编写单元测试。后续更换传感器型号时只需要修改hardware模块不影响计分逻辑。代码复用性更强以后做其他运动类计分项目可以直接复用scoring和display。8.2 状态机优于直接顺序执行在 Arduino 开发中很多初学者倾向于在loop()里用delay()串起整个流程等待球发出 - delay - 等待球到 - delay - 检测倒瓶 - delay - 复位这种方式在 demo 中可行但存在明显问题delay()阻塞期间无法响应传感器中断也无法刷新 OLED更无法处理 BLE 请求。一旦某个环节传感器没有正常触发整个程序就会卡死也没有办法通过日志定位问题。状态机的优势是每一轮loop()只处理一个状态每个状态的持续时间很短系统响应能力更强而且通过串口打印可以非常清楚地看到当前走到了哪一步。8.3 机械结构先于代码验证这个项目最大的坑不在代码而在机械结构。我调试过程中遇到的多数问题比如球瓶扫不齐、球卡在回收槽、传感器被球直接撞飞都不是软件问题而是结构设计问题。建议按这个顺序推进先用纸板或洞洞板手工搭建结构验证传感器检测逻辑。确认传感器逻辑稳定后再编写完整计分逻辑。最后优化机械外观实现自动复位机构。整个过程先跑通最小闭环再逐步增加功能避免一次性把所有模块接好后连问题出在哪都找不到。8.4 安全与生产环境建议虽然这是一个桌面 DIY 项目但涉及舵机和电源仍然需要提醒几点舵机连杆机构运动范围较大调试时手不要放在运动路径上防止夹伤。外部电源建议选择带过流保护的稳压模块避免短路时烧毁设备。长时间运行测试时注意舵机温度。如果舵机发烫明显说明扭矩不足或堵转时间过长需要调整结构或更换更大扭矩舵机。项目如果需要带入生产或展示环境建议增加看门狗重启逻辑确保意外情况后系统能自动恢复。9. 总结与下一步方向这套 ESP32 Bowling System 涵盖了嵌入式开发中几个非常典型的场景传感器输入、状态机设计、规则引擎、闭环控制、无线通信。完成这个项目后你会对“如何把一套业务流程转换成嵌入式状态机”有更直观的理解而不只是停留在点灯和读传感器层面。如果接下来想继续深入可以考虑三个方向把计分引擎移植到更通用的平台比如在 PC 上用 Python 复刻同一套十格计分逻辑方便做自动化测试。增加 Web 配网功能让 ESP32 连接家庭 Wi-Fi把分数上报到本地服务器或微信小程序支持多人对战记录排行榜。优化机械结构采用步进电机替代舵机提高摆瓶机构的精度和可重复性。如果你对项目中某个模块有疑问比如 BLE 通信细节、传感器消抖策略或者机械结构设计欢迎在评论区留言交流。需要完整代码或 PCB 原理图的也可以告诉我后续可以根据反馈再整理一篇硬件结构设计篇。收藏本文备用动手搭建时能少走不少弯路。
返回列表