4、Sensor驱动核心代码实现:上电时序、初始化序列、分辨率切换、帧率控制
驱动代码写得好不好,直接决定Sensor能不能正常工作。我见过不少方案,硬件设计没问题,就是驱动里时序写错了,结果图像出不来。说白了,驱动就是Sensor的“翻译官”——你要用代码告诉它:什么时候供电、怎么初始化、怎么切分辨率、跑多快。
这一节,我们就把这四个核心模块拆开揉碎了讲。
4.1 上电时序:Sensor的“开机仪式”
每个Sensor都有严格的上电要求。你先给哪路电、等多久、再拉哪个GPIO,差一点都不行。我早期踩过一个坑:某款OV的Sensor,DVDD和AVDD上电间隔差了0.5ms,结果图像一直有条纹。查了两天才发现是时序问题。
典型的MIPI Sensor上电流程是这样的:
- 拉高DVDD(数字核心电压,通常1.2V或1.8V)
- 等待T1(通常1~5ms)
- 拉高AVDD(模拟电压,通常2.8V)
- 等待T2(通常1~5ms)
- 拉高IOVDD(IO电压,通常1.8V或2.8V)
- 等待T3(通常1~10ms)
- 拉高MCLK(主时钟)
- 等待T4(通常1ms)
- 拉高RST(复位引脚)
- 等待T5(通常10~20ms)
- 通过I2C写入初始化序列
代码实现上,我习惯用gpiod_set_value_cansleep配合msleep来控制。举个例子:
static int sensor_power_on(struct device *dev) {
struct sensor_dev *sensor = dev_get_drvdata(dev);
// 1. 拉高DVDD
regulator_enable(sensor->dvdd);
msleep(2);
// 2. 拉高AVDD
regulator_enable(sensor->avdd);
msleep(2);
// 3. 拉高IOVDD
regulator_enable(sensor->iovdd);
msleep(5);
// 4. 提供MCLK
clk_prepare_enable(sensor->mclk);
msleep(1);
// 5. 释放复位
gpiod_set_value_cansleep(sensor->reset_gpio, 1);
msleep(20);
return 0;
}
#define T_POWER_UP_DVDD_AVDD 2。
4.2 初始化序列:Sensor的“出厂设置”
上电完成后,Sensor还处于“裸机”状态。你需要通过I2C写入一串寄存器配置,告诉它输出什么格式、多少分辨率、增益范围等等。这就是初始化序列。
初始化序列通常由Sensor厂商提供,但你不能直接照搬。为什么?因为厂商给的配置是针对他们的评估板的。你的板子走线长度、电源纹波、时钟质量都不一样,直接套用很可能出问题。
我一般把初始化序列组织成结构体数组:
struct sensor_reg {
u16 addr;
u8 val;
};
static const struct sensor_reg sensor_init_regs[] = {
// 软件复位
{0x0103, 0x01},
{0x0103, 0x00},
msleep(10),
// 设置输出格式:RAW10
{0x0100, 0x00}, // 先进入待机模式
{0x0340, 0x04}, // 输出高度高字节
{0x0341, 0x38}, // 输出高度低字节 (1080)
{0x0342, 0x07}, // 输出宽度高字节
{0x0343, 0x80}, // 输出宽度低字节 (1920)
// 模拟增益设置
{0x0204, 0x00}, // 增益高字节
{0x0205, 0x10}, // 增益低字节 (1x)
// 其他配置...
{0x0100, 0x01}, // 退出待机模式
};
写入时,我习惯用一个通用函数来遍历这个数组:
static int sensor_write_regs(struct i2c_client *client,
const struct sensor_reg *regs, int len) {
int i, ret;
for (i = 0; i < len; i++) {
ret = sensor_write_reg(client, regs[i].addr, regs[i].val);
if (ret < 0) {
dev_err(&client->dev, "写寄存器失败: addr=0x%04x\n", regs[i].addr);
return ret;
}
}
return 0;
}
msleep等待,不要写在数组里。我通常把需要等待的步骤单独抽出来,在驱动代码里显式调用msleep。这样更清晰,也方便调试。
4.3 分辨率切换:别让画面“卡住”
分辨率切换,说白了就是让Sensor从一种输出尺寸切换到另一种。比如从1080P切到720P。这个过程如果处理不好,画面会卡住或者花掉。
为什么会这样?因为切换分辨率时,Sensor内部的PLL、行缓存、输出接口都需要重新配置。你必须先让Sensor进入待机模式,改完配置,再唤醒它。
标准流程如下:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 停止视频流 | 写寄存器0x0100 = 0x00,进入待机 |
| 2 | 等待帧结束 | 等待当前帧传输完成,约1帧时间 |
| 3 | 写入新分辨率配置 | 修改输出宽度、高度、PLL分频等 |
| 4 | 重新启动视频流 | 写寄存器0x0100 = 0x01 |
| 5 | 等待稳定 | 等待Sensor输出新分辨率的图像 |
代码实现上,我习惯这样写:
static int sensor_set_resolution(struct i2c_client *client, int width, int height) {
// 1. 停止流
sensor_write_reg(client, 0x0100, 0x00);
msleep(20); // 等待帧结束
// 2. 根据分辨率选择配置
if (width == 1920 && height == 1080) {
sensor_write_regs(client, sensor_1080p_regs, ARRAY_SIZE(sensor_1080p_regs));
} else if (width == 1280 && height == 720) {
sensor_write_regs(client, sensor_720p_regs, ARRAY_SIZE(sensor_720p_regs));
} else {
dev_err(&client->dev, "不支持的分辨率: %dx%d\n", width, height);
return -EINVAL;
}
// 3. 重新启动流
sensor_write_reg(client, 0x0100, 0x01);
msleep(50); // 等待稳定
return 0;
}
msleep(20),问题解决。记住:切换分辨率前,一定要确保当前帧已经传输完成。
4.4 帧率控制:让Sensor跑在“正确节奏”上
帧率控制,本质上是控制Sensor输出一帧图像的时间。这个时间由两部分决定:
- 行时间(HTS):一行像素的传输时间
- 帧行数(VTS):一帧图像包含多少行
帧率计算公式很简单:
帧率 = PCLK / (HTS × VTS)
其中PCLK是像素时钟,由PLL配置决定。HTS和VTS都可以通过寄存器调整。
举个例子,假设PCLK是72MHz,HTS是2200,VTS是1125:
帧率 = 72,000,000 / (2200 × 1125) ≈ 29.1 fps
如果你想跑到30fps,可以减小VTS:
VTS = 72,000,000 / (2200 × 30) ≈ 1091
代码实现上,我一般把帧率控制封装成一个函数:
static int sensor_set_framerate(struct i2c_client *client, int fps) {
u16 vts;
u16 hts = 2200; // 固定行时间
u32 pclk = 72000000; // 像素时钟
// 计算VTS
vts = pclk / (hts * fps);
// 写入VTS寄存器
sensor_write_reg(client, 0x0340, (vts >> 8) & 0xFF); // 高字节
sensor_write_reg(client, 0x0341, vts & 0xFF); // 低字节
dev_info(&client->dev, "设置帧率: %d fps, VTS=%d\n", fps, vts);
return 0;
}
4.5 知识结构总览
下面这张图,把四个核心模块的关系串起来了:
这四个模块是递进关系:上电时序是前提,初始化序列是基础,分辨率切换是功能扩展,帧率控制是性能调优。你想想看,任何一个环节出问题,图像质量都无从谈起。