驱动开发环境搭建:Linux内核配置、V4L2框架介绍、I2C/SPI通信基础

做Camera驱动,第一件事不是写代码,而是把环境搭对。我见过太多新手,代码写得挺漂亮,结果编译不过、内核跑不起来,最后发现是内核配置漏了东西。今天咱们就把这摊事理清楚。

Linux内核配置——别让内核成为你的绊脚石

你想想看,一个Camera sensor挂在I2C总线上,数据通过CSI接口进来,如果内核里连V4L2、I2C、Media Controller这些子系统都没打开,驱动写得再好也白搭。

我个人习惯,拿到一个新平台,先做三件事:

  1. 确认内核版本(uname -r)
  2. 检查当前配置(zcat /proc/config.gz 或 cat /boot/config-$(uname -r))
  3. 用 make menuconfig 逐项核对关键选项

下面这张表,是我做项目时必开的配置项,你可以直接拿来当checklist:

配置项 路径 说明
CONFIG_VIDEO_V4L2 Device Drivers → Multimedia support V4L2核心框架,不开这个啥都别谈
CONFIG_VIDEO_V4L2_SUBDEV_API 同上 子设备API,sensor驱动必须依赖
CONFIG_MEDIA_CONTROLLER 同上 媒体控制器,Pipeline管理的基础
CONFIG_I2C Device Drivers → I2C support I2C总线支持,sensor控制通道
CONFIG_SPI Device Drivers → SPI support 部分sensor或EEPROM用SPI通信
CONFIG_VIDEO_OV5640(举例) Device Drivers → Multimedia support → Camera sensor drivers 具体sensor驱动,按实际型号选

我曾经踩过的坑: 有一次移植OV5640驱动,折腾了两天sensor就是不出图。最后发现内核配置里 CONFIG_MEDIA_CONTROLLER 没开,导致Pipeline创建失败。嗯,从那以后我每次配内核都会用 grep 扫一遍关键选项。

V4L2框架介绍——Camera驱动的骨架

V4L2,全称Video for Linux 2,是Linux下视频设备驱动的标准框架。说白了,它定义了一套统一的接口,让应用程序可以不管底层是什么sensor、什么桥接芯片,都能用同一套API操作摄像头。

V4L2的核心概念,我总结成四个字:设备、队列、格式、控制。

  • 设备节点:/dev/videoX、/dev/v4l-subdevX,用户空间通过它们操作硬件
  • 缓冲区队列:VIDIOC_REQBUFS、VIDIOC_QBUF、VIDIOC_DQBUF,数据流的核心
  • 格式协商:VIDIOC_S_FMT、VIDIOC_G_FMT,分辨率、像素格式的约定
  • 控制接口:VIDIOC_S_CTRL、VIDIOC_G_CTRL,调节曝光、增益、白平衡等参数

下面这张SVG图,展示了V4L2框架中各个组件的关系:

V4L2框架核心组件关系图 用户空间应用程序 系统调用(open/ioctl/mmap/poll) V4L2核心框架 videodev.ko / v4l2-dev.c 视频设备节点 /dev/videoX 子设备节点 /dev/v4l-subdevX 桥接驱动(CSI/ISP) Sensor驱动(子设备) 硬件(Sensor + CSI + ISP) 说明:用户空间通过设备节点与V4L2核心交互,核心将请求分发到桥接驱动或子设备驱动,最终操作硬件

关键理解: V4L2把Camera驱动拆成了两个角色——桥接驱动(负责数据流、DMA、ISP配置)和子设备驱动(负责sensor寄存器配置)。这种分层设计,让你换sensor时只需要重写子设备驱动,桥接部分基本不动。

I2C/SPI通信基础——控制通道的命脉

Camera sensor的控制通道,99%的情况是I2C,偶尔用SPI。为什么?因为sensor只需要配置寄存器,数据量小,I2C两根线就够了。

I2C通信要点

I2C在Camera驱动里就干一件事:写寄存器、读寄存器。但这里有个坑——sensor的寄存器地址长度和值长度各不相同。

我整理了一份常见sensor的I2C地址格式:

Sensor型号 寄存器地址宽度 数据宽度 典型I2C地址
OV5640 16位 8位 0x3C
IMX219 16位 8位 0x20
AR0144 8位 8位 0x48
MT9V034 16位 16位 0x90

在Linux驱动里,I2C读写通常用 i2c_transfer 或 i2c_smbus_read/write 系列函数。我个人习惯封装一个通用读写函数:

static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    struct i2c_msg msg;
    u8 buf[3];
    int ret;

    buf[0] = reg >> 8;   // 高字节
    buf[1] = reg & 0xFF;  // 低字节
    buf[2] = val;         // 数据

    msg.addr = client->addr;
    msg.flags = 0;        // 写操作
    msg.len = 3;
    msg.buf = buf;

    ret = i2c_transfer(client->adapter, &msg, 1);
    if (ret < 0) {
        dev_err(&client->dev, "I2C write failed: %d\n", ret);
        return ret;
    }
    return 0;
}

小技巧: 调试I2C通信时,先用 i2cdetect -y <bus> 确认sensor是否在总线上。如果检测不到,八成是硬件问题——上拉电阻没焊、地址不对、或者sensor没上电。我在项目里至少有一半的I2C问题出在硬件上,不是软件。

SPI通信要点

SPI在Camera领域用得少,但有些高速sensor或EEPROM会用。SPI比I2C快,但线多(4根),适合大数据量传输。

Linux内核里SPI驱动用 spi_write、spi_read、spi_write_then_read 这些接口。注意SPI是全双工的,读写可以同时进行。

static int sensor_spi_write(struct spi_device *spi, u8 reg, u8 val)
{
    u8 tx_buf[2] = {reg, val};
    return spi_write(spi, tx_buf, 2);
}

我曾经踩过的坑: 有一次用SPI接口的sensor,死活读不到正确的ID。折腾了半天,发现是SPI的时钟极性(CPOL)和相位(CPHA)配置错了。sensor手册里写的是模式0(CPOL=0, CPHA=0),我默认用了模式3。嗯,从那以后我每次都会仔细看sensor手册里的SPI时序图。

环境搭建的完整流程

最后,我把整个环境搭建的步骤串起来,你照着做就行:

  1. 获取内核源码:从芯片厂商(如Rockchip、Allwinner、NXP)获取BSP内核,或从kernel.org下载主线内核
  2. 配置内核:make ARCH=arm64 defconfig(按平台选),然后用 make menuconfig 打开V4L2、I2C、Media Controller等选项
  3. 编译内核:make -j$(nproc),生成zImage/Image和dtb文件
  4. 编译设备树:在dts文件中添加sensor节点,指定I2C地址、复位引脚、电源等
  5. 烧录并启动:将内核、dtb、根文件系统烧录到目标板,启动后用 dmesg | grep -i sensor 检查驱动是否加载
  6. 验证通信:用 i2cdetect 确认I2C设备,用 v4l2-ctl --list-devices 查看V4L2设备节点

环境搭好了,后面写驱动、调ISP才有根基。别急,一步一步来。