学完电机公式,我以为能读懂机器人代码了,结果卡在更底层的地方:一堆节点在 CAN 总线上收发报文,IMU 吐出加速度和角速度,然后一段融合算法把它们揉成一个姿态。每个词都认识,连起来不知道在干嘛。
后来我换了个角度想,一下就通了:这套东西就是机器人的神经系统。
- CAN 总线是神经——把各个节点的信号传来传去。
- IMU 是内耳——感知自己的姿态和运动,就像你闭着眼也知道头有没有歪。
- 传感器融合是大脑——把几个各有毛病的感官信号对齐,凑出一个可信的判断。
而这三样,你在分布式系统里全见过。CAN 是一个带优先级的消息总线(pub/sub 的硬件版);IMU 的陀螺仪积分漂移就是浮点误差累积;融合算法本质是拿几个不可靠的副本做加权 quorum。你不是在学新东西,是在给已有的直觉换一层皮。
这篇接着上一篇《软件人学电子电气》的路子——把陌生概念挂到你会的东西上,公式当函数、变量当挂钩。方法一样,只是从”单个电机”上升到”多个节点怎么通信、怎么感知、怎么达成一致”。
结构:先讲机器人为什么不用 TCP 用总线,再把 CAN 拆开(你点名的重点),然后 IMU 三个传感器各自的病,再讲融合怎么把病治好,最后是时间同步、工程英语和学习计划。
读完你该能做到几件具体的事:看到一帧 CAN 报文,说得出 ID 是什么、数据段怎么解析、为什么这个 ID 优先级高;看到 IMU 融合代码,知道 gyro、acc、kalman_gain、q0~q3 各自在干嘛;通信调不通、姿态会漂时,脑子里有一张排查清单而不是抓瞎。目标不是让你会设计这些系统,是让你读懂别人写的机器人代码时,每个变量都能落到一个你熟悉的概念上。
预备:先约定单位和坐标系
正式开始前,先解决两个软件人最容易栽的地方——单位和坐标系。跟上一篇一样,读硬件代码翻车,很少栽在算法上,多半栽在这种约定上。
传感器的单位要认脸:
加速度 g(1g ≈ 9.8 m/s²)或直接 m/s²
角速度 dps(°/s)或 rad/s —— 陀螺常用 dps
磁场 μT(微特斯拉)或 Gauss
角度 代码里多用弧度 rad,仪表给人看用度 °
看到 gyro = 250 先问单位——250 dps 是很快的转动,250 rad/s 就离谱了,多半是单位看错。角度那对更坑:公式吃弧度、显示用度,中间差一个 π/180,跟上一篇 rpm 和 rad/s 的换算一个道理。
坐标系是第二个坑,也是更隐蔽的一个。IMU 的数据永远是相对某个坐标系说的:
- 机体坐标系(body frame):钉在设备上,跟着设备一起转。IMU 原始数据就在这个系里。
- 世界坐标系(world / NED frame):不动的参考系,比如北-东-地。姿态角说的就是”机体相对世界转了多少”。
姿态的本质,就是这两个坐标系之间的旋转关系。软件类比:像本地坐标和世界坐标的变换矩阵,你在图形学里天天做。
真正的坑在于——各家 IMU 的轴方向定义不统一,有的 x 朝前、有的 x 朝右,z 朝上还是朝下也不一定。符号搞反,姿态就镜像了,查起来极其难受。这跟大小端、行主序列主序不统一是同一类坑:数据本身没错,是”怎么解释它”的约定没对齐。拿到一颗新 IMU 的数据,第一件事是确认它的单位和轴定义,别默认跟你上一颗一样。
一、为什么机器人不用 TCP,用总线
软件人第一个困惑:节点之间通信,为什么不像后端服务那样走 TCP/HTTP,非要搞 CAN、RS485 这些听都没听过的东西?
答案是场景不一样。机器人内部是几十上百个小节点(电机驱动、传感器、按钮)挤在一台设备里,要求是:实时、便宜、抗干扰、线要少。TCP/以太网太重了——每个节点塞一套网络协议栈、一个网口,成本和功耗都受不了,而且以太网的延迟不确定,控制回路等不起。
所以底层用总线(bus):一根线挂多个节点,大家共享,广播式收发。类比很直接——总线就是一条共享的消息通道,谁都能往上发、谁都能听,跟你用过的 pub/sub 事件总线、CAN 的思路是一个模子。
几种常见总线,先建个全景,知道各自的位置:
| 总线 | 典型场景 | 软件类比 | 速度档 |
|---|---|---|---|
CAN |
车、机器人关节、多节点控制 | 带优先级的 pub/sub | 中(≤1Mbps 经典) |
RS485 / Modbus |
工业设备、变频器、PLC | 主从轮询的请求-响应 | 中低 |
SPI |
芯片间高速(IMU、ADC、Flash) | 点对点全双工,主设备发时钟 | 高(几十 Mbps) |
I2C |
板内低速小传感器 | 两根线挂一串设备,按地址点名 | 低(100k~400k) |
UART |
调试串口、GPS、简单模块 | 一对一裸字节流,没有寻址 | 低中 |
一句话记住分工:板内芯片对芯片用 SPI/I2C(快、近),设备内跨节点用 CAN(多、稳),工业设备间用 RS485/Modbus(远、简单)。 你读机器人代码看到这些名字,先定位它在哪一层,别混。
其他几个各补一句,认个脸:
- SPI:一主多从,主设备发时钟,全双工,速度最快。IMU、高速 ADC、Flash 芯片跟 MCU 之间多用它。代价是线多(时钟、两条数据、每个从设备一条片选)。变量
spi_cs、mosi、miso、sclk。 - I2C:只用两根线(数据 SDA、时钟 SCL)挂一串设备,每个设备有个地址,主设备按地址点名。省线但慢,适合板内低速小传感器。变量
i2c_addr、sda、scl。 - RS485 / Modbus:RS485 管物理层(差分、能拉很远),Modbus 是跑在上面的主从问答协议——主设备问一句,从设备答一句。工业变频器、PLC 的标配。变量
slave_addr、reg_addr、modbus_fc(功能码)。 - UART:最原始的一对一串口,裸字节流,没有寻址也没有仲裁。调试打印、GPS、简单模块用它。变量
uart_rx、baud。
这篇重点讲 CAN,因为它是机器人内部多节点通信的主力,也是你点名认知最少的那块。其他几个知道是干嘛的就够,用到再查。
二、CAN 总线:一个带优先级的广播消息队列
CAN 全称 Controller Area Network,1986 年博世给汽车做的。核心设计目标就一个:让一堆节点在一根(差分)线上可靠、实时地广播消息,还不怕电磁干扰。 机器人直接把它拿来用了。
把它当成一个硬件版的消息队列来理解,几乎每个特性都能对上号。
它是基于消息,不是基于地址
这是 CAN 最反直觉、也最关键的一点。CAN 报文里没有”发给谁”的地址,只有一个 ID 标识”这是什么消息”。
一帧 CAN 报文(简化):
[ ID ][ DLC ][ Data 0~8 字节 ][ CRC ][ ACK ]
↑标识符 ↑数据长度 ↑ 载荷 ↑校验 ↑确认
ID 不是地址,是主题(topic)。节点发一帧”电机 3 的转速 = 1500″,不指定收件人,往总线上一广播。谁关心这个 topic,谁就配置好过滤器把它收下;不关心的节点硬件直接丢掉。
这不就是 pub/sub 吗。发布者不知道订阅者是谁,消息按 topic 路由,订阅方自己决定订哪些。CAN 的报文 ID 就是 topic,节点的接收过滤器(acceptance filter)就是订阅规则。想通这层,CAN 就从”陌生硬件”变成”你天天用的消息中间件”。代码里的 can_id、filter_mask 就是这套。
关于 ID 还有两个细节,读代码会撞上。一是 ID 有两种长度:标准帧 11 位(够 2048 个)、扩展帧 29 位(几亿个,多用于 J1939 这种需要塞很多信息进 ID 的场景),代码里用 ide 位区分、ext_id 存扩展 ID。二是除了正常的数据帧,还有一种远程帧(RTR,remote frame)——不带数据,作用是”喂,谁有 ID=X 的数据,发一下”,相当于一个按 topic 发起的请求。变量 rtr、ide、std_id/ext_id 就是这几个标志位。现在的机器人系统大多不太用远程帧,认识就行。
仲裁:ID 小的赢,而且冲突不浪费
多个节点同时开口怎么办?以太网的做法是碰撞了大家都退避重来(CSMA/CD),浪费一次。CAN 更聪明——无损仲裁。
原理是差分线上有”显性位”和”隐性位”,显性(逻辑 0)会盖过隐性(逻辑 1)。所有节点边发 ID 边监听总线:
发送时逐位比较自己发的和总线上实际的电平:
- 我发隐性(1),但总线是显性(0) → 说明有更高优先级的在发 → 我立刻闭嘴退让
- 一路没被盖过 → 我赢了,继续把整帧发完
结果:ID 数值越小 → 优先级越高 → 赢得总线
输的节点不重来,等这帧发完自动重发,一个 bit 都没浪费
这就是无锁的优先级竞争。多个线程抢一个资源,优先级最高的直接拿到,其他的不是失败重试,而是有序让路。CAN 用硬件在几个 bit 时间内完成了这个仲裁,而且天然保证:最紧急的消息(比如急停)ID 设最小,永远第一个上总线。你设计报文 ID 分配时,本质是在排优先级——急停、故障给小 ID,普通状态上报给大 ID。
但这个优先级机制有个副作用,也是你熟的:队列饥饿。CAN 是一根共享线,所有流量挤在一起,用总线负载率衡量(实际占用的带宽 ÷ 总带宽)。负载一旦高上去(工程经验超过 70%~80% 就危险),低优先级的报文会一直抢不过高优先级的,延迟暴涨甚至彻底发不出去——高优先级消息把总线占满,把低优先级饿死。
这跟你见过的优先级反转、高优先级任务把 CPU 占满导致后台任务饥饿,是同一个病。所以设计时不能把总线塞满,周期报文的频率要留余量。变量 bus_load、tx_queue、priority 就是这层。新手容易犯的错:加了一堆高频状态上报,把总线压到 90%,结果某个低优先级指令偶发丢失,查半天——其实是饿死了。
差分信号:为什么抗干扰
CAN 用两根线 CAN_H 和 CAN_L,传的是两者的电压差,不是对地的绝对电压。
好处是抗干扰。外界的电磁噪声会同时窜到两根线上(共模干扰),但因为它俩一起动,差值不变,噪声就被抵消了。类比就是发数据同时发一份校验基准,接收端只看两者的相对关系,共同的干扰自动约掉。这也是为什么 CAN 线要用双绞线,两根绞在一起保证受到的干扰尽量一样。变量里 can_h、can_l、diff 就是这层物理。
波特率与采样点:速度和距离的折中
CAN 的速度不是随便设的,跟总线长度反着走:
常见波特率:125k / 250k / 500k / 1Mbps
速度越高 → 允许的总线越短(1Mbps 约 ≤40 米,125k 可到 500 米)
为什么?信号在线上跑要时间,仲裁又要求所有节点在同一 bit 内比较电平,线太长信号还没传到远端,一个 bit 就过去了,仲裁就乱了。所以高速必须短线。
每个 bit 内部还分几段,有个采样点(sample point)决定在 bit 的百分之多少处读电平,通常 75%~87.5%。这个值全总线必须一致,不然大家读的时刻错开就通信失败。调 CAN 通信起不来,十有八九是波特率或采样点没对齐——这跟你调串口波特率不匹配是一个类型的坑。
配置 CAN 时你会看到几个寄存器 prescaler、tseg1、tseg2、sjw,它们其实就是在凑波特率和采样点:
一个 bit 被切成若干"时间份"(time quantum):
1 bit = 同步段(1) + tseg1 + tseg2
波特率 = 时钟 / (prescaler × 每 bit 的份数)
采样点 = (同步段 + tseg1) / 每 bit 的份数
SJW = 允许的重同步跳变宽度,吸收节点间的小时钟误差
不用手算,很多工具能替你生成这几个值。但要知道它们是干嘛的:调不通时对着两端的这几个数比一比,往往一眼看出波特率或采样点没对上。变量 bitrate、sample_point、tseg1/tseg2、sjw 就是这层。
上层协议:CANopen 和 DBC
裸 CAN 只规定了”怎么把一帧字节安全送到”,没规定”这帧字节是什么意思”。意思由上层协议约定,机器人/工业界最常见两个:
- CANopen:一套标准的对象字典 + 通信规范,规定了节点怎么启动、怎么周期上报(PDO)、怎么点对点读写参数(SDO)。类比就是在消息总线上又架了一层标准化的 RPC + 状态机。变量
cob_id、pdo、sdo、node_id都是 CANopen 的。 - DBC 文件:描述每个 CAN ID 里的字节各代表什么信号(第几位是转速、缩放多少、单位是什么)。它就是 CAN 报文的 schema / IDL——没有它,收到 8 个字节你也不知道怎么解析。
一帧报文怎么读:DBC 就是 schema
抽象讲完,拿一帧真实报文走一遍你就有感觉了。假设你抓到这么一帧:
ID = 0x181 DLC = 4 Data = [0x10, 0x27, 0x00, 0x00]
光看这串字节啥也不是。得配上 DBC(报文的 schema)才能翻译:
查 DBC:ID 0x181 = 1号电机状态上报(PDO)
字节0~1 = 转速,小端(little-endian),缩放 0.1 rpm
解析:字节0~1 小端拼 → 0x2710 = 10000
乘缩放 0.1 → 1000 rpm
结论:1号电机当前转 1000 rpm
so-what:没有 DBC,你收到的永远只是一串字节,无法反推含义。 DBC 之于 CAN,就像 Protobuf 的 .proto、Thrift 的 IDL 之于你的 RPC——一份双方约定的字段定义。读 CAN 代码时先找 DBC/对象字典,别对着裸字节硬猜。变量 scale、offset、endian、signal 就是解析这一步。
错误处理:CAN 自带熔断器
CAN 还有一套很优雅的错误机制,读驱动代码必然撞上,而且它的设计正好是你熟的熔断器(circuit breaker)。
每个节点维护两个错误计数器(发送 TEC、接收 REC),根据计数值在三个状态间转移:
error-active 正常态,出错还能吼一嗓子(发错误帧) ← 熔断器 closed
error-passive 错误攒多了(TEC≥128),发言权受限 ← 半开,谨慎
bus-off 错误太多(TEC≥256),自我隔离,不再上总线 ← 熔断器 open
一个节点如果坏了、疯狂出错,不会一直污染总线——错误攒够就自动 bus-off 把自己踢下线,保护整条总线不被一个坏节点拖垮。恢复时再尝试重新上线(half-open)。这跟你给下游服务加熔断、坏了就断开保护上游、过一会儿再试探是一模一样的容错思想。
读代码看到 bus_off、error_passive、tec/rec、error_count,就是这套状态机。新手常见 bug:节点进了 bus-off 却没写恢复逻辑,表现就是”通信突然全没了、重启才好”。 处理它就是监测状态、满足条件后重启 CAN 控制器。
一句话收束 CAN:它是一个带硬件优先级仲裁、自带熔断容错的广播消息队列,ID 是 topic 也是优先级,差分线抗干扰,上层用 CANopen/DBC 补语义。 拿这套心智模型去读 CAN 代码,那些 can_id、dlc、pdo、bus_off 就都有位置了。
三、IMU:机器人的内耳,三个传感器各有各的病
通信讲完,说感知。IMU(Inertial Measurement Unit,惯性测量单元)是机器人判断自己姿态的核心器件。它其实是几个传感器打包在一起:
- 6 轴 IMU = 加速度计(3 轴)+ 陀螺仪(3 轴)
- 9 轴 IMU = 再加磁力计(3 轴)
理解 IMU 的关键,不是记它们测什么,而是记住每个传感器都有一种病。融合算法存在的全部理由,就是治这些病。先把三个病人各自的毛病搞清楚。
加速度计:长期诚实,短期话痨
加速度计测的是比力——通俗说,它能感觉到重力的方向。静止时它只感受到重力,于是你能算出设备相对水平面倾斜了多少(俯仰角 pitch、横滚角 roll)。
静止时用加速度算倾角:
pitch ≈ atan2(acc_x, √(acc_y² + acc_z²))
单位:加速度常用 g(1g ≈ 9.8 m/s²)
拿数字感受一下:设备水平静止,三轴读数约 (0, 0, 1g)——只有 z 轴顶着重力。往前倾 30°,重力被分到 x 轴上,读数变成约 (0.5g, 0, 0.87g),代入上面公式 atan2(0.5, 0.87) ≈ 30°,倾角就出来了。静止时它这么算很准。
它的病:一旦设备在动,运动产生的加速度会和重力混在一起,它分不清哪个是重力哪个是运动。你正常走路时它抖得像话痨,短期读数根本不能信。但它有个好处——不漂移,重力永远指向地心,长期看它是诚实的。
一句话记性格:加速度计短期吵、长期稳。 还有个死穴——它测不出绕重力轴的旋转(yaw),因为你原地转圈重力方向不变。变量 acc_x/acc_y/acc_z、accel 就是它。
陀螺仪:短期精准,长期跑偏
陀螺仪测的是角速度(转多快),单位 dps(度/秒)或 rad/s。你把角速度对时间积分,就得到转过的角度。
陀螺积分求角度:
angle += gyro × dt
单位:dps(°/s)
它的病正好和加速度计相反。短期内它非常准、非常跟手,抖动小。但它有个零偏(bias)——哪怕静止不动,读数也不是正好 0,而是一个小偏差。这个小偏差被积分一直累加,角度就慢慢跑偏,几十秒到几分钟就飘出去了。
这个”病”软件人太熟了:积分把一个恒定的小误差累积成越来越大的偏差,就是浮点误差累积、就是没有校正的 dead reckoning(航位推算)。 陀螺仪单靠自己,转一圈回来会告诉你没回到原点。变量 gyro_x/gyro_y/gyro_z、bias_gyro、drift 就是它。
一句话记性格:陀螺仪短期准、长期漂。
磁力计:给航向,但容易被带偏
磁力计是个数字指南针,测地磁场方向,主要用来定 yaw(航向)——补上加速度计定不了的那个角。
它的病是容易被环境污染。周围的铁、磁铁、通电的导线都会扭曲本地磁场,读数就偏。所以磁力计用前必须校准(硬铁/软铁校准),而且在电机、大电流附近基本没法信。变量 mag_x/mag_y/mag_z、heading、yaw 是它。
要不要磁力计,是实际项目的一个取舍:不带(6 轴)就没有绝对航向,yaw 只能靠陀螺积分,会慢慢漂;带上(9 轴)能校正 yaw,但得接受它在强磁环境下不可信、还得做校准。机器人本身电机多、磁场脏,很多方案干脆不信磁力计的 yaw,改用轮式里程计或视觉来定航向。你看代码里 yaw 是怎么来的——纯陀螺、磁力计融合、还是外部源——就能反推这台设备的设计取舍。
量程与分辨率:读寄存器配置会撞上
配置 IMU 时会遇到两个要设的参数,直接影响数据含义,得懂:
量程(range):
加速度计 ±2g / ±4g / ±8g / ±16g
陀螺仪 ±250 / ±500 / ±1000 / ±2000 dps
输出数据率(ODR):每秒采多少次,如 100Hz / 1kHz
量程和分辨率是一对冤家,跟上一篇 ADC 的 LSB 完全一个道理:芯片位数固定(比如 16 位),量程越大,这些格子要摊到越大的范围上,每格就越粗,能分辨的最小变化越差。
- 选 ±2g:平稳设备,测得细,但一撞超过 2g 就削顶(饱和)读不准。
- 选 ±16g:能扛剧烈冲击、无人机翻滚,但静止时的小倾斜看得糙。
所以量程按应用选:平稳场景要精度选小量程,剧烈运动怕削顶选大量程。原始码值换算成物理量,就是乘一个由量程决定的缩放系数(灵敏度)。变量 fs_sel、range、odr、sensitivity 就是这些配置。读传感器驱动的初始化代码,多半就在设这几个寄存器。
三个病人凑一桌:互补性
把三个的毛病列一起,融合的思路就自己冒出来了:
传感器 短期 长期 擅长
加速度计 吵(不可信) 稳(不漂) 定 pitch/roll
陀螺仪 准 漂 测快速转动
磁力计 易受干扰 给绝对航向 定 yaw
看这张表——加速度计和陀螺仪的病正好互补:一个短期烂长期好,一个短期好长期烂。那把它俩的优点各取一半不就行了?这就是下一节融合的全部动机。没有哪个传感器是可信的,但它们不可信的地方不一样,凑起来就能可信。 这跟你用多个不可靠副本做 quorum、用多个弱信号投票是同一个哲学。
四、传感器融合:把不可靠的凑成可靠的
这是整篇的主戏,也是软件人最有优势的一节——因为融合的本质就是加权平均,你写过无数次。
互补滤波:一行公式,先建直觉
治加速度计和陀螺仪的互补病,最简单的办法叫互补滤波。思路直白到不可思议:短期信陀螺,长期信加速度。
angle = α × (angle + gyro × dt) + (1 − α) × acc_angle
└─── 陀螺积分(短期) ───┘ └─ 加速度(长期) ─┘
α 通常取 0.95~0.99
拆开看:angle + gyro×dt 是用陀螺推算的新角度(短期准),acc_angle 是加速度直接算的角度(长期稳)。α 取 0.98,就是98% 听陀螺、2% 用加速度慢慢往回拉。
陀螺的漂移是慢慢累积的,那 2% 的加速度分量正好持续把它往真实值上拽,抵消漂移;而加速度的高频抖动,因为只占 2% 权重,被压得几乎没影响。
拿数字走一步就懂了。设 α=0.98、采样 100Hz(dt=0.01s),上一步姿态角 10.0°:
陀螺这一刻读 5°/s → 陀螺推算 = 10.0 + 5×0.01 = 10.05°
加速度这一刻算出 10.6°(含 0.6° 的运动噪声)
融合 = 0.98×10.05 + 0.02×10.6 = 9.849 + 0.212 = 10.06°
看结果:加速度那 0.6° 的噪声,最后只贡献了 0.01°——被压掉了 98%。但只要陀螺持续往一个方向偏,这 2% 的加速度分量会一帧一帧地把它往回拉,长期不让它漂走。短期几乎全听陀螺(跟手不抖),长期靠加速度兜底(不漂),一个 α 就把两件事办了。
这跟你上一篇学的一阶低通滤波(EMA)是同一个式子。 只不过这里 α 那一路走的是”陀螺积分”而不是”上一次的值”。给陀螺加高通(只信它的快变化)、给加速度加低通(只信它的慢趋势),两条一加就互补了。变量 alpha、comp_filter、angle_est 就是它。
互补滤波的好处是简单、便宜、够用,很多低成本设备就用它。缺点是 α 靠手调,且没有考虑”当前到底该更信谁”。要更聪明,就上卡尔曼。
卡尔曼滤波:会自己调权重的加权平均
卡尔曼滤波(Kalman Filter)名字唬人,但对软件人,一句话就能戳破:它是一个会根据不确定度自动调整权重的加权平均,是互补滤波的升级版。
互补滤波的 α 是你写死的常数。卡尔曼的”α”(叫卡尔曼增益 K)是每一步动态算出来的——谁这一刻更可信(方差更小),就多信谁。
它跑两步,循环往复:
① 预测(predict):用陀螺往前推一步,同时把不确定度调大
"我根据角速度猜现在转到了这,但我对这个猜测的信心在下降"
② 更新(update):拿加速度的观测来校正
K = 预测方差 / (预测方差 + 观测方差) ← 卡尔曼增益,就是动态的 α
估计 = 预测 + K × (观测 − 预测) ← 按可信度加权修正
同时把不确定度调小
看那个 K 的式子——如果观测很吵(观测方差大),K 就小,几乎不信观测、维持预测;如果预测越来越飘(预测方差大),K 就大,赶紧用观测拉回来。 它在实时地判断”这一刻该信陀螺还是信加速度”,而不是像互补滤波那样一刀切。
软件类比:这就是带置信度的多副本读。你从两个副本读数据,一个延迟低但可能脏、一个慢但权威,你怎么合并?按各自的可信度加权——可信度不是固定的,随状态变。卡尔曼把这个思想做成了数学最优。变量 kalman_gain、P(协方差/不确定度)、Q(过程噪声)、R(观测噪声)、x_est 就是它这五个角色。
实际调卡尔曼,你拧的就两个旋钮 Q 和 R,含义很好记:
Q(过程噪声):你多不信自己的运动模型(陀螺)
Q 调大 → 觉得模型不靠谱 → 更快去信观测
R(观测噪声):你多不信观测(加速度)
R 调大 → 觉得观测很吵 → 更依赖模型预测
Q 和 R 的比值,决定了增益 K 大概落在哪。调滤波”太迟钝跟不上”还是”太抖信噪声”,本质就是在这两个之间找平衡——和你调互补滤波的 α、调 EMA 的权重是同一种手感,只不过卡尔曼让它随不确定度自动变。变量 Q、R、P 就是这三个不确定度。
不用现在会推矩阵形式。记住这个骨架就有了读代码的地图:预测(信运动模型)→ 更新(用观测校正)→ 增益 K 决定这次更信谁。工程上还有扩展卡尔曼 EKF、无迹卡尔曼 UKF,是处理非线性的变体,思想不变——机器人姿态、里程计、SLAM 里到处是它们。
姿态怎么表示:欧拉角 vs 四元数
融合算出姿态后,得用某种方式存它。两种表示,读代码都会撞上:
- 欧拉角(roll / pitch / yaw):三个角度,直观,人能看懂。但有个致命病叫万向锁(gimbal lock)——某些角度下两个旋转轴重合,丢一个自由度,姿态会突然翻转、算不动。
- 四元数(q0, q1, q2, q3,四个数):没有万向锁,计算稳定高效,是机器人/无人机内部真正用的表示。缺点是四个数看不出物理含义。
万向锁对你不该陌生——如果你碰过 3D 图形、游戏引擎的相机旋转,就踩过它。 四元数就是当年图形学里用来治万向锁的同一个工具,机器人姿态直接拿来用。看到 q0~q3、quat、euler、gimbal 这些名字,就是姿态表示这层。AHRS(Attitude and Heading Reference System,姿态航向参考系统)就是”IMU + 融合算法 + 姿态输出”打包后的叫法,变量 ahrs_roll、attitude 是它的产物。
实际项目里你未必自己写卡尔曼,多半直接用现成的开源融合算法——最常见的是 Madgwick 和 Mahony 两个。它们可以看成”用四元数表示、计算量比卡尔曼小”的高级互补滤波,跑在小 MCU 上也不吃力。看到 beta(Madgwick 的增益)、Kp/Ki(Mahony 的增益)这些变量,就是在调这类滤波器”多信加速度/磁力计一点还是少信一点”,本质还是那句话——在几个各有缺陷的传感器之间调权重。
还有个容易忽略的工程点:融合的更新率要够高。姿态变化多快,融合就得跑多快去追,跟上一篇的采样定理是一个道理——追一个快速翻滚的动作,融合环通常跑几百 Hz 到 1kHz。跑太慢,姿态就会滞后、跟不上真实运动。变量 update_rate、loop_hz、dt 就是它。所以你常看到 IMU 设成 1kHz 采样、融合环也在 1kHz,是为了不丢快速运动。
一句话收束融合:几个各有缺陷的传感器,用加权平均取长补短——固定权重就是互补滤波,动态权重就是卡尔曼。你缺的不是数学,是把它翻译成”带置信度的多副本合并”这句话。
五、时间同步:分布式系统的老朋友换了个场
融合有个前提常被新手忽略:你揉在一起的那几个数据,得是同一时刻的。 加速度计 1kHz、陀螺仪 1kHz、磁力计 100Hz、相机 30fps,采样率不一样、延迟也不一样,直接拿最新值凑一块,其实是把不同时刻的状态混在算——机器人动得快时,这误差足以让姿态算歪。
拿数字感受一下危害:设备以 200°/s 转动(不算快),如果加速度和陀螺的数据差了 5ms 才凑到一起,这 5ms 里姿态已经变了 200 × 0.005 = 1°——你把 1° 前的数据和现在的数据揉在一起算,融合结果就带着这个凭空的误差。转得越快、时间差越大,误差越离谱。静止时你根本发现不了,一动就露馅。
这个问题你太熟了:多个数据源、各自的时钟和延迟、要按时间对齐——这就是分布式系统的时钟同步。 日志聚合要处理乱序事件、事件溯源要按时间戳排序、跨服务追踪要对齐 span,全是一回事,只是这里的”服务”变成了传感器。
几个关键点,都能对上你的旧知识:
- 时间戳打在哪:软件时间戳(数据到了 CPU 才盖章)会受调度、中断、缓冲的影响不准;硬件时间戳(传感器采样瞬间就盖章)才靠谱。类比就是在事件产生时刻打戳,而不是在消费时刻打戳,后者被排队延迟污染。变量
timestamp、ts_hw、t_sample就是这层。 - 抖动(jitter)比延迟更可怕:固定的延迟好补偿,忽大忽小的抖动难办。控制回路最怕抖动——你以为每 1ms 采一次,实际时快时慢,微分项就乱了。这跟实时系统怕 GC 停顿、怕调度尖峰是同一种恐惧。变量
jitter、period、deadline是它。 - PTP / 时间触发:多节点要共享一个时钟基准,工业上用 PTP(精密时间协议,IEEE 1588),跟你知道的 NTP 是同一族,只是精度高到微秒/纳秒级。机器人里多个 CAN 节点、多个传感器对时,就靠这类机制。
一句话记住:融合之前先对齐时间,别拿不同时刻的数据硬凑。 这条新手最容易漏,出的 bug 又最难查——姿态偶尔跳一下、动态下精度掉,八成是时间没对齐,不是算法错。你调分布式系统乱序问题的那套直觉,直接搬过来用。
串起来:一帧数据在机器人里的旅程
前面拆开讲了通信和感知,最后把它们接成一条链,你脑子里就有整张图了。一次控制循环里,数据是这么流动的:
① IMU 采样(打硬件时间戳)
↓ SPI/I2C
② MCU 收到原始 acc/gyro/mag
↓ 融合算法(互补滤波 / 卡尔曼)
③ 算出姿态 roll/pitch/yaw(或四元数)
↓ 打包成 CAN 帧,广播
④ 控制节点收到姿态,算电机指令(PID / 上一篇的那套)
↓ CAN 帧,发给电机驱动
⑤ 电机驱动收到指令,调 PWM 占空比,电机动
↓ 设备姿态改变
①' IMU 再次采样……闭环
这就是一条带时间戳的事件流水线——每一跳有序列化(打包成 CAN 帧)、有传输(总线可能拥塞、可能丢帧重发)、有时间对齐。你搭过的数据管道、事件溯源系统,拓扑上没差别,只是节点从微服务变成了电机和传感器,消息队列从 Kafka 变成了 CAN。
读机器人代码前,先在脑子里画出这条链,然后把看到的每个变量、每个模块定位到链上的哪一跳:acc_raw 在①、pitch_est 在③、torque_cmd 在④、duty 在⑤。落好位,代码就不再是一团乱麻,而是一条你熟悉的流水线。
六、工程英语:总线与传感器的变量拆解
老规矩,最后一关是读懂变量名。上一篇的三层拆解法(词根 + 后缀 + 缩写)继续用,这里补上通信和传感器这个领域的专属词。后缀表(_ref/_est/_raw/_filt/_meas)和上一篇通用,不重复,直接上新词。
通信与总线词根
| 英文 | 中文 | 常见变量 |
|---|---|---|
| Bus | 总线 | bus, canbus |
| Frame | 帧(一条报文) | frame, msg |
| Identifier | 标识符/ID | id, can_id, cob_id |
| Payload | 载荷/数据段 | payload, data |
| Node | 节点 | node, node_id |
| Baudrate / Bitrate | 波特率/位速率 | baud, bitrate |
| Arbitration | 仲裁 | arb, arb_id |
| Acknowledge | 应答 | ack |
| Filter / Mask | 过滤器/掩码 | filter, mask |
| Timeout | 超时 | timeout, to |
| Retry | 重试 | retry, retries |
| Heartbeat | 心跳(周期存活报文) | heartbeat, hb |
传感器与姿态词根
| 英文 | 中文 | 常见变量 |
|---|---|---|
| Accelerometer | 加速度计 | acc, accel, ax/ay/az |
| Gyroscope | 陀螺仪 | gyro, gx/gy/gz |
| Magnetometer | 磁力计 | mag, mx/my/mz |
| Bias | 零偏 | bias, offset |
| Drift | 漂移 | drift |
| Roll / Pitch / Yaw | 横滚/俯仰/航向 | roll, pitch, yaw |
| Quaternion | 四元数 | q0~q3, quat |
| Heading | 航向 | heading, hdg |
| Attitude | 姿态 | attitude, att |
| Covariance | 协方差(不确定度) | P, cov |
| Gain | 增益 | k, gain, kalman_gain |
| Fusion | 融合 | fusion, fused |
20 个真实变量拆解练习
从真实机器人/嵌入式代码里挑的,先自己拆再看答案:
1. can_id_std → CAN标识符 · 标准(11位)
2. rx_frame → 接收(receive)的 · 帧
3. tx_timeout → 发送(transmit) · 超时
4. node_hb_lost → 节点 · 心跳丢失
5. gyro_bias_z → 陀螺仪 · z轴 · 零偏
6. acc_raw → 加速度计 · 原始值
7. pitch_est → 俯仰角 · 估计值
8. q_ref → 四元数 · 目标姿态
9. mag_calib → 磁力计 · 校准(calibration)
10. kalman_gain → 卡尔曼 · 增益
11. P_pred → 协方差 · 预测值
12. imu_ts_hw → IMU · 硬件时间戳
13. arb_id → 仲裁 · ID
14. dlc → 数据长度码(Data Length Code)
15. yaw_drift_rate → 航向 · 漂移速率
16. comp_alpha → 互补滤波 · 权重系数
17. can_bus_off → CAN总线 · 关闭(错误过多进入的保护态)
18. accel_g → 加速度 · 以 g 为单位
19. period_jitter → 周期 · 抖动
20. ahrs_roll_deg → 姿态系统 · 横滚角 · 单位度
行业高频术语表
CAN(Controller Area Network):多主广播消息总线,ID 即优先级,见第二节。CANopen:架在 CAN 上的标准协议,含对象字典、PDO(周期数据)、SDO(点对点读写)。DBC:描述 CAN 报文里各信号含义的文件,相当于报文的 schema。arbitration(仲裁):多节点同时发时,ID 小的无损胜出。bus-off:某节点错误计数超限被总线隔离的保护状态,代码里要处理恢复。IMU(Inertial Measurement Unit):加速度计 + 陀螺仪(+ 磁力计)的组合。AHRS(Attitude and Heading Reference System):IMU 加融合算法后输出姿态航向的系统。bias / drift(零偏 / 漂移):陀螺仪的病,静止有偏、积分累积成漂移。complementary filter(互补滤波):固定权重融合陀螺和加速度,一行公式。Kalman filter(卡尔曼滤波):动态权重融合,增益 K 按不确定度自调。EKF / UKF:卡尔曼处理非线性系统的两个变体,思想不变。quaternion(四元数):无万向锁的姿态表示,机器人内部主用。gimbal lock(万向锁):欧拉角在特定角度丢自由度,姿态跳变。NED / body frame:世界坐标系(北-东-地)和机体坐标系,姿态是两者的相对关系。
七、怎么学,附速查卡
这块比公式更依赖”动手看真东西”,因为通信和传感器的坑都在实测里。给你一个两天的路子。
Day 1:建立”总线 = 消息队列、融合 = 加权平均”的映射
第一天只搭骨架,用软件类比复述,不碰细节:
- 上午:过第一、二节,把 CAN 讲成 pub/sub——ID 是 topic、仲裁是无锁优先级竞争、CANopen 是标准 RPC。讲不顺回去看。
- 下午:过第三、四节,记住两句话——”三个传感器各有各的病,病还互补”、”融合就是带置信度的加权平均,卡尔曼是会自调权重的互补滤波”。
验收标准:能用软件里的东西类比出 CAN 和卡尔曼分别像什么。 说得出就过。
Day 2:看真报文、拆真变量
第二天动手,目标是看到真实数据不发懵:
- 上午:找一份真实 CAN 日志(或 DBC 文件),对着看 ID、DLC、数据段怎么排。有硬件的话用
candump(Linux SocketCAN)抓一段:
# Linux 下抓 CAN 报文(需要 can-utils)
sudo ip link set can0 up type can bitrate 500000
candump can0
- 下午:做第六节那 20 个变量拆解,遮答案自己拆。再翻一份开源 IMU 融合库(比如 Madgwick/Mahony 滤波的实现),逼自己认里面的
q0~q3、gyro_bias、beta这些变量。
验收标准:20 个变量拆对 15 个以上,看 CAN 报文能说出 ID 和数据段。
三个最容易卡住的坑
这块的坑几乎都在硬件实测里,光看代码看不出来。把这三个记下,能省你几天。
坑一:CAN 通信完全起不来。 十有八九是物理层没对,按这个顺序查:
□ 波特率两端一致?(500k 对 250k 直接不通)
□ 采样点一致?(差太多在长线上会偶发错帧)
□ 总线两端各有一个 120Ω 终端电阻?(少了或多了都通信异常)
□ 所有节点共地(GND 连通)?
□ CAN 收发器供电正常?CAN_H / CAN_L 没接反?
那个 120Ω 终端电阻是纯软件人最想不到的坑——CAN 总线两端必须各接一个 120Ω 电阻吸收信号反射,不接或只接一端,短线可能碰运气能通、长线必挂。查通信问题先量这个。
坑二:IMU 姿态漂或不准。 通常是没做标定:
□ 陀螺零偏(bias)标了吗?——上电静止几秒,测出各轴静止读数当零点减掉
□ 磁力计校准了吗?装到设备上、绕八字转一圈做硬铁/软铁校准
□ 磁力计是不是装在电机/大电流旁?——那里磁场被污染,基本没法用
□ 温度变了吗?——bias 会随温漂,高精度场景要温补
坑三:静止好好的,一动姿态就跳。 大概率是时间没对齐(见第五节)——不同传感器采样率/延迟不同,融合时拿了不同时刻的数据。先确认时间戳对齐,再怀疑算法。
速查卡(贴代码旁边)
【总线选型】
SPI/I2C 板内芯片对芯片,快、近
CAN 设备内多节点,ID即优先级,差分抗干扰
RS485 设备间远距离,主从轮询(Modbus)
UART 一对一裸字节流,调试/GPS
【CAN 要点】
基于消息不基于地址 ID = topic(pub/sub)
仲裁 ID小者胜,无损(无锁优先级竞争)
波特率↔距离 1Mbps≈40m,125k≈500m
采样点 全总线必须一致,通常75%~87.5%
上层 CANopen(PDO/SDO) / DBC(schema)
【IMU 三个病】
加速度计 短期吵、长期稳 定 pitch/roll,测不了 yaw
陀螺仪 短期准、长期漂 积分求角,bias累积成drift
磁力计 给航向、易被干扰 定 yaw,需校准
【融合】
互补滤波 angle = α(angle+gyro·dt) + (1−α)·acc_angle 固定权重
卡尔曼 K = P_pred/(P_pred+R) 动态权重
估计 = 预测 + K(观测−预测)
姿态 欧拉角(有万向锁) / 四元数(q0~q3,无锁)
【时间同步】
硬件时间戳 > 软件时间戳 采样瞬间打戳
抖动比延迟难办 控制回路怕jitter
融合前先对齐时间 不同时刻的数据别硬凑
自测清单
答不上来就回对应章节:
- CAN 为什么说它是”基于消息不是基于地址”,和 pub/sub 像在哪?(第二节)
- 两个节点同时发,谁能占住总线,输的那个怎么办?(第二节)
- 陀螺仪明明短期很准,为什么单靠它算姿态会跑偏?(第三节)
- 互补滤波的 α 和卡尔曼增益 K,本质区别是什么?(第四节)
- 融合前为什么要先对齐时间戳,不对齐会怎样?(第五节)
- 看到
gyro_bias_z和kalman_gain,分别是什么?(第六节) - CAN 总线负载压到 90% 会出什么问题,和软件里的什么现象是一回事?(第二节)
最后一句
这块的知识大多藏在实测里,光看讲解记不牢,一定要抓一次真报文、拆一份真代码。没硬件也没关系——Linux 的 SocketCAN 能用软件虚拟一条 CAN 总线,在电脑上就能收发练手,先把 ID、仲裁、报文结构摸熟:
# 建一条虚拟 CAN 总线 vcan0
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
# 一个终端监听
candump vcan0
# 另一个终端发一帧:ID=0x181,数据 10 27 00 00
cansend vcan0 181#10270000
监听端会收到你发的帧,对着它把 ID、数据段的字节顺序、DLC 摸一遍。想看仲裁,就用不同 ID 同时发,观察谁先出去。这套 can-utils(candump/cansend)几乎是读 CAN 代码前的必经练习,零成本。
融合那部分,遇到卡尔曼别被公式吓住,先在脑子里把它降级成”一个会自己调权重的加权平均”,再回头看代码里的 P、Q、R、K 各是谁。想通了这句话,剩下的就是线性代数的记账,不难。
到这一步,机器人的”神经”和”内耳”你就摸清了。回头看这两篇连起来的路子:上一篇《软件人学电子电气》讲的是单个电机怎么被驱动(电压、电流、PWM、FOC),这一篇讲多个节点怎么通信、怎么感知、怎么达成一致。合起来就是——机器人怎么感知自己、怎么把信号传来传去。
缺的最后一块是”大脑”:运动学和控制,怎么把感知到的姿态和目标,变成每个关节该转多少、该出多大力。旋转矩阵、四元数、正逆解、状态空间——那是下一篇的事。
给你一个落地的次序建议:先动手,再补理论。别等把卡尔曼推导看懂了才开始——先用 vcan 收发几帧 CAN,先读一份开源 IMU 融合库把变量对上号,先把姿态在屏幕上画出来动起来。手上有了真实反馈,再回头看公式,你会发现它们只是在给你已经看到的现象记账。这块知识藏在实测里,光看是长不到脑子里的。

