4、I2C/SPI寄存器读写驱动:Linux内核I2C子系统框架、I2C设备驱动编写(probe/remove/ioctl)、SPI设备驱动编写、寄存器读写封装与调试。

各位同学,欢迎来到第四章。

这一章,我们聊聊传感器驱动里最核心的活儿——寄存器读写

你想想看,图像传感器本质上就是个复杂的模拟前端加数字控制逻辑。你要让它工作,就得通过I2C或SPI总线,往它肚子里塞一堆配置参数。曝光时间、增益、帧率、输出格式……全得靠寄存器来搞定。

我刚开始做传感器驱动时,觉得I2C读写不就是调个函数吗?后来发现,坑全在细节里。总线时序不对、设备地址搞错、寄存器地址长度没对齐……随便一个都能让你折腾半天。

好,咱们直接进入正题。

4.1 Linux内核I2C子系统框架

Linux内核的I2C子系统,说白了就是三层结构:

  • I2C核心层:管理总线、设备、驱动的注册与匹配。
  • I2C总线驱动层:处理硬件时序,比如i2c-imx、i2c-tegra这些。
  • I2C设备驱动层:咱们要写的部分,负责和具体传感器通信。

我个人习惯把I2C子系统想象成一个“中介”。你不需要关心底层怎么拉高拉低SCL/SDA线,只需要告诉核心层:我要读哪个设备、哪个寄存器、读几个字节。剩下的,核心层帮你搞定。

这里有个关键数据结构——i2c_client。它代表一个挂在I2C总线上的设备。驱动里所有操作,都围绕这个client展开。

struct i2c_client {
    unsigned short addr;    // 7位或10位设备地址
    char name[I2C_NAME_SIZE];
    struct i2c_adapter *adapter; // 所属的I2C适配器
    // ... 其他成员
};
提示:设备地址通常是7位的,左移1位后才是你看到的8位地址。比如传感器地址0x30,实际i2c_client里存的是0x18。这个坑我踩过,调试时对着数据手册看了半天才发现。

4.2 I2C设备驱动编写:probe/remove/ioctl

一个标准的I2C设备驱动,核心就三个回调函数:proberemoveioctl

probe函数:设备匹配成功时调用。在这里做初始化、分配资源、注册设备节点。

static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct sensor_dev *sdev;
    int ret;

    // 分配私有数据结构
    sdev = devm_kzalloc(&client->dev, sizeof(*sdev), GFP_KERNEL);
    if (!sdev)
        return -ENOMEM;

    sdev->client = client;
    i2c_set_clientdata(client, sdev);

    // 初始化互斥锁
    mutex_init(&sdev->lock);

    // 注册字符设备或V4L2设备
    // ... 这里省略具体注册代码

    dev_info(&client->dev, "sensor probe success\n");
    return 0;
}

remove函数:设备移除时调用。释放资源、注销设备。

static int sensor_remove(struct i2c_client *client)
{
    struct sensor_dev *sdev = i2c_get_clientdata(client);

    // 注销设备
    // ... 清理工作

    mutex_destroy(&sdev->lock);
    dev_info(&client->dev, "sensor removed\n");
    return 0;
}

ioctl函数:用户空间通过设备节点发命令。比如设置曝光时间、读取传感器温度。

static long sensor_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct sensor_dev *sdev = file->private_data;
    int ret = 0;

    switch (cmd) {
    case SENSOR_IOC_SET_EXPOSURE:
        // 从arg解析曝光值,写入寄存器
        ret = sensor_write_reg(sdev, REG_EXPOSURE, (u16)arg);
        break;
    case SENSOR_IOC_GET_TEMP:
        // 读取温度寄存器
        ret = sensor_read_reg(sdev, REG_TEMP, &temp);
        if (ret == 0)
            ret = copy_to_user((void __user *)arg, &temp, sizeof(temp));
        break;
    default:
        return -ENOTTY;
    }
    return ret;
}
注意:ioctl里一定要做权限检查。我曾经见过一个驱动,用户态随便传个非法命令,直接导致传感器进入测试模式,画面全花了。加个_IOC_DIR(cmd)检查,花不了几行代码。

4.3 SPI设备驱动编写

SPI和I2C最大的区别是什么?

SPI是四线制(MOSI、MISO、SCLK、CS),全双工,速度更快。图像传感器里,很多高帧率、高分辨率的型号都用SPI来配置寄存器,因为I2C的100kHz/400kHz速度有时候跟不上。

SPI设备驱动的框架和I2C类似,但通信方式不同。核心结构体是spi_device

static int sensor_spi_probe(struct spi_device *spi)
{
    struct sensor_dev *sdev;

    // 设置SPI模式、时钟频率
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10 * 1000 * 1000; // 10MHz
    spi_setup(spi);

    sdev = devm_kzalloc(&spi->dev, sizeof(*sdev), GFP_KERNEL);
    if (!sdev)
        return -ENOMEM;

    sdev->spi = spi;
    spi_set_drvdata(spi, sdev);

    // 后续初始化...
    return 0;
}

SPI读写寄存器时,我习惯封装一个通用函数。因为很多传感器要求先发地址,再发数据,或者地址和数据一起发。

static int sensor_spi_write_reg(struct sensor_dev *sdev, u16 reg, u8 val)
{
    u8 tx_buf[3];
    struct spi_transfer t = {
        .tx_buf = tx_buf,
        .len = 3,
    };
    struct spi_message m;

    tx_buf[0] = (reg >> 8) & 0xFF;  // 寄存器高字节
    tx_buf[1] = reg & 0xFF;         // 寄存器低字节
    tx_buf[2] = val;                // 要写入的数据

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(sdev->spi, &m);
}
经验之谈:SPI通信时,CS片选信号的时序很关键。有些传感器要求CS在传输前后保持一定时间的空闲电平。我遇到过一款传感器,CS拉低后必须等1us才能开始传时钟,否则第一个字节会错位。这种细节,数据手册里往往藏在“Timing Characteristics”章节,别漏了。

4.4 寄存器读写封装与调试

这部分,我觉得是驱动开发里最体现功力的地方。

好的封装,能让上层代码完全不知道底层是I2C还是SPI。我一般会定义一组抽象接口:

struct sensor_reg_ops {
    int (*read)(struct sensor_dev *sdev, u16 reg, u8 *val);
    int (*write)(struct sensor_dev *sdev, u16 reg, u8 val);
    int (*read_multi)(struct sensor_dev *sdev, u16 reg, u8 *buf, int len);
    int (*write_multi)(struct sensor_dev *sdev, u16 reg, u8 *buf, int len);
};

然后在probe时根据总线类型,挂载不同的实现。这样,上层配置算法调用sdev->ops->write(sdev, 0x3000, 0x42)就行了,不用关心底层细节。

调试技巧,嗯,这里我得好好说说。

寄存器读写出问题,最常见的现象就是传感器没反应,或者图像异常。我一般按这个顺序排查:

  1. 确认设备地址:用i2cdetect或spidev_test工具扫一下,看设备是否应答。
  2. 检查时序:用示波器或逻辑分析仪抓波形。SCL频率对不对?数据是否在时钟上升沿稳定?
  3. 验证寄存器地址:有些传感器寄存器地址是16位的,有些是8位的。写错了地址,读回来的全是0xFF或0x00。
  4. 加打印:在读写函数里加dev_dbg,打印每次操作的寄存器地址和值。配合动态调试,可以精确定位问题。
核心建议:写一个寄存器dump函数,把传感器所有关键寄存器的值一次性读出来,打印到内核日志。这样初始化后看一眼,就知道配置是否生效。我每个传感器驱动里都会加这个函数,调试效率提升不止一倍。

最后,分享一个我曾经踩过的坑。

有一次调试一款SPI接口的传感器,读寄存器总是返回0。折腾了两天,换芯片、换线、换逻辑分析仪……最后发现,传感器要求寄存器地址发送时,最高位要置1表示读操作。数据手册里写的是“Bit15: R/W#”,我愣是没注意。从那以后,我读任何数据手册,都会先把“Register Access”章节翻三遍。

好了,这一章的内容就到这里。

I2C和SPI的寄存器读写,是传感器驱动的基石。你把这个基础打牢了,后面写V4L2驱动、写控制算法,都会顺手很多。

下一章,我们聊聊V4L2框架和视频数据流。到时候见。