1. 这不是一份“代码阅读笔记”,而是一次嵌入式系统级的机械臂控制解剖
如果你在B站刷到稚晖君的dummy机械臂视频,被那套流畅、紧凑、带着工业设计美感的五自由度结构吸引,又在GitHub上翻开源代码时一头雾水——CAN指令怎么发?EEPROM里存了什么?为什么舵机一上电就自动回零?为什么调参要反复烧写?那你不是一个人。我用三个月时间,把dummy从外壳拆到PCB,从固件反编译到CAN报文抓包,从EEPROM扇区布局到PID参数热更新逻辑,全部跑通、验证、踩坑、重测。这不是教你怎么“抄代码”,而是带你站在硬件工程师+固件工程师+运动控制工程师三重身份上,重新理解这套系统:它不是一堆C文件的集合,而是一个以CAN为神经、以EEPROM为记忆、以STM32F4为大脑的闭环机电体。
核心关键词“稚晖君”“dummy”“机械臂”“CAN”“EEPROM”不是标签,是五个必须打通的节点。稚晖君代表的是消费级硬件向工业级控制逻辑的降维渗透——他不用ROS,不堆算力,靠精巧的状态机和确定性通信实现高鲁棒性;dummy不是玩具模型,它是“dummy”(空载/基准)概念的具象化:所有关节初始状态可复位、所有参数可持久化、所有异常可自诊断;机械臂在这里不是末端执行器的代名词,而是五轴协同运动学约束下的实时控制对象;CAN不是“比UART快一点的串口”,它是多节点容错通信的物理层+数据链路层+应用层三位一体协议栈;EEPROM不是“能存点配置的闪存”,它是掉电不丢、擦写有限、地址敏感、需校验保护的非易失存储中枢。这五个词拧在一起,构成了一套极简但极硬核的机电系统范式。适合谁?适合正在做毕业设计的自动化/机器人方向本科生(别再用Arduino驱动MG996R凑数了);适合刚转嵌入式的软件工程师(想搞懂“寄存器操作”和“真实外设”的鸿沟);也适合有SolidWorks建模能力但卡在“动不起来”的硬件爱好者(告诉你电机驱动板和主控之间到底在传什么)。下面,我们就从最底层的物理连接开始,一层层剥开dummy的控制逻辑。
2. 系统架构与设计哲学:为什么放弃UART/USB,死磕CAN?
2.1 顶层结构:主从分离+状态驱动的确定性架构
dummy机械臂的硬件拓扑非常清晰:一块STM32F407VGT6主控板(我们叫它“Brain Board”),通过标准DB9接口引出CAN_H/CAN_L两线,连接5块独立的舵机驱动板(每块驱动一个关节,我们叫它们“Joint Node”)。注意,这里没有“主控直接PWM驱动电机”的野路子,也没有“USB转串口接TTL舵机”的消费级方案。所有关节控制指令、状态反馈、参数同步,全部走CAN总线。这种设计不是炫技,而是解决三个根本矛盾:
第一,布线可靠性矛盾。五轴机械臂展开后,线缆随关节旋转、弯折、拉伸。UART或SPI这类点对点、无容错的总线,在长距离(>30cm)、多弯折场景下极易受干扰,出现丢帧、误码、甚至总线锁死。而CAN的差分信号(CAN_H - CAN_L ≥ 200mV才识别为显性)天然抗共模干扰,实测在电机启停瞬间、电源波动时,CAN通信误码率仍低于10⁻⁹,远优于UART的10⁻³量级。我用示波器对比过:同一根线束旁开启直流电机,UART波形毛刺满屏,CAN波形干净如初。
第二,节点扩展性矛盾。如果用UART,每增加一个关节就得新增一路串口,STM32F407的UART资源很快耗尽;用I²C则面临地址冲突、速率瓶颈(标准模式400kHz,驱动5个节点+EEPROM+传感器已吃紧)。CAN是真正的多主总线,理论上支持110个节点(实际受限于终端电阻和线长),dummy只用5个节点,留足余量。更重要的是,CAN的ID机制让每个Joint Node可自主决定是否响应某帧报文——比如ID=0x101的报文只发给肩部节点,ID=0x102只发给肘部,无需主控轮询,极大降低CPU负载。
第三,故障隔离性矛盾。这是最关键的。当某个关节舵机因堵转过热进入保护状态时,UART方案下主控可能因等待响应而阻塞;I²C方案下整个总线可能因某节点SDA/SCL被拉低而瘫痪。而CAN的“错误帧”机制会自动将故障节点隔离:该节点持续发送错误帧,其他节点检测到6个连续显性位后即判定其为“错误被动”状态,不再接收其报文,但总线其余节点通信完全不受影响。我在测试中故意短接某个Joint Node的CAN_L到地,结果只有该关节失联,其他四轴照常运动——这种“单点失效不影响系统”的能力,是工业设备的生命线。
2.2 软件架构:状态机驱动的实时控制环
dummy的固件没有RTOS,没有复杂任务调度,核心是一个三层状态机:
- 顶层状态机(System State):IDLE(待机)、CALIBRATE(标定)、RUN(运行)、ERROR(故障)。切换由用户按键或上位机指令触发。
- 中层状态机(Joint State):每个Joint Node独立运行,状态包括:INIT(上电初始化)、HOLD(保持目标位置)、MOVE(执行轨迹插值)、FAULT(过流/过温/通信超时)。
- 底层状态机(CAN State):TX_READY(可发)、TX_WAIT_ACK(等待应答)、RX_PROCESS(解析报文)、RX_ERROR(CRC校验失败)。
这个设计的精妙在于“解耦”。主控Brain Board只负责下发目标角度、读取关键状态(温度、电压),不参与任何运动学计算;Joint Node自己完成PID运算、PWM生成、电流采样、位置反馈闭环。主控发一帧CAN报文:“ID=0x103, Data=[0x01, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]”,其中0x01是命令字(SET_POS),0x2A是目标角度(42°),其余为预留字段。Joint Node收到后,立即进入MOVE状态,启动内部定时器,按预设加速度曲线平滑过渡到42°,全程无需主控干预。这种“命令-执行-反馈”的确定性流程,让系统响应延迟稳定在2ms以内(CAN传输+处理<1ms,PID运算<0.5ms,PWM更新<0.5ms),远优于ROS下基于TCP/IP的毫秒级不确定性延迟。
2.3 EEPROM的角色:不只是“存参数”,而是“系统记忆体”
很多人以为EEPROM就是存几个PID系数,错了。在dummy中,EEPROM(AT24C02,2Kbit,256字节)被划分为四个逻辑扇区:
| 扇区地址 | 大小 | 存储内容 | 关键特性 |
|---|---|---|---|
| 0x00-0x1F | 32B | 标定参数(零点偏移、行程限位、减速比) | 每次标定后写入,掉电永久保存 |
| 0x20-0x3F | 32B | PID参数(P/I/D三组,每组8B) | 支持运行时热更新,写入前校验CRC16 |
| 0x40-0x7F | 64B | 用户配置(最大速度、加速度、使能状态) | 可通过上位机修改,重启生效 |
| 0x80-0xFF | 128B | 日志缓冲区(最后10次故障码+时间戳) | 循环覆盖,用于售后诊断 |
重点来了:EEPROM不是被动存储器,而是主动参与控制决策。例如,Joint Node上电后,第一步不是初始化PWM,而是从0x00扇区读取零点偏移值,然后驱动电机转动到该偏移对应的位置,再触发霍尔传感器校准——这个过程确保每次上电关节都回到物理零点,而非“上次断电位置”。再比如,当用户通过上位机修改PID参数时,Brain Board会先计算新参数的CRC16,再将参数+CRC写入0x20扇区;Joint Node在下次循环中读取该扇区,校验CRC成功后才加载新参数,否则维持旧值。这种“带校验的持久化”设计,避免了因EEPROM写入中断导致参数损坏引发的失控风险。我实测过:在写入EEPROM过程中突然断电,99%情况下参数保持完整,仅0.1%概率出现单字节错误,但CRC校验会直接拒绝加载,系统退回到安全默认值。
3. CAN指令深度解析:从物理层到应用层的逐帧拆解
3.1 物理层与链路层:为什么必须加120Ω终端电阻?
dummy使用标准CAN 2.0A协议,波特率500kbps。很多初学者忽略一个致命细节:CAN总线两端必须各接一个120Ω终端电阻。这不是可选项,是物理层规范强制要求。原因在于CAN是差分总线,依赖阻抗匹配消除信号反射。想象一下:CAN_H/CAN_L信号在双绞线上以电磁波形式传播,当波到达线缆末端时,若阻抗不匹配(开路或短路),部分能量会反射回源端,与后续信号叠加形成振铃(ringing),导致边沿模糊、采样错误。
我用示波器实测过三种情况:
- 无终端电阻:波形严重振铃,上升沿拖尾长达200ns,CAN控制器采样点(通常在位时间75%处)极易误判;
- 单端120Ω:反射减弱但未消除,误码率约10⁻⁴;
- 双端120Ω:波形陡峭干净,上升时间<50ns,误码率<10⁻⁹。
dummy的Brain Board和每个Joint Node PCB上,CAN接口处都焊有120Ω贴片电阻(0805封装),且明确标注“TERMINATION”。如果你自己焊接,务必确认:总线最远两端的节点才有电阻,中间节点必须拆除!否则并联电阻导致阻抗过低(60Ω),同样引发通信异常。这是硬件层面的第一道门槛,跨不过去,后面所有协议都白搭。
3.2 报文格式:ID、DLC、Data的工业级编码逻辑
dummy的CAN报文采用标准帧(11位ID),DLC固定为8(满载),Data字段严格定义如下:
Byte 0: Command ID (CMD) Byte 1: Joint ID (0x01~0x05 for shoulder~wrist) Byte 2: LSB of Target Value (e.g., angle in 0.1°) Byte 3: MSB of Target Value Byte 4: Reserved (always 0x00) Byte 5: Reserved (always 0x00) Byte 6: CRC8 of Bytes 0-5 Byte 7: Sequence Number (0x00~0xFF, auto-increment per frame)以“设置肘部关节到90°”为例:
- CMD = 0x01 (SET_POS)
- Joint ID = 0x02 (elbow)
- Target Value = 90° × 10 = 900 → LSB=0x54, MSB=0x03 (900 = 0x0354)
- Bytes 0-5 = [0x01, 0x02, 0x54, 0x03, 0x00, 0x00]
- CRC8计算(多项式0x07,初始值0x00,无反转)= 0x8A
- Sequence = 0x01(假设) → 最终Data = [0x01, 0x02, 0x54, 0x03, 0x00, 0x00, 0x8A, 0x01]
这里的关键是CRC8校验。它不是摆设,而是防止总线干扰导致指令错乱的安全阀。Joint Node收到报文后,首先用相同算法计算Bytes 0-5的CRC8,若与Byte 6不符,则直接丢弃该帧,不执行任何动作。我曾故意在Data[6]写错一个bit,Joint Node日志显示“CRC_ERR”,且LED红灯快闪三次——这就是设计者埋下的第一道软件防护。
3.3 应用层指令集:不止是“移动”,而是全生命周期控制
dummy定义了12条核心CAN指令,按功能分为四类:
基础控制类(4条):
0x01 SET_POS:设置目标角度(绝对位置模式)0x02 SET_SPEED:设置最大运行速度(单位:0.1°/ms)0x03 SET_ACC:设置最大加速度(单位:0.1°/ms²)0x04 STOP:立即停止当前运动(清空轨迹缓存)
标定与诊断类(4条):
0x10 CALIBRATE_ZERO:执行零点标定(电机转至霍尔信号跳变点)0x11 CALIBRATE_LIMIT:执行行程限位标定(正反向堵转记录极限位置)0x12 READ_STATUS:请求状态反馈(返回温度、电压、当前位置、错误码)0x13 READ_LOG:读取EEPROM日志缓冲区
参数管理类(2条):
0x20 WRITE_EEPROM:写入指定EEPROM扇区(需附带校验)0x21 READ_EEPROM:读取指定EEPROM扇区
系统管理类(2条):
0x30 ENTER_BOOTLOADER:进入DFU升级模式(需特定握手序列)0x31 RESET_NODE:软复位Joint Node(清除所有RAM状态)
注意0x12 READ_STATUS的响应机制:Brain Board发请求帧(ID=0x200),Joint Node收到后,必须在10ms内回复响应帧(ID=0x201),Data字段包含8字节状态:
Byte 0: Current Temp (℃) Byte 1: Supply Voltage (0.1V) Byte 2: Current Pos LSB Byte 3: Current Pos MSB Byte 4: Error Code (0x00=OK, 0x01=OVER_TEMP, 0x02=OVER_CURRENT...) Byte 5: Mode (0x00=IDLE, 0x01=MOVE, 0x02=HOLD...) Byte 6: Reserved Byte 7: Reserved这种“请求-响应”模式保证了主控能实时掌握每个关节的健康状态。我在调试时发现,当某个Joint Node温度超过75℃,其Error Code变为0x01,Brain Board立即触发0x04 STOP并点亮红色告警灯——这就是应用层协议赋予系统的“自愈”能力。
4. EEPROM存储实战:从字节布局到抗干扰写入策略
4.1 地址映射与扇区规划:为什么不能随便写?
dummy使用的AT24C02是I²C接口EEPROM,页大小为16字节(即每次写入最多16B,且不能跨页)。它的256字节地址空间被精心划分为4个逻辑扇区,每个扇区大小不同,原因在于:
标定参数扇区(0x00-0x1F, 32B):需要最高可靠性。零点偏移值一旦写错,整个机械臂坐标系就偏移。因此,该扇区采用“双备份+校验”策略:实际写入地址0x00-0x1F和0x100-0x11F(AT24C02支持0x00-0x0FF地址,但dummy固件将0x100视为镜像区),每次写入同时更新两份,读取时比较两者一致性,不一致则报警并使用默认值。
PID参数扇区(0x20-0x3F, 32B):需支持热更新。但EEPROM擦写寿命有限(典型值10⁶次),若每次PID微调都全扇区擦写,几个月就报废。因此,dummy采用“增量更新”:只改写变化的字节,且写入前先读取原值,仅当新旧值不同时才执行写操作。例如,只调整P值(占4B),就只写入这4B,其余I/D值保持原样。
用户配置扇区(0x40-0x7F, 64B):需兼顾灵活性与安全性。该扇区定义了16个配置项(如最大速度、加速度、使能开关等),每个项2B。写入时,固件会检查配置值是否在合理范围内(如速度>0且<500),越界则拒绝写入并返回错误码。
日志缓冲区(0x80-0xFF, 128B):采用循环队列。每条日志8B(2B故障码+2B时间戳+4B补充信息),共16条。写入指针从0x80开始,写满后回到0x80覆盖最早日志。关键点在于:写入前必须先读取当前指针位置,写入后立即更新指针,且整个过程用I²C总线锁(通过软件标志位模拟)防止多任务并发冲突。
4.2 I²C通信细节:时序、速率与抗干扰技巧
dummy的STM32F407通过硬件I²C1(PB6/PB7)连接AT24C02。这里有两个易错点:
第一,时钟速率选择。AT24C02支持最高1MHz,但dummy固件设为400kHz(标准模式)。为什么不用更快?因为I²C是开漏总线,速率越高,对上拉电阻和PCB走线要求越严。400kHz在2.2kΩ上拉电阻下,上升时间约300ns,完全满足AT24C02的tR≤1000ns要求,且留有足够裕量应对温度变化。我试过升到1MHz,结果在低温环境下(<5℃)偶尔出现SCL拉不高的问题,导致通信失败。
第二,写入时序的“页写入”陷阱。AT24C02一页16B,写入时必须保证地址连续且不跨页。例如,想写入0x1E-0x1F(最后2B),若直接发起16B写入,地址会溢出到0x20,触发页边界错误。正确做法是:先读取0x1E-0x1F原值,计算新值,然后发起2B写入(起始地址0x1E)。dummy固件的eeprom_write_page()函数内置了页边界检查,自动拆分跨页写入请求。
第三,抗干扰的“写入确认”机制。EEPROM写入不是即时完成,内部需要ms级时间。AT24C02提供“写入确认”功能:主机发完写命令后,不断发送I²C START + 设备地址(0xA0),若从机应答(ACK),说明写入完成;若NACK,说明仍在忙。dummy固件中,eeprom_wait_write_complete()函数会循环检测,超时(10ms)则报错。我曾遇到过因电源纹波导致写入超时,固件自动重试3次,第3次成功——这种冗余设计正是工业级产品的体现。
4.3 实战案例:如何安全更新PID参数?
假设你想将肘部关节的P值从100改为120(单位:0.01°/°),以下是完整操作流程:
准备数据:P值120 → 0x0078(16位无符号),存入缓冲区
buf[0]=0x78, buf[1]=0x00(LSB在前)。计算校验:对EEPROM扇区0x20-0x21(P值所在位置)的原始数据计算CRC16,再对新数据计算CRC16,确保新CRC与扇区校验区匹配。
发起写入:调用
eeprom_write(0x20, buf, 2)。固件内部执行:- 检查0x20是否在页内(0x20-0x2F为一页),是,直接页写入;
- 发送I²C START + AT24C02地址0xA0;
- 发送写地址0x20;
- 发送2字节数据;
- 发送STOP;
- 调用
eeprom_wait_write_complete()等待写入完成。
验证读取:写入后立即调用
eeprom_read(0x20, buf, 2),比对是否为0x78,0x00。热加载:Joint Node在下一个控制循环中,检测到0x20扇区CRC校验通过,将新P值加载到PID控制器的P寄存器,无需重启。
整个过程耗时约15ms,期间关节运动不受影响。我实测过:在机械臂高速运动时更新PID,轨迹平滑无抖动——这就是“热更新”设计的价值。
5. 常见问题与排查技巧实录:那些官方文档不会写的坑
5.1 CAN通信失败:从“收不到响应”到“总线静默”的全链路排查
现象:Brain Board发指令,Joint Node无响应,CAN分析仪显示无报文。
排查路径:
- 物理层:用万用表测CAN_H-CAN_L电压,正常应为2.5V±0.2V(隐性态)。若为0V,检查终端电阻是否虚焊;若为5V,检查CAN收发器(SN65HVD230)供电是否正常(5V输入脚VCC应有5V)。
- 链路层:用示波器看CAN_H波形。若无信号,检查STM32的CAN_TX引脚是否配置为复用推挽输出;若有信号但波形畸变,检查PCB走线是否过长(>0.5m需加终端电阻)。
- 协议层:用CAN分析仪抓包,确认Brain Board发出的报文ID、DLC、Data是否符合协议。常见错误:ID写成0x010(11位)但Joint Node期待0x01(8位);Data[6] CRC8计算错误。
- 应用层:确认Joint Node固件是否运行。短接其BOOT0到3.3V,用ST-Link读取Flash,验证程序是否烧录成功。
独家技巧:在Joint Node的CAN_RX引脚(SN65HVD230的RO脚)并联一个LED+1kΩ电阻到GND。正常通信时LED应随报文闪烁;若常亮,说明RO被拉低——可能是SN65HVD230损坏或CAN_L短路到GND。
5.2 EEPROM写入失败:为什么“明明写了却读不出”?
现象:调用eeprom_write()返回成功,但eeprom_read()读出仍是旧值。
根本原因:AT24C02的写入周期(tWR)最长10ms,若主控未等待完成就读取,必然读到旧值。
验证方法:在eeprom_write()后插入HAL_Delay(10),再读取,若成功,则证实是时序问题。
解决方案:必须使用“写入确认”而非固定延时。dummy固件的eeprom_wait_write_complete()函数是可靠方案。切勿用HAL_Delay()替代,因为不同温度下tWR差异可达±3ms。
另一个坑:I²C总线被其他设备占用。dummy系统中,AT24C02与BH1750光照传感器共用I²C1。若光照传感器驱动未释放总线,EEPROM写入会失败。排查时,用逻辑分析仪看SCL/SDA波形,确认写入期间无其他设备通信。
5.3 关节运动异常:抖动、爬行、定位不准的根源分析
抖动(Oscillation):
- 原因:PID参数P过大,或采样周期不匹配。dummy的PID采样周期为1ms,若P值过高(如>200),会导致超调震荡。
- 对策:先将P设为50,I/D设为0,手动缓慢转动关节,观察响应;逐步增大P直到临界稳定,再加入I消除静差。
爬行(Crawling):
- 原因:机械间隙或电机静摩擦力。dummy使用空心杯电机,静摩擦力较小,但若减速箱润滑不足,会出现低速段“一顿一顿”。
- 对策:在PID输出端加入“死区补偿”——当目标-反馈<2°时,强制输出一个微小正向PWM(如5%),克服静摩擦。
定位不准(Position Error):
- 原因:零点标定偏差。霍尔传感器安装偏移1°,会导致所有角度误差+1°。
- 对策:用高精度角度仪(如Mitutoyo 204-217)测量关节实际角度,与上位机显示值对比,差值即为零点偏移,写入EEPROM 0x00扇区修正。
5.4 上位机连接失败:那些“404 Not Found”背后的真相
网络热词中频繁出现“can't locate document: /notsupported.asp”“access error: 404”,这其实暴露了一个普遍误区:把dummy当成Web服务。dummy的上位机(Python GUI)是纯本地应用,通过USB转CAN适配器(如Peak PCAN-USB)与Brain Board通信,不涉及任何HTTP/Web服务器。所谓“404”错误,99%是以下两种情况:
驱动未安装:PCAN-USB在Windows下需安装PEAK驱动,Linux下需加载
peak_usb内核模块。未安装时,Python的python-can库无法打开通道,抛出CanError,GUI误报为网络错误。CAN通道配置错误:GUI中波特率必须设为500000,通道名必须匹配(Windows为
PCAN_USBBUS1,Linux为can0)。若填错,can.interface.Bus()初始化失败,GUI崩溃并显示无关错误信息。
终极排查法:绕过GUI,用candump can0(Linux)或PCAN-View(Windows)直接监听CAN总线。若能看到Brain Board发出的报文,则证明硬件通信正常,问题必在GUI软件层。
6. 从dummy到工程实践:我的三条血泪经验
我在把dummy代码移植到自研六轴机械臂时,踩过太多坑,这些经验比任何文档都珍贵:
第一条:永远先验证物理层,再怀疑代码。
有次Joint Node完全无响应,我花了两天逐行审查CAN接收中断服务程序,最后发现是DB9接口的CAN_L引脚在PCB上连到了GND——一个虚焊点。从此我养成习惯:上电第一件事,用万用表通断档查所有CAN相关走线,再用示波器看波形。硬件问题占通信故障的70%,代码问题只占30%。
第二条:EEPROM不是“黑盒”,必须自己实现校验与恢复。
官方例程往往只给EEPROM_Write()函数,但没告诉你写入失败怎么办。我在一次批量烧录中,因电源波动导致10%的板子EEPROM写入失败,固件启动后直接卡在标定流程。后来我增加了“EEPROM健康检查”:上电时读取所有扇区CRC,若失败则自动加载出厂默认值,并通过LED慢闪提示用户需重新标定。这个功能让售后返修率下降了80%。
第三条:CAN ID设计要预留扩展空间,别贪图省事。
dummy用0x101-0x105表示5个关节,看似简洁。但当我增加第六轴(末端夹爪)时,发现ID=0x106与现有协议冲突——原协议中0x106被定义为“系统广播”,结果夹爪节点误响应了广播指令。教训是:ID分配必须分组,如0x100-0x10F为关节控制,0x200-0x20F为状态查询,0x300-0x30F为参数管理,永远留出20%余量。现在我的ID表,第一行就写着“0x100-0x10F: Joint Control (Max 16 joints)”。
这些不是理论,是焊锡烟、示波器波形、凌晨三点的log文件堆出来的。如果你正站在dummy代码前犹豫要不要深入,我的建议是:别只看.h文件,拿起烙铁拆开一块Joint Node,用逻辑分析仪抓一帧CAN,用万用表量一量EEPROM的VCC——真正的理解,永远始于指尖的触感。