4. RKISP驱动初始化流程

好,咱们今天来聊聊RKISP驱动的初始化流程。说实话,这部分是驱动开发的骨架,你把它搭稳了,后面写控制逻辑、调图像质量才有底气。我刚开始接触RKISP时,也对着datasheet和内核代码发懵,后来摸清了probe这条主线,一切就豁然开朗了。

4.1 platform driver probe流程——驱动入口的“第一把火”

RKISP在Linux内核里是以platform driver的形式存在的。为什么用platform?说白了,ISP这种IP核在SoC内部是静态映射的,不像USB、PCIe那样可以热插拔。内核通过设备树匹配到我们的驱动,然后调用probe函数。

看一下典型的probe流程,我习惯把它拆成几个阶段:

  1. 匹配与入口:of_match_table把设备树节点和驱动绑定。
  2. 资源获取:解析设备树,拿到寄存器地址、中断号、时钟等。
  3. 硬件初始化:时钟使能、电源上电、复位释放。
  4. 框架注册:注册v4l2_device、media_device、video_device。
  5. 中断与缓冲:申请中断、初始化dma缓冲队列。

代码骨架大概长这样:

static int rkisp_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct rkisp_device *isp;
    int ret;

    // 1. 分配核心结构体
    isp = devm_kzalloc(dev, sizeof(*isp), GFP_KERNEL);
    if (!isp)
        return -ENOMEM;

    // 2. 映射寄存器地址
    isp->base_addr = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(isp->base_addr))
        return PTR_ERR(isp->base_addr);

    // 3. 获取中断号
    isp->irq = platform_get_irq(pdev, 0);
    if (isp->irq < 0)
        return isp->irq;

    // 4. 解析时钟与电源(后面细讲)
    ret = rkisp_init_clocks(isp);
    if (ret)
        return ret;

    ret = rkisp_init_power(isp);
    if (ret)
        return ret;

    // 5. 注册v4l2设备
    ret = rkisp_register_v4l2(isp);
    if (ret)
        return ret;

    // 6. 申请中断
    ret = devm_request_irq(dev, isp->irq, rkisp_irq_handler,
                           0, dev_name(dev), isp);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, isp);
    dev_info(dev, "RKISP probe success\n");
    return 0;
}

这里有个细节:devm_系列函数是managed device resource,你想想看,如果probe中途失败,内核会自动帮你释放已经申请的资源,省得你写一堆goto err标签。我个人习惯尽量用devm,少写错误处理代码,清爽很多。

4.2 设备树解析——驱动与硬件的“翻译官”

设备树就是驱动和硬件之间的桥梁。RKISP的设备树节点一般长这样:

isp: rkisp@ff4a0000 {
    compatible = "rockchip,rk3399-isp";
    reg = <0x0 0xff4a0000 0x0 0x8000>;
    interrupts = <GIC_SPI 43 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru ACLK_ISP>, <&cru HCLK_ISP>;
    clock-names = "aclk_isp", "hclk_isp";
    power-domains = <&power RK3399_PD_ISP>;
    iommus = <&isp_mmu>;
    status = "disabled";
};

驱动里怎么解析?我一般封装一个函数:

static int rkisp_parse_dt(struct rkisp_device *isp)
{
    struct device *dev = isp->dev;
    struct device_node *node = dev->of_node;

    // 解析时钟
    isp->clk_info.aclk = devm_clk_get(dev, "aclk_isp");
    if (IS_ERR(isp->clk_info.aclk))
        return PTR_ERR(isp->clk_info.aclk);

    isp->clk_info.hclk = devm_clk_get(dev, "hclk_isp");
    if (IS_ERR(isp->clk_info.hclk))
        return PTR_ERR(isp->clk_info.hclk);

    // 解析电源域
    isp->power_domain = dev_pm_domain_attach(dev, true);
    if (IS_ERR(isp->power_domain))
        return PTR_ERR(isp->power_domain);

    // 解析iommu
    isp->iommu = device_iommu_mapped(dev);
    if (!isp->iommu)
        dev_warn(dev, "No IOMMU found, DMA may fail\n");

    return 0;
}
注意:设备树里的status = "disabled"很常见。我曾经遇到一个坑——板级dts里忘了改成okay,结果probe根本进不去。排查了半天,最后发现是设备树没使能。所以调试时先确认/sys/firmware/devicetree/下节点状态对不对。

4.3 时钟与电源管理初始化——ISP的“心跳”和“能量”

ISP对时钟很敏感。为什么?因为像素时钟、总线时钟、核心时钟必须按特定顺序使能,否则硬件会锁死或者输出乱码。我遇到过一版芯片,先开HCLK再开ACLK就会挂,后来查勘误表才发现有依赖顺序。

时钟初始化的典型做法:

static int rkisp_clk_init(struct rkisp_device *isp)
{
    int ret;

    // 先准备时钟,但不使能
    ret = clk_prepare(isp->clk_info.aclk);
    if (ret)
        return ret;

    ret = clk_prepare(isp->clk_info.hclk);
    if (ret) {
        clk_unprepare(isp->clk_info.aclk);
        return ret;
    }

    // 设置频率(如果有PLL)
    clk_set_rate(isp->clk_info.aclk, 300 * 1000 * 1000); // 300MHz

    return 0;
}

static int rkisp_clk_enable(struct rkisp_device *isp)
{
    // 注意使能顺序:先HCLK,再ACLK
    clk_enable(isp->clk_info.hclk);
    clk_enable(isp->clk_info.aclk);
    return 0;
}

static void rkisp_clk_disable(struct rkisp_device *isp)
{
    // 关闭顺序相反
    clk_disable(isp->clk_info.aclk);
    clk_disable(isp->clk_info.hclk);
}

电源管理这块,RKISP通常有独立的power domain。在probe时用dev_pm_domain_attach挂上,运行时用pm_runtime_get_sync来上电。我建议把时钟和电源的使能/禁用在runtime PM回调里统一管理:

static int __maybe_unused rkisp_runtime_suspend(struct device *dev)
{
    struct rkisp_device *isp = dev_get_drvdata(dev);
    rkisp_clk_disable(isp);
    return 0;
}

static int __maybe_unused rkisp_runtime_resume(struct device *dev)
{
    struct rkisp_device *isp = dev_get_drvdata(dev);
    return rkisp_clk_enable(isp);
}

static const struct dev_pm_ops rkisp_pm_ops = {
    SET_RUNTIME_PM_OPS(rkisp_runtime_suspend,
                       rkisp_runtime_resume, NULL)
};

这样做的好处是:当没有数据流时,内核可以自动关掉ISP的时钟和电源,省电。你想想看,摄像头不用的时候还开着ISP,电池扛不住的。

4.4 中断注册——ISP的“神经信号”

ISP的中断是事件驱动的核心。帧开始、帧结束、缓冲区满、错误等都会触发中断。注册中断时,我习惯用devm_request_irq,省去手动释放的麻烦。

中断处理函数的设计要点:

  • 上半部(hardirq):只做最紧急的事——读状态寄存器、清中断标志、唤醒下半部。
  • 下半部(tasklet/threaded irq):处理缓冲区、通知v4l2框架、更新统计信息。

示例代码:

static irqreturn_t rkisp_irq_handler(int irq, void *dev_id)
{
    struct rkisp_device *isp = dev_id;
    u32 irq_status;

    // 读中断状态寄存器
    irq_status = readl(isp->base_addr + ISP_IRQ_STATUS);
    // 清中断(写1清除)
    writel(irq_status, isp->base_addr + ISP_IRQ_CLEAR);

    if (irq_status & ISP_FRAME_END) {
        // 帧结束中断——唤醒下半部处理
        schedule_work(&isp->frame_work);
    }

    if (irq_status & ISP_ERROR) {
        // 错误中断——记录并复位
        dev_err(isp->dev, "ISP error: 0x%x\n", irq_status);
        // 触发复位流程
    }

    return IRQ_HANDLED;
}
小技巧:中断标志的清除时机很重要。我曾经在清标志之前读了太多寄存器,导致中断被重复触发。建议:先读状态,立刻清标志,再处理业务逻辑。这样能避免中断风暴。

4.5 整体初始化流程图

下面我用一张SVG图把整个流程串起来,方便你理解各个步骤的依赖关系:

RKISP驱动初始化流程 platform_driver.probe devm_kzalloc 分配核心结构体 devm_platform_ioremap_resource 设备树解析 (时钟/电源/IOMMU) 时钟准备与频率设置 电源域 attach & runtime PM devm_request_irq 注册中断 v4l2_device / media_device 注册 probe 成功 ★ 关键步骤 ★ 关键步骤

这张图里我标了两个关键步骤:设备树解析和中断注册。为什么?因为设备树解析一旦出错,后面所有资源都拿不到;中断注册失败,ISP没法响应事件,驱动就是个“植物人”。

4.6 避坑指南与个人经验

最后分享几个我踩过的坑:

  • 时钟顺序:我曾经在某个RK芯片上先使能了ACLK再使能HCLK,结果ISP直接挂死。后来查了TRM,发现必须按HCLK→ACLK的顺序。不同芯片可能不同,一定要看datasheet。
  • 中断共享:RKISP的中断有时会和其它外设共享一个GIC SPI。如果中断号冲突,probe时request_irq会返回-EBUSY。我建议在设备树里确认interrupts属性是否唯一。
  • 电源域未就绪:如果power-domains属性指向的节点还没初始化,dev_pm_domain_attach会失败。这时候probe应该返回-EPROBE_DEFER,让内核稍后重试。千万别直接返回-EINVAL,否则驱动永远起不来。
  • IOMMU问题:ISP通常需要IOMMU做DMA地址转换。如果IOMMU驱动没加载,ISP分配buffer时会出错。我在调试时习惯先看dmesg里有没有iommu相关的错误。

核心要点总结:

  • probe流程要按顺序:资源映射 → 设备树解析 → 时钟/电源 → 中断 → v4l2注册
  • 设备树解析是基础,务必检查每个of函数返回值
  • 时钟使能顺序和电源域依赖是硬件特性,不能想当然
  • 中断处理要分上半部和下半部,避免在中断上下文做耗时操作
  • 善用devm_系列函数和runtime PM,让代码更简洁可靠

嗯,这部分内容就到这里。初始化流程是驱动开发的基石,你把它吃透了,后面写控制逻辑、调图像质量才能游刃有余。记住,probe成功只是第一步,真正的挑战还在后面。

可可精品资料库