第3章:Sensor驱动编写——基于RK官方SDK的实战
好,咱们进入正题。这一章要干的事,说白了就是让RK平台认识你的Sensor。我见过不少新手,拿到SDK就急着往上堆代码,结果点不亮、调不通,最后回头查才发现是驱动层就没写对。咱们今天就把这块掰开揉碎了讲清楚。
3.1 驱动框架概览
RK官方的Sensor驱动,其实是一个标准的V4L2子设备驱动。它挂在I2C总线上,通过I2C寄存器读写来控制Sensor。整个驱动需要实现三个核心能力:
- 分辨率/帧率配置 —— 告诉Sensor输出多大画面、多快速度
- 曝光控制 —— 调节积分时间,说白了就是让Sensor“看”多久
- 增益控制 —— 放大信号,但也会放大噪声,这是个平衡活
我习惯把驱动拆成三层来看:
| 层级 | 职责 | 对应文件/函数 |
|---|---|---|
| 平台适配层 | 注册V4L2子设备、处理DTS匹配 | probe()、remove() |
| 控制逻辑层 | 实现s_ctrl回调、曝光/增益计算 | sensor_s_ctrl() |
| 硬件操作层 | I2C读写、寄存器配置序列 | sensor_write_array() |
关键点:驱动不是从头造轮子。RK SDK里已经有完整的模板,你只需要填充Sensor特有的寄存器配置和控制逻辑。
3.2 驱动入口与DTS匹配
先看驱动入口。每个Sensor驱动都有一个probe函数,它负责初始化I2C设备、读取电源时序、注册子设备。我一般这么写:
static const struct i2c_device_id sensor_id[] = {
{"imx415", 0},
{}
};
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "sony,imx415" },
{}
};
static struct i2c_driver sensor_i2c_driver = {
.driver = {
.name = "imx415",
.of_match_table = sensor_of_match,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id,
};
module_i2c_driver(sensor_i2c_driver);
这里要注意compatible字符串,它必须和DTS里的节点完全一致。我曾经遇到一个坑,DTS里写的是"imx415",驱动里写的是"imx415_v2",结果内核根本不会调用你的probe。嗯,这种低级错误排查起来最浪费时间。
3.3 分辨率与帧率配置
Sensor的寄存器配置,通常以数组形式存放在驱动里。每个分辨率对应一组寄存器序列。我习惯用结构体来组织:
struct sensor_regval {
u16 reg;
u8 val;
};
static const struct sensor_regval sensor_1920x1080_30fps[] = {
{0x3000, 0x00}, // 软复位
{0x3001, 0x01}, // 开始配置
{0x3010, 0x43}, // 水平输出尺寸高字节
{0x3011, 0x20}, // 水平输出尺寸低字节 (0x4320 = 1920)
{0x3012, 0x02}, // 垂直输出尺寸高字节
{0x3013, 0x58}, // 垂直输出尺寸低字节 (0x0258 = 600)
// ... 省略中间配置
{0x3014, 0x2E}, // 帧率控制
{0x3015, 0x1C}, // PLL配置
{0x3030, 0x01}, // 开始输出
};
驱动里通过一个枚举来管理这些模式:
enum sensor_mode {
SENSOR_MODE_1920X1080_30FPS,
SENSOR_MODE_1920X1080_60FPS,
SENSOR_MODE_3840X2160_30FPS,
SENSOR_MODE_MAX,
};
在s_fmt回调里,根据用户请求的分辨率,选择对应的寄存器数组,然后通过I2C批量写入。我建议把写入函数封装一下:
static int sensor_write_array(struct i2c_client *client,
const struct sensor_regval *regs,
u32 size)
{
int i, ret;
for (i = 0; i < size; i++) {
ret = sensor_write_reg(client, regs[i].reg, regs[i].val);
if (ret < 0)
return ret;
}
return 0;
}
小技巧:写入寄存器序列时,最好在每两个寄存器之间加一个1ms的延时。有些Sensor对写入速度有要求,写太快会丢寄存器。我踩过这个坑,画面出现条纹,排查了半天才发现是写入太快导致的。
3.4 曝光与增益控制接口
曝光和增益控制,是通过V4L2的s_ctrl回调实现的。RK平台会调用这个回调来设置曝光时间和增益值。核心逻辑如下:
static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
struct sensor *sensor = container_of(ctrl->handler,
struct sensor, ctrl_handler);
switch (ctrl->id) {
case V4L2_CID_EXPOSURE:
// 曝光值单位是行数,需要转换成Sensor的寄存器值
sensor->exposure = ctrl->val;
sensor_set_exposure(sensor, sensor->exposure);
break;
case V4L2_CID_GAIN:
// 增益值单位是dB,需要查表或计算
sensor->gain = ctrl->val;
sensor_set_gain(sensor, sensor->gain);
break;
case V4L2_CID_VBLANK:
// 垂直消隐区,影响帧率
sensor->vblank = ctrl->val;
sensor_set_vblank(sensor, sensor->vblank);
break;
default:
return -EINVAL;
}
return 0;
}
曝光控制的实现,说白了就是写两个寄存器:粗调(Coarse Integration Time)和细调(Fine Integration Time)。大部分Sensor只用粗调就够了:
static int sensor_set_exposure(struct sensor *sensor, u32 exposure)
{
// 曝光值不能超过一帧的总行数
u32 max_exposure = sensor->vts - 4; // 留4行余量
if (exposure > max_exposure)
exposure = max_exposure;
sensor_write_reg(sensor, 0x3020, (exposure >> 8) & 0xFF);
sensor_write_reg(sensor, 0x3021, exposure & 0xFF);
return 0;
}
增益控制稍微复杂一点。Sensor的增益通常分为模拟增益和数字增益。模拟增益噪声小,但范围有限;数字增益范围大,但会放大噪声。我一般优先使用模拟增益:
static int sensor_set_gain(struct sensor *sensor, u32 gain)
{
u32 analog_gain, digital_gain;
if (gain <= 64) {
// 0dB ~ 12dB 用模拟增益
analog_gain = gain;
digital_gain = 0;
} else {
// 超过12dB,模拟增益固定,数字增益补足
analog_gain = 64;
digital_gain = gain - 64;
}
sensor_write_reg(sensor, 0x3030, analog_gain);
sensor_write_reg(sensor, 0x3031, digital_gain);
return 0;
}
注意:曝光和增益的写入顺序很重要。必须先写增益,再写曝光。因为改变增益会影响Sensor的灵敏度,如果先写曝光再写增益,会导致这一帧的曝光量不准。我调试时遇到过画面闪烁,就是顺序搞反了。
3.5 驱动注册与初始化流程
整个驱动的初始化流程,我画了一张图,方便你理解:
你看,整个流程其实不复杂。但每一步都可能出问题。我调试时最常遇到的是Sensor ID校验失败,往往是I2C地址写错了,或者Sensor还没完全上电就去读寄存器。
3.6 避坑指南
最后,我总结几个实战中容易踩的坑:
- I2C地址要确认 —— Sensor的I2C地址通常是7位还是8位?RK平台用的是7位地址,但有些Sensor手册给的是8位。记得右移一位。
- 帧率计算要准确 —— 帧率 = 主时钟 / (HTS * VTS)。HTS和VTS要从寄存器读出来,不要想当然。
- 曝光单位要统一 —— 有些Sensor曝光单位是行数,有些是微秒。RK平台默认用行数,如果Sensor用微秒,你得在驱动里做转换。
- 增益表要查对 —— 很多Sensor的增益不是线性的,需要查表。我建议把增益表放在驱动里,用二分查找来匹配。
我的习惯:每次写完驱动,先用i2cdetect工具确认I2C通信正常,再用v4l2-ctl -d /dev/v4l-subdevX --list-ctrls确认控制接口注册成功。这两个命令能帮你快速定位问题。