软件人学机器人底层:CAN 总线、IMU 与传感器融合

学完电机公式,我以为能读懂机器人代码了,结果卡在更底层的地方:一堆节点在 CAN 总线上收发报文,IMU 吐出加速度和角速度,然后一段融合算法把它们揉成一个姿态。每个词都认识,连起来不知道在干嘛。

后来我换了个角度想,一下就通了:这套东西就是机器人的神经系统。

  • CAN 总线是神经——把各个节点的信号传来传去。
  • IMU 是内耳——感知自己的姿态和运动,就像你闭着眼也知道头有没有歪。
  • 传感器融合是大脑——把几个各有毛病的感官信号对齐,凑出一个可信的判断。

而这三样,你在分布式系统里全见过。CAN 是一个带优先级的消息总线(pub/sub 的硬件版);IMU 的陀螺仪积分漂移就是浮点误差累积;融合算法本质是拿几个不可靠的副本做加权 quorum。你不是在学新东西,是在给已有的直觉换一层皮。

这篇接着上一篇《软件人学电子电气》的路子——把陌生概念挂到你会的东西上,公式当函数、变量当挂钩。方法一样,只是从”单个电机”上升到”多个节点怎么通信、怎么感知、怎么达成一致”。

结构:先讲机器人为什么不用 TCP 用总线,再把 CAN 拆开(你点名的重点),然后 IMU 三个传感器各自的病,再讲融合怎么把病治好,最后是时间同步、工程英语和学习计划。

读完你该能做到几件具体的事:看到一帧 CAN 报文,说得出 ID 是什么、数据段怎么解析、为什么这个 ID 优先级高;看到 IMU 融合代码,知道 gyroacckalman_gainq0~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_csmosimisosclk
  • I2C:只用两根线(数据 SDA、时钟 SCL)挂一串设备,每个设备有个地址,主设备按地址点名。省线但慢,适合板内低速小传感器。变量 i2c_addrsdascl
  • RS485 / Modbus:RS485 管物理层(差分、能拉很远),Modbus 是跑在上面的主从问答协议——主设备问一句,从设备答一句。工业变频器、PLC 的标配。变量 slave_addrreg_addrmodbus_fc(功能码)。
  • UART:最原始的一对一串口,裸字节流,没有寻址也没有仲裁。调试打印、GPS、简单模块用它。变量 uart_rxbaud

这篇重点讲 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_idfilter_mask 就是这套。

关于 ID 还有两个细节,读代码会撞上。一是 ID 有两种长度:标准帧 11 位(够 2048 个)、扩展帧 29 位(几亿个,多用于 J1939 这种需要塞很多信息进 ID 的场景),代码里用 ide 位区分、ext_id 存扩展 ID。二是除了正常的数据帧,还有一种远程帧(RTR,remote frame)——不带数据,作用是”喂,谁有 ID=X 的数据,发一下”,相当于一个按 topic 发起的请求。变量 rtridestd_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_loadtx_queuepriority 就是这层。新手容易犯的错:加了一堆高频状态上报,把总线压到 90%,结果某个低优先级指令偶发丢失,查半天——其实是饿死了。

差分信号:为什么抗干扰

CAN 用两根线 CAN_H 和 CAN_L,传的是两者的电压差,不是对地的绝对电压。

好处是抗干扰。外界的电磁噪声会同时窜到两根线上(共模干扰),但因为它俩一起动,差值不变,噪声就被抵消了。类比就是发数据同时发一份校验基准,接收端只看两者的相对关系,共同的干扰自动约掉。这也是为什么 CAN 线要用双绞线,两根绞在一起保证受到的干扰尽量一样。变量里 can_hcan_ldiff 就是这层物理。

波特率与采样点:速度和距离的折中

CAN 的速度不是随便设的,跟总线长度反着走:

常见波特率:125k / 250k / 500k / 1Mbps
速度越高 → 允许的总线越短(1Mbps 约 ≤40 米,125k 可到 500 米)

为什么?信号在线上跑要时间,仲裁又要求所有节点在同一 bit 内比较电平,线太长信号还没传到远端,一个 bit 就过去了,仲裁就乱了。所以高速必须短线。

每个 bit 内部还分几段,有个采样点(sample point)决定在 bit 的百分之多少处读电平,通常 75%~87.5%。这个值全总线必须一致,不然大家读的时刻错开就通信失败。调 CAN 通信起不来,十有八九是波特率或采样点没对齐——这跟你调串口波特率不匹配是一个类型的坑。

配置 CAN 时你会看到几个寄存器 prescalertseg1tseg2sjw,它们其实就是在凑波特率和采样点:

一个 bit 被切成若干"时间份"(time quantum):
  1 bit = 同步段(1) + tseg1 + tseg2
波特率     = 时钟 / (prescaler × 每 bit 的份数)
采样点     = (同步段 + tseg1) / 每 bit 的份数
SJW        = 允许的重同步跳变宽度,吸收节点间的小时钟误差

不用手算,很多工具能替你生成这几个值。但要知道它们是干嘛的:调不通时对着两端的这几个数比一比,往往一眼看出波特率或采样点没对上。变量 bitratesample_pointtseg1/tseg2sjw 就是这层。

上层协议:CANopen 和 DBC

裸 CAN 只规定了”怎么把一帧字节安全送到”,没规定”这帧字节是什么意思”。意思由上层协议约定,机器人/工业界最常见两个:

  • CANopen:一套标准的对象字典 + 通信规范,规定了节点怎么启动、怎么周期上报(PDO)、怎么点对点读写参数(SDO)。类比就是在消息总线上又架了一层标准化的 RPC + 状态机。变量 cob_idpdosdonode_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/对象字典,别对着裸字节硬猜。变量 scaleoffsetendiansignal 就是解析这一步。

错误处理:CAN 自带熔断器

CAN 还有一套很优雅的错误机制,读驱动代码必然撞上,而且它的设计正好是你熟的熔断器(circuit breaker)

每个节点维护两个错误计数器(发送 TEC、接收 REC),根据计数值在三个状态间转移:

error-active   正常态,出错还能吼一嗓子(发错误帧)     ← 熔断器 closed
error-passive  错误攒多了(TEC≥128),发言权受限        ← 半开,谨慎
bus-off        错误太多(TEC≥256),自我隔离,不再上总线  ← 熔断器 open

一个节点如果坏了、疯狂出错,不会一直污染总线——错误攒够就自动 bus-off 把自己踢下线,保护整条总线不被一个坏节点拖垮。恢复时再尝试重新上线(half-open)。这跟你给下游服务加熔断、坏了就断开保护上游、过一会儿再试探是一模一样的容错思想。

读代码看到 bus_offerror_passivetec/recerror_count,就是这套状态机。新手常见 bug:节点进了 bus-off 却没写恢复逻辑,表现就是”通信突然全没了、重启才好”。 处理它就是监测状态、满足条件后重启 CAN 控制器。

一句话收束 CAN:它是一个带硬件优先级仲裁、自带熔断容错的广播消息队列,ID 是 topic 也是优先级,差分线抗干扰,上层用 CANopen/DBC 补语义。 拿这套心智模型去读 CAN 代码,那些 can_iddlcpdobus_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_zaccel 就是它。

陀螺仪:短期精准,长期跑偏

陀螺仪测的是角速度(转多快),单位 dps(度/秒)或 rad/s。你把角速度对时间积分,就得到转过的角度。

陀螺积分求角度:
angle += gyro × dt
单位:dps(°/s)

它的病正好和加速度计相反。短期内它非常准、非常跟手,抖动小。但它有个零偏(bias)——哪怕静止不动,读数也不是正好 0,而是一个小偏差。这个小偏差被积分一直累加,角度就慢慢跑偏,几十秒到几分钟就飘出去了。

这个”病”软件人太熟了:积分把一个恒定的小误差累积成越来越大的偏差,就是浮点误差累积、就是没有校正的 dead reckoning(航位推算)。 陀螺仪单靠自己,转一圈回来会告诉你没回到原点。变量 gyro_x/gyro_y/gyro_zbias_gyrodrift 就是它。

一句话记性格:陀螺仪短期准、长期漂。

磁力计:给航向,但容易被带偏

磁力计是个数字指南针,测地磁场方向,主要用来定 yaw(航向)——补上加速度计定不了的那个角。

它的病是容易被环境污染。周围的铁、磁铁、通电的导线都会扭曲本地磁场,读数就偏。所以磁力计用前必须校准(硬铁/软铁校准),而且在电机、大电流附近基本没法信。变量 mag_x/mag_y/mag_zheadingyaw 是它。

要不要磁力计,是实际项目的一个取舍:不带(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_selrangeodrsensitivity 就是这些配置。读传感器驱动的初始化代码,多半就在设这几个寄存器。

三个病人凑一桌:互补性

把三个的毛病列一起,融合的思路就自己冒出来了:

传感器      短期        长期        擅长
加速度计    吵(不可信)   稳(不漂)    定 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)是同一个式子。 只不过这里 α 那一路走的是”陀螺积分”而不是”上一次的值”。给陀螺加高通(只信它的快变化)、给加速度加低通(只信它的慢趋势),两条一加就互补了。变量 alphacomp_filterangle_est 就是它。

互补滤波的好处是简单、便宜、够用,很多低成本设备就用它。缺点是 α 靠手调,且没有考虑”当前到底该更信谁”。要更聪明,就上卡尔曼。

卡尔曼滤波:会自己调权重的加权平均

卡尔曼滤波(Kalman Filter)名字唬人,但对软件人,一句话就能戳破:它是一个会根据不确定度自动调整权重的加权平均,是互补滤波的升级版。

互补滤波的 α 是你写死的常数。卡尔曼的”α”(叫卡尔曼增益 K)是每一步动态算出来的——谁这一刻更可信(方差更小),就多信谁。

它跑两步,循环往复:

① 预测(predict):用陀螺往前推一步,同时把不确定度调大
   "我根据角速度猜现在转到了这,但我对这个猜测的信心在下降"

② 更新(update):拿加速度的观测来校正
   K = 预测方差 / (预测方差 + 观测方差)     ← 卡尔曼增益,就是动态的 α
   估计 = 预测 + K × (观测 − 预测)          ← 按可信度加权修正
   同时把不确定度调小

看那个 K 的式子——如果观测很吵(观测方差大),K 就小,几乎不信观测、维持预测;如果预测越来越飘(预测方差大),K 就大,赶紧用观测拉回来。 它在实时地判断”这一刻该信陀螺还是信加速度”,而不是像互补滤波那样一刀切。

软件类比:这就是带置信度的多副本读。你从两个副本读数据,一个延迟低但可能脏、一个慢但权威,你怎么合并?按各自的可信度加权——可信度不是固定的,随状态变。卡尔曼把这个思想做成了数学最优。变量 kalman_gainP(协方差/不确定度)、Q(过程噪声)、R(观测噪声)、x_est 就是它这五个角色。

实际调卡尔曼,你拧的就两个旋钮 Q 和 R,含义很好记:

Q(过程噪声):你多不信自己的运动模型(陀螺)
   Q 调大 → 觉得模型不靠谱 → 更快去信观测
R(观测噪声):你多不信观测(加速度)
   R 调大 → 觉得观测很吵 → 更依赖模型预测

Q 和 R 的比值,决定了增益 K 大概落在哪。调滤波”太迟钝跟不上”还是”太抖信噪声”,本质就是在这两个之间找平衡——和你调互补滤波的 α、调 EMA 的权重是同一种手感,只不过卡尔曼让它随不确定度自动变。变量 QRP 就是这三个不确定度。

不用现在会推矩阵形式。记住这个骨架就有了读代码的地图:预测(信运动模型)→ 更新(用观测校正)→ 增益 K 决定这次更信谁。工程上还有扩展卡尔曼 EKF、无迹卡尔曼 UKF,是处理非线性的变体,思想不变——机器人姿态、里程计、SLAM 里到处是它们。

姿态怎么表示:欧拉角 vs 四元数

融合算出姿态后,得用某种方式存它。两种表示,读代码都会撞上:

  • 欧拉角(roll / pitch / yaw):三个角度,直观,人能看懂。但有个致命病叫万向锁(gimbal lock)——某些角度下两个旋转轴重合,丢一个自由度,姿态会突然翻转、算不动。
  • 四元数(q0, q1, q2, q3,四个数):没有万向锁,计算稳定高效,是机器人/无人机内部真正用的表示。缺点是四个数看不出物理含义。

万向锁对你不该陌生——如果你碰过 3D 图形、游戏引擎的相机旋转,就踩过它。 四元数就是当年图形学里用来治万向锁的同一个工具,机器人姿态直接拿来用。看到 q0~q3quateulergimbal 这些名字,就是姿态表示这层。AHRS(Attitude and Heading Reference System,姿态航向参考系统)就是”IMU + 融合算法 + 姿态输出”打包后的叫法,变量 ahrs_rollattitude 是它的产物。

实际项目里你未必自己写卡尔曼,多半直接用现成的开源融合算法——最常见的是 Madgwick 和 Mahony 两个。它们可以看成”用四元数表示、计算量比卡尔曼小”的高级互补滤波,跑在小 MCU 上也不吃力。看到 beta(Madgwick 的增益)、Kp/Ki(Mahony 的增益)这些变量,就是在调这类滤波器”多信加速度/磁力计一点还是少信一点”,本质还是那句话——在几个各有缺陷的传感器之间调权重

还有个容易忽略的工程点:融合的更新率要够高。姿态变化多快,融合就得跑多快去追,跟上一篇的采样定理是一个道理——追一个快速翻滚的动作,融合环通常跑几百 Hz 到 1kHz。跑太慢,姿态就会滞后、跟不上真实运动。变量 update_rateloop_hzdt 就是它。所以你常看到 IMU 设成 1kHz 采样、融合环也在 1kHz,是为了不丢快速运动。

一句话收束融合:几个各有缺陷的传感器,用加权平均取长补短——固定权重就是互补滤波,动态权重就是卡尔曼。你缺的不是数学,是把它翻译成”带置信度的多副本合并”这句话。

五、时间同步:分布式系统的老朋友换了个场

融合有个前提常被新手忽略:你揉在一起的那几个数据,得是同一时刻的。 加速度计 1kHz、陀螺仪 1kHz、磁力计 100Hz、相机 30fps,采样率不一样、延迟也不一样,直接拿最新值凑一块,其实是把不同时刻的状态混在算——机器人动得快时,这误差足以让姿态算歪。

拿数字感受一下危害:设备以 200°/s 转动(不算快),如果加速度和陀螺的数据差了 5ms 才凑到一起,这 5ms 里姿态已经变了 200 × 0.005 = 1°——你把 1° 前的数据和现在的数据揉在一起算,融合结果就带着这个凭空的误差。转得越快、时间差越大,误差越离谱。静止时你根本发现不了,一动就露馅。

这个问题你太熟了:多个数据源、各自的时钟和延迟、要按时间对齐——这就是分布式系统的时钟同步。 日志聚合要处理乱序事件、事件溯源要按时间戳排序、跨服务追踪要对齐 span,全是一回事,只是这里的”服务”变成了传感器。

几个关键点,都能对上你的旧知识:

  • 时间戳打在哪:软件时间戳(数据到了 CPU 才盖章)会受调度、中断、缓冲的影响不准;硬件时间戳(传感器采样瞬间就盖章)才靠谱。类比就是在事件产生时刻打戳,而不是在消费时刻打戳,后者被排队延迟污染。变量 timestampts_hwt_sample 就是这层。
  • 抖动(jitter)比延迟更可怕:固定的延迟好补偿,忽大忽小的抖动难办。控制回路最怕抖动——你以为每 1ms 采一次,实际时快时慢,微分项就乱了。这跟实时系统怕 GC 停顿、怕调度尖峰是同一种恐惧。变量 jitterperioddeadline 是它。
  • 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~q3gyro_biasbeta 这些变量。

验收标准: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_zkalman_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-utilscandump/cansend)几乎是读 CAN 代码前的必经练习,零成本。

融合那部分,遇到卡尔曼别被公式吓住,先在脑子里把它降级成”一个会自己调权重的加权平均”,再回头看代码里的 PQRK 各是谁。想通了这句话,剩下的就是线性代数的记账,不难。

到这一步,机器人的”神经”和”内耳”你就摸清了。回头看这两篇连起来的路子:上一篇《软件人学电子电气》讲的是单个电机怎么被驱动(电压、电流、PWM、FOC),这一篇讲多个节点怎么通信、怎么感知、怎么达成一致。合起来就是——机器人怎么感知自己、怎么把信号传来传去

缺的最后一块是”大脑”:运动学和控制,怎么把感知到的姿态和目标,变成每个关节该转多少、该出多大力。旋转矩阵、四元数、正逆解、状态空间——那是下一篇的事。

给你一个落地的次序建议:先动手,再补理论。别等把卡尔曼推导看懂了才开始——先用 vcan 收发几帧 CAN,先读一份开源 IMU 融合库把变量对上号,先把姿态在屏幕上画出来动起来。手上有了真实反馈,再回头看公式,你会发现它们只是在给你已经看到的现象记账。这块知识藏在实测里,光看是长不到脑子里的。

C code80.ai · AI 编码 API 聚合 Claude / GPT 多模型统一接入,稳定不限速,按量计费,几行配置接入 Claude Code。 了解一下 ›

抢沙发

评论前必须登录!

立即登录   注册