4、寄存器读写实战:I2C读写封装、寄存器map解析、使用i2c-tools调试

做Camera驱动,说白了就是跟寄存器打交道。Sensor是颗芯片,CPU要让它干活,就得通过I2C总线给它写寄存器。今天我们就来把这套流程彻底搞明白。

我个人习惯,拿到一颗新Sensor,第一件事不是看驱动代码,而是先拿i2c-tools在终端里敲几下,确认I2C能通、寄存器能读写。这一步走通了,后面写驱动心里才有底。

4.1 I2C协议基础与Sensor通信模型

I2C总线就两根线:SCL(时钟)和SDA(数据)。Sensor作为从设备,有唯一的7位地址。比如OV5640的地址通常是0x3C(写)或0x3D(读)。

通信流程很简单:

  • 主机发START信号
  • 主机发从设备地址+写标志
  • 从设备应答ACK
  • 主机发寄存器地址(高8位)
  • 从设备应答ACK
  • 主机发寄存器地址(低8位)
  • 从设备应答ACK
  • 主机发数据字节
  • 从设备应答ACK
  • 主机发STOP信号

嗯,这里要注意:很多Sensor的寄存器地址是16位的,分高字节和低字节两次发送。但也有8位地址的Sensor,比如GC系列。写驱动前一定要看datasheet确认。

核心要点:I2C读写Sensor寄存器,本质就是“地址+数据”的打包发送。封装好读写函数,后面所有操作都基于这两个函数。

4.2 I2C读写函数封装实战

在Linux内核里,I2C读写有标准接口。我一般封装成下面这样:

/* sensor_i2c.c */
#include <linux/i2c.h>
#include <linux/delay.h>

/* 写16位寄存器地址,8位数据 */
int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    int ret;
    u8 buf[3];
    struct i2c_msg msg;
    
    buf[0] = (reg >> 8) & 0xFF;  /* 寄存器高8位 */
    buf[1] = reg & 0xFF;         /* 寄存器低8位 */
    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 != 1) {
        dev_err(&client->dev, "I2C write reg 0x%04x failed\n", reg);
        return -EIO;
    }
    
    return 0;
}

/* 读16位寄存器地址,8位数据 */
int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    int ret;
    u8 addr_buf[2];
    struct i2c_msg msg[2];
    
    /* 先写寄存器地址 */
    addr_buf[0] = (reg >> 8) & 0xFF;
    addr_buf[1] = reg & 0xFF;
    
    msg[0].addr = client->addr;
    msg[0].flags = 0;            /* 写 */
    msg[0].len = 2;
    msg[0].buf = addr_buf;
    
    /* 再读数据 */
    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;    /* 读 */
    msg[1].len = 1;
    msg[1].buf = val;
    
    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret != 2) {
        dev_err(&client->dev, "I2C read reg 0x%04x failed\n", reg);
        return -EIO;
    }
    
    return 0;
}

我的经验:有些Sensor支持连续读写(burst mode),一次传多个字节。比如读一整段OTP数据,用burst模式效率高很多。封装时可以把burst读写也加上,后面调ISP参数时会用到。

4.3 寄存器Map解析——读懂Sensor的“内存地图”

寄存器Map就是Sensor内部所有寄存器的地址和功能说明。拿到datasheet,先看寄存器Map章节。我一般把它整理成表格:

地址 名称 默认值 位域 功能描述
0x3000 CHIP_ID_H 0x56 [7:0] 芯片ID高8位
0x3001 CHIP_ID_L 0x40 [7:0] 芯片ID低8位
0x3010 MODE_SELECT 0x00 [7:0] 0x00: 软件待机, 0x01: 流模式
0x3034 PLL_MULTIPLIER 0x28 [7:0] PLL倍频系数
0x3035 PLL_DIVIDER 0x02 [7:0] PLL分频系数
0x3808 ISP_WIN_H 0x0A [3:0] ISP输出宽度高4位
0x3809 ISP_WIN_L 0x80 [7:0] ISP输出宽度低8位

你想想看,如果不知道寄存器Map,你连Sensor的ID都读不出来,更别说配置分辨率、帧率了。所以拿到新Sensor,先把关键寄存器标出来:

  • 芯片ID寄存器——验证I2C通信是否正常
  • 软件复位寄存器——让Sensor回到初始状态
  • 模式控制寄存器——切换待机/流模式
  • PLL配置寄存器——设置时钟频率
  • 输出尺寸寄存器——配置分辨率

我曾经踩过的坑:有一次读Sensor ID,读出来全是0xFF。查了半天,发现是I2C地址搞错了。datasheet上写的是7位地址0x3C,但Linux i2c驱动里要左移1位变成0x78。这个细节很容易忽略。

4.4 使用i2c-tools进行寄存器调试

i2c-tools是Linux用户空间下的I2C调试利器。我调试Sensor时,90%的问题先用它定位。常用命令就这几个:

# 1. 扫描I2C总线上的设备
i2cdetect -y 0          # 扫描I2C总线0
i2cdetect -y 1          # 扫描I2C总线1

# 2. 读寄存器(8位地址)
i2cget -y 0 0x3c 0x3000 w  # 从设备0x3c读寄存器0x3000(16位地址)

# 3. 写寄存器
i2cset -y 0 0x3c 0x3010 0x01  # 写0x01到寄存器0x3010

# 4. 连续读多个寄存器
i2cdump -y 0 0x3c        # dump所有寄存器值

# 5. 批量写寄存器(从文件读取)
i2ctransfer -y 0 w3@0x3c 0x30 0x10 0x01  # 写3字节:地址+数据

我个人习惯的调试流程:

  1. 先扫总线——i2cdetect -y 0,看Sensor地址有没有出现
  2. 读芯片ID——i2cget -y 0 0x3c 0x3000 w,验证通信
  3. 写寄存器配置——按datasheet配置PLL、分辨率
  4. 读回验证——写完后读回来,确认写入成功
  5. dump全寄存器——看所有寄存器的当前状态

实战技巧:把Sensor的初始化序列保存成文本文件,每行格式为“寄存器地址 数据值”。然后用脚本批量调用i2cset写入。这样调试时改参数特别快,不用每次都编译内核驱动。

4.5 知识结构图:I2C调试与寄存器操作全景

下面这张图把本章的核心知识点串起来了:

I2C调试与寄存器操作知识结构图 I2C调试与寄存器操作知识结构 I2C寄存器读写 I2C协议基础 SCL/SDA双线通信 START/ACK/STOP时序 16位/8位寄存器地址 驱动读写封装 sensor_write_reg() sensor_read_reg() burst批量读写 i2c-tools调试 i2cdetect扫描设备 i2cget/i2cset读写 i2cdump批量查看 寄存器Map解析 核心流程:I2C协议 → 驱动封装 → i2c-tools验证 → 寄存器Map配置 先工具后驱动,先读ID后配置,步步为营

4.6 实战案例:用i2c-tools点亮Sensor

假设你拿到一颗OV5640 Sensor,上电后MCLK给了24MHz。按下面步骤验证:

# 步骤1:扫描I2C总线,确认设备存在
root@board:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
...

# 步骤2:读芯片ID,验证通信
root@board:~# i2cget -y 0 0x3c 0x3000 w
0x5640    # 读到0x5640,说明通信正常

# 步骤3:配置Sensor进入流模式
root@board:~# i2cset -y 0 0x3c 0x3010 0x01

# 步骤4:读回确认
root@board:~# i2cget -y 0 0x3c 0x3010 w
0x01      # 确认写入成功

我的习惯:把常用Sensor的初始化序列写成shell脚本,比如init_ov5640.sh。调试时一键执行,省去手动敲命令的麻烦。脚本里还可以加读回校验,自动检查写入是否成功。

4.7 常见问题与避坑指南

我曾经遇到的几个坑:

  • I2C地址不对:datasheet给的是7位地址,Linux驱动里要用8位地址(左移1位)。比如0x3C变成0x78。
  • 寄存器地址位数搞错:有些Sensor用8位寄存器地址,有些用16位。写i2cget时参数要对应,否则读到错误数据。
  • 上电时序没满足:Sensor上电后需要等待稳定时间(通常1-10ms),立刻读写可能失败。我习惯在读写前加msleep(10)。
  • I2C总线被占用:调试时如果i2cdetect扫不到设备,先检查硬件连接,再用示波器看SCL/SDA波形。

嗯,以上就是寄存器读写的全部实战内容。从I2C协议到驱动封装,再到i2c-tools调试,每一步都是后面调ISP的基础。记住:先工具后驱动,先读ID后配置,步步为营,稳扎稳打。