2、CSI摄像头驱动:CSI接口原理、摄像头设备树配置、v4l2框架基础

2.1 CSI接口原理:从物理层到协议层

CSI,全称是Camera Serial Interface。说白了,就是摄像头和处理器之间的一条专用高速公路。

我最早接触CSI时,总觉得它和USB摄像头差不多。后来踩了坑才发现,两者完全是两码事。USB摄像头自带处理芯片,而CSI摄像头只是把原始图像数据一股脑儿丢给SoC。所以,驱动的工作量也完全不同。

CSI接口分两层:

  • 物理层(C-PHY / D-PHY):负责信号的电气特性。Jetson Nano用的是D-PHY,4条lane,每条lane速率最高1Gbps。嗯,这个速度足够跑1080P 30fps了。
  • 协议层(CSI-2):定义数据怎么打包。图像数据被拆成一个个packet,每个packet有包头、负载和校验。

你可能会问:为什么要拆包?直接传不行吗?

我在项目中遇到过一个问题:摄像头画面偶尔出现绿条。查了半天,发现是CSI链路上的数据校验没做。拆包后,每个packet都有CRC校验,接收端能发现错误并请求重传。所以,拆包不是多此一举,而是为了保证可靠性。

核心要点:CSI-2协议定义了三种数据类型——RAW(原始Bayer数据)、YUV(颜色分量)、JPEG(压缩数据)。Jetson上最常用的是RAW10和RAW12,也就是每个像素10位或12位。

2.2 摄像头设备树配置:让内核认识你的摄像头

设备树,英文叫Device Tree。它的作用就是告诉内核:你的板子上接了哪些外设,它们怎么连接。

我个人习惯,拿到一块新摄像头模组,第一件事就是看它的数据手册,找到I2C地址和CSI lane映射。然后打开Jetson的设备树文件,开始改配置。

一个典型的CSI摄像头设备树节点长这样:

// 摄像头节点
cam0: camera@10 {
    compatible = "sony,imx219";
    reg = <0x10>;  // I2C地址
    clocks = <&camera_clk>;
    clock-names = "xvclk";

    // CSI lane映射
    port {
        cam0_out: endpoint {
            remote-endpoint = <&csi_in>;
            data-lanes = <1 2>;  // 使用lane 1和2
        };
    };
};

// CSI接收端节点
csi_base: nvcsi@150c0000 {
    port {
        csi_in: endpoint {
            remote-endpoint = <&cam0_out>;
            data-lanes = <1 2>;
        };
    };
};

这里有几个坑,我一个个说:

  • data-lanes的顺序:不是随便写的。lane 0是时钟通道,lane 1~4才是数据通道。如果你写错了,画面会花屏。
  • I2C地址:IMX219默认是0x10,但有些模组厂商会改。我曾经因为没仔细看数据手册,把地址写成了0x20,结果摄像头死活不工作。
  • 时钟频率:摄像头需要外部时钟,通常是24MHz或27MHz。这个值必须和模组匹配,否则图像帧率会乱。

避坑指南:我曾经在设备树里漏配了电源引脚,导致摄像头上电时序不对。现象是:驱动加载成功,但就是没有图像数据。后来用示波器一量,发现摄像头根本没启动。所以,记得检查VDD、VANA、VIO这些电源引脚是否都配上了。

2.3 v4l2框架基础:用户态和内核态的桥梁

v4l2,全称Video for Linux 2。它是Linux内核里专门处理视频设备的框架。

你想想看,如果没有v4l2,每个摄像头厂商都要自己写一套用户态API。那开发者得疯掉。v4l2统一了接口,不管你用的是什么摄像头,操作方式都一样:open、ioctl、mmap、close。

v4l2的核心概念就三个:

概念 说明 我常用的ioctl
Video Device 对应/dev/video0这样的设备节点 VIDIOC_QUERYCAP
Buffer 存放图像数据的内存区域 VIDIOC_REQBUFS, VIDIOC_QBUF, VIDIOC_DQBUF
Format 图像的宽、高、像素格式 VIDIOC_S_FMT, VIDIOC_G_FMT

一个典型的v4l2采集流程是这样的:

  1. 打开设备:fd = open("/dev/video0", O_RDWR)
  2. 查询能力:ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备支持视频采集
  3. 设置格式:ioctl(fd, VIDIOC_S_FMT, &fmt),告诉内核我要640x480的YUV图像
  4. 申请缓冲区:ioctl(fd, VIDIOC_REQBUFS, &req),一般申请4个buffer
  5. 映射到用户空间:mmap,这样应用程序可以直接读写buffer
  6. 入队所有buffer:ioctl(fd, VIDIOC_QBUF, &buf)
  7. 开始采集:ioctl(fd, VIDIOC_STREAMON, &type)
  8. 循环出队、处理、入队:DQBUF -> 处理图像 -> QBUF

个人经验:buffer数量建议至少4个。为什么?因为如果只有2个buffer,当应用程序处理图像时,摄像头只能干等着。4个buffer可以保证流水线不中断。我在做高速采集项目时,甚至用到过8个buffer。

说到v4l2,还有一个东西不得不提——Media Controller框架。Jetson上的摄像头驱动用了这个框架,它把摄像头、CSI接收器、ISP等硬件模块抽象成一个个entity,然后用link连接起来。

你可以用media-ctl工具查看这些连接:

# 查看media拓扑
media-ctl -d /dev/media0 -p

# 设置数据流路径
media-ctl -d /dev/media0 -l '"imx219 0-0010":0 -> "nvcsi":0 [1]'

嗯,这里要注意:Media Controller的配置顺序很重要。必须先设置好link,再设置format,最后才能开始采集。顺序错了,驱动会报错。

最后,我分享一个调试技巧。当摄像头不出图时,先别急着看代码。用v4l2-ctl工具测一下:

# 列出所有视频设备
v4l2-ctl --list-devices

# 查看当前格式
v4l2-ctl -d /dev/video0 --get-fmt-video

# 抓一帧看看
v4l2-ctl -d /dev/video0 --stream-mmap --stream-to=test.raw --stream-count=1

如果v4l2-ctl能正常抓图,说明驱动没问题,问题出在你的应用程序里。如果抓不到图,那就回头检查设备树和硬件连接吧。

好了,这一章的内容就到这里。CSI接口、设备树、v4l2框架,这三者是Jetson摄像头开发的基石。下一章,我会带你手写一个完整的摄像头驱动,把今天讲的理论全部落地。