第一章:设备树与平台驱动——NPU驱动的基础设施

各位同学,咱们今天聊聊设备树和平台驱动。说实话,这两个东西是手机SoC驱动开发的基石。你想想看,NPU作为一个复杂的硬件加速器,怎么让内核知道它在哪里?怎么告诉驱动它用哪些中断、哪些内存?答案就在设备树里。

1.1 设备树语法基础——硬件描述的语言

设备树(Device Tree)说白了就是一个描述硬件拓扑的文本文件。我刚开始接触时也觉得奇怪,为什么不用ACPI?后来在嵌入式领域干久了才明白,设备树轻量、灵活,特别适合手机这种硬件千变万化的场景。

先看一个最简单的节点:

/dts-v1/;

/ {
    compatible = "qcom,msm8998";
    
    cpu@0 {
        compatible = "arm,cortex-a53";
        reg = <0x00>;
    };
};

这里有几个关键点:

  • 根节点:用 / 表示,整个硬件的起点
  • 节点名cpu@0,@后面是地址
  • compatible:最重要的属性,驱动靠它来匹配硬件
  • reg:描述地址空间,格式是 <起始地址 长度>

核心原则:设备树只描述硬件,不描述行为。驱动怎么用这些资源,那是驱动的事。

我在项目中遇到过一个问题:某款NPU的寄存器地址写错了,导致驱动一加载就死机。查了两天才发现是设备树里reg少写了一个0。嗯,这种低级错误,犯过一次就再也不会犯了。

1.2 NPU设备节点编写——从零开始搭一个NPU

现在我们来写一个真实的NPU节点。以某款主流SoC为例:

npu@1a300000 {
    compatible = "vendor,npu-v1";
    reg = <0x1a300000 0x100000>;
    interrupts = <GIC_SPI 128 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clock_npu 0>, <&clock_npu 1>;
    clock-names = "core", "bus";
    power-domains = <&pd_npu>;
    memory-region = <&npu_mem>;
    status = "okay";
};

咱们拆开看看每个字段的含义:

属性 含义 我的经验
compatible 驱动匹配字符串 建议用"厂商,型号-版本"格式,避免冲突
reg 寄存器基地址和大小 一定要和芯片手册对清楚,我吃过亏
interrupts 中断号、类型 NPU通常用SPI中断,注意电平触发方式
clocks 时钟源 NPU一般需要core clock和bus clock两个
power-domains 电源域 省电关键,NPU休眠时得关掉
memory-region 预留内存 NPU推理时的大块内存,要提前预留

小技巧:我习惯在设备树里加一个 npu-version 自定义属性,方便驱动里做版本兼容。比如 npu-version = <0x0100>; 表示v1.0。

1.3 platform_driver注册流程——驱动怎么和设备树牵手

设备树写好了,驱动怎么找到它?这就靠platform_driver了。流程其实不复杂,我画个图你就明白了:

platform_driver 注册流程 1. 定义 platform_driver 2. 填充 .of_match_table 3. platform_driver_register() 4. 内核遍历设备树,匹配 compatible 5. 调用 probe() 函数,驱动开始工作 匹配成功 → probe() 被调用 → 驱动接管硬件

看明白了吗?核心就是三步:定义、匹配、注册。我写个实际代码:

static const struct of_device_id npu_of_match[] = {
    { .compatible = "vendor,npu-v1", },
    { .compatible = "vendor,npu-v2", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, npu_of_match);

static int npu_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct resource *res;
    
    // 从设备树获取寄存器地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(dev, "failed to get memory resource\n");
        return -ENXIO;
    }
    
    // 获取中断号
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(dev, "failed to get irq\n");
        return irq;
    }
    
    dev_info(dev, "NPU probed at 0x%llx, irq %d\n", 
             res->start, irq);
    return 0;
}

static int npu_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "NPU removed\n");
    return 0;
}

static struct platform_driver npu_driver = {
    .probe  = npu_probe,
    .remove = npu_remove,
    .driver = {
        .name = "npu",
        .of_match_table = npu_of_match,
    },
};

module_platform_driver(npu_driver);

注意module_platform_driver 这个宏会自动处理模块加载和卸载。我曾经手写 platform_driver_registerplatform_driver_unregister,结果忘了在模块卸载时注销,导致内核崩溃。后来就只用这个宏了,省心。

1.4 避坑指南——我踩过的那些坑

做NPU驱动这几年,我在设备树和平台驱动上栽过不少跟头。分享几个典型的:

  • compatible 字符串不匹配:驱动里写的是 "vendor,npu-v1",设备树里写的是 "vendor,npu-v1.0"。内核匹配不上,probe 根本不会调用。我曾经因为这个查了一整天。
  • reg 地址写错:NPU的寄存器基地址是0x1a300000,结果我写成了0x1a30000。少一个0,驱动访问的全是错误地址。
  • 中断类型搞错:NPU用的是电平触发,我配成了边沿触发。结果中断来了只触发一次,后面全丢了。
  • 忘记加 status = "okay":设备树默认 status 是 "disabled",不加这一行,内核根本不会去匹配。

调试技巧:我习惯在 probe 函数开头加一句 dev_dbg,打印出设备树传进来的所有资源。这样一开机就能看到驱动有没有正确获取到信息。命令 cat /proc/device-tree/npu@*/ 也能直接查看设备树解析结果。

好了,这一章的内容就到这。设备树和平台驱动是NPU驱动的基础,后面所有章节都会用到这些知识。记住一句话:设备树是硬件和驱动之间的桥梁,写对了,后面就顺了。


公众号:蓝海资料掘金营,微信deep3321