3、摄像头驱动初始化:platform driver注册、设备树解析、时钟与电源管理、中断注册

摄像头驱动跑起来的第一步,就是初始化。说白了,就是让内核知道「嘿,我在这儿,这是我的资源,给我干活」。这一步要是没做好,后面调得再好也白搭。我刚开始接触R-Car平台时,就吃过设备树写错的亏,折腾了两天才发现是时钟节点名拼错了。

这一节,我们拆成四个核心环节来讲:

  • platform driver注册 —— 驱动怎么挂到内核里
  • 设备树解析 —— 硬件信息怎么读进来
  • 时钟与电源管理 —— 让摄像头芯片「活」过来
  • 中断注册 —— 让CPU能响应摄像头的事件

核心思路: 初始化不是一股脑把所有事都做完,而是按依赖关系分层推进。先注册驱动,再解析资源,然后上电配时钟,最后注册中断。顺序错了,系统可能直接崩。

3.1 platform driver注册:驱动与设备的「相亲」

在R-Car平台上,摄像头控制器通常挂载在ISP或VIN总线上,用platform driver模型来管理。你想想看,内核怎么知道哪个驱动对应哪个硬件?靠的就是compatible字符串。

我个人习惯把驱动结构体写成这样:

static const struct of_device_id rcar_vin_of_table[] = {
    { .compatible = "renesas,vin-r8a7795", .data = &rcar_vin_info_r8a7795 },
    { .compatible = "renesas,vin-r8a7796", .data = &rcar_vin_info_r8a7796 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rcar_vin_of_table);

static struct platform_driver rcar_vin_driver = {
    .driver = {
        .name = "rcar-vin",
        .of_match_table = rcar_vin_of_table,
        .pm = &rcar_vin_pm_ops,
    },
    .probe = rcar_vin_probe,
    .remove = rcar_vin_remove,
};

module_platform_driver(rcar_vin_driver);

这里有个细节:module_platform_driver 是个宏,它帮你把 module_init 和 module_exit 都包好了。我早期手写过 platform_driver_register,后来发现用这个宏更干净,少写两行代码,也少犯低级错误。

小技巧: 如果你的驱动支持多个SoC版本(比如R-Car H3、M3、V3U),of_device_id 里的 .data 字段可以指向一个结构体,里面放每个版本的差异参数。probe时用 of_device_get_match_data() 取出来,避免写一堆if-else。

3.2 设备树解析:把硬件信息「读」出来

设备树(DT)是R-Car平台描述硬件的主要方式。摄像头节点里通常包含:寄存器地址、中断号、时钟源、GPIO引脚、电源域等信息。

probe函数里,我一般这样解析:

static int rcar_vin_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct rcar_vin_priv *priv;
    struct resource *res;
    int ret;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    platform_set_drvdata(pdev, priv);

    // 1. 解析寄存器地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->base = devm_ioremap_resource(dev, res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    // 2. 解析中断
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    // 3. 解析时钟
    priv->clk = devm_clk_get(dev, "vin_clk");
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);

    // 4. 解析电源
    priv->rstc = devm_reset_control_get_exclusive(dev, NULL);
    if (IS_ERR(priv->rstc))
        return PTR_ERR(priv->rstc);

    // 5. 解析GPIO(如果有)
    priv->pwdn_gpio = devm_gpiod_get_optional(dev, "powerdown", GPIOD_OUT_HIGH);

    // ... 后续初始化
}

嗯,这里要注意:devm_ 系列函数是「托管资源」,probe失败或remove时,内核会自动释放。我建议能用 devm_ 就用,省得自己写一堆错误处理。

我曾经踩过的坑: 设备树里 clocks 属性配了两个时钟,但代码里只 devm_clk_get 了一个。结果第二个时钟没使能,摄像头初始化到一半就卡死了。后来我养成了习惯:设备树里配了几个时钟,代码里就一定要全部获取并检查。

3.3 时钟与电源管理:让摄像头「活」过来

摄像头芯片要工作,离不开三样东西:时钟、电源、复位。顺序很重要——先上电,再给时钟,最后释放复位。

R-Car平台上的时钟管理,我一般这样处理:

static int rcar_vin_power_on(struct rcar_vin_priv *priv)
{
    int ret;

    // 1. 先使能时钟
    ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;

    // 2. 释放复位
    ret = reset_control_deassert(priv->rstc);
    if (ret) {
        clk_disable_unprepare(priv->clk);
        return ret;
    }

    // 3. 如果有外部电源控制,拉高GPIO
    if (priv->pwdn_gpio)
        gpiod_set_value(priv->pwdn_gpio, 0);  // 0 = 正常模式

    return 0;
}

static void rcar_vin_power_off(struct rcar_vin_priv *priv)
{
    if (priv->pwdn_gpio)
        gpiod_set_value(priv->pwdn_gpio, 1);

    reset_control_assert(priv->rstc);
    clk_disable_unprepare(priv->clk);
}

你想想看,如果先释放复位再给时钟,芯片内部逻辑可能处于不确定状态,轻则功能异常,重则锁死总线。我在调试一个OV5640模组时就遇到过,时序反了导致I2C通信一直NACK。

另外,R-Car的电源管理还涉及Runtime PM。我个人习惯在probe里注册 pm_runtime 回调:

pm_runtime_enable(dev);
if (!pm_runtime_enabled(dev)) {
    ret = rcar_vin_power_on(priv);
    if (ret)
        goto err_power;
}

这样,系统可以在摄像头空闲时自动关掉时钟和电源,省电又降温。车载场景下,这个功能特别实用。

3.4 中断注册:让CPU「听」到摄像头的声音

摄像头驱动里,中断是用来通知CPU「帧准备好了」、「出错啦」、「同步信号来了」的。R-Car VIN模块的中断类型挺多,我一般只注册最关键的几个:

中断类型 触发条件 处理动作
帧开始 (VS) 检测到垂直同步信号 清DMA缓冲区,准备接收
帧结束 (FE) 一帧数据采集完成 唤醒等待队列,通知应用层
错误 (ERR) 数据溢出、CRC错误等 统计错误计数,尝试恢复
行结束 (LE) 一行数据采集完成 可选,用于调试或逐行处理

注册中断的代码很简单:

ret = devm_request_irq(dev, priv->irq, rcar_vin_irq_handler,
                       IRQF_TRIGGER_RISING, "rcar-vin", priv);
if (ret) {
    dev_err(dev, "failed to request irq %d\n", priv->irq);
    return ret;
}

中断处理函数里,我习惯先读状态寄存器,判断是哪种中断,然后清标志位,最后做对应处理:

static irqreturn_t rcar_vin_irq_handler(int irq, void *data)
{
    struct rcar_vin_priv *priv = data;
    u32 status;

    status = readl(priv->base + VIN_INTSTS);
    writel(status, priv->base + VIN_INTSTS);  // 写1清中断

    if (status & VIN_INT_FE) {
        // 帧结束:通知DMA完成
        complete(&priv->frame_completion);
    }

    if (status & VIN_INT_ERR) {
        // 错误:记录并尝试恢复
        priv->err_count++;
        dev_warn_ratelimited(priv->dev, "VIN error, count=%d\n", priv->err_count);
    }

    return IRQ_HANDLED;
}

避坑指南: 中断标志位一定要先读再写清,而且清的时候只写已读到的位。我曾经图省事直接写 0xFFFFFFFF,结果把不该清的中断也清了,导致丢帧。另外,错误中断建议用 dev_warn_ratelimited,防止中断风暴把日志刷爆。

3.5 初始化流程总览

把上面四个环节串起来,整个初始化的流程就清晰了。我画了一张图,方便你对照理解:

摄像头驱动初始化流程 摄像头驱动初始化流程 module_platform_driver probe() 入口 设备树解析 寄存器 + 中断号 时钟 + 电源 + GPIO 时钟与电源管理 中断注册 💡 关键顺序 ① 先注册驱动 ② 再解析资源 ③ 然后上电配时钟 ④ 最后注册中断

从图上你能看到,整个初始化是线性推进的。每一步都依赖上一步的结果。我个人建议在probe函数里,每一步都加 dev_dbg 打印,方便调试时定位问题。