1. 从零开始:为什么我要折腾V3s的MIPI摄像头驱动
大家好,我是老张,一个在嵌入式圈子里摸爬滚打了十多年的老鸟。最近因为一个项目,我又一次和全志V3s这颗“国民级”芯片杠上了,目标很明确:给它接上一个MIPI接口的OV5640摄像头,让这个小小的开发板能“看见”世界。说实话,这事儿听起来简单,但真做起来,尤其是当你手头的开发板资料匮乏、官方支持约等于零的时候,那感觉就像是在一片漆黑的森林里找路。
我用的这块板子是在淘宝上随便淘的,价格很香,但配套的软件就有点“复古”了——用的是全志内部的老版本Linux,连设备树都没用上。更坑的是,它原配的摄像头模块贵得离谱,这谁受得了?于是我就把目光投向了OV5640,这玩意儿在市场上量大管饱,价格亲民,而且同时支持MIPI和DVP接口,灵活性很高。但问题来了,V3s的官方SDK里,MIPI CSI-2的驱动支持并不完善,或者说,对于非官方推荐的摄像头模组,基本就是“三不管”地带。
所以,这篇记录就是我在这个“三不管”地带里趟出来的一条路。整个过程,说白了就是三件事:找驱动、改设备树、调内核。我会把我踩过的每一个坑、绕过的每一个弯都详细写下来,目标就是让你看完之后,能拿着一块类似的板子和一个OV5640模组,一步步地把摄像头给点亮。我不讲太多高深的理论,就聊怎么动手把它做出来。咱们先从最头疼的“找驱动”开始。
2. 驱动源码的获取与整合:把“别人家”的驱动请进门
驱动是整个项目的基石。全志V3s虽然内置了MIPI CSI-2控制器,但内核源码里对应的驱动(sun6i-csi)可能功能不全或者对第三方传感器支持不好。我们需要一个更通用、更活跃的开源驱动。我在Github和Gitee上翻了好久,最终锁定了sun6i-mipi-csi这个项目。它最初应该是为全志A系列芯片写的,但经过社区维护,对V3s的兼容性也不错。
第一步,下载与解压。你可以从Gitee的镜像仓库获取,地址是https://gitee.com/prtemly/sun6i-mipi-csi。我下载下来的压缩包叫sun6i-mipi-csi-master.zip。假设你的内核源码目录在/home/linux/v3s/linux-zero-5.2.y,我们把它解压到媒体驱动相关的目录下:
unzip sun6i-mipi-csi-master.zip -d /home/linux/v3s/linux-zero-5.2.y/drivers/staging/media/sunxi解压后会得到一个sun6i-mipi-csi-master文件夹。为了保持内核目录的命名一致性,我习惯把它改名为sun6i-csi:
cd /home/linux/v3s/linux-zero-5.2.y/drivers/staging/media/sunxi mv sun6i-mipi-csi-master sun6i-csi第二步,让内核构建系统认识我们的新驱动。这就像给家里新来的客人登记户口。需要修改两个文件:
修改
Kconfig:编辑drivers/staging/media/sunxi/Kconfig文件。找到if VIDEO_SUNXI和endif之间的区域,在里面添加一行:source "drivers/staging/media/sunxi/sun6i-csi/Kconfig"这行代码的作用是告诉上层的配置菜单,我们这个
sun6i-csi目录下也有配置项需要被加载和显示。修改
Makefile:编辑同目录下的Makefile文件。在文件末尾附近,你会看到类似obj-$(CONFIG_VIDEO_SUNXI_CEDRUS) += cedrus/的行。我们在它后面添加新的一行:obj-$(CONFIG_VIDEO_SUN6I_MIPI_CSI) += sun6i-csi/这行代码是编译指令,意思是当配置选项
CONFIG_VIDEO_SUN6I_MIPI_CSI被选中时,就去编译sun6i-csi目录下的代码。
做完这两步,驱动源码就算在 kernel 里安家了。但光安家还不够,我们还得告诉内核,在咱们这块具体的板子上,这个控制器和传感器具体连接在哪个引脚、用什么时钟、怎么供电。这就是接下来设备树(Device Tree)要干的活儿。设备树就像一份硬件的“地图”,内核启动时按图索骥,才能正确初始化设备。
3. 设备树配置详解:给硬件画一张精确的“地图”
设备树的修改是移植工作的核心,也是最容易出错的地方。我们需要修改两个主要的设备树文件:一个是针对具体开发板的.dts文件(比如我用的sun8i-v3s-licheepi-zero.dts),另一个是包含SoC通用定义的.dtsi文件(sun8i-v3s.dtsi)。修改的原则是:在.dtsi里补充SoC级别的通用节点定义(如果缺失的话),在.dts里启用和配置这些节点,并描述板级特有的连接。
首先,在板级设备树文件(sun8i-v3s-licheepi-zero.dts)中添加内容。
在根节点下添加CSI控制器和电源管理节点。打开文件,在根节点
/{ ... };的大括号内添加。这里我们主要添加两个csi控制器节点(V3s有两个CSI接口)和三个为OV5640供电的稳压器节点。注意,这些节点的status默认都是"disabled",我们稍后再启用。// 添加CSI控制器 csi0: csi@01cb0000 { compatible = "allwinner,sun8i-v3s-csi"; reg = <0x01cb0000 0x3000>; interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>; clocks = <&ccu CLK_BUS_CSI>, <&ccu CLK_CSI1_SCLK>, <&ccu CLK_DRAM_CSI>, <&ccu CLK_MIPI_CSI>; clock-names = "bus", "mod", "ram", "dphy"; resets = <&ccu RST_BUS_CSI>; status = "disabled"; }; // 添加OV5640所需的三个固定电压稳压器 dovdd: regulator-dovdd { compatible = "regulator-fixed"; regulator-name = "DOVDD"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; enable-active-high; }; // ... 类似地添加 dvdd (1.8V) 和 avdd (2.8V) 节点关键点解读:
compatible:驱动匹配的关键字,必须和驱动源码里定义的一致。reg:控制器在内存中的映射地址和范围,来自芯片手册。clocks和clock-names:指定控制器需要的所有时钟源。dphy是MIPI D-PHY的时钟,对于MIPI模式至关重要。- 三个稳压器(
dovdd,dvdd,avdd)对应OV5640模组需要的三种电压(通常是1.8V、1.8V、2.8V)。如果你的板子有专门的电源管理芯片供电,这里可能需要引用已有的稳压器节点,而不是新建regulator-fixed。
在非根节点下启用并连接设备。接着,在文件的后面部分(通常是在各个节点引用的地方),我们来启用和配置这些设备。
// 启用并配置CSI0控制器 &csi0 { status = "okay"; allwinner,mipi-csi-bps = <400000000>; // MIPI通道速率,单位比特每秒 port { csi0_ep: endpoint { remote-endpoint = <&ov5640_0>; // 指向摄像头传感器端点 clock-lanes = <0>; // 使用第0条时钟通道 >/ { soc { pio: pinctrl@1c20800 { // ... 其他引脚组定义 ... i2c1_pins: i2c1-pins { pins = "PE21", "PE22"; // I2C1的SDA和SCL引脚 function = "i2c1"; }; }; i2c1: i2c@1c2b000 { compatible = "allwinner,sun6i-a31-i2c"; reg = <0x01c2b000 0x400>; interrupts = <GIC_SPI 7 IRQ_TYPE_LEVEL_HIGH>; clocks = <&ccu CLK_BUS_I2C1>; resets = <&ccu RST_BUS_I2C1>; pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; // 引用上面定义的引脚组 clock-frequency = <100000>; // I2C总线速度,100kHz status = "disabled"; #address-cells = <1>; #size-cells = <0>; }; }; };这里添加了
i2c1_pins引脚组定义,并在i2c1节点中通过pinctrl-0引用它,同时设置了clock-frequency。这些补充确保了I2C控制器能正确工作。设备树修改是个精细活,一个标点符号错了都可能导致内核无法启动或者设备无法识别。改完后,强烈建议用
dtc(设备树编译器)检查一下语法:dtc -I dts -O dtb -o /dev/null your_dts_file.dts。没有报错再继续。4. 内核配置与编译:勾选正确的选项
设备树的“地图”画好了,接下来就要告诉内核,我们需要把哪些“功能模块”编译进去。这里主要通过
make menuconfig进行图形化配置。进入内核配置界面:
cd /home/linux/v3s/linux-zero-5.2.y make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig配置MIPI CSI驱动:按照以下路径找到选项:
Device Drivers ---> Staging drivers ---> [*] Media staging drivers ---> [*] Allwinner sunXi family Video Devices ---> <*> Allwinner V3s Camera Sensor Interface driver with MIPI-CSI2 support注意,这个选项的名字
CONFIG_VIDEO_SUN6I_MIPI_CSI应该和我们之前在Makefile里添加的obj-$(CONFIG_VIDEO_SUN6I_MIPI_CSI)完全对应。把它编译进内核(<*>),而不是模块(<M>),对于启动时自动加载更省心。配置OV5640传感器驱动:这个驱动不在Staging目录下,在主线的多媒体支持里:
Device Drivers ---> <*> Multimedia support ---> [ ] Autoselect ancillary drivers (tuners, sensors, i2c, spi, frontends) // 注意:先**取消选中**这一项! -*- I2C Encoders, decoders, sensors and other helper chips ---> <*> OmniVision OV5640 sensor support这里有个大坑:默认情况下,
Autoselect ancillary drivers是选中的,它会自动隐藏很多传感器驱动选项。你必须先把它取消选中,然后进入I2C Encoders...子菜单,才能看到OmniVision OV5640 sensor support这个选项。把它也选为<*>编译进内核。保存并编译:配置完成后,保存退出。然后就是漫长的编译过程:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)编译成功后,你会得到新的内核镜像(如
arch/arm/boot/zImage)和设备树二进制文件(如arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dtb)。把它们烧录到开发板,或者通过TFTP等方式加载启动。
5. 系统启动与功能验证:点亮摄像头的那一刻
如果一切顺利,开发板启动后,摄像头驱动应该已经成功加载。我们可以通过一系列命令来验证。
检查设备节点:首先登录到开发板的Linux系统。驱动加载成功后,在
/dev目录下应该会出现视频设备节点,通常是/dev/video0。同时,V4L2子系统还会创建subdev节点,位于/dev/v4l-subdev*。我们可以用v4l2-ctl这个强大的工具来查看:v4l2-ctl --list-devices这个命令会列出所有V4L2设备。你期望看到类似这样的输出,其中包含
sun6i-csi和ov5640的字样:sun6i-csi (platform:1cb0000.csi): /dev/video0 /dev/v4l-subdev0 ov5640 3-003c (platform:1c2b000.i2c): /dev/v4l-subdev1这证明CSI控制器和OV5640传感器都被内核正确识别并注册了。
获取摄像头能力信息:进一步,我们可以查看
/dev/video0支持哪些格式和分辨率:v4l2-ctl --device=/dev/video0 --list-formats-ext这条命令会输出一长串信息,显示设备支持的像素格式(如
NV12,YUYV等)以及每种格式下支持的分辨率(如640x480,1920x1080等)。OV5640通常支持多种分辨率,驱动会把这些信息上报。捕获第一张图片(最激动人心的步骤):确认设备存在后,我们就可以尝试拍照了。这里我们用
v4l2-ctl的--stream-mmap模式(内存映射)来捕获一帧图像:v4l2-ctl --device=/dev/video0 \ --set-fmt-video=width=640,height=480,pixelformat=NV12 \ --stream-mmap \ --stream-to=output.yuv \ --stream-count=1- 参数解释:
--set-fmt-video:设置捕获的视频格式。这里我们选择NV12(一种YUV格式),分辨率640x480。--stream-mmap:使用内存映射方式进行图像捕获,效率较高。--stream-to:指定输出文件名。--stream-count:指定捕获的帧数,这里设为1,就是拍一张照片。 执行成功后,当前目录下会生成一个output.yuv文件。这个文件是原始的YUV数据,不能直接用普通图片查看器打开。
- 参数解释:
在电脑上查看YUV图片:把
output.yuv文件从开发板拷贝到你的Ubuntu主机上(可以用SCP、NFS共享等方式)。然后使用ffplay(FFmpeg的一部分)来播放这个原始数据文件:# 首先确保电脑上安装了ffmpeg: sudo apt install ffmpeg ffplay -video_size 640x480 -pixel_format nv12 -framerate 10 -i output.yuv- 参数解释:
-video_size:必须和捕获时设置的分辨率一致。-pixel_format:必须和捕获时设置的格式一致(nv12)。-framerate:由于我们只捕获了一帧,这里随便设一个值(如10)即可。-i:指定输入文件。 如果一切正常,屏幕上会短暂弹出一个窗口,显示你捕获到的图像!如果图像是彩色的、没有严重扭曲,那么恭喜你,驱动移植和适配基本成功了。如果图像是绿色的、花屏的或者根本打不开,那可能还需要回头检查数据通道配置、时钟频率或者电源时序。
- 参数解释:
6. 避坑指南与深度调试
在实际操作中,一次成功的概率不大。下面是我遇到过的几个典型问题及排查思路:
问题一:内核启动时找不到设备,
/dev/video0不存在。- 检查设备树:用
cat /proc/device-tree/相关路径或dtc反编译DTB文件,确认csi0和i2c1节点的status是否为"okay",compatible属性是否正确。 - 检查内核日志:
dmesg | grep -E \"(csi|ov5640|i2c)\"。重点关注是否有驱动探测(probe)成功的消息,或者是否有错误提示(比如寄存器读写失败、时钟获取失败、供电问题等)。 - 检查I2C通信:在系统启动后,用
i2cdetect -y 1(假设I2C总线编号是1)扫描I2C总线,看地址0x3c上是否有设备响应。如果没有,检查硬件连接、上拉电阻、以及设备树中I2C引脚的配置。
问题二:有
/dev/video0,但v4l2-ctl --list-formats-ext没有输出或格式不对。- 检查MIPI数据通道和时钟:确认设备树中
data-lanes和clock-lanes的设置与物理连接完全匹配。OV5640的MIPI接口可能需要特定的上电和初始化序列,检查驱动源码里的ov5640.c,看其s_power或s_stream操作是否正常。 - 检查时钟和电源:用示波器或逻辑分析仪测量OV5640的XCLK(24MHz)是否正常,
DOVDD、DVDD、AVDD电压是否到位,PWDN和RESET引脚的电平时序是否符合传感器手册要求。很多时候问题出在硬件上。
问题三:能捕获图像,但图像颜色异常、有条纹或错位。
- 调整MIPI速率:设备树中的
allwinner,mipi-csi-bps属性可能需要调整。OV5640在不同的分辨率下,所需的MIPI通道速率不同。可以尝试降低这个值(比如从4亿降到3亿)。 - 检查像素格式:确保
v4l2-ctl设置的pixelformat与传感器实际输出的格式、以及你查看工具指定的格式完全一致。除了NV12,还可以尝试YUYV等。 - 深入V4L2调试:可以打开内核的V4L2调试信息,重新编译内核。在
make menuconfig中,Device Drivers -> Multimedia support -> Media core drivers下,可以启用Media drivers debug等相关选项。更详细的日志有助于定位问题在驱动链的哪个环节。
整个移植过程,就是不断地在软件配置和硬件信号之间来回验证。耐心和细致的排查是关键。当你第一次在屏幕上看到来自OV5640的清晰图像时,那种成就感,绝对是驱动开发者最好的奖励。这份记录希望能帮你少走些弯路,如果遇到新问题,多查查内核文档、芯片手册和传感器数据手册,社区的讨论也很有价值。