4. Sensor驱动开发:基于I2C/SPI的寄存器读写
好,咱们进入正题。Sensor驱动,说白了就是让主控芯片和图像传感器能“说话”。怎么说话?通过I2C或者SPI总线。你写一个值到某个寄存器,Sensor就改变一个行为;你读一个寄存器,就知道Sensor当前的状态。
我刚开始做驱动时,总觉得这玩意儿简单——不就是读写寄存器嘛。直到有一次,一个Sensor死活出不了图,查了三天,最后发现是初始化序列里一个寄存器的写入时序不对。嗯,从那以后,我再也不敢小看驱动开发了。
4.1 I2C vs SPI:选哪个?
先看个对比表,帮你快速决策:
| 特性 | I2C | SPI |
|---|---|---|
| 引脚数 | 2线(SCL, SDA) | 4线(SCLK, MOSI, MISO, CS) |
| 速度 | 标准100kHz~400kHz,快模式可达1MHz | 通常10MHz~50MHz,甚至更高 |
| 寻址方式 | 7位/10位设备地址 | 片选信号CS选择设备 |
| 多设备支持 | 总线挂载多个设备,地址不冲突即可 | 每个设备需要独立CS,引脚开销大 |
| 典型场景 | 配置寄存器、读取状态 | 高速配置、固件下载、数据流 |
我个人习惯:配置类用I2C,数据类用SPI。大部分手机摄像头Sensor的寄存器配置走I2C,因为引脚少、布线简单。但如果你在做高速工业相机,或者需要下载固件到Sensor,SPI是更好的选择。
4.2 寄存器读写:基础操作
先写一个最基础的I2C读写函数。别小看这几行代码,它是整个驱动的基石。
// sensor_i2c.c
#include "sensor_drv.h"
static int sensor_i2c_write(struct sensor_device *sdev,
uint16_t reg_addr,
uint8_t *data,
uint16_t len)
{
struct i2c_msg msg;
uint8_t buf[2 + len];
// 构造发送缓冲区:寄存器地址 + 数据
buf[0] = (reg_addr >> 8) & 0xFF; // 高字节
buf[1] = reg_addr & 0xFF; // 低字节
memcpy(&buf[2], data, len);
msg.addr = sdev->i2c_addr;
msg.flags = 0; // 写
msg.len = 2 + len;
msg.buf = buf;
return i2c_transfer(sdev->i2c_adap, &msg, 1);
}
static int sensor_i2c_read(struct sensor_device *sdev,
uint16_t reg_addr,
uint8_t *data,
uint16_t len)
{
struct i2c_msg msgs[2];
uint8_t addr_buf[2];
addr_buf[0] = (reg_addr >> 8) & 0xFF;
addr_buf[1] = reg_addr & 0xFF;
// 先写寄存器地址
msgs[0].addr = sdev->i2c_addr;
msgs[0].flags = 0;
msgs[0].len = 2;
msgs[0].buf = addr_buf;
// 再读数据
msgs[1].addr = sdev->i2c_addr;
msgs[1].flags = I2C_M_RD;
msgs[1].len = len;
msgs[1].buf = data;
return i2c_transfer(sdev->i2c_adap, msgs, 2);
}
注意看,我用了组合传输(先写地址再读数据)。为什么?因为大部分Sensor的读操作需要先告诉它你要读哪个寄存器,然后才能拿到数据。有些驱动偷懒,分两次调用,中间可能被其他设备打断——我曾经就被这个坑过,读回来的数据全是错的。
4.3 初始化序列:从手册到代码
拿到Sensor的datasheet,你会看到一张长长的寄存器配置表。比如:
| 寄存器地址 | 值 | 说明 |
|---|---|---|
| 0x0103 | 0x01 | 软件复位 |
| 0x3034 | 0x1A | PLL配置 |
| 0x3010 | 0x00 | MIPI Lane设置 |
| 0x4800 | 0x24 | 曝光控制 |
| ... | ... | ... |
我的做法是:把初始化序列定义成结构体数组,而不是散落在代码里。
// sensor_ov5640.h
struct sensor_regval {
uint16_t reg;
uint8_t val;
uint8_t delay_ms; // 写入后的等待时间
};
static const struct sensor_regval ov5640_init_seq[] = {
{0x0103, 0x01, 10}, // 软件复位,等10ms
{0x3034, 0x1A, 0}, // PLL配置,无需等待
{0x3010, 0x00, 0},
{0x4800, 0x24, 0},
// ... 可能几百行
{0xFFFF, 0xFF, 0}, // 结束标记
};
然后写一个通用的加载函数:
int sensor_load_init_seq(struct sensor_device *sdev,
const struct sensor_regval *seq)
{
int ret;
while (seq->reg != 0xFFFF) {
ret = sensor_i2c_write(sdev, seq->reg, &seq->val, 1);
if (ret < 0) {
dev_err(sdev->dev, "写寄存器 0x%04X 失败\n", seq->reg);
return ret;
}
if (seq->delay_ms > 0)
msleep(seq->delay_ms);
seq++;
}
return 0;
}
4.4 流控:Stream On / Stream Off
流控是驱动里最容易出bug的地方。Stream On让Sensor开始输出图像数据,Stream Off让它停止。但这里有个关键点:不是所有寄存器都能随便写。
我画了一张流控的状态图,帮你理清逻辑:
Stream On的典型实现:
int sensor_stream_on(struct sensor_device *sdev)
{
int ret;
uint8_t val = 0x01;
// 1. 检查当前状态
if (sdev->state != SENSOR_STATE_CONFIGURED) {
dev_err(sdev->dev, "状态错误,无法启动流\n");
return -EINVAL;
}
// 2. 写入Stream On寄存器
// 不同Sensor的寄存器不同,常见的有0x0100、0x3000等
ret = sensor_i2c_write(sdev, 0x0100, &val, 1);
if (ret < 0)
return ret;
// 3. 等待VSYNC稳定
msleep(100); // 至少等一帧
sdev->state = SENSOR_STATE_STREAMING;
dev_info(sdev->dev, "Stream On 成功\n");
return 0;
}
int sensor_stream_off(struct sensor_device *sdev)
{
int ret;
uint8_t val = 0x00;
if (sdev->state != SENSOR_STATE_STREAMING) {
dev_warn(sdev->dev, "当前不在Streaming状态\n");
return 0;
}
// 写入Stream Off寄存器
ret = sensor_i2c_write(sdev, 0x0100, &val, 1);
if (ret < 0)
return ret;
// 等待最后一帧输出完毕
msleep(50);
sdev->state = SENSOR_STATE_CONFIGURED;
dev_info(sdev->dev, "Stream Off 成功\n");
return 0;
}
4.5 通用Sensor驱动框架
好了,把上面的东西串起来,就是一个通用的驱动框架。我把它分成三层:
| 层级 | 文件 | 职责 |
|---|---|---|
| 硬件抽象层 | sensor_i2c.c / sensor_spi.c | 封装I2C/SPI读写,处理时序 |
| 核心驱动层 | sensor_core.c | 初始化序列加载、流控、电源管理 |
| Sensor适配层 | sensor_ov5640.c | 具体型号的寄存器表、特殊配置 |
核心数据结构:
struct sensor_device {
struct device *dev;
struct i2c_adapter *i2c_adap;
uint16_t i2c_addr;
enum sensor_state state; // 当前状态
const struct sensor_regval *init_seq; // 初始化序列
const struct sensor_regval *stream_on_seq; // 流控序列
const struct sensor_regval *stream_off_seq;
int (*write)(struct sensor_device *, uint16_t reg, uint8_t *data, uint16_t len);
int (*read)(struct sensor_device *, uint16_t reg, uint8_t *data, uint16_t len);
};
这个框架的好处是:换Sensor只需要换适配层。核心驱动和硬件抽象层基本不用动。我在一个项目里用这个框架同时支持了OV5640、IMX219、AR0230三个Sensor,切换时只需要改设备树和加载不同的适配文件。
- 硬件操作与业务逻辑分离
- 初始化序列数据驱动,不写死在代码里
- 流控状态机清晰,防止非法操作
- 错误处理完整,每个I2C操作都要检查返回值
嗯,到这里,一个通用的Sensor驱动框架就搭起来了。你可能会问:这就能用了吗?说实话,这只是基础。实际项目中还有电源时序、MIPI D-PHY配置、时钟树、中断处理等等。但有了这个骨架,后面的事情就是往里面填肉了。