3. I2C/SPI 控制总线驱动:传感器寄存器读写、时序分析、Linux 内核 I2C 子系统、驱动框架编写

好,我们进入第三章。这一章聊的是控制总线——I2C 和 SPI。

你想想看,图像传感器再厉害,也得有人给它下指令。曝光时间、增益、帧率、输出格式……这些参数都藏在传感器的寄存器里。怎么读写?靠的就是 I2C 或 SPI 总线。

说白了,控制总线就是嵌入式系统和传感器之间的“对话通道”。通道不通,后面所有调优都是空谈。我在项目中遇到过好几次,图像出问题,查了半天发现是 I2C 时序不对,寄存器根本没写进去。

3.1 传感器寄存器读写:从原理到实战

传感器的寄存器,本质就是一堆 8 位或 16 位的存储单元。每个寄存器有一个地址,里面存着配置值。

读写流程其实很简单:

  • 写操作:主机发送设备地址 + 写标志 → 寄存器地址 → 要写入的数据。
  • 读操作:主机发送设备地址 + 写标志 → 寄存器地址 → 重启条件 → 设备地址 + 读标志 → 读取数据。

关键点:很多传感器支持“连续读写”。你只需要发送起始地址,然后连续发时钟,传感器会自动递增地址。批量配置时效率高很多。

我个人的习惯是,拿到一个新传感器,先用逻辑分析仪抓一下官方驱动板的读写波形。确认地址、数据格式、字节顺序(大端/小端)都对得上,再开始写驱动。这一步能省后面大量调试时间。

3.2 时序分析:为什么你的 I2C 不通?

时序问题,是控制总线最常见的坑。尤其是 I2C,两根线(SCL、SDA)要满足严格的时序要求。

参数 标准模式 (100kHz) 快速模式 (400kHz) 说明
SCL 时钟频率 ≤ 100 kHz ≤ 400 kHz 由主机决定
起始条件保持时间 ≥ 4.0 μs ≥ 0.6 μs SCL 高电平时 SDA 下降
数据建立时间 ≥ 250 ns ≥ 100 ns SCL 上升沿前数据稳定
数据保持时间 ≥ 0 ns ≥ 0 ns SCL 下降沿后数据保持
停止条件建立时间 ≥ 4.0 μs ≥ 0.6 μs SCL 高电平时 SDA 上升

我曾经在一个项目里,I2C 总线长度超过 30cm,上拉电阻用了 4.7kΩ,结果在 400kHz 下波形严重畸变。后来换成 2.2kΩ 上拉,问题解决。嗯,这里要注意:总线电容和上拉电阻要匹配。长线或从设备多时,适当减小上拉电阻。

避坑指南:我曾经遇到过传感器在高温下 I2C 通信失败。查了半天,发现是 PCB 走线太长,信号反射导致建立时间不足。最后在 SDA 和 SCL 上各串了一个 22Ω 的电阻,问题解决。所以,时序问题不光是软件的事,硬件设计也很关键。

3.3 Linux 内核 I2C 子系统:架构与核心数据结构

Linux 内核的 I2C 子系统,设计得很清晰。它分三层:

  • I2C 核心层:提供统一的接口,管理适配器和设备。
  • I2C 适配器层:对应具体的硬件控制器(如 SoC 的 I2C 控制器)。
  • I2C 设备驱动层:我们写的就是这一层。

核心数据结构就三个:

  • struct i2c_adapter:代表一个 I2C 控制器。
  • struct i2c_client:代表一个 I2C 从设备(传感器)。
  • struct i2c_driver:代表设备驱动。

为什么说这个架构好?因为它把硬件细节隔离了。你写传感器驱动时,根本不用管底层怎么发时钟、怎么处理中断。直接用 i2c_transfer() 或 i2c_smbus_read_byte_data() 就行。

个人经验:我建议新手先用 i2c_smbus_* 系列函数。它们封装了常见的读写模式,代码更简洁。等需要优化性能时,再改用 i2c_transfer() 直接操作消息队列。

3.4 驱动框架编写:从零开始写一个传感器驱动

好,我们动手写一个。假设传感器是 OV5640,I2C 地址 0x3C(7 位地址 0x1E)。

驱动的基本骨架:

#include <linux/i2c.h>
#include <linux/module.h>

static int ov5640_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    // 1. 初始化传感器
    // 2. 注册 V4L2 子设备
    // 3. 创建 sysfs 接口(可选)
    return 0;
}

static int ov5640_remove(struct i2c_client *client)
{
    // 反初始化
    return 0;
}

static const struct i2c_device_id ov5640_id[] = {
    { "ov5640", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ov5640_id);

static struct i2c_driver ov5640_driver = {
    .driver = {
        .name = "ov5640",
    },
    .probe    = ov5640_probe,
    .remove   = ov5640_remove,
    .id_table = ov5640_id,
};

module_i2c_driver(ov5640_driver);

MODULE_LICENSE("GPL");

寄存器读写函数,我习惯这样封装:

static int ov5640_write_reg(struct i2c_client *client,
                            u16 reg, u8 val)
{
    u8 buf[3];
    buf[0] = reg >> 8;   // 高字节
    buf[1] = reg & 0xFF; // 低字节
    buf[2] = val;

    return i2c_master_send(client, buf, 3);
}

static int ov5640_read_reg(struct i2c_client *client,
                           u16 reg, u8 *val)
{
    u8 reg_buf[2];
    reg_buf[0] = reg >> 8;
    reg_buf[1] = reg & 0xFF;

    struct i2c_msg msg[2] = {
        {
            .addr = client->addr,
            .flags = 0,
            .len = 2,
            .buf = reg_buf,
        },
        {
            .addr = client->addr,
            .flags = I2C_M_RD,
            .len = 1,
            .buf = val,
        },
    };

    return i2c_transfer(client->adapter, msg, 2);
}

注意:OV5640 的寄存器地址是 16 位的,所以要先发高字节再发低字节。不同传感器的地址格式可能不同,一定要看 datasheet。

驱动写好后,怎么和设备树匹配?在设备树里这样写:

&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    ov5640: camera@3c {
        compatible = "ovti,ov5640";
        reg = <0x3c>;
        // 其他属性:电源、复位、时钟等
    };
};

内核启动时,I2C 核心会根据 compatible 属性和 id_table 匹配,自动调用 probe 函数。

3.5 知识体系总览

这一章的内容,我用一张图帮你理清脉络:

I2C/SPI 控制总线驱动知识体系 物理层 时序层 协议层 Linux 内核 I2C 子系统 I2C 核心层 适配器层 设备驱动层 传感器驱动开发 probe/remove 寄存器读写 V4L2 集成 设备树匹配 核心:理解物理层 → 时序 → 协议 → 内核子系统 → 驱动开发,层层递进

3.6 调试技巧与常见问题

驱动写完了,怎么验证?我一般用这几个方法:

  • i2cdetect:扫描总线,确认设备地址是否正确。
  • i2cget / i2cset:手动读写寄存器,验证通信。
  • 逻辑分析仪:抓波形,看时序是否满足要求。
  • 内核调试打印:在驱动里加 dev_dbg(),跟踪每次读写的结果。

小技巧:我习惯在 probe 函数里先读传感器的 ID 寄存器。如果读到的值跟 datasheet 对不上,直接返回 -ENODEV。这样能快速判断 I2C 通信是否正常。

常见问题:

  • 设备地址不对:7 位地址和 8 位地址搞混。I2C 设备地址通常是 7 位,但很多 datasheet 给的是 8 位(左移一位后的值)。
  • 时序不满足:时钟频率太高,或上拉电阻不合适。
  • 寄存器地址格式错误:16 位地址和 8 位地址混用。
  • 设备树匹配失败:compatible 字符串和 id_table 不一致。

嗯,这一章的内容就到这里。控制总线驱动是传感器驱动的基石,把 I2C/SPI 搞透,后面写任何传感器驱动都会顺手很多。