结论:仿真感知链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。发布时戳不是物理曝光时戳——仿真把同一个 logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF;重复样本也不等于传感器频率——主循环每圈发 5 条加速度、5 条陀螺仪与 10 条 GPS。所有频率推导均以 SimulatorBridge 主循环达到标称 100 Hz 为前提,实际频率未测。固定快照基线为 c8fb906。
图像从 MetaDrive 到 VisionIPC
MetaDriveBridge.spawn_world() 为窄路相机及可选广角相机配置 RGB 图像源,分辨率取 W=1928, H=1208。MetaDriveWorld 用 multiprocessing.Array 暴露两块 RGB 内存;metadrive_process() 把渲染图像写入共享数组后对 image_lock.release() 计数;桥接侧另有目标 20 Hz 的相机线程,在 send_camera_images() 开头 acquire() 一次,再把当前数组转为 NV12 并发布。
这里的 image_lock 实际是初值 0 的 multiprocessing.Semaphore,用于等待至少一帧,而非读写互斥:生产者写图前不获取它,消费者转换 RGB 时也不阻止下一次写入。因此静态上存在读取一幅图像过程中被生产者覆盖的窗口;若生产速度超过消费者,semaphore 计数积压,消费者可能对同一块「最新图像」连续取样。是否出现撕裂或重复帧需要像素哈希 / 帧号探针验证。
| 环节 | 实现事实 |
|---|---|
| 色彩转换 | rgb_to_nv12() 用整数 BT.601 近似系数生成 Y,2×2 box filter 后交错 U / V;要求偶数宽高;调用方固定断言 1928×1208×3 且 uint8。 |
| 缓冲 | Camerad.__init__() 为每路建立 5 个 VisionIPC 缓冲;窄路总建,广角按 dual_camera 决定。 |
| 发布 | _send_yuv() 先构造外层 Event.valid=True 的 *CameraState,把同一个 dat.logMonoTime 同时当作 SOF / EOF,再发布独立递增的仿真帧号和单位变换矩阵。 |
| 有效位 | 底层 VisionIpcServer.send() 另把 extra.valid 固定为 False;CameraState 外层有效位与 VisionIPC extra 有效位不同。 |
三项原类探针确认单路只分配窄路五个缓冲、双路帧号分别从零递增、图像发送先于状态消息发布,且每帧 SOF = EOF。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 的色彩、曝光或 DMA 缓存行为。探针还确认:若假 VisionIPC 已接受图像而假 PubMaster.send() 抛错,cam_send_yuv_road() 尚未递增帧号;调用方若捕获异常并再次调用,会用同一帧号发送另一份图像、但产生新时间戳——这是函数边界的部分提交状态,不证明现有桥接线程会自动重试。
IMU、GPS、驾驶员与外设占位
SimulatorBridge._run() 以目标 100 Hz 的主循环调用 SimulatedSensors.update(),另起目标 20 Hz 相机线程。每次 update() 中,send_imu_message() 连续发布 5 条加速度和 5 条陀螺仪消息,数据都取同一份 SimulatorState.imu,各条的内层 timestamp 使用各自消息的 logMonoTime,并非模拟器采样时刻。
| 消息 | 仿真节拍与标称值 |
|---|---|
| IMU(加速度 / 陀螺仪) | 每圈各 5 条;100 Hz 主循环下约 500 条 / 秒 / 主题;服务表标称 104 Hz 不是实际硬件节拍。 |
| GPS | 仅在 simulator_state.valid 时,每圈发布 10 条 gpsLocationExternal,约 1000 条 / 秒;服务表标称 10 Hz。 |
| 驾驶员状态 | update() 以 DT_DMON/2 检查驾驶员消息,以 0.25 秒检查外设状态,并非严格 20 / 4 Hz 定时器。 |
send_gps_message() 把当前系统墙钟填成 unixTimestampMillis、固定 flags=1、精度常量和 source=ublox;这些重复包不能用于证明 GNSS 热启动、时钟校准、观测协方差或实际链路吞吐。send_fake_driver_monitoring() 把两侧脸概率置 1、分心置假、注意力置满;driverMonitoringState 外层显式有效,而 driverStateV2 使用 new_message() 默认外层 valid=False。不可把这些「健康」字段当作自研传感器或驾驶员监测的物理验证。
假时钟探针还确认两个节拍门都使用严格 >:恰好到 0.25 秒时不发外设状态,到 0.251 秒才发;只有发送后才更新对应的上次发布时间。无效仿真状态并不阻止驾驶员 / 外设占位消息,且 driverStateV2.valid=False 与 driverMonitoringState.valid=True 会同时出现。
退出、测试和插件化边界
仿真启动脚本 launch_openpilot.sh 设置 NOBOARD=1、SIMULATION=1,屏蔽真实 camerad、encoderd、loggerd 等进程。主循环、相机线程和 MetaDrive 子进程的生命周期分散在桥接层;相机线程的 Semaphore.acquire() 没有超时,模拟器不再产帧时单靠线程 exit_event 不能立即唤醒它。现有 test_driving() 被 unittest.skip 标记,不能据此声称当前闭环已通过。
自研硬件插件应分别验证图像采样帧号、读出时戳、消息外层 / 内层有效位、stride 与缓冲持有期,以及 IMU / GNSS 的独立采样时刻和真实频率。建议先在隔离命名空间记录每路源帧号、共享内存哈希、发布时戳和消费时戳,再注入掉帧 / 停发 / 速率偏差。
常见问题
仿真里 SOF 和 EOF 为什么不等于曝光时间?
因为 _send_yuv() 把同一个 dat.logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF,表示「在软件发布时打的同一时刻」,不是传感器曝光 / 读出测量。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 行为。
「CameraState 有效」和「图像有效」是一回事吗?
不是。仿真里 *CameraState 外层 Event.valid=True,而底层 VisionIpcServer.send() 把 extra.valid 固定为 False。两者是不同层级的有效位,必须分别检查。
仿真里 IMU 和 GPS 的频率是多少?
取决于主循环实际频率。每次 update() 发 5 条加速度、5 条陀螺仪与 10 条 GPS;若主循环确为 100 Hz,则约 500 条 / 秒 / IMU 主题、约 1000 条 / 秒 GPS,而服务表标称 IMU 104 Hz、GPS 10 Hz。重复样本不等于传感器频率。
仿真通过能代替传感器标定吗?
不能。仿真链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。若要验证自研硬件,应分别记录源帧号、共享内存哈希、发布 / 消费时戳,并注入掉帧、停发与速率偏差。
延伸阅读
- 相机请求与缓冲生命周期:谁持有那一帧 —— 与本文 VisionIPC 缓冲持有期对照。
- IMU 中断与 I2C 采样契约:data-ready 与时间戳 —— 解释真实 IMU 采样时刻与仿真占位的差别。
- 实车测试验收:日志时间与相机模型契约 —— 承接仿真与实车的时序对照。
- GNSS UBX 二进制解析:从字节到定位消息 —— 说明仿真 GPS 占位与真实链路差异。
参考资料
- 项目 02 ——
openpilot-research/SIM_CAMERA_SENSOR_TIMING_DEEP_DIVE.md(取用了图像缓冲与 semaphore 行为、IMU / GPS 仿真节拍、有效位与插件化边界分析)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复