Microduck 的深度感知:8×8 像素的 ToF 传感器怎么变成鸭子的眼睛
做过机器人感知的人看到 8×8 这个分辨率可能会笑。这是什么?一个能看清什么的矩阵?64 个像素能干什么?但 Microduck 的头部藏着一颗 ST 的 VL53L5/CX 飞行时间传感器,就靠这 64 个像素,鸭子能知道你把手伸过来了、前面有堵墙、或者自己正对着镜子。今天拆开看这个最小的感知子系统怎么工作。
为什么不是相机
Microduck 的主控板 RK3566 上跑着七个守护进程,其中 mediad 管相机和 WebRTC。相机能拍到 720p 的画面,做物体检测、远程监控都没问题。那为什么还要一个 8×8 的 ToF?
答案是延迟和独立性。相机走的是 mediad 的 GStreamer 管道,编码、传输、检测一整套下来,延迟在 100 毫秒级别。ToF 传感器不一样,它直接测光飞行时间——发射一束红外光,等它回来,算时间差乘以光速除以二。8×8 的矩阵一次测量就是 64 个距离值,不需要编码、不需要推理,从硬件到 socket 就是几毫秒的事。
更关键的是总线独立性。ToF 传感器不走 Dynamixel 那条 UART,也不走 USB 相机那条路。它坐在 HAT 板的 I²C 总线上(beta 板用 SPI),跟音频 codec 共享一条总线但物理隔离于舵机控制。这意味着即使 robotd 挂了、相机管道崩了、网络断了,tofd 还在跑,深度数据还在发布。
tofd:七守护进程里最特立独行的那个
Microduck 的架构文档里对 tofd 的描述很有意思:”publishes, answers nothing”。它发布数据,不回答任何请求。这在七个守护进程里是独一份。
其他守护进程都是 JSON-RPC 2.0 的服务端——你问它答,robotctl 发请求它响应。tofd 不这么干。它打开 /run/tofd/tof.sock 这个 unix socket,然后以固定频率往外推 tof.stream 消息。没人订阅它也推,有人订阅它也推。它不关心谁在听。
这个设计是有意的。tofd 的工作是持续读传感器、发布深度帧。读传感器的过程涉及 I²C 通信和固件交互——VL53L5/CX 上电时需要上传大约 90 KB 的固件到传感器内部,这个过程需要几秒钟。如果 tofd 跟其他守护进程一样是请求-响应模式,每次有人要数据才去读传感器,冷启动延迟会让调用方超时。持续推送意味着数据永远是热的,订阅者来了就能立刻拿到。
90 KB 固件和 I²C 总线共享
VL53L5/CX 是 ST 的多区飞行时间传感器。芯片本身内置了一个微控制器,负责管理 SPAD(单光子雪崩二极管)阵列和时间数字转换器。但这个微控制器上电后没有固件——需要主机通过 I²C 把固件上传进去,大约 90 KB。
90 KB 在 I²C 总线上是什么概念?I²C 标准模式 100 kHz,一个字节需要 9 个时钟周期,理论传输速度约 11 KB/s。实际用 fast mode 400 kHz 的话大约 44 KB/s。90 KB 需要约 2 秒。这就是架构文档里说的”bringing a VL53L5/8CX up uploads ~90 KB of firmware over I²C taking seconds”。
这几秒钟是 tofd 必须独立于 robotd 的核心原因。想象一下如果 tofd 是 robotd 里的一个线程:robotd 启动时初始化 ToF 传感器,上传 90 KB 固件需要 2 秒,这 2 秒里 I²C 总线被占住。而 I²C 总线上还挂着音频 codec——HAT 板的设计让两者共享总线。2 秒的总线占用可能打断音频初始化。更糟糕的是,如果 ToF 传感器初始化失败需要重试,一个无限重试的 I²C 操作挂在控制电机的主进程里,随时可能把 50 Hz 循环卡住。
把 tofd 拆出来,问题消失了。tofd 启动时慢慢上传固件,失败了重试,不影响任何其他守护进程。传感器没装?tofd 照常启动,报告说没有传感器,然后安静地待着。架构文档原话是”A board with no sensor fitted runs it anyway and says so”——没装传感器的板子也跑 tofd,只是它会告诉你没有数据。
8×8 矩阵的数据格式
VL53L5/CX 的 8×8 矩阵覆盖一个约 45°×45° 的视场角。每个像素返回一个距离值,单位毫米,有效测量范围大概 4 厘米到 4 米。64 个距离值组成一帧,tofd 把这帧数据序列化后推到 socket 上。
距离值的精度不是均匀的。近距离(4 厘米到 50 厘米)精度较高,大约 ±5 毫米。远距离(1 米以上)精度下降,受目标反射率和环境光影响。在强日光下,红外干扰会让远距离测量基本失效——这是所有 ToF 传感器的通病,不是 VL53L5/CX 独有。
8×8 的分辨率意味着每个像素覆盖的角度大约 5.6°(45°÷8)。一个人的手在 30 厘米远处大概占 10×10 厘米,对应传感器上的 2×2 个像素。一只鸭子的大小?在 50 厘米外大概占 1×1 个像素。所以 Microduck 能检测到”面前有个东西”,甚至能分辨”那是一只手还是一个杯子”,但别指望它认出”那是谁的手”。
robotctl monitor 里的 ToF 可视化
cheatsheet 里描述了 tofd 数据在 robotctl monitor 里的可视化方式,这是整个系统里最有意思的 UI 设计之一。
当 ToF 传感器在推数据时,monitor 里会在鸭子 3D 模型的同一个场景中画出它看到的东西。黄色点表示检测到物体,绿色点表示地面。关键细节是:这些点会跟鸭子自身的 3D 模型做深度测试(depth-tested),如果一个点在鸭子嘴巴后面,它会被嘴巴挡住。这个设计解决了一个很实际的问题——”传感器看到的是我的手,还是它自己的嘴巴?”。没有深度测试,你只能看到一堆点,分不清哪些是外部物体哪些是鸭子身体部位。
按 t 键可以打开 ToF 矩阵的原始视图——就是 8×8 的网格,每格显示距离值。这个视图在调试传感器问题时有用:如果某个像素一直返回 0 或 65535,说明那个方向的 SPAD 可能坏了或者被遮挡了。
谁在消费 tofd 的数据
架构文档说”mediad and robotd read it”。但仔细看 robotd 的控制循环,50 Hz tick 里没有任何地方读 tofd 的数据。观测向量是 61 维,全是关节角度、关节速度、IMU 和命令——没有深度信息。
这意味着当前版本里,tofd 的数据主要消费者是 mediad。mediad 拿到深度帧后可以做一些基础的感知任务——比如检测前方障碍物,或者在远程监控画面上叠加深度信息。但深度数据不参与运动控制决策。策略网络不知道前面有墙,它只管走。避障逻辑如果有的话,是在更高层做的——mediad 检测到障碍物后通过 robot.move 指令让 robotd 减速或停止。
这跟 Microduck 的 sim2real 设计一致。训练时策略没有深度输入,部署时也不应该依赖深度输入。ToF 传感器是”锦上添花”的感知能力,不是步态控制的核心输入。如果你把 ToF 传感器拆掉,鸭子照样能走路、摔倒、恢复——只是不知道前面有什么。
tofd 为什么不属于 mediad
架构文档里专门解释了这个问题。看起来 ToF 传感器跟相机一样是”感知”,归 mediad 管不是很自然吗?Pollen Robotics 的回答是:”depth is a sensor on a bus, not a media pipeline”。
这个区分很有意思。相机是媒体管道的一部分——采集、编码、传输、渲染,整条链路跟视频处理强相关。ToF 传感器不是。它就是一个 I²C 设备,读出来的是距离数组,不需要编码也不需要渲染。把它塞进 mediad 的 GStreamer 管道里会增加耦合:mediad 崩了 tofd 就没了,tofd 初始化慢会拖慢 mediad 启动。
更深层的考虑是”useful long before there is a camera to annotate”。Microduck 不是每台都装相机,但 ToF 传感器可能是标配。一个没有相机的鸭子仍然能用 ToF 做基础避障。如果 tofd 寄生在 mediad 里,没相机的鸭子就没法用 tofd。
tofd::link:I²C 还是 SPI
架构文档里提到 tofd 通过 tof::link 配置总线类型。HAT 板用 I²C,beta 板用 SPI。
I²C 的优势是总线共享——ToF 传感器跟音频 codec 共用一条 I²C 总线,省了一个接口。代价是带宽和独占性。I²C 是半双工的,一个设备传输时其他设备必须等待。90 KB 固件上传的 2 秒里,音频 codec 不能通信。
SPI 的优势是全双工和更高带宽。VL53L5/CX 的 SPI 接口可以跑到 3 MHz,上传 90 KB 固件不到 0.3 秒。但 SPI 需要独占一条总线,每个设备需要自己的片选线。beta 板用 SPI 大概是因为开发时遇到了 I²C 带宽或共享问题。
配置文件里 [head_imu] 这个 section 在 zero3 板上归 tofd 管,在 beta 板上归 robotd 管。这说明不同硬件版本上传感器总线的分配不一样——zero3 的 ToF 传感器和 IMU 可能共享某些 I²C 资源,而 beta 板的 IMU 走了独立通道。
对复刻者的实际意义
如果你在复刻 Microduck 或者设计类似的小型机器人,tofd 的设计有几个直接参考:
传感器守护进程要独立。 不管你的传感器是什么——ToF、超声波、激光雷达——给它一个独立的进程。不要把传感器初始化塞进控制循环所在的主进程。固件上传慢、I²C 重试、传感器热插拔这些情况都需要独立处理,不能拖累 50 Hz 的电机控制。
发布-订阅优于请求-响应。 传感器数据的消费者可能不止一个——robotd 要读、mediad 要读、未来的感知模块也要读。发布-订阅模式让消费者来去自由,传感器进程不需要知道谁在听。
低分辨率不是没用的。 8×8 看起来可怜,但对”前方有没有障碍”这种二值判断足够了。64 个像素覆盖 45° 视场,1 米内的物体至少能占 3×3 个像素,检测存在性绰绰有余。不要因为分辨率低就放弃深度感知——它的延迟和独立性比分辨率重要得多。
总线选择影响启动时间。 I²C 的 2 秒固件上传在产品里是可以接受的——tofd 跟其他守护进程并行启动,2 秒内完成初始化。但如果你的传感器固件更大、或者总线上有延迟敏感设备,SPI 是更安全的选择。
数据来源
- GitHub: pollen-robotics/microduck — docs/design/architecture.md(七守护进程架构、tofd 发布-订阅设计、I²C/SPI 总线配置、VL53L5/CX 90KB 固件上传)
- GitHub: pollen-robotics/microduck — docs/design/robotd-design.md(robotd 控制循环、总线独占权、tofd 独立于 robotd 的原因)
- GitHub: pollen-robotics/microduck — docs/robot/cheatsheet.md(robotctl monitor ToF 可视化、tof.stream socket、t 键打开 ToF 矩阵、depth-tested 渲染)
- ST VL53L5/CX 数据手册(8×8 SPAD 阵列、45° 视场、I²C/SPI 接口、90 KB 固件)
