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里必须做这几件事:

  1. 解析dts,拿到硬件资源(I2C client、GPIO、PWM等)。
  2. 初始化马达,比如设置初始位置、使能。
  3. 注册V4L2子设备。
  4. 注册控制句柄(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驱动框架数据流图 应用层(Camera HAL / 3A算法) V4L2框架(Video for Linux 2) AF V4L2子设备(subdev) AF驱动核心层(s_ctrl / 状态机) 硬件抽象层(I2C / PWM / GPIO) 物理设备(VCM马达 / 音圈电机) dts配置 (硬件参数) 关键路径 • s_ctrl 下发对焦命令 • s_stream 控制流开关 • 状态机管理马达位置

嗯,这张图基本把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参数配错了。

驱动框架这部分,说白了就是搭好架子、配好参数、注册好接口。架子搭稳了,后面的调试工作才能顺利进行。