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字节:地址+数据
我个人习惯的调试流程:
- 先扫总线——
i2cdetect -y 0,看Sensor地址有没有出现 - 读芯片ID——
i2cget -y 0 0x3c 0x3000 w,验证通信 - 写寄存器配置——按datasheet配置PLL、分辨率
- 读回验证——写完后读回来,确认写入成功
- dump全寄存器——看所有寄存器的当前状态
实战技巧:把Sensor的初始化序列保存成文本文件,每行格式为“寄存器地址 数据值”。然后用脚本批量调用i2cset写入。这样调试时改参数特别快,不用每次都编译内核驱动。
4.5 知识结构图:I2C调试与寄存器操作全景
下面这张图把本章的核心知识点串起来了:
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后配置,步步为营,稳扎稳打。