行空板M10配Xbox手柄:半齿轮皮筋旋转发射平台

2026-10-31 16:11:11 7阅读

一、项目简介

这是一个用行空板M10 + Xbox蓝牙手柄控制的旋转发射平台。手柄左摇杆控制平台水平旋转,A键击发。发射器采用激光切割亚克力制作,通过半齿轮 + 齿条 + 皮筋的机械结构实现快速击发,电磁继电器控制电机启停。扩展板板载 WS2812 提供彩灯效果。作品整体外观:底座上立着激光切割亚克力拼装的发射器,半齿轮、齿条与皮筋组成的击发机构清晰可见,平台由水平舵机带动旋转

说明:原设计为两轴平台(水平 + 俯仰),后因俯仰舵机受重力负载持续抖动,且发射器重量对舵机扭矩要求过高,最终去掉了俯仰轴,只保留水平旋转。这一取舍过程也写进了"踩坑记录",供参考。

核心功能:

  • 左摇杆左右 → 平台水平旋转(舵机 P2)
  • A键 → 击发(继电器控制电机)
  • X键 → 开启扩展板板载 WS2812 彩虹灯效
  • B键 → 关闭灯效
  • Y键 → 紧急停止(舵机回中 + 继电器断开)

二、效果演示

演示视频

工作流程:

  1. 手柄左摇杆 → 水平舵机旋转到目标方位
  2. 按 A 键 → 继电器吸合,电机带动半齿轮旋转
  3. 半齿轮转到"无齿段"时,齿条在皮筋拉力下瞬间回弹
  4. 撞杆击打弹仓落下的"子弹",完成击发

三、硬件清单

部件数量说明
行空板 M101主控
行空板扩展板1引出 P2 / P11 / P13 等引脚
Xbox Series X 手柄1蓝牙连接
水平舵机1接 P2,驱动平台旋转
电磁继电器模块1接 P11,控制击发电机
WS2812 灯板1扩展板板载,3 颗,接 P13
直流电机1驱动半齿轮
激光切割亚克力板若干3mm/5mm,发射器结构件
皮筋2~3 根提供回弹力
半齿轮 + 齿条1 套击发核心机构
独立 5V/3A 电源1舵机供电
杜邦线、螺丝、铜柱若干装配

硬件全貌:行空板 M10、扩展板、Xbox 手柄、水平舵机、继电器、直流电机与亚克力结构件摆放齐全

四、机械结构设计

4.1 发射器结构

发射器全部用激光切割亚克力拼接,主要包含:

┌─────────────────────────────────┐
│ 发射器侧视图 │
│ │
│ [弹仓] │
│ │ │
│ ▼ 子弹落入 │
│ ┌─────┐ │
│ │撞杆 │◄── 皮筋拉伸 │
│ └──┬──┘ │
│ │ │
│ ┌──▼───┐ ┌────────┐ │
│ │ 齿条 │◄──►│ 半齿轮 │◄─ 电机 │
│ └──────┘ └────────┘ │
│ │
└─────────────────────────────────┘

工作原理:

  1. 上膛阶段:电机转动,半齿轮的"有齿段"咬合齿条,把齿条向后拉,同时拉伸皮筋储能
  2. 击发阶段:半齿轮继续转到"无齿段",齿条失去咬合约束,在皮筋拉力下瞬间向前回弹
  3. 撞击阶段:齿条带动撞杆高速前冲,击打从弹仓孔落入的"子弹"

这种"半齿轮 + 齿条 + 皮筋"的方案是玩具枪里非常成熟的机构,优点是结构简单、击发力度大、响应快。发射器结构装配细节:弹仓、撞杆、齿条与半齿轮在亚克力板上对位安装,皮筋拉住撞杆储能

4.2 旋转平台结构

  • 舵机固定在底座上,舵盘连接上层平台
  • 发射器固定在平台上,随舵机水平旋转
  • 舵机轴线平行于重力方向,负载只有平台的旋转惯性,没有持续阻力矩

4.3 亚克力设计要点

  • 齿轮与齿条的中心距要精确,否则会卡齿
  • 弹仓孔径略大于子弹直径 0.5~1mm,保证顺利落下但不会漏出
  • 撞杆行程要留足,皮筋预紧力适中:太松打不远,太紧拉不动

底座与旋转平台:舵机固定在底座中央,舵盘连接上层平台,发射器随平台水平转动

五、电路连接

行空板 M10 │ ├── P2 ──→ 水平舵机(信号线) ├── P11 ──→ 继电器 IN ├── P13 ──→ WS2812 灯板 DIN │ ├── 5V ──→ 继电器 VCC(若为5V继电器) ├── GND ──→ 继电器 GND、灯板 GND、舵机 GND │ └── USB ──→ Xbox 手柄蓝牙(板载蓝牙) 外部 5V/3A 电源 │ ├── 舵机 VCC └── GND ──→ 与行空板 GND 共地 ← 关键!

关键提示:

  • 舵机一定要独立供电!行空板 5V 输出带舵机容易电压跌落,导致抖动或复位
  • 电源和行空板必须共地,否则 PWM 信号会漂移
  • 继电器如果是 5V 的,可以从行空板 5V 取电(电流小,约 70mA)

完成接线后的整机:行空板接 P2 舵机、P11 继电器、P13 灯板,外部 5V 电源与主板共地后上电试转通电联调现场:手柄接入后拨动摇杆,平台随之转向,继电器吸合时电机带动半齿轮完成一次击发

六、软件设计

6.1 开发环境

  • 行空板系统:V0.4.5 及以上
  • Python 3.7
  • 依赖库:pygame(手柄)、pinpong(硬件控制)
  • 两个库行空板都预装了,无需额外安装

6.2 核心代码

# -*- coding: utf-8 -*-
"""
行空板M10 + Xbox蓝牙手柄 + 单舵机 + 电磁继电器 + WS2812彩灯
引脚:舵机=P2 继电器=P11 彩灯=P13
""" import sys
import time
import colorsys
import pygame
from pinpong.board import Board, Pin, Servo, NeoPixel # ==================== 配置 ====================
SERVO_H_PIN = Pin.P2
RELAY_PIN = Pin.P11
LED_PIN = Pin.P13
LED_COUNT = 3 H_MIN, H_MAX = 0, 180
H_CENTER = 90 AXIS_H = 0 # 左摇杆水平 DEADZONE = 0.05 LOOP_INTERVAL = 0.05
FIRE_DURATION = 0.5 LED_UPDATE_INTERVAL = 0.05
LED_HUE_STEP = 5 # ==================== 按钮索引(蓝牙Xbox Series X实测) ====================
BTN_A = 0 # 击发
BTN_B = 1 # 关闭彩灯
BTN_X = 3 # 开启彩灯
BTN_Y = 4 # 紧急停止 def apply_deadzone(value, deadzone=DEADZONE): return 0.0 if abs(value) < deadzone else value def map_joystick_to_angle(value, out_min, out_max): normalized = (value + 1.0) / 2.0 return out_min + normalized * (out_max - out_min) def hsv_to_rgb255(h, s, v): r, g, b = colorsys.hsv_to_rgb(h / 360.0, s, v) return int(r * 255), int(g * 255), int(b * 255) def init_hardware(): print("[硬件] 初始化行空板...") Board("UNIHIKER").begin() servo_h = Servo(Pin(SERVO_H_PIN)) relay = Pin(RELAY_PIN, Pin.OUT) relay.write_digital(0) np = NeoPixel(Pin(LED_PIN), LED_COUNT) np.range_color(0, LED_COUNT - 1, 0x000000) servo_h.angle(H_CENTER) print("[硬件] 完成:舵机=P2 继电器=P11 彩灯=P13") return servo_h, relay, np def init_joystick(): print("[手柄] 初始化 pygame...") pygame.init() pygame.joystick.init() for i in range(15): pygame.event.pump() if pygame.joystick.get_count() > 0: js = pygame.joystick.Joystick(0) js.init() print(f"[手柄] 已连接: {js.get_name()}") return js print(f"[手柄] 等待连接... ({i+1}/15)") time.sleep(1) raise RuntimeError("未检测到手柄") def main(): servo_h, relay, np = init_hardware() try: joystick = init_joystick() except RuntimeError as e: print(f"[错误] {e}") sys.exit(1) firing = False fire_start_time = 0 prev_a = prev_b = prev_x = prev_y = False last_angle_h = H_CENTER last_led_update = 0 led_on = False led_hue = 0 print(" 控制就绪:左摇杆→水平 A→击发 X→开灯 B→关灯 Y→急停") try: while True: pygame.event.pump() now = time.time() # 摇杆 → 舵机(axis_h 前加负号可反向) axis_h = apply_deadzone(joystick.get_axis(AXIS_H)) angle_h = int(round(map_joystick_to_angle(-axis_h, H_MIN, H_MAX))) angle_h = max(H_MIN, min(H_MAX, angle_h)) if angle_h != last_angle_h: servo_h.angle(angle_h) last_angle_h = angle_h # 按键 a = joystick.get_button(BTN_A) b = joystick.get_button(BTN_B) x = joystick.get_button(BTN_X) y = joystick.get_button(BTN_Y) if a and not prev_a: relay.write_digital(1) firing = True fire_start_time = now print(">>> 击发!") prev_a = a if x and not prev_x: led_on = True print(">>> 彩灯开") prev_x = x if b and not prev_b: led_on = False np.range_color(0, LED_COUNT - 1, 0x000000) print(">>> 彩灯关") prev_b = b if y and not prev_y: servo_h.angle(H_CENTER) last_angle_h = H_CENTER relay.write_digital(0) firing = False print(">>> 紧急停止") prev_y = y if firing and (now - fire_start_time) >= FIRE_DURATION: relay.write_digital(0) firing = False # 彩灯彩虹效果 if led_on and (now - last_led_update) >= LED_UPDATE_INTERVAL: last_led_update = now for i in range(LED_COUNT): hue = (led_hue + i * 120) % 360 r, g, b_ = hsv_to_rgb255(hue, 1.0, 1.0) np.range_color(i, i, (r << 16) | (g << 8) | b_) led_hue = (led_hue + LED_HUE_STEP) % 360 time.sleep(LOOP_INTERVAL) except KeyboardInterrupt: print("\n[退出]") finally: try: relay.write_digital(0) servo_h.angle(H_CENTER) np.range_color(0, LED_COUNT - 1, 0x000000) except Exception: pass pygame.quit() if __name__ == "__main__": main()

6.3 蓝牙手柄配对配置

Xbox 手柄与行空板通过蓝牙连接。行空板首次使用前需完成一次配对,之后手柄开机后会自动回连,无需重复操作。

6.3.1 手柄进入配对模式

按住 Xbox 手柄顶部的配对按钮(手柄正面 logo 旁的小圆形按钮),直到 logo 指示灯快速闪烁,表示已进入配对模式。

6.3.2 行空板终端执行配对

通过 SSH 或行空板桌面上的终端应用,依次执行以下命令:

# 进入蓝牙控制工具
bluetoothctl # 打开蓝牙适配器
power on # 开始扫描设备
scan on

等待终端出现手柄的 MAC 地址,例如:

[NEW] Device 40:8E:2C:22:84:31 Xbox Wireless Controller

记下这个 MAC 地址,然后:

# 停止扫描
scan off # 配对(替换为实际 MAC)
pair 40:8E:2C:22:84:31 # 信任设备 —— 关键,否则重启后要重新配对
trust 40:8E:2C:22:84:31 # 连接
connect 40:8E:2C:22:84:31 # 退出
exit

6.3.3 验证连接

# 应输出 Connected: yes 和 Trusted: yes
bluetoothctl info 40:8E:2C:22:84:31 | grep -E "Connected|Trusted|Paired" # 应出现 /dev/input/js0 设备节点
ls /dev/input/js*

还可以用jstest工具实时查看手柄输入:

jstest /dev/input/js0

拨动摇杆、按 A/B/X/Y,如果终端数值正常响应,说明配对完全成功。

七、踩坑记录

这部分是项目最耗时的部分,记录下所有踩过的坑,供大家参考。

坑1:Xbox 手柄蓝牙连接无限"连接-断开"循环

现象:

[CHG] Device 40:8E:2C:22:84:31 Connected: yes
[CHG] Device 40:8E:2C:22:84:31 Connected: no
(重复无限循环)

原因:行空板原固件的蓝牙协议栈(BlueZ)与 Xbox 手柄存在L2CAP 配置兼容问题,ERTM 模式下握手失败。

尝试过的方案:

  • 禁用 ERTM:echo 'options bluetooth disable_ertm=1' | sudo tee /etc/modprobe.d/xbox_bt.conf
  • 修改main.conf的 LE 参数
  • 清理配对缓存重新配对

最终解决:重新烧录行空板固件到 V0.4.5,问题彻底消失。这是最省事的方案。

坑2:Xbox 手柄按钮索引与标准映射不一致

现象:程序里get_button(2)应该是 X 键,但按 X 却触发了 Y 键的功能。

原因:Xbox Series X 手柄通过蓝牙连接时,按钮索引与 USB 连接不同。

解决方案:写一个检测脚本,把 A/B/X/Y 依次按一遍,看打印的索引:

import pygame, time
pygame.init(); pygame.joystick.init()
js = pygame.joystick.Joystick(0); js.init()
prev = [False] * js.get_numbuttons()
while True: pygame.event.pump() for i in range(js.get_numbuttons()): p = js.get_button(i) if p and not prev[i]: print(f"按钮 [{i}] 被按下") prev[i] = p time.sleep(0.02)

我的实测结果:

A=0, B=1, X=3, Y=4

注意索引 2 被跳过(可能是 View 键的位置)。

坑3:P7 不能用作舵机引脚

现象:Servo(Pin(Pin.P7))报错

ValueError: 7 is not in list

原因:行空板 M10 支持的舵机引脚是P0、P2、P3、P8、P9、P10、P16、P21、P22、P23,P7 不在其中。

解决:改用 P2。做项目前一定先查引脚表,避免走弯路。

坑4:pinpong 的 NeoPixel.write() 不能无参调用

现象:

np[i] = (r, g, b)
np.write() # TypeError: write() missing 2 required positional arguments

原因:pinpong 库中NeoPixel的write()方法与常见的 Adafruit 库不同,需要指定索引和值。

解决:改用range_color()方法:

# 清除所有灯
np.range_color(0, LED_COUNT - 1, 0x000000) # 设置单颗灯
np.range_color(i, i, (r << 16) | (g << 8) | b)

坑5:舵机到达目标后左右抖动

现象:摇杆停稳后,舵机到达目标角度,但会轻微左右抖动,然后停止。

排查过程:

第一轮尝试(加了算法反而更糟):

  • 摇杆死区 0.20
  • 滑动平均滤波窗口 5
  • 角度变化阈值 1.5°
  • 平滑逼近步长 4°

结果:舵机跟随变钝,抖动没有根本改善。

第二轮(回归最简):

  • 去掉所有滤波和平滑
  • 只保留if angle != last_angle才下发指令
  • 死区从 0.20 降到 0.05

结果:响应快、几乎无抖动。

根因分析:

  • 原算法里的"平滑逼近"会让舵机持续接收小幅变化指令,每次都可能触发舵机内部死区修正,产生抖动
  • 最简版只在角度整数变化时才发指令,舵机到位后完全静默,内部电机不再被反复触发

结论:过度滤波反而可能引入抖动。先用最简版本验证硬件,再按需加算法。

坑6:为什么最终去掉了俯仰轴

初始设计:两轴平台(水平旋转 + 上下俯仰),两个舵机。

问题:水平舵机工作正常,俯仰舵机持续抖动。

根因:

  • 水平舵机旋转轴平行重力,平台旋转时重力不产生阻力矩
  • 俯仰舵机旋转轴垂直重力,发射器重力形成持续的阻力矩
  • 舵机为了保持位置,内部电机必须持续通电抵抗重力 → 反复微调 → 抖动

尝试过的缓解方案:

  • 软件层加 2° 阈值,减少无效指令:抖动减轻但不根治
  • 加大死区、滤波:同上
  • 拆下发射器空载测试:不抖 → 确认为重力负载问题

最终取舍:

  • 配重方案需要重新设计发射器重心,工作量大
  • 换大扭矩舵机(MG996R、DS3218)成本较高,且需重新适配安装孔位
  • 本项目对俯仰精度的需求不高,去掉俯仰轴后,整机结构更简洁、可靠性更高

经验总结:舵机控制旋转轴平行重力时很稳定,垂直重力时需谨慎评估负载。如果非要保留俯仰轴,建议:

  1. 发射器配重,让重心靠近旋转轴
  2. 换大扭矩金属齿轮舵机
  3. 独立供电 + 滤波电容

八、装配与调试要点

8.1 上电顺序

  1. 先接好所有线,确认 GND 共地
  2. 外部 5V 电源先开
  3. 再开行空板
  4. 最后开 Xbox 手柄(会自动回连)

8.2 舵机中立位校准

首次运行前,手动把平台转到物理中位,再运行程序让H_CENTER = 90与之匹配。如果不一致,调整H_CENTER的值。

8.3 击发时长的调整

FIRE_DURATION = 0.5 # 单位:秒
  • 太短:半齿轮没转到位,无法完成一次完整上膛击发
  • 太长:电机多转,齿轮可能重复咬合,造成卡顿

建议实测:把发射器空转,观察半齿轮转一圈需要多久,然后把FIRE_DURATION设成略大于一圈的时间。

8.4 皮筋张力的调整

  • 皮筋太松:子弹打不远
  • 皮筋太紧:电机负载大,回弹噪音大

建议左右各用 1 根普通橡皮筋(直径约 1.5mm),张力适中。

8.5 舵机旋转方向

如果摇杆左推舵机往右转,把映射改成:

angle_h = int(round(map_joystick_to_angle(-axis_h, H_MIN, H_MAX)))

加一个负号即可反向。

九、改进方向

这个项目还有不少可以优化的地方,欢迎大家一起讨论:

  1. 恢复俯仰轴:加配重或换大扭矩舵机,实现真正的两轴控制
  2. 加入扳机模拟:使用 RT 扳机(轴 5)替代 A 键,更接近真实射击手感
  3. 弹仓扩容:改为弹链供弹或重力式弹匣
  4. 命中反馈:加一个红外或超声波传感器,射击后自动检测命中

十、写在最后

这个项目从"想做一个能用手柄遥控的发射平台"的简单想法开始,踩了蓝牙、按钮、引脚、舵机抖动一圈坑,最后回归到"最简单的控制逻辑往往最稳定"这个朴素的结论。去掉俯仰轴也是这个思路的延续——与其和重力负载死磕,不如做减法,让项目真正能跑起来。

特别感谢开源社区与行空板官方文档的公开分享。结构图纸随项目一并公开,欢迎复刻和改进。


作者:老墨
链接:https://www.aiddithome.com/p/10f9ddb5ef6b82.html
本作遵循 [CC BY 4.0 署名] 许可,版权归作者所有。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
4条评论
阿伟不emo
1楼 · 55分钟前

半齿轮、齿条加皮筋这套击发机构真是老玩具枪的精髓,结构简单又打得动,比堆电机强多了。

板砖搬不动
2楼 · 55分钟前

坑5那段太真实了:加了一堆滤波和平滑反而更抖,最后砍掉只留角度变化才发指令,舵机到位就静默,这个回归最简的思路值得记在本子上。

调参到凌晨
3楼 · 55分钟前

俯仰轴和重力死磕那段讲得明白,舵机轴平行重力几乎没有阻力矩,垂直重力就得一直通电硬扛,抖是必然的。换成大扭矩舵机成本上去还不一定根治,做减法是对的,不过以后要真恢复两轴,配重把重心挪到转轴附近可能是更省钱的走法。

刷视频进来围观
4楼 · 55分钟前

蓝牙手柄那个无限连断循环最后靠重刷固件解决,遇到同类问题的同学可以少绕几圈。