标签: IMU

  • 摄像头采集与 IMU 采样:SOF/EOF、18 个缓冲与轴系契约

    结论:车载视觉与惯性输入必须把图像时间、帧同步、曝光、IMU 时间和故障降级当作独立契约,不能只满足「能输出 NV12 / XYZ」。原生 camerad 与 sensord 是两个独立时钟源的生产者:logMonoTime 是发布封装时刻,不等于 IMU 中断时间或图像 SOF;源码中的 EOF 是由内核 SOF 时间加传感器读出时间推算的,不是直接测得的硬件 EOF。固定快照基线为 c8fb906,本文为静态代码阅读,未连接相机模组、I2C / GPIO 实板或路测数据。

    原生摄像头:启动到发布

    进程选择由 USE_WEBCAM 决定:置位时原生 camerad 不启用;原生程序设置 CPU 亲和性后进入 camerad_thread()。该 C++ 目标在非 Darwin 平台构建,PC / macOS 不能仅因目录存在便声称原生采集可运行。

    板级配置里广角、窄角、座舱固定 camera_num 0 / 1 / 2,各有 VisionIPC stream、焦距、发布主题和 PHY;窄 / 广角同步 SOF,座舱期望半周期错相。即使禁用某路图像,源码仍会尝试探测 / 配置传感器,以避免 strobe GPIO 电气副作用。设备初始化打开 Qualcomm req_mgr / cam_sync / ISP / ICP,订阅 SOF 事件;逐路先探测 OS04C10、再探测 OX03C10,探测失败会禁用该路。

    阶段 实现事实
    缓冲 为处理模式分配原始缓冲,并为各流创建 18 个 YUV VisionIPC 缓冲;发布时附带 frameId、SOF、EOF。
    逐帧 poll 读取 req_mgr SOF,按 session_handle 路由,验证请求 / 帧序号,等 IFE / BPS sync,处理帧后送 VisionIPC 与对应 *CameraState,再按亮度更新曝光寄存器。
    状态消息 记录帧号、requestId、SOF / EOF、曝光 / 增益及处理耗时;图像主数据走 VisionIPC,FrameData 是另一条状态消息。

    图像主数据走 VisionIPC,而 FrameData 是另一条状态消息;FD 握手、缓冲复用与通知一致性需与图像数据契约分开验证。

    同步与故障边界

    validateEvent() 对 requestId=0、帧号 / 请求号跳变重排队;waitForFrameReady() 等 IFE 最多 100 ms、BPS 最多 50 ms,失败清队列并重排,避免使用可能撕裂的帧——该恢复动作不等于图像连续性已保住。

    syncFirstFrame() 要求非错相路面相机约 0.2 ms 内、错相座舱相机在 25 ms 偏移的约 0.2 ms 内对齐。若 IFE 帧号超过 40 仍未同步,源码记录超时并将 first_frame_synced 设真,后续可继续发布;因此「有图像」不证明同步质量合格。processFrame() 用内核 SOF 时间加传感器 readout_time_ns 推算 EOF;set_camera_exposure() 根据抽样 Y 平面亮度、目标灰度和前 3 帧曝光状态调整积分时间、模拟增益及高转换增益,测光区域依赖焦距 / 输出尺寸。替换镜头 / 传感器必须重新标定焦距、ROI、曝光延迟和增益表。

    webcam:开发替代,不等价于车载模组

    Camerad.__init__() 从环境变量选择窄路面、可选广路面和驾驶员摄像头,创建 VisionIPC 缓冲;Camera.read_frames() 经 OpenCV 读取、旋转 180 度并用 PyAV 转 NV12;camera_runner() 以 20 Hz 发布。_send_yuv() 的 SOF / EOF 值用 frame_id * 0.05 s 合成,FrameData 只填帧号与单位变换;没有原生传感器 SOF / EOF、跨相机硬件同步、曝光 / 增益状态。它是开发输入替代,不足以验证车载相机时间契约。

    sensord:采样、坐标与时间

    I2C bus 1 的 LSM6DS3 加速度、陀螺仪和温度共享设备;先 reset / init,再启动 IRQ 和温度轮询线程。GPIO 边沿时间戳来自内核,用 time.time_ns - time.monotonic_ns 折算到单调时钟;时钟偏移突变超过 10 ms 时跳过当次事件并更新偏移。

    层 实现事实
    加速度 读取 data-ready 与三轴寄存器,按 ±2g / 16-bit 换算 m/s²,输出轴顺序为 [y, -x, z];没有 data-ready 时不发布。
    角速度 读取 data-ready 与三轴寄存器,按量程换算 rad/s,同样输出 [y, -x, z];与加速度共享 IRQ,但逐传感器检查可读状态。
    温度 按 2 Hz 轮询,使用单调时间并按芯片变体选温度比例。
    启动有效性 从首次调用有效性检查起,前 0.5 s 不发布有效数据;自测仅在对应环境变量触发。

    传感器插件如果改轴定义而不更新定位观测模型,会产生系统性估计误差。定位 / 模型消费端应依据观测时间戳和有效性判断新鲜度,而不是依赖发布时刻的 logMonoTime。

    验证状态与硬件任务

    test_camerad.py 标记 COMMA_HARDWARE_TEST,覆盖曝光收敛、帧连续、路面同步 / 座舱错相及故障注入;test_sensord.py 同样要求硬件,检查频率、时间戳、数值范围和中断停机。本次仅确认测试存在,未运行。自制硬件的最小验收应记录:各路图像 SOF / EOF / 帧号、读出时间、丢帧及重排次数、测光 / 曝光响应;IMU IRQ 时间、轴向 / 量程、启动稳定、掉线和时钟跳变。再以回放、台架和实板测试逐项比较,不以 webcam 或仿真通过替代。

    常见问题

    「有图像」为什么不能说明相机同步合格?

    因为 syncFirstFrame() 在 IFE 帧号超过 40 仍未同步时,源码记录超时并直接把 first_frame_synced 设真,后续可继续发布。允许发布不等于对齐质量达标,需记录实际对齐误差与重排次数。

    图像 EOF 是硬件测得的吗?

    不是。processFrame() 用内核 SOF 时间加传感器 readout_time_ns 推算 EOF,文中 EOF 并非直接测得的硬件 EOF。新相机插件必须提供时间域、读出时间、请求序列及同步质量,不能把采集完成时刻冒充 SOF。

    IMU 的 logMonoTime 就是采样时间吗?

    不是。logMonoTime 是发布封装时刻,不等于 IMU 中断时间;GPIO 边沿时间戳来自内核再折算到单调时钟。定位 / 模型消费端应依据观测时间戳和有效性判断新鲜度。

    IMU 轴顺序为什么重要?

    因为 LSM6DS3_Accel 与 LSM6DS3_Gyro 都输出 [y, -x, z]。传感器插件若改轴定义而不更新定位观测模型,会产生系统性估计误差;新硬件必须显式定义启动稳定时间、量程、轴系和异常状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_IMU_DEEP_DIVE.md(取用了摄像头启动到发布、同步 / 故障边界、webcam 替代路径与 sensord 采样轴系分析)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • LSM6DS3 采样契约:104 Hz 与第一条 IRQ 的取舍

    结论:LSM6DS3 采集链的风险不在「读不到数」,而在三处静默丢样本与一处未校准语义:一次 os.read() 可能带回多条 GPIO 事件却只处理第一条;启动有效性门从第一次有效性检查起计 0.5 秒,期间样本读取成功但不发布;source 在 ID 核对前是 velodyne 占位值。以下基于固定源码快照 c8fb906 的静态分析与假 SMBus/GPIO 探针,未打开 GPIO/I2C、未在真实芯片上执行自测。

    实例、总线与启动顺序

    sensord.main() 实例化加速度、陀螺仪、温度三个传感器对象,都指向 I2C bus 1,并以 0x6A 地址访问同一颗 LSM6DS3。每个对象通过 Sensor.__init__() 独立打开 SMBus,寄存器访问经 Linux ioctl,最多读 32 字节块。main() 先逐对象 reset、再逐对象 init;温度类不覆盖 reset,所以真正写复位的是加速度和陀螺仪两个对象。

    init() 接受 WHO_AM_I 0x69/0x6A,据此把 source 设为 LSM6DS3 或 LSM6DS3TR-C;加速度设置 104 Hz、默认 ±2g、自动地址递增和 INT1 数据就绪,陀螺仪设置 104 Hz、默认 ±250 dps 和 INT1 数据就绪。三个对象共享芯片寄存器,后续适配不能把它们当作彼此独立的物理器件热插拔。

    初始化异常只记录日志,不从 sensors_cfg 中移除该对象,IRQ 线程仍会尝试读取它。自测也不一致:加速度仅在 LSM_SELF_TEST == "1" 时运行,陀螺仪在变量存在时运行、即使值是 0 也会触发;自测中的 _wait_for_data_ready() 没有总超时,芯片不产生 data-ready 时初始化可长时间停在忙等。

    中断读取:只处理第一条事件

    gpiochip_get_ro_value_fd() 请求 GPIO chip 0、line 84 的双边沿事件;interrupt_loop() 轮询 100 ms,并尝试把 IRQ 336 或 335 绑定到 CPU 1。代码把 time.time_ns() - time.monotonic_ns() 视为内核 GPIO 事件时间到单调时间的偏移,每次中断重新估计,偏移跳变超过 10 ms 时跳过本次事件并更新基准——这个换算依赖内核事件时间戳的时钟域,不能在新内核/驱动上未经核对就复用。

    关键取舍在 os.read():它一次请求最多 16 * sizeof(gpioevent_data) 字节,但随后只调用一次 from_buffer_copy(dat) 并处理第一条事件。若内核实际返回多条完整记录,其余事件已从 fd 读走却不进入逐传感器处理,可造成样本稀疏或时间间隔变大。IRQ 还是双边沿,代码没有按 gpioevent_data.id 区分上升/下降沿。

    构造输入 观察到的原函数路径 未验证范围
    一次读取串接两个 GPIO 事件,时间戳 9100/9200 ns,假时钟偏移 9000 ns 传给传感器的时间只有首条的 100 ns,发布一条消息;第二条未进入处理循环 真实 GPIO 驱动是否一次返回多条、积压率及时间戳时钟域
    返回不足一条 gpioevent_data 的字节 from_buffer_copy() 抛 ValueError,异常发生在逐传感器 try 外 真实内核短读条件与线程监督/重启行为
    温度传感器连续三次未过启动有效性门 三次读取均执行,但 Ratekeeper.keep_time() 调用数为零 假 Ratekeeper 不测 CPU 占用或 I2C 物理总线频率

    样本时间与 0.5 秒有效性门

    Sensor.is_data_valid() 从第一次调用有效性检查起计 0.5 秒,而不是从供电或 init() 开始;在此期间样本读取可能成功但不发布。温度由 polling_loop() 按 SERVICE_LIST 的 2 Hz 轮询,LSM6DS3_Temp.get_event() 在读取时以单调时钟自行打时间戳,并按芯片变体使用 16 或 256 LSB/°C。

    启动 0.5 秒内温度循环的 continue 位于 Ratekeeper.keep_time() 之前,所以该短阶段可能密集轮询 I2C;不能把它的初期调用频率视为稳定 2 Hz。

    数值换算与上下游健康门

    每次合格 GPIO 事件用同一个 ts 依次查询加速度和陀螺仪:加速度先检查 XLDA,再读 6 字节三轴有符号 16 位值,按 9.81*2/32768 转 m/s²;陀螺仪检查 GDA 后按 8.75 mdps/LSB 转 rad/s。两者都输出 [y, -x, z],并把换算后的中断时间填入 SensorEventData.timestamp;DataNotReady 被静默跳过,其他 I2C/消息异常只记录。

    构造输入 原类观察结果 边界
    16 位原始值 0xFFFF、0x7FFF、0x8000 分别解析为 -1、32767、-32768 只验证主机有符号转换,不验证芯片实际量程
    首次有效性检查从 t=10.0 开始 恰好 t=10.5 仍为假,超过 0.5 秒才为真 假单调时钟,不证明 IRQ 与初始化时序
    WHO_AM_I 正确、错误与空块 正确 ID 返回;错误 ID 触发断言;空块在 [0] 抛 IndexError 若 Python 以 -O 运行,assert 保护可被移除
    加速度状态未就绪与三轴原始字节 未就绪抛 DataNotReady;就绪后按 [y,-x,z] 输出,示例 [-9.81,-9.81,0] m/s² 假总线提供完整六字节,短块与真实安装方向未验证
    陀螺仪 +1000,-1000,0 与温度原始 16 轴序为 [y,-x,z];两芯片来源分别得 26.0 与 25.0625 °C 假消息只确认计算路径,不验证温度标定

    下游 locationd.handle_log() 对加速度/陀螺仪先比较事件内时间与消息 logMonoTime,相差超过 100 ms 时忽略,再核对来源与数值一致性;sensor_all_checks() 要求两路消息有效且最近 0.1 秒有接收。source 在 ID 核对前默认是 velodyne 占位值,而 locationd._validate_sensor_source() 仅排除 bmx055,因此初始化失败后的来源健康不能仅凭消息 valid=True 或该来源门判定。

    几处探针细节补充了这一层的取舍:不足一窗时仍会发送原始音频,两个半窗合成一次分析,一次超大回调只留下尾部。Sensor.is_data_valid() 的 0.5 秒是时间门而非数值门,而主机 SMBus 在回包长度为零时会回退请求长度、传感器层不会独立检查数据是否来自新的物理样本——因此传输成功、数据就绪、样本新鲜度与数值有效性应分成不同状态。

    常见问题

    为什么一次中断可能丢掉样本?

    因为 os.read() 一次最多请求 16 条事件结构,但代码随后只处理第一条;其余完整记录已从 fd 读走却不进入逐传感器处理,表现为样本稀疏或时间间隔变大。

    启动后前 0.5 秒为什么没有样本发布?

    Sensor.is_data_valid() 从第一次有效性检查起计 0.5 秒,而不是从供电或 init() 开始;在此期间样本读取可能成功,但不会发布。

    加速度就读绪了,为什么还可能是无效来源?

    source 在 ID 核对前默认是 velodyne 占位值,而 _validate_sensor_source() 只排除 bmx055;初始化失败后的来源健康不能只靠消息 valid=True 或该来源门判断。

    自测失败会自动恢复芯片寄存器吗?

    不一定。探针确认:差值低于阈值且两轮读取正常时,实现会先写寄存器关闭自测/采样再抛异常;但若中途 I2C 读取异常,关闭写入没有执行,且实际芯片是否保留测试状态尚未验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/IMU_IRQ_I2C_SAMPLE_CONTRACT_DEEP_DIVE.md(LSM6DS3 中断、I2C 样本与定位健康契约,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_sensord_loop_contract.py(中断/温度循环三项假依赖探针)
    • 项目 02 —— openpilot-research/probes/test_imu_sensor_conversion_contract.py(数值换算与异常五项隔离探针)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11