第一章:设备树与平台驱动——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了。流程其实不复杂,我画个图你就明白了:
看明白了吗?核心就是三步:定义、匹配、注册。我写个实际代码:
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_register 和 platform_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