# CCtrl：从空间鼠标到六轴机械臂控制器 · Owlcarp

CCtrl：从空间鼠标到六轴机械臂控制器，记录两代自定义控制器的机械设计、PCB、嵌入式固件与机器人控制实践。

作者：Owlcarp

原文：https://owlcarp.me/cctrl/

CCtrl

# 从空间鼠标到六轴机械臂控制器

2025.12—2026.夏｜RoboMaster 2026｜机械设计、PCB、嵌入式与机器人控制

[![CCtrl 控制器透明渲染图](https://owlcarp.me/articles/cctrl/v1-cover.webp)

CCtrl

四连杆与 IMU，描述手在空间中的位置和姿态。

GVD20/CCtrl查看仓库](https://github.com/GVD20/CCtrl)[![CCtrl V2 控制器透明渲染图](https://owlcarp.me/articles/cctrl/v2-cover.webp)

CCtrl V2

六轴构型与磁编码器，让关节角直接对应机械臂。

GVD20/CCtrl-V2查看仓库](https://github.com/GVD20/CCtrl-V2)

在 RoboMaster 比赛中，工程机器人需要完成夹取、搬运和装配等操作。机械臂有六个自由度，而操作手面对的通常只有屏幕和一套输入设备。怎样让人的动作既自然地表达出来，又能可靠地传到机器人的关节上，是一个看似简单、做起来却涉及很多层的问题。

CCtrl 就是我在悍匠战队期间为此制作的自定义控制器。第一代确实试图直接描述手在空间中的位置和姿态，第二代则更接近一台可以徒手操作的微型机械臂。因此，虽然都叫 CCtrl，它们实际采用了两套很不一样的控制思路。

项目从 2025 年底开始，经历了 2026 年春夏的分区赛与全国赛，前后做出了两套构型差别很大的硬件。机械结构、元器件选型、原理图、PCB、装配、嵌入式固件和控制链路由我独立负责，真正接到工程机器人上使用时，则需要与战队的机械臂和操作系统联调。规模算不上大，但这是我在大学期间少数从一张草图一直做到比赛操作间的完整项目。

这篇文章想记录的也正是这段过程。第一代为什么选择了更通用的空间输入？它在哪里表现得很好，又为什么在真正的机械臂面前遇到困难？以及，第二代为什么最终选择了一条看起来没有那么“优雅”、却更适合当时比赛的路线。

![CCtrl 第一代的最终 CAD 装配](https://owlcarp.me/images/responsive/articles/cctrl/02-480.webp)

**CCtrl V1：四连杆、旋转轴、末端手柄与底座主机。**

## 一个四连杆的想法

最初我想做一只六自由度的“3D 鼠标”。普通鼠标把手在平面上的移动转成二维坐标；如果能直接测出手在三维空间中的位置，再测出它的朝向，就可以把抓取、移动、旋转这些动作转换为六自由度指令。使用者不需要逐个指定机械臂的关节角，控制器也不必和某一种机械臂绑定。

构型参考了 Minverse 一类空间鼠标的思路。我画过一张有些潦草的机构草图：以四连杆承担平面内的伸展和升降，再增加一个空间转动轴，使末端的手柄能在有限工作范围内定位。四根连杆主要用于形成机构约束，实际决定位置的独立角度只有三个。

![早期四连杆机构草图](https://owlcarp.me/images/responsive/articles/cctrl/01-480.webp)

**最早保存下来的构型草图。旁边标出了末端运动范围和转轴方向。**

将这个想法变成 CAD 以后，结构很快变得具体：哪里放轴承、哪里安装磁铁和编码器、连杆转到边界时手柄会不会撞到底座，以及怎样让人真正握住它。后来加上的滚轮、摇杆、微动开关与底座快捷键，也让它逐渐有了一台完整输入设备的样子。

这张早期实物照片是它还在实验桌上联调时的样子：机构与线束还是原型阶段的状态。不过它和后来出现在赛场上的那台设备已经能看出是同一个想法。

![V1 早期控制器的实验桌测试](https://owlcarp.me/images/responsive/articles/cctrl/03-480.webp)

**V1 早期实测：连杆机构、临时线束与笔记本上的三维坐标仿真。**

末端手柄并不只有一块电路板：主板之外，我还把滚轮和摇杆分别放在两块副板上。CAD 里这些部件排列得很整齐，外壳与电路的相互制约则要到实际装配时才更明显。

![V1 手柄主板与滚轮、摇杆副板](https://owlcarp.me/images/responsive/articles/cctrl/07-480.webp)

**手柄内部装配：主板与滚轮、摇杆副板的位置关系。**

第一代的手柄最初用了全包裹外壳，外形完整，却把三块 PCB、滚轮机构和线束都关在了狭小的内部空间里。

![V1 早期全包裹手柄 CAD](https://owlcarp.me/images/responsive/articles/cctrl/08-480.webp)

**早期封闭式手柄外壳：顶部滚轮、侧盖与连杆连接处均集成在内。**

真正开始装配时，整理线缆和返工都很不方便，手柄本身也偏重。后来我把壳体改为蜂窝镂空结构，让内部零件可以从侧面接近。这个版本少了一些“消费电子产品”的完整感，实际装配与维护却舒服得多。

![第一代后期镂空手柄](https://owlcarp.me/images/responsive/articles/cctrl/09-480.webp)

**后期改用镂空外壳，滚轮、手柄 PCB 和线束区域更容易接近。**

手柄重量在这样一台装置上尤其敏感。它位于连杆末端，质量增加不仅使握持更累，也放大了机构转动时的惯性和受力。这是我第一次比较直观地意识到：机械建模时看起来只是一个漂亮的外壳，到了操作手手里就变成了持续存在的负担。

## 从三个角度到一组空间坐标

CCtrl V1 的位置测量使用三颗 AS5600 磁编码器。每个转轴附近固定一块自己设计的圆形 PCB，传感器对着随轴转动的径向磁铁；无需机械接触，便能读出一圈 4096 个刻度的绝对角度。理论上每格约为 0.088°，不过控制器最终的空间精度还会受到磁铁偏心、关节间隙和机构装配误差的影响。

![AS5600 编码器节点 PCB 与装配](https://owlcarp.me/images/responsive/articles/cctrl/04-480.webp)

**自制编码器节点的 PCB、走线和装配实物。转轴背面布置磁铁，传感器读取角位置。**

主控拿到三个经过零位和方向标定的角度后，可以直接通过机构几何计算出手柄参考点的位置。固件中的有效杆长是两段各 100 mm，另外还有 42.5 mm 的径向偏置。如果把角度记为 θ₀、θ₁、θ₂，计算可以简化成：

```
r = 42.5 + 100 cos(θ₁) + 100 cos(θ₁ + θ₂)
x = r cos(θ₀)
y = 100 sin(θ₁) + 100 sin(θ₁ + θ₂)
z = -r sin(θ₀)
```

这里的长度单位是毫米。θ₁ 与 θ₂ 决定连杆在一个平面内如何伸展，θ₀ 再把这个径向位置转到空间中。这个模型很轻量：ESP32 不必解一遍通用机械臂的逆运动学，几次三角函数就足以把编码器读数变成三维坐标。

手柄姿态则交给 ICM-20948 惯性测量单元。它能够提供加速度、角速度和磁场数据；实际主路径在 ESP32-S3 上使用 Mahony 四元数滤波，对陀螺仪与加速度计做六轴融合，再输出姿态。磁场数据虽然在链路中保留，当时的实际融合配置没有启用磁力计修正。因此，倾斜方向能够借助重力约束稳定下来，航向仍可能随陀螺仪积分产生漂移。

![手柄节点 PCB 走线与三维渲染](https://owlcarp.me/images/responsive/articles/cctrl/05-480.webp)

**手柄节点：IMU、微控制器、按键与连接器集中在狭长的板形里。**

![手工焊接后的 ICM-20948](https://owlcarp.me/images/responsive/articles/cctrl/06-480.webp)

**ICM-20948 焊接实物。小封装元件也是第一代硬件制作中比较考验手工的一部分。**

把位置与姿态分开测量有个直接的好处：我不需要对加速度做两次积分来求位移。惯性积分获得的位置很容易随时间漂移；有机械约束和绝对编码器，手柄在哪里主要由真实机构决定，IMU 只需负责它朝向哪里。

但这个方案也意味着，**控制器测得的是人的末端位姿，而机器人必须再想办法实现这个位姿**。这两件事之间的距离，后来成了第一代最重要的经验。

## 四根线串起整台控制器

如果只看传感器数量，CCtrl 的电子架构似乎很简单：三个编码器，一个 IMU，再加几颗按键和摇杆。真正布线时问题就出现了。AS5600 使用 I²C，三个节点地址相同；手柄上还有独立的 IMU、滚轮和开关。若把所有信号线都从末端拉回主控，跨越多个转轴的线束会越来越臃肿，还可能限制旋转范围。

我的办法是给每个节点都放一颗小型 MCU，让它先在局部读取自己的传感器，再用 UART 把数据沿着机构一段段送下去。第一代节点选择了 ATmega328P-AU，主要是考虑到 Arduino 与 PlatformIO 开发生态比较成熟。当时也清楚它的成本还有优化空间，只是第一版更需要尽快把整个系统跑通。

节点之间只需要四根线：3.3 V、GND、串口数据线以及返回路径。主机发出一个包后，包会逐级经过各个节点，最后沿返回链路回到主控。这是一条由 MCU 主动转发的菊花链，而不是简单把四个器件并在一条 I²C 总线上。

![三编码器与手柄节点的菊花链测试](https://owlcarp.me/images/responsive/articles/cctrl/10-480.webp)

**工作台上的链路测试：三个圆形编码器节点和一块手柄节点，通过四线连接。**

上电后的第一件事是枚举。每块板运行同一份节点固件，先判断接上的是 AS5600 还是 ICM-20948，再按照物理连接顺序领取自己的 ID。主机因而能知道链上有多少块板、每块板是什么类型，而不需要为每个节点事先写死地址。之后轮询数据时，各节点在经过的包中追加自己的记录，并重新计算 CRC。

第一代通信速率为 250 kbit/s，三颗编码器加一块手柄最终形成 46 字节的回包。节点上有接收缓冲、转发队列和看门狗；链路超时还会报告断联来源，主控可以重新发起枚举。这些机制未必会出现在外观照片里，却决定了一根松动的线到底是让整台控制器毫无反应，还是留下足以定位故障的信息。

底座的主控采用 ESP32-S3。我为它设计了包括电源、OLED、机械按键、蜂鸣器、状态灯和 RS232 的 PCB；既能通过 USB 供电，也考虑了比赛使用的 6S 电池供电与串口连接。两个核心上的任务分别处理显示交互和业务链路，使菜单动画不会直接阻塞传感器轮询。

![ESP32-S3 主控 PCB 布局](https://owlcarp.me/images/responsive/articles/cctrl/11-480.webp)

**两代沿用的 ESP32-S3 主板。左侧是 PCB Layout，右侧是三维装配渲染。**

主控约以 45 Hz 轮询节点，V1 的对外业务输出则约为 29 Hz。它沿用 RoboMaster `0x0302` 命令帧，30 字节载荷中用半精度浮点数装入三维位置和姿态，另外保存按键、摇杆、滚轮、编码器角度及当前输出模式。这样电脑端拿到的已经是可解释的空间输入，而不是一堆原始传感器寄存器。

我也为它做了一个 OLED 菜单。界面基于 WouoUI，磁贴图标用图像生成工具制作后再转为二值图；蜂鸣器甚至可以按音符表播放一段 MIDI 启动旋律。这些东西不会提高机械臂精度，但对一台真正要被人拿起来操作、在赛场上排查状态的设备来说，能看到模式、能听到反馈，比接上串口再猜发生了什么好得多。

[CCtrl V1 主机上电](https://owlcarp.me/articles/cctrl/12.mp4)

播放

**主控上电，OLED 与状态灯亮起，蜂鸣器演奏启动旋律。**

[WouoUI 动画菜单](https://owlcarp.me/articles/cctrl/18.mp4)

播放

**主控 OLED 上的二值磁贴菜单与切换动画。**

## 当虚拟方块遇上真正的机械臂

第一代最令人开心的阶段，是看到那些独立的模块终于连成了一条链。手握住控制器、移动连杆，电脑上的三维坐标跟着变化；转动手柄，虚拟对象的朝向也能随之改变。早期实验桌上还摆着尚未整理完的线束，屏幕里却已经有了一台能被手直接操纵的空间设备。

[第一代末端位置正解的电脑仿真](https://owlcarp.me/articles/cctrl/13.mp4?v=bt2390-balanced-v2)

播放

**实物控制器与上位机三维仿真联动，验证编码器正运动学输出。**

我给手柄上的一个微动开关设计了类似“离合键”的行为：按下时，把当前位置和姿态记为参考；之后输出相对于按下瞬间的位移与旋转；松开后，相对量回零，再按下便重新开始一段操作。姿态差分使用四元数相乘求得，而不是简单地把三个欧拉角各自相减。

这样一来，机构即使只有有限的运动范围，也能像鼠标抬起再落下一样分段操作。事实上，CCtrl V1 用来移动虚拟物体已经很自然。我还做过一个简单的抓取演示：把散落的彩色方块移动到屏幕里的篮筐中。它把“人在空间里做什么，电脑里就发生什么”的初衷实现得相当直观。

[第一代移动虚拟方块的演示](https://owlcarp.me/articles/cctrl/15.mp4)

播放

**用 CCtrl 抓取虚拟方块并放入篮筐。第一代很适合这种直接操作三维物体的场景。**

难题出现在接入六轴机械臂之后。虚拟方块可以毫无顾虑地移动和旋转，真实机械臂却有各关节的运动范围、速度限制和奇异构型。手柄要求的末端位置虽然在可达空间内，机械臂也未必能沿着这条路径平滑走过去。

我试过把位姿增量通过 ROS 送入机械臂控制链路。开发资料中保留了两个阶段：早期逐帧计算目标位姿，再调用逆运动学；后来改为把相邻两帧的位置与四元数变化转换成速度指令，让 MoveIt Servo 根据机械臂的雅可比矩阵求关节运动。

后者可以把控制写成一个很简洁的关系：末端速度 **v** 经过雅可比伪逆 **J⁺**，得到关节速度 **q̇ ≈ J⁺v**。但当某些构型使雅可比接近奇异时，同样一点末端运动可能对应很大的关节速度。Servo 可以在接近奇异位置时减速或停机；这有助于保证约束，却也会让操作手感到某些方向突然“推不动”。

[V1 经 MoveIt Servo 驱动六轴机械臂模型](https://owlcarp.me/articles/cctrl/14.mp4)

播放

**通过 MoveIt Servo 控制 RViz 中的六轴机械臂。基础运动能够完成，连续操作的边界问题也在这一阶段浮现。**

技术上还能继续优化：调整目标姿态的权重、限制运动增量、改进奇异附近的行为，或换一套适合操作任务的交互策略。我的桥接程序也做过这类处理，比如在出现特定 Servo 警告时进一步削弱角速度，让平移操作更容易保留下来。但它们只能改善一部分症状。对于当时需要快速完成特定比赛任务的操作手，这套“在空间中移动手柄，再由软件猜测机械臂该怎么走”的链路，学习与调试成本依然偏高。

2026 年 6 月，完成粉白配色、镂空手柄和底座固定结构的 CCtrl V1 参加了南部赛区比赛。它确实进入了正式操作间，用于控制工程机械臂夹取能量单元。看着自己从年底开始画的草图真正出现在比赛画面里，当然很有成就感；与此同时，比赛也给了我一个比实验室演示更清楚的判断：作为通用空间输入设备，第一代已经证明了自己的方向；作为当时那台工程机械臂的主要比赛控制器，它还不够顺手。

![CCtrl V1 区域赛前的完成装配](https://owlcarp.me/images/responsive/articles/cctrl/16-480.webp)

**南部赛区出发前一晚的最终装配，粉白配色和桌面固定结构已经完成。**

主控板的安装也采用了比较直接的办法：透明亚克力板配合 3D 打印支架完成固定，侧面为连接接口留出空间。这种装配方式方便观察板上的状态，也方便临时检修。

![V1 主机亚克力板与打印支架](https://owlcarp.me/images/responsive/articles/cctrl/17-480.webp)

**主机安装细节：透明亚克力固定板、打印支架、OLED 与机械按键。**

![CCtrl V1 在南部赛区比赛现场](https://owlcarp.me/images/responsive/articles/cctrl/19-480.webp)

**2026 RoboMaster 南部赛区的实际操作画面。图片来源：RoboMaster 2026 赛季挑战赛南部赛区图片直播。**

## 第二代：把控制问题还给关节

分区赛之后，我重新考虑了最初的构型假设。要控制一台六轴机械臂，是否一定要先把人的操作转换为笛卡尔位姿，再交给另一套复杂的逆运动学？如果目标明确，而且比赛更看重操作的可预测性，直接让控制器的六个关节与机器人六个关节建立对应关系，可能更务实。

于是 CCtrl V2 转向了类同构控制器：控制器本身也是一台六关节的被动机构，六颗编码器测量对应转角，接收端再根据机械臂的零位、方向和关节定义完成映射。这样可以把原先必须实时求解的整臂末端位姿问题，转化成更直接的关节输入问题。

不过，同构不等于照着机械臂缩小一遍。真正的机械臂可以让连杆、腕部和工件占据自己的工作空间，人手却需要在其中握住一个控制器。V2 的 J2、J3 和 J4 因此做了侧向偏移，腾出操作手伸入的空间，再通过末端三轴腕部补回需要的轴系关系。我保留了两段等长连杆的设计，也把握持姿态、关节受力和线束路径一起放进 CAD 中考虑。

外观也换成了橙黑配色，部分连杆使用碳纤维管和碳纤维板来提高比强度。手柄参考了大疆穿越摇杆一类垂直握持设备的布局，上方留给摇杆，手指自然覆盖按键与扳机。这一代的目标更明确了：它首先是一件要被人连续握持、操作的工具，然后才是一台漂亮的桌面机构。

![第二代侧偏连杆和三轴腕部构型](https://owlcarp.me/images/responsive/articles/cctrl/20-480.webp)

**CCtrl V2 的六轴构型。可以看到主体连杆偏在一侧，手柄被留在更容易操作的位置。**

![第二代手柄 CAD](https://owlcarp.me/images/responsive/articles/cctrl/26-480.webp)

**V2 的竖向握柄、摇杆区域和末端腕部。**

第一代的重量问题也促使我在第二代尽量少使用主动驱动。V2 的关节主要依赖普通轴承和扭簧实现回中或部分重力补偿，而没有把电机力反馈作为这次交付的必要条件。对于操作手来说，扭簧提供的被动力矩已经能在一些姿态下减轻持握负担；对于开发来说，它没有电机驱动器、控制回路和额外布线要调试。

![同轴扭簧和回中结构](https://owlcarp.me/images/responsive/articles/cctrl/24-480.webp)

**同轴扭簧及其受力结构。纯机械的补偿把一部分操作手感问题提前解决在机构里。**

[扭簧回中效果](https://owlcarp.me/articles/cctrl/25.mp4?v=bt2390-balanced-v2)

播放

**手动移动第二代连杆，观察扭簧补偿和回中的效果。**

这当然牺牲了主动反馈的可能性：控制器无法由电机主动施加来自机械臂的接触力，也不能根据软件策略随意改变关节力矩。但在那段有限的开发时间里，我更愿意先得到一台轻、稳定、容易维护、操作手敢用的控制器。

## 没有装进最终版本的滑环

旋转机构总绕不开线缆。V1 已经让我吃到了线束干涉的苦头，所以在 V2 初期，我认真尝试过自己做 PCB 导电滑环：将 AS5600 编码器和同心导电环合在一块板上，再用另一块带弹片电刷的 PCB 接触这些环形铜箔。理想状态下，转轴负责转动，信号和电源从滑动触点穿过，不再被线缆拧住。

![自制滑环与编码器节点实物](https://owlcarp.me/images/responsive/articles/cctrl/22-480.webp)

**编码器与同心导电环一体化的 PCB，以及安装在粉色关节座里的电刷弹片。**

![导电滑环和电刷 PCB Layout](https://owlcarp.me/images/responsive/articles/cctrl/23-480.webp)

**两块圆形 PCB 的布局：一侧是环形触点，另一侧是电刷及其连接电路。**

我实际测试过导通，沉金表面配合润滑脂的接触表现也符合预期。但做得出导电结构，与能把它可靠地装进高速反复转动、承受侧向力的机构里，差得很远。电刷接触压力、同轴度、安装空间和中心过线都要同时兼顾；试装时甚至出现过滑环中孔磨损线缆的情况。

最后我没有把这套方案用在比赛最终装配上。不是因为电路原理不可行，而是机械集成和维护难度已经超出了它当时能带来的收益。与其带着一个漂亮却需要小心伺候的滑环上场，不如保留传统线束，并通过走线和结构设计控制风险。

同样的取舍也发生在一些更小的部件上。做手柄扳机时，我不小心弄丢了原本要装的扭簧，索性画了一块带卷曲弹性梁的一体式 3D 打印零件，让弹性变形本身承担回弹。实际按起来效果很好，少了一个需要单独装配的小零件。

![一体打印弹性扳机 CAD](https://owlcarp.me/images/responsive/articles/cctrl/28-480.webp)

**丢失小扭簧后设计的替代件：把弹性结构与扳机部件做成一体。**

[弹性扳机回弹演示](https://owlcarp.me/articles/cctrl/29.mp4?v=bt2390-balanced-v2)

播放

**打印弹性结构的按压与回弹测试。**

这些调整看起来很琐碎，却有一种很具体的工程逻辑：一个组件的价值，要放进整套系统的装配时间、故障风险和操作体验中计算。有时增加一块 PCB 会让系统更整洁；有时删掉一块 PCB，反而是更成熟的决定。

## 六个编码器，以及比关节角更难的小问题

V2 沿用了 ESP32-S3 主板和四线菊花链的基本思想，采样部分则扩展为六个 AS5600 编码器节点和一个手柄节点。为了降低节点成本，我把新节点的主控换成了 CH32V006；第一代的 AVR 固件也保留为另一种兼容构建。对同类 CH32 节点而言，固件会检测本地连接的是 AS5600 还是 TMAG5273，再决定自己作为编码器还是手柄运行。

其中的手柄节点还留下了一张焊接时的照片。PCB 平放在小型加热台上，传感器和其他贴片元件正逐渐从设计文件变成实物。

![V2 七块节点板](https://owlcarp.me/images/responsive/articles/cctrl/30-480.webp)

**第二代全部七块传感器节点：六块圆形编码器板、一块带摇杆的手柄板。**

![V2 手柄节点 PCB 加热台焊接](https://owlcarp.me/images/responsive/articles/cctrl/27-480.webp)

**V2 手柄节点在小型加热台上焊接的过程。**

这次的协议也比第一代严谨一些。枚举记录里加入节点类型、平台和能力信息，主机将“物理链路里的节点 ID”和“机械臂的 A1—A6 轴号”分开处理，避免手柄节点插在不同位置就打乱轴号。轮询帧使用版本号、序列号和 CRC16；典型的六编码器加手柄回帧为 68 字节。在 250 kbit/s 的串联转发链上，所有逐段发送的纯线路时间约为 11.4 ms，主控则以约 45 Hz 发起轮询。对外 RS232 输出约为 30 Hz。

六个 AS5600 的读数首先要经过各自的零位、方向标定，再从一圈 0—4095 的原始值转换成有符号角度计数。主控把整组标定保存到非易失存储中，写入时带版本及 CRC 校验。磁场过强、过弱、I²C 失联和读数过期，也分别保留了诊断状态。比赛时需要的不只是“收到一个角度”，还要知道这个角度值是否值得相信。

手柄输入反而是软件细节最多的部分之一。我把摇杆改成了更紧凑的 Joy-Con 规格，并增加了一只线性扳机。扳机没有用传统电位器，而是让一颗 TMAG5273 三轴磁传感器读取随扳机转动的磁铁。直觉上可以拿某一个磁场轴的值直接换算行程；实际测试却发现，靠近全按下位置时，部分轴的数据已经趋于平缓，继续按下的变化主要出现在另外的轴上。

所以最后的扳机模型使用三维磁场轨迹。标定时记录多次按压与释放的数据，把行程整理成 17 个 XYZ 标定点。运行时将新的磁场向量投影到相邻点构成的 16 条线段上，选择距离最近的位置，映射成 0—4095 的扳机行程。实现时使用定点整数运算，整张标定表只需要 102 字节的原始坐标数据。

```
磁场读数 M = (Mx, My, Mz)
↓
寻找 17 个标定点所形成的最近线段
↓
计算该线段上的投影比例 t
↓
行程 = (线段编号 + t) / 16
```

这个方法的好处是，不用先假设磁场在某一轴上一定线性，也不要求传感器和磁铁的机械关系恰好符合某个简单公式。它接受实际装配形成的曲线，再用很小的计算量反推操作行程。归档采样和标定工具保存了这段开发记录。

后续排查中，我还发现过一种很隐蔽的零值问题：一段 1350 帧的记录里，扳机输出短暂归零了 19 帧，但这些帧的串口 CRC 和序列号并没有异常。问题出在某些时刻没有有效的手柄采样时，主控把业务值清零了。这等于把“暂时没有更新”误写成“操作者已经松手”。修复后，缺少有效采样时保留上一次可信输入，另用有效位和诊断状态明确告诉下游发生了什么。

我很喜欢这个案例，因为它提醒了我一个在嵌入式系统里容易忽略的区别：**数值为零、传感器没有新数据、传感器发生故障，是三件不同的事。** 对于一个控制器，误把后两者当成第一种，最终就会变成机械臂上一下毫无缘由的动作。

为了能在现场看清这些数据，V2 增加了一条手动开启的 USB 调试链路。网页可以通过 Web Serial 读取原始角度、校准结果、腕部转换和诊断状态，也能提交校准配置；比赛业务通信继续独立走 RS232。两代虽然沿用同一类 RoboMaster 外层帧，但 V2 的 30 字节业务载荷改成了六轴计数、摇杆与扳机输入，和 V1 的空间位姿载荷并不通用。

## 一组角度，为什么需要这么多处理

V2 不再求整臂的末端逆运动学，腕部仍留下了一个值得认真处理的问题。目标机械臂的腕部使用 ZXZ 轴系；我为手柄选择的是更适合握持布局的 XZY 轴系。两种表示可以描述相同的空间姿态，但机械转轴的排列方式不同，因此不能直接把 A4、A5、A6 的读数按顺序发给机器人。

![欧拉角与 Cardan 角轴系示意](https://owlcarp.me/images/responsive/articles/cctrl/21-480.webp)

**旋转轴序示意，参考 ETH Zürich《Robot Dynamics》课程资料。图中展示了不同欧拉角与 Cardan 角旋转序列的区别。**

固件采用统一的旋转约定，先用手柄后三轴构造姿态，再解算目标腕部的 ZXZ 角度。用旋转矩阵表达，大致就是：

```
R = Rx(-A4) · Rz(A5) · Ry(-A6)
= Rz(α)  · Rx(β) · Rz(γ)
```

矩阵相等意味着描述的是同一姿态，但求出来的 α、β、γ 并不总是唯一的。比如 ZXZ 的 β 接近 0° 时，前后两个 Z 轴几乎重合，此时稳定可观测的是 α+γ，而不是两者各自的绝对值。如果每帧都只按一个固定公式计算，控制器明明转得很平稳，输出角却可能突然跳到另一组相差很大的等价解。

我在 ESP32 的转换中做了连续解分支选择：比较两组等价解与上一帧的距离，优先沿着变化较小的方向走。接近奇异区域时改为维护可观测的角度和差值，并设置进入 3°、退出 5° 的迟滞；离开奇异区后，再以每个输出周期每轴最多 9° 的步长逐渐回到解析结果。J5 的角度还设置了非对称软限位，在接近边界时逐渐压缩输出，而不是突然硬截断。

这些策略不会让欧拉角的奇异性消失。它们解决的是一个更现实的问题：在数学表达本来不唯一的地方，怎样给出一条对机械臂和操作手来说都尽可能连续、可预期的指令路径。

另外，V2 还有一项上电休息位姿保护。主控保存控制器的物理休息姿态，上电后先保持相应的业务输出；当六轴回到预设区域并稳定一段时间，才解除启动门控。这避免了控制器通电时正摆在某个随意姿态，机械臂立即试图跟随它运动。对比赛现场来说，启动过程怎么定义，有时比多增加一种操作模式更重要。

## 从工作台到比赛桌

临近全国赛，第二代进入了真正的机械臂联调。此时它已不再像第一代那样向机器人发出抽象的空间位姿，而是提供六轴角度与手柄操作量。控制端的结构更接近操作目标，很多动作因而更容易直观对应；代价是它也更依赖目标机械臂的关节定义，通用性比第一代小得多。

[V2 与真实工程机械臂联调](https://owlcarp.me/articles/cctrl/31.mp4)

播放

**第二代控制器与工程机器人机械臂联调。**

和第一代一样，V2 的最终装配也赶在比赛出发前一晚完成。橙黑配色、碳纤维连杆、三轴腕部、主板和底座终于组成完整的一台机器。放在工作台上，它比第一代更像一件真正面向特定任务的工具。

![V2 最终装配实物](https://owlcarp.me/images/responsive/articles/cctrl/32-480.webp)

**全国赛前的最终装配：橙黑配色、碳纤维构件、被动六轴机构与复用的主控板。**

[V2 比赛前训练操作](https://owlcarp.me/articles/cctrl/33.mp4)

播放

**比赛前在道具训练区使用第二代控制器进行操作。**

有一个细节很能代表这次迭代。V1 的底座用橡胶吸盘固定，实验室桌面上没什么问题；正式比赛时，操作台铺有鼠标垫，吸盘便失去了作用。V2 我索性换了方案：在底座设计两个矿泉水瓶插槽，使用时插入装满水的瓶子作为配重。

![V2 使用矿泉水瓶配重的底座](https://owlcarp.me/images/responsive/articles/cctrl/34-480.webp)

**底座专门预留了两个水瓶位。配重在需要时获得，不必随设备一直携带。**

矿泉水几乎随处可得；运输时瓶子可以清空，使用时再装满，甚至比赛结束后还可以喝掉。这个设计没有复杂的机构，也谈不上什么高深技术，却解决了一个在真实场地里出现、而 CAD 和实验室测试都没有替我解决的问题。

回看 CCtrl 的两代设计，有些地方恰好形成了对照。V1 用四连杆加 IMU 换取通用空间输入，却把更多复杂性留给目标机械臂的控制算法；V2 用六关节测量换取更直接的操控，却必须认真处理腕部轴系、输入标定和实际人机工学。V1 把重心放在运动的表达上，V2 更关心指令怎样在真实机器人上稳定执行。

我不会把第二代理解成第一代的“全面升级”。第一代在虚拟空间交互、不同机械臂的通用输入和空间控制实验上仍有它的价值；第二代则服务于一个具体的比赛目标。它们回答的是两个不同的问题，也各自留下了值得继续做的事情。

如果说这个项目有什么特别的收获，大概是我终于把过去分散学到的 CAD、PCB、嵌入式、通信和机器人运动学，放到了一台必须有人握着、必须能拆装、必须在赛场上工作的设备上。技术方案并不会在屏幕里自动变成好产品。它最终还要经过一把螺丝刀、一束线、一次不太成功的测试，以及真实的操作手。

CCtrl 留给我的最珍贵的东西，正是这些不那么整齐、但确实发生过的开发过程。
