1. 为什么需要多路相机同步曝光?

在实际的机器视觉和自动驾驶项目中,我们经常需要同时使用多个相机从不同角度捕捉同一瞬间的场景。比如在立体视觉系统中,左右相机必须严格同步曝光才能计算准确的深度信息;在高速运动分析中,多路相机需要同时抓拍才能还原完整的运动轨迹。如果相机之间曝光时间有微小的差异,就会导致图像数据不对齐,后续的处理算法就会产生误差。

树莓派5虽然自带两个CSI接口,但原生系统并不支持多路相机的硬件同步。这就是GMSL相机和专用转接板发挥作用的地方。通过艾利光科技的GMSL转接板,我们可以将最多4路GMSL相机连接到树莓派5,并通过GPIO触发实现微秒级的同步精度。这种方案不仅解决了同步问题,还提供了更好的抗干扰能力和更长的传输距离,特别适合车载、机器人等移动应用场景。

我在实际项目中就遇到过同步问题。当时用两个普通USB相机做立体视觉,虽然软件上同时触发,但图像总是有半个帧的延迟。后来发现是USB总线的调度机制导致的,这种软同步根本达不到机器视觉的精度要求。换成GMSL硬件同步方案后,问题迎刃而解。

2. 硬件连接全解析

2.1 核心组件介绍

要实现GMSL相机的同步曝光,我们需要三个核心组件:树莓派5开发板、艾利光GMSL转接板和支持Slave模式的GMSL相机。转接板采用MAX9296A解串器芯片,支持GMSL1和GMSL2协议,每路最高带宽达到6Gbps,足以传输4K分辨率视频流。

转接板提供了两个关键的同步接口:Trigger输入和GPIO控制。Trigger接口用于接收外部同步信号,GPIO接口则用于控制相机的复位和电源管理。这些接口都需要通过杜邦线连接到树莓派5的40针GPIO排针上。

电源连接要特别注意!GMSL相机需要12V供电,而树莓派5只需要5V。转接板上有两个电源接口:一个是5V的树莓派供电接口,另一个是12V的相机供电接口。如果只接5V电源,相机是无法工作的。我建议使用两个独立的电源适配器,或者使用一个多输出电源,确保功率充足。

2.2 同步触发线路连接

同步触发的核心是将树莓派5的GPIO引脚与转接板的Trigger接口正确连接。树莓派5的GPIO12(BCM编码)是一个很好的选择,因为它支持硬件PWM输出,可以产生精确的触发脉冲。

具体连接方式如下:用一根杜邦线将树莓派5的GPIO12引脚连接到转接板的Trigger输入接口,同时确保两边的GND引脚也连接在一起。接地线很重要!如果只接信号线不接地线,触发信号会很不稳定,容易受到电磁干扰。

对于多路相机系统,所有相机的Trigger接口都并联到同一个GPIO引脚上。这样当树莓派输出一个触发脉冲时,所有相机会同时开始曝光。实测下来,这种方案的同步精度可以达到微秒级,完全满足大多数机器视觉应用的需求。

2.3 CSI接口连接细节

树莓派5有两个CSI接口:CSI0和CSI1。每个CSI接口都是4 Lane的MIPI通道,理论带宽足够同时传输两路1080p视频流。转接板通过22pin FPC线缆连接到这些CSI接口。

连接时要注意方向!FPC线缆有防呆设计,但用力过猛还是会插反。我建议先确认线缆金色触点的一面朝向树莓派5的PCB板方向。插好后再轻轻拉一下,确认连接牢固。接触不良会导致视频流中断或者出现雪花噪点。

线缆长度也很重要。对于高分辨率相机,建议使用尽量短的FPC线缆(5-10cm),因为长线缆会有信号衰减。如果确实需要长距离传输,可以考虑使用GMSL延长方案,而不是简单的FPC延长线。

3. 软件配置与驱动加载

3.1 设备树配置详解

树莓派使用设备树覆盖(DTBO)机制来配置外设硬件。对于GMSL转接板,我们需要在/boot/firmware/config.txt中添加相应的设备树配置。

首先将编译好的max9296.dtbo文件复制到overlays目录:

sudo cp max9296.dtbo /boot/firmware/overlays/

然后在config.txt中添加设备树配置。对于ISX031相机,配置如下:

dtoverlay=max9296,sensor-type=ISX031_MAX96717_2CH
dtoverlay=max9296,cam0,sensor-type=ISX031_MAX96717_2CH

这里sensor-type参数指定了相机型号和串行器配置。ISX031_MAX96717_2CH表示使用ISX031传感器和MAX96717串行器,工作在双通道模式。如果是OX08B相机,则需要使用OX08B_MAX96717_1CH配置。

配置完成后重启树莓派,可以通过检查设备树节点来确认配置是否生效:

cd /proc/device-tree/axi/pcie@120000/rp1/i2c@80000/max9296@10
ls

如果能看到一系列设备树文件,说明配置成功了。

3.2 内核驱动加载

驱动加载使用modprobe而不是insmod,因为v4l2驱动有复杂的依赖关系。首先将编译好的ko文件放到正确的目录:

sudo cp max9296.ko /lib/modules/$(uname -r)/

然后更新模块依赖关系:

sudo depmod -a

最后加载驱动:

sudo modprobe max9296

加载成功后,用dmesg查看内核日志,应该能看到GMSL驱动初始化的信息。如果出现错误,很可能是设备树配置不对或者硬件连接有问题。

3.3 Media Pipeline配置

树莓派5的原生相机流水线(CFE)没有直接连接CSI2接口,所以需要手动配置media pipeline。艾利光提供了一个方便的配置脚本aili_run_camera.sh。

使用这个脚本需要指定几个参数:CSI通道、分辨率宽度、分辨率高度和deserdes通道数。例如,要同时使用两个CSI接口,每个接口接两路相机,命令如下:

./aili_run_camera.sh all 1920 1536 2ch

这个命令会配置media pipeline,将四个相机节点连接到对应的CSI接口。配置完成后,可以用v4l2-ctl工具列出所有视频设备:

v4l2-ctl --list-devices

应该能看到四个video设备节点,分别对应四个相机。

4. 同步曝光实战配置

4.1 GPIO触发信号生成

树莓派5的GPIO支持硬件PWM,可以产生精确的脉冲信号。我们使用WiringPi库来控制GPIO,首先安装必要的工具:

sudo apt install wiringpi

然后创建一个简单的C程序来生成触发脉冲:

#include <wiringPi.h>
#include <stdio.h>

#define TRIGGER_PIN 12

int main(void) {
    wiringPiSetupGpio();
    pinMode(TRIGGER_PIN, OUTPUT);
    
    while(1) {
        digitalWrite(TRIGGER_PIN, HIGH);
        delayMicroseconds(10);  // 脉冲宽度10微秒
        digitalWrite(TRIGGER_PIN, LOW);
        delay(33);  // 30fps,周期33毫秒
    }
    return 0;
}

编译并运行这个程序,它会在GPIO12上产生30Hz的触发脉冲。用示波器可以测量脉冲的精确 timing,确保脉冲宽度和间隔符合相机的要求。

4.2 相机触发模式配置

GMSL相机默认可能工作在自由运行模式,需要配置为外部触发模式。通过v4l2接口可以设置相机的触发模式:

# 设置触发模式为外部触发
v4l2-ctl -d /dev/video0 --set-ctrl trigger_mode=1

# 设置触发源为GPIO
v4l2-ctl -d /dev/video0 --set-ctrl trigger_source=0

# 设置触发极性为上升沿
v4l2-ctl -d /dev/video0 --set-ctrl trigger_activation=0

这些命令需要针对每个video设备执行一次。为了简化操作,可以写一个脚本批量配置所有相机。

4.3 同步精度验证

验证同步精度需要一个简单的测试场景。我通常用一个LED灯和高速旋转的编码盘,或者直接用计算机屏幕显示移动的图案。

首先确保所有相机的时间戳同步:

v4l2-ctl -d /dev/video0 --set-ctrl timestamp_sync=1

然后同时触发所有相机采集图像,比较图像的时间戳差异。理想情况下,所有相机的时间戳应该完全相同。在实际测试中,我测量的同步误差小于10微秒,完全满足大多数应用的需求。

5. 多路采集与图像处理

5.1 GStreamer多路视频流采集

GStreamer是处理多路视频流的理想工具。我们可以创建一个复杂的pipeline,同时采集四路视频流并进行处理。

下面是一个示例pipeline,同时显示四路相机视频:

gst-launch-1.0 \
    v4l2src device=/dev/video0 ! queue ! videoconvert ! videoscale ! video/x-raw,width=640,height=480 ! tee name=t0 \
    v4l2src device=/dev/video1 ! queue ! videoconvert ! videoscale ! video/x-raw,width=640,height=480 ! tee name=t1 \
    v4l2src device=/dev/video2 ! queue ! videoconvert ! videoscale ! video/x-raw,width=640,height=480 ! tee name=t2 \
    v4l2src device=/dev/video3 ! queue ! videoconvert ! videoscale ! video/x-raw,width=640,height=480 ! tee name=t3 \
    \
    t0. ! queue ! videomixer name=mixer sink_0::xpos=0 sink_0::ypos=0 \
    t1. ! queue ! mixer. sink_1::xpos=640 sink_1::ypos=0 \
    t2. ! queue ! mixer. sink_2::xpos=0 sink_2::ypos=480 \
    t3. ! queue ! mixer. sink_3::xpos=640 sink_3::ypos=480 \
    \
    mixer. ! videoconvert ! autovideosink

这个pipeline将四个相机的视频流缩放到640x480分辨率,然后排列成2x2的网格显示。在实际应用中,你可能需要根据具体需求调整分辨率和平铺方式。

5.2 OpenCV图像处理集成

OpenCV是计算机视觉项目的标配。我们可以用Python编写一个简单的多路相机采集程序:

import cv2
import threading

class CameraThread(threading.Thread):
    def __init__(self, camera_id):
        threading.Thread.__init__(self)
        self.camera_id = camera_id
        self.frame = None
        self.running = True
        
    def run(self):
        cap = cv2.VideoCapture(self.camera_id)
        while self.running:
            ret, frame = cap.read()
            if ret:
                self.frame = frame
        cap.release()
    
    def stop(self):
        self.running = False

# 创建四个相机线程
cameras = []
for i in range(4):
    cam = CameraThread(i)
    cam.start()
    cameras.append(cam)

# 主循环
while True:
    frames = []
    for cam in cameras:
        if cam.frame is not None:
            frames.append(cam.frame.copy())
    
    # 在这里处理帧数据
    # ...
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 清理
for cam in cameras:
    cam.stop()
    cam.join()

这个程序为每个相机创建一个独立的采集线程,避免因为某个相机掉帧影响整体性能。在实际使用中,你可能需要添加更多的错误处理和同步机制。

5.3 性能优化技巧

多路视频采集对树莓派5的性能是个挑战。以下是一些优化建议:

首先调整相机分辨率

更多推荐