尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多摄像头时间戳同步实战:从硬件触发到驱动打点的完整排查指南

多摄像头时间戳同步实战:从硬件触发到驱动打点的完整排查指南 调试双目相机的时候我碰到过一个非常隐蔽的问题左右两路图像单独看都正常但一旦把两路合成深度图物体边缘总是有一条明显的错位。去查两路的时间戳发现每一帧的差值在十几毫秒到几十毫秒之间来回跳。第一次遇到这种情况第一反应是接线没接对毕竟多摄像头系统里同步信号没接好就会这样。可反复确认了硬件触发信号也没问题时间戳依然对不齐。后来才意识到问题根本不在“信号有没有接”而在于“时间戳到底是在哪一刻被记录下来的”。这篇文章就把我在多摄像头同步这件事上踩过的坑、排查链路和最终确认有效的做法完整写出来希望对正在做双目、三目以及环绕系统的朋友有帮助。适合遇到类似“图像没毛病、时间戳很离谱”问题的嵌入式开发者、机器人工程师也适合准备在方案阶段就把同步设计进去的软硬件团队。1. 先弄清楚时间戳记录的到底是哪一刻从sensor曝光到应用拿帧的完整链路1.1 一个画面从“光子打到感光面”到“应用读到时间戳”要经过多少步“时间戳对不齐”这句话大多数人一开始都没说清楚这个时间戳到底是sensor曝光的物理时刻还是驱动收到中断的时刻还是应用程序从buffer队列里拿到这一帧的时刻这三者之间的差别就是所有“对不齐”问题的源头。拿最典型的V4L2摄像头链路来说一帧画面从物理世界进到应用层要走完下面几步sensor内部完成逐行曝光逐行读出像素读出一整帧后sensor通过VSYNC/FSYNC引脚向host输出一个帧同步信号也可能是曝光开始信号host收到这个信号触发GPIO中断驱动在中断上下文里记录一个时间戳并随buffer送到上层应用通过DQBUF把帧从驱动队列里取出读到的“时间戳”字段就是驱动在第二步填进去的。这里最关键的问题在于驱动到底是在哪一步打的时间戳是VSYNC这个硬件事件发生的瞬间还是中断真正被执行到的瞬间还是帧被应用取走的瞬间把三种时间分别叫作“物理时刻”、“中断时刻”和“应用时刻”误差量级完全不同排查方向也完全不同。1.2 三种“时间”的误差量级根本不是一码事实际测试中如果驱动在VSYNC中断里调用内核时间函数打时间戳那么“物理时刻”和“中断时刻”之间的差通常只有几微秒到几十微秒取决于中断优先级、CPU负载、中断被其他irq抢占的程度。这个误差对大多数视觉应用是可以接受的。但如果驱动偷懒在DQBUFdequeue的时候才去补时间戳问题就大了。DQBUF返回之前buffer可能已经在驱动的done队列里排队等待从帧可用到应用取走中间可能隔着几十毫秒甚至更久——帧率低、队列深、应用处理慢的时候尤其明显。这等于把“帧捕捉时间”变成了“取帧时间”和真实曝光时刻的关系基本靠运气。还有更隐蔽的情况某些驱动把“sensor配置曝光参数的时刻”当成时间戳或者把“host发起CSI接收的时刻”当成时间戳。这些时刻在单路摄像头下看起来没什么问题因为误差是恒定偏移但多路摄像头之间各自的偏移方向和大小不一样时间戳自然也就对不齐了。提示拿到一个“对不齐”的问题第一件事不是去怀疑sensor而是去看驱动代码里时间戳字段是在哪个函数里填的。这一步能帮你排除至少一半的干扰项。1.3 滚动快门和全局快门对时间戳的要求完全不同sensor分成全局快门Global Shutter和滚动快门Rolling Shutter两大类它们对时间戳的语义要求差别很大。全局快门所有像素同时开始曝光、同时结束VSYNC通常能比较精确地对应曝光开始或结束的物理时刻直接拿VSYNC中断时间当时间戳误差就是中断延迟那几微秒。滚动快门则是一行一行来第一行先开始曝光后面每行延迟固定时间直到最后一行的曝光结束才算是整帧完成。通常720p的sensor一行延迟大约10~20微秒整个帧扫描周期就是行数乘这个值能达到小几十毫秒。你拿到一个VSYNC它到底对应的是第一行开始曝光还是最后一行曝光结束如果是后者那画面的“中间行”实际曝光完成的时间比VSYNC早了差不多半个帧读出时间——在无补偿的情况下这相当于每帧都有一个几毫秒的系统性偏移。多路摄像头一起工作的时候如果每路sensor的曝光时间、行数、读出速度不一样这个偏移量每路都不相同。这就是“明明信号都接了时间戳还是歪”的一个常见根源。解决办法后面在驱动层那一部分会给出“曝光中间点补偿”的具体做法。2. 时间戳对不齐的三大根源时钟源、打点时机与语义错位2.1 各路摄像头使用了不同的时钟域先看时钟源。摄像头时间戳要能互相比较前提是它们都运行在同一个时钟域里。很多多路系统实际是这么接的两路sensor挂在同一个ISP上看起来共用一块板子但驱动一个用CLOCK_MONOTONIC打时间戳另一个却用了CLOCK_REALTIME。如果系统里有人在跑NTP同步或者手动改过系统时间CLOCK_REALTIME会跳变CLOCK_MONOTONIC不会两路一对比差值直接飞掉。还有更隐蔽的情况一路摄像头挂在SoC的CSI接口上另一路是USB UVC摄像头。UVC摄像头的时间戳由USB host控制器生成它基于的时钟域可能和SoC内部CSI的时钟域不是同一个。在系统里表现为两路时间戳各自单调递增但相对偏移会轻轻松松漂移掉几毫秒。要修这种情况光改驱动没用得先在软件层把两个时钟域统一起来。要求更高的系统CPU的本地时钟都嫌不够精确通常会引入一颗支持PTP/PTP4L的网卡或者外置时钟芯片作为整个系统的“共享时钟基准”所有sensor的中断打点都换算到这个硬件时钟域里。如果板子上没有这类硬件至少要在驱动层明确统一使用CLOCK_MONOTONIC这是最基础、成本最低的一步。2.2 中断打点的实际延迟会比你想的大得多我见过不少驱动代码里确实是在ISR里打时间戳但时间戳还是对不齐。这时候要看你是在哪个“IRQ上下文”里打的。如果一个驱动把vsync中断注册成request_threaded_irq并且主要逻辑放在threaded handler里那么从硬件信号到线程真正跑起来中间可能已经过了几百微秒。系统里如果有SD卡、网络、USB等高优先级中断在抢CPU几百微秒甚至几毫秒都是有的。如果每路摄像头的中断服务线程受到的调度竞争不一样各路之间的相对时间戳就会出现肉眼可见的抖动。一个经验法则时间戳要打就打在硬中断hardirq里调用内核时间函数的地方不要在threaded irq或者workqueue里补打。如果ISR里确实来不及做完整处理可以在ISR里先记录一个“事件时间”把buffer处理放到后面千万别反过来等处理完buffer再来取当前时间当作帧时间。另外Linux内核里有些时间API在不同版本上行为不一样。老内核里do_gettimeofday给的是CLOCK_REALTIME新内核推荐用ktime_get_ts64或ktime_get_nsCLOCK_MONOTONIC。升级内核之后同一套驱动代码打出来的时间戳时钟域可能悄悄变了这也是升级后同步忽然坏掉的常见原因。2.3 驱动把“dequeue时间”当成“曝光时间”是最容易被忽视的语义错位这个坑尤其多。V4L2的struct v4l2_buffer里有一个timestamp字段规范要求驱动把它填成“帧被捕获的时刻”。但不少驱动实现为了省事在DQBUF那一刻才取系统时间填进去。单路摄像头无所谓多路就会出大问题一路buffer在队列里多等了一帧另一路立即被取走时间戳直接就差了一个帧周期。这个帧周期级别的误差远大于中断延迟的微秒级误差也是最容易让时间戳“看起来完全对不上”的元凶。排查这个问题的办法很简单在应用里打印每一帧的时间戳差值如果发现时间戳差值经常等于整数倍的帧间隔比如30fps约33.33ms基本可以断定驱动把dequeue时间当成了捕获时间。正确的做法是在采集线程里通过内核vb2框架把中断里生成的时间戳随buffer一起送到应用而不是在DQBUF读取时再去现取一个时间。3. 硬件同步方案主从模式、外部触发以及为什么“信号接了还是歪”3.1 主从模式让一路sensor的VSYNC去触发另一路很多sensor比如车载上常用的AR0144、AR0233以及OV系列的不少型号都支持外部同步触发把主sensor的VSYNC输出直接连到从sensor的FSIN/XSHUTTER/TRIGGER引脚从sensor被配置为“外部触发模式”检测到触发沿后延迟固定时间开始曝光。这样能保证两路sensor的曝光开始时刻相对固定。实际连接的时候要注意几个细节。主从sensor必须共用同一路MCLK否则各路内部PLL锁相后的行/场时钟会漂移触发沿对上了行同步还是对不齐。从sensor的“触发输入到实际曝光开始”之间的延迟由寄存器里的trigger delay控制不同sensor手册给出的是名义值实际会有温漂高精度系统要实测校准。主sensor的VSYNC输出极性和从sensor的触发极性也要匹配很多sensor支持配置上升沿或下降沿触发不匹配的话从机会一直不漏触发表现为随机跳帧。主从模式的问题在于主机的VSYNC本身可能并不稳定。如果主sensor也开了自动曝光不同帧的曝光时间不同VSYNC的相位会随着曝光时间变化从机跟着这个不断变化的边沿走最终两路画面在时间上会有相对偏移。所以做主从同步最好先把曝光固定或者至少固定从机的曝光参数。3.2 外部触发同一个信号源同时给所有sensor如果系统里有FPGA、MCU或者SoC的PWM可以直接用这些模块输出一路周期性的帧同步信号同时进所有sensor的FSIN/XSHUTTER引脚。这样做比主从模式更可控因为各路sensor面对的是同一个信号源不依赖某一路的VSYNC作为基准。外部触发的频率必须和sensor配置的帧率一致。比如目标30fps触发周期就是33.33ms。注意sensor对外部触发的“恢复时间”很敏感上一帧还没读出完下一帧触发就来了sensor可能干脆不响应或造成帧读出错乱。所以在时序设计里要给足blanking gap。具体需要看sensor datasheet里的frame readout time和minimum trigger interval。信号完整性也不能忽视。帧同步信号是边沿敏感的如果PCB走线太长或者终端阻抗不匹配边沿会有回沟和振铃。多路sensor共用一条同步线时扇出电路要注意驱动能力必要时加buffer后再扇出不能直接把一个GPIO硬拖多路输入。我在一块没有做扇出缓冲的板子上就吃过亏信号到了第三路sensor那里上升沿已经严重劣化表现为偶发丢触发。后来在同步线上加了专门的时钟buffer问题才消失。3.3 接了同步信号还是歪电气与时序上的隐藏因素信号线没问题、触发沿也对齐了时间戳还是歪这时要从这些地方找。第一各路sensor的曝光命令到达时间不一致。多路sensor共用I2C总线时主机需要分时访问每一路写曝光寄存器的时间点不同。如果sensor是“接收到曝光命令后异步开始曝光”的那即使有同步线曝光起始的绝对时刻也是错开的。解决办法是全部改到frame sync信号触发曝光配置完参数等同步信号来统一生效。第二各路自动曝光时间不同。滚动快门sensor的时间戳和曝光时间强相关。开了AE之后每一路sensor的曝光时间都随着场景亮度变化把VSYNC当作唯一基准时间戳里就带上了曝光时间差的一半。做同步系统建议先把AE锁上或者只允许主sensor调节、从sensor沿用固定参数。第三各路sensor重启恢复速度不同。系统休眠唤醒或sensor动态上下电后各路sensor重新初始化耗时不同导致后续帧的相位发生未知偏移。这类问题在启动时序上尤其容易复现建议在初始化完成后做一次同步校正重新对准相位。4. 驱动层避坑在中断里打点、做曝光补偿、统一时钟域4.1 时间戳打点代码的正确与错误姿势用一个简化示例说明。错误姿势Adequeue时打时间戳。static int mycam_dqbuf(struct file *file, void *fh, struct v4l2_buffer *b) { // 错误b-timestamp 在这里取的是取帧时间不是曝光时间 do_gettimeofday(b-timestamp); return vb2_ioctl_dqbuf(file, fh, b); }正确姿势在vsync中断里打点并通过vb2 buffer的timestamp字段随帧走。irqreturn_t mycam_vsync_isr(int irq, void *dev_id) { struct mycam_dev *dev dev_id; u64 now ktime_get_ns(); /* CLOCK_MONOTONIC */ /* 如果sensor的VSYNC对应曝光结束做滚动快门中间点补偿 */ u64 exposure_ns mycam_get_exposure_ns(dev); u64 frame_ts now - exposure_ns / 2; dev-current_ts frame_ts; dev-vsync_seen true; /* 唤醒采集线程把它填到对应的v4l2_buffer里 */ return IRQ_WAKE_THREAD; }要强调一点ktime_get_ns在中断上下文是可用的但不要在ISR里做I2C读写、寄存器配置等耗时操作否则中断被拖住时间戳本身可能没歪其他实时任务却歪了。还有一类坑值得单独说有些驱动为了省中断资源把多个sensor注册成共享中断IRQF_SHARED。这时候ISR回调里要先判断是不是属于自己的sensor事件再打时间戳别把别人的vsync当成自己的。我见过两路摄像头共享irq的情况下时间戳周期性错位的案例排查了很久才发现是中断处理里没有做device识别。4.2 滚动快门必须做的曝光中间点补偿假设VSYNC在曝光结束时触发具体看sensor手册确认全局快门可以直接把vsync时间当帧时间滚动快门最好取画面中某一行比如中间行的曝光中点作为帧时间戳。近似公式frame_ts vsync_ts - exposure_time / 2;其中exposure_time是当前帧的曝光时间。如果开AEexposure_time每帧都变必须在每一帧曝光完成时读取当前曝光寄存器不能用固定值。这也是同步系统建议关闭AE的原因之一变量越少时间戳越稳。如果sensor手册明确VSYNC对应曝光开始那就要把公式换成vsync_ts exposure_time / 2如果sensor提供帧内行号或偏移寄存器就按实际行位置补偿。没有统一公式必须对着datasheet的sync timing章节和实测结果来调整。这个补偿值虽然不大但它是系统性误差不会因为多采几帧平均掉只能靠计算公式修正。4.3 Android HAL层的特殊坑SENSOR_TIMESTAMP到底从哪里来Android camera HAL3里一个request的timestamp也不是随便就能拿到的。规范要求HAL填sensor exposure start time但不同厂商实现不同。有的HAL在processCaptureRequest里打时间戳有的在processCaptureResult里打有的干脆填的是当前CLOCK_BOOTTIME而不是sensor帧同步信号对应的时间。如果上层同时用两路camera一个HAL实现是这样另一个是那样时间戳比较就会很混乱。排查Android多摄时间戳问题时可以先从dumpsys media.camera或camera trace log里看timestamp的分布再对照logcat里VSYNC/帧事件出现的时刻确认HAL到底是在哪个时序点填的timestamp。如果HAL是黑盒可以考虑在framework层做“曝光中间点”的估算补偿但这毕竟是事后弥补不如在HAL驱动层修好。5. 怎么证明你的时间戳真的对齐了实测与校准方法5.1 示波器和GPIO翻转最硬的物证在驱动ISR里所有sensor的vsync中断到来时都翻转一个GPIO。用示波器同时看外部触发信号或主sensor的VSYNC输出、每路sensor的VSYNC输出、以及驱动ISR里翻转的GPIO。四个波形拉到同一个时间轴上对比。如果外部信号和每路sensor曝光同步沿的相位差是固定的说明硬件同步没问题如果ISR里翻转的GPIO相对sensor信号有明显抖动说明驱动层面打点时机不可靠需要优先修驱动。很多SoC的GPIO翻转也受总线延迟影响但作为纳秒级到微秒级问题的粗筛已经足够。如果连微秒级都要较真就要上逻辑分析仪或者用SoC的硬件timestamp引脚来记录vsync计数。5.2 拍可控LED或机械秒表软件侧验证硬件物证有了再从软件端验证最终时间戳。做一个高亮LED用单片机的定时器让它以精确的周期亮灭比如每秒亮暗切换一次或者每一帧亮1ms。把LED放在两路摄像头都能拍到的视野里录制一段时间然后逐帧分析两路画面里LED是亮还是暗。如果时间戳完全对齐那么同一帧对应的物理时刻LED亮暗状态应该一致如果有一路差了几十毫秒就会看到LED在某一帧里一处亮、另一处暗错位帧数乘上帧周期就是时间戳偏差。这个测试建议在同步调试时第一时间跑。它不需要复杂设备一根杜邦线加一个LED加一个单片机就能完成而且能直接反映“应用拿到的帧和真实物理时刻的关系”。机械秒表也可以但秒表指针本身有视觉延迟检定到10ms级别比较困难LED法的稳定度远高于秒表。5.3 无法改硬件时的软件校准方案如果板子已经定型、没法补同步线也没法改驱动只能在算法层做后处理校准。做法是在固定场景下连续采集N帧对两路图像里的同一事件比如一个高亮LED亮起的瞬间做互相关估计得到两路之间的平均时间偏移offset然后在一个较短时间内认为这个offset稳定把从机的所有时间戳统一换算到主cam的时钟域上。这个方案能解决“恒定偏移”但解决不了“每帧随机抖动”。如果抖动来源是中断延迟和dequeue时间互相关也无法精确对齐。所以软件校准只能当最后手段真正可靠还是得从硬件同步和驱动打点入手。下面这个表格是我在实际项目里用的排查清单遇到多摄时间戳问题可以照着走一遍。检查项快速验证方法常见结果硬件同步信号连接示波器看各sensor VSYNC/FSIN信号有但相位漂移驱动时间戳打点位置代码搜索timestamp赋值语句在DQBUF里打点时钟域打印CLOCK_MONOTONIC值对比各路使用不同时钟滚动快门补偿固定曝光/关闭AE测试时间戳随曝光变化漂移Android HAL timestamp对照logcat和dumpsysHAL填的是request时间这么多年做多摄同步我的个人体会是时间戳对齐的问题90%可以在方案阶段避免10%要靠调试经验收尾。如果你也在被“时间戳对不齐”折磨先做三件事示波器量同步信号相位、关掉自动曝光、确认驱动里的timestamp赋值位置。多数情况下答案就藏在这三个步骤里。当然每颗sensor的细节都不同遇到具体型号还是先翻datasheet的sync timing章节再动手改代码。
返回列表