3. AF驱动框架:Linux V4L2子设备、RK AF驱动代码结构、dts配置与解析
驱动框架这东西,说白了就是硬件和上层应用之间的翻译官。AF驱动也不例外。我刚开始接触RK平台时,面对V4L2那一堆结构体,说实话有点懵。但摸清楚套路后,你会发现它其实很规整。
3.1 V4L2子设备模型:AF驱动的“身份证”
在Linux内核中,摄像头传感器、闪光灯、AF马达这些外设,都被抽象成V4L2子设备(subdev)。AF驱动就是其中一个子设备。它通过V4L2框架注册自己,告诉系统:“我能调焦,这是我的控制接口。”
核心结构体是 struct v4l2_subdev 和 struct v4l2_subdev_ops。AF驱动主要实现 core 和 video 两个操作集。
关键点:AF子设备必须实现 s_ctrl 回调,这是上层下发对焦命令的入口。我见过不少驱动注册成功了,但上层发命令没反应,一查就是 s_ctrl 没挂上。
// V4L2子设备核心结构体
struct v4l2_subdev {
struct media_entity entity;
struct list_head list;
const struct v4l2_subdev_ops *ops;
// ... 其他成员
};
// AF驱动需要实现的ops
static const struct v4l2_subdev_ops af_subdev_ops = {
.core = &af_core_ops, // 包含 s_ctrl
.video = &af_video_ops, // 包含 s_stream 等
};
3.2 RK AF驱动代码结构:从probe到release
RK平台的AF驱动,我习惯把它拆成三层来看:
- 平台层:处理I2C通信、GPIO控制、PWM生成。这部分跟硬件强相关。
- 核心层:实现V4L2子设备接口、控制逻辑、状态机管理。
- 算法层:虽然AF算法通常在用户空间,但驱动里也会有一些简单的爬山算法或位置映射。
驱动入口是 probe 函数。我写驱动时,probe里必须做这几件事:
- 解析dts,拿到硬件资源(I2C client、GPIO、PWM等)。
- 初始化马达,比如设置初始位置、使能。
- 注册V4L2子设备。
- 注册控制句柄(controls),比如
V4L2_CID_FOCUS_ABSOLUTE。
个人习惯:我会在probe最后加一个延时操作,等马达稳定后再注册子设备。曾经有个项目,probe后马上发对焦命令,马达还没准备好,导致第一次对焦总是失败。加个50ms延时就解决了。
static int af_driver_probe(struct i2c_client *client) {
struct af_device *af_dev;
int ret;
// 1. 解析dts
af_dev = devm_kzalloc(&client->dev, sizeof(*af_dev), GFP_KERNEL);
ret = of_property_read_u32(client->dev.of_node, "rockchip,af-init-pos",
&af_dev->init_pos);
// 2. 初始化硬件
af_dev->client = client;
af_dev->pwm = devm_pwm_get(&client->dev, NULL);
// ... 初始化马达
// 3. 注册V4L2子设备
v4l2_i2c_subdev_init(&af_dev->sd, client, &af_subdev_ops);
af_dev->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
// 4. 注册控制句柄
af_dev->ctrl_handler = &af_dev->ctrl_handler;
v4l2_ctrl_handler_init(af_dev->ctrl_handler, 2);
v4l2_ctrl_new_std(af_dev->ctrl_handler, &af_ctrl_ops,
V4L2_CID_FOCUS_ABSOLUTE, 0, 1023, 1, 0);
// 5. 注册子设备
ret = v4l2_async_register_subdev(&af_dev->sd);
return ret;
}
3.3 dts配置与解析:硬件信息的“户口本”
dts(Device Tree Source)是驱动获取硬件信息的唯一来源。AF驱动需要从dts里拿到马达型号、I2C地址、GPIO引脚、PWM通道、初始位置等参数。
我整理了一份典型的AF dts配置模板:
| 属性名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
compatible |
string | 驱动匹配名称 | "rockchip,af-dw9714" |
reg |
u32 | I2C设备地址 | 0x18 |
rockchip,af-init-pos |
u32 | 马达初始位置(0-1023) | 512 |
rockchip,af-pwm |
phandle | PWM控制器引用 | &pwm0 |
rockchip,af-power-gpios |
gpio | 马达使能GPIO | &gpio1 RK_PB2 GPIO_ACTIVE_HIGH |
rockchip,af-range |
u32 array | 对焦范围 [min, max] | <0 1023> |
我曾经踩过的坑:dts里写错了I2C地址,驱动probe时一直返回-ENODEV。查了两天才发现是地址少了一位偏移。记住,I2C地址是7位,但dts里要写8位(左移1位)。比如设备地址0x0C,dts里写0x18。
驱动解析dts的代码通常放在probe里,用 of_property_read_* 系列函数:
// dts解析示例
static int af_parse_dt(struct af_device *af_dev) {
struct device_node *np = af_dev->client->dev.of_node;
u32 val;
// 读取初始位置
if (of_property_read_u32(np, "rockchip,af-init-pos", &val) == 0) {
af_dev->init_pos = val;
} else {
af_dev->init_pos = 512; // 默认值
dev_warn(&af_dev->client->dev, "use default init pos: 512\n");
}
// 读取对焦范围
if (of_property_read_u32_array(np, "rockchip,af-range",
af_dev->range, 2)) {
af_dev->range[0] = 0;
af_dev->range[1] = 1023;
}
// 获取PWM句柄
af_dev->pwm = devm_pwm_get(&af_dev->client->dev, NULL);
if (IS_ERR(af_dev->pwm)) {
dev_err(&af_dev->client->dev, "failed to get pwm\n");
return PTR_ERR(af_dev->pwm);
}
return 0;
}
3.4 整体框架图:从dts到应用层的数据流
下面这张图展示了AF驱动在整个系统中的位置和数据流向。我画图时习惯把关键路径标出来,方便调试时快速定位问题。
嗯,这张图基本把AF驱动的骨架画出来了。从上到下依次是:应用层发命令 → V4L2框架转发 → 子设备接收 → 核心层处理 → 硬件抽象层执行 → 物理马达动作。dts配置在驱动核心层被解析,提供硬件参数。
3.5 调试要点:三板斧
驱动写好了,怎么验证它跑通了?我总结了三个快速检查点:
- 检查dmesg:看probe有没有成功,有没有打印“AF driver registered”之类的信息。
- 检查设备节点:
/dev/v4l-subdev*下有没有新增节点。用ls -l /dev/v4l-subdev*查看。 - 检查控制句柄:用
v4l2-ctl -d /dev/v4l-subdevX --list-ctrls查看focus_absolute是否出现。
我的调试习惯:在 s_ctrl 回调里加一条 dev_dbg,打印收到的控制ID和值。这样上层发命令时,驱动有没有收到、收到的是什么值,一目了然。曾经有个问题,上层发了对焦值500,驱动收到的是0,最后发现是控制句柄的step参数配错了。
驱动框架这部分,说白了就是搭好架子、配好参数、注册好接口。架子搭稳了,后面的调试工作才能顺利进行。