第3章:Sensor驱动开发——上电时序与I2C读写
各位好,我是老张。今天咱们聊聊Sensor驱动里最基础、也最容易翻车的一块——上电时序和I2C读写。
说实话,我见过太多工程师在Sensor初始化上栽跟头。明明代码逻辑没问题,Sensor就是不出图。最后查出来,要么是上电顺序搞反了,要么是I2C时序差那么一点点。嗯,咱们今天就把这些坑一个个填上。
3.1 上电时序:AVDD、DVDD、IOVDD
Sensor要工作,得先“吃饱饭”。这个“饭”就是三路电源:
- AVDD:模拟电源,一般2.8V。给像素阵列、模拟电路供电。
- DVDD:数字核心电源,常见1.2V或1.8V。给数字逻辑、PLL供电。
- IOVDD:IO接口电源,通常1.8V。给I2C、MIPI等接口供电。
这三路电,谁先谁后?
标准顺序是:AVDD → DVDD → IOVDD。或者反过来,IOVDD → DVDD → AVDD。具体看Sensor datasheet。我个人习惯是先上AVDD,再上DVDD,最后IOVDD。为什么?因为模拟电路稳定需要时间,先给它供电,等数字部分起来时,模拟已经稳了。
上电时序的典型代码实现(以GPIO控制LDO使能为例):
// 上电时序:AVDD -> DVDD -> IOVDD
void sensor_power_on(void) {
// 1. 先开AVDD
gpio_set_value(GPIO_AVDD_EN, 1);
mdelay(5); // 等待5ms,让AVDD稳定
// 2. 再开DVDD
gpio_set_value(GPIO_DVDD_EN, 1);
mdelay(2); // 等待2ms
// 3. 最后开IOVDD
gpio_set_value(GPIO_IOVDD_EN, 1);
mdelay(10); // 等待IOVDD稳定,准备I2C通信
}
下电顺序正好反过来:IOVDD → DVDD → AVDD。而且下电后要等一段时间(至少10ms)才能再次上电。为什么?因为电容放电需要时间,没放干净就重新上电,电压可能还没掉到0,Sensor会进入不确定状态。
3.2 I2C读写时序
电源搞定后,就该通过I2C和Sensor“对话”了。I2C读写看似简单,但坑也不少。
标准I2C写时序:
// 写Sensor寄存器
// 格式:START + 设备地址(写) + ACK + 寄存器地址高字节 + ACK + 寄存器地址低字节 + ACK + 数据 + ACK + STOP
int sensor_i2c_write(struct i2c_client *client, u16 reg, u8 val) {
u8 buf[3];
struct i2c_msg msg;
int ret;
buf[0] = (reg >> 8) & 0xFF; // 寄存器地址高字节
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 != 1) {
dev_err(&client->dev, "I2C write failed: reg=0x%04x, val=0x%02x\n", reg, val);
return -EIO;
}
return 0;
}
标准I2C读时序:
// 读Sensor寄存器
// 格式:START + 设备地址(写) + ACK + 寄存器地址高字节 + ACK + 寄存器地址低字节 + ACK +
// RESTART + 设备地址(读) + ACK + 数据 + NACK + STOP
int sensor_i2c_read(struct i2c_client *client, u16 reg, u8 *val) {
u8 reg_buf[2];
struct i2c_msg msgs[2];
int ret;
reg_buf[0] = (reg >> 8) & 0xFF;
reg_buf[1] = reg & 0xFF;
// 第一条消息:写寄存器地址
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 写
msgs[0].len = 2;
msgs[0].buf = reg_buf;
// 第二条消息:读数据
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 读
msgs[1].len = 1;
msgs[1].buf = val;
ret = i2c_transfer(client->adapter, msgs, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed: reg=0x%04x\n", reg);
return -EIO;
}
return 0;
}
这里有个细节:读操作时,写完寄存器地址后,要发一个RESTART信号,而不是STOP再START。为什么?因为如果发STOP,其他设备可能抢占总线,导致读操作失败。我刚开始做驱动时没注意这个,结果读回来的数据全是0xFF,查了两天才发现是时序问题。
3.3 Sensor初始化序列配置
上电、I2C通信都正常后,就该给Sensor“喂”初始化序列了。说白了,就是往一堆寄存器里写特定值,让Sensor进入工作状态。
初始化序列通常包含:
- 软件复位:让Sensor回到默认状态
- PLL配置:设置时钟频率、分频比
- 输出格式:设置分辨率、帧率、数据格式(RAW10/RAW12等)
- 模拟增益:设置模拟增益、数字增益
- 测试模式:输出测试图案,验证通路
初始化序列一般以表格形式给出:
| 寄存器地址 | 寄存器值 | 说明 |
|---|---|---|
| 0x0103 | 0x01 | 软件复位 |
| 0x0300 | 0x04 | PLL配置:分频比 |
| 0x0301 | 0x10 | PLL配置:倍频比 |
| 0x0340 | 0x07 | 输出分辨率:行数高字节 |
| 0x0341 | 0x80 | 输出分辨率:行数低字节 |
| 0x0342 | 0x0F | 输出分辨率:列数高字节 |
| 0x0343 | 0xA0 | 输出分辨率:列数低字节 |
代码实现时,我习惯把初始化序列定义为一个结构体数组:
struct sensor_reg {
u16 reg;
u8 val;
};
static const struct sensor_reg sensor_init_seq[] = {
{0x0103, 0x01}, // 软件复位
{0x0300, 0x04}, // PLL分频
{0x0301, 0x10}, // PLL倍频
{0x0340, 0x07}, // 行数高字节
{0x0341, 0x80}, // 行数低字节
{0x0342, 0x0F}, // 列数高字节
{0x0343, 0xA0}, // 列数低字节
// ... 更多配置
};
int sensor_init(struct i2c_client *client) {
int i, ret;
for (i = 0; i < ARRAY_SIZE(sensor_init_seq); i++) {
ret = sensor_i2c_write(client, sensor_init_seq[i].reg, sensor_init_seq[i].val);
if (ret) {
dev_err(&client->dev, "Init failed at reg=0x%04x\n", sensor_init_seq[i].reg);
return ret;
}
// 有些寄存器写入后需要等待
if (sensor_init_seq[i].reg == 0x0103) {
mdelay(10); // 复位后等待10ms
}
}
return 0;
}
3.4 寄存器读写封装
好的驱动,一定要有好的封装。我见过有些工程师把I2C读写直接写在初始化函数里,代码又长又乱。我的做法是:
- 底层封装:sensor_i2c_read/sensor_i2c_write,只处理I2C协议
- 中间层:sensor_reg_read/sensor_reg_write,加一些校验、重试逻辑
- 上层接口:sensor_init/sensor_start_stream,直接调用中间层
举个例子,中间层可以加个“写后读验证”:
int sensor_reg_write_verify(struct i2c_client *client, u16 reg, u8 val) {
int ret;
u8 read_val;
// 先写
ret = sensor_i2c_write(client, reg, val);
if (ret) return ret;
// 再读回来验证
ret = sensor_i2c_read(client, reg, &read_val);
if (ret) return ret;
if (read_val != val) {
dev_err(&client->dev, "Verify failed: reg=0x%04x, wrote=0x%02x, read=0x%02x\n",
reg, val, read_val);
return -EIO;
}
return 0;
}
这样封装的好处是:上层代码只需要关心“写什么值”,不用管底层怎么通信。而且验证逻辑统一,不会漏掉。
好了,这一章的内容就到这里。Sensor驱动开发,说白了就是“上电→通信→配置”三步走。每一步都有坑,但只要你按照datasheet来,加上我提到的那些小技巧,基本不会出大问题。
下一章咱们聊聊MIPI接口和CSI-2协议,那可是Sensor和ISP之间的“高速公路”。到时候见!