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采集流程是这样的:
- 打开设备:
fd = open("/dev/video0", O_RDWR) - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备支持视频采集 - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt),告诉内核我要640x480的YUV图像 - 申请缓冲区:
ioctl(fd, VIDIOC_REQBUFS, &req),一般申请4个buffer - 映射到用户空间:
mmap,这样应用程序可以直接读写buffer - 入队所有buffer:
ioctl(fd, VIDIOC_QBUF, &buf) - 开始采集:
ioctl(fd, VIDIOC_STREAMON, &type) - 循环出队、处理、入队:
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摄像头开发的基石。下一章,我会带你手写一个完整的摄像头驱动,把今天讲的理论全部落地。