第四节:设备树与平台设备
设备树,说白了就是嵌入式Linux世界里的一张「硬件地图」。我刚开始接触时也觉得这东西有点玄乎,不就是描述硬件嘛,搞这么复杂干嘛?直到我在一个项目里,因为一个GPIO管脚号写错了,整块板子死活起不来网络——嗯,从那以后我再也不敢小看设备树了。
4.1 设备树语法基础
设备树(Device Tree)是一种描述硬件资源的数据结构。它用树形结构来描述CPU、内存、外设、中断、GPIO等信息。内核在启动时解析这个树,然后根据描述来初始化驱动。
先看一个最简单的设备树节点长什么样:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard";
cpus {
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
};
这里有几个关键概念,我一个个说:
- 节点(Node):每个硬件设备就是一个节点,用大括号包裹。比如上面的
cpus、memory都是节点。 - 属性(Property):节点的特征描述,比如
compatible、reg这些都是属性。 - 标签(Label):给节点起个名字,方便其他地方引用。比如
&uart1这种写法。
我个人习惯:写设备树时,先把 compatible 属性写好。因为内核驱动就是靠这个字符串来匹配设备的。名字起错了,驱动根本认不出你。
设备树的语法其实不复杂,核心就几条规则:
- 每个节点必须有
compatible属性,这是驱动匹配的关键。 reg属性描述地址和大小,格式是<地址 长度>。interrupts属性描述中断号,需要配合interrupt-parent使用。- 节点可以嵌套,子节点继承父节点的某些属性。
你想想看,如果没有设备树,我们得在板级文件里写一堆 platform_device 结构体,改一个管脚就得重新编译内核。有了设备树,改硬件配置只需要改 .dts 文件,方便太多了。
4.2 网卡节点绑定
网卡设备树节点,是咱们课程的重点。我以最常见的 DesignWare MAC(就是 STM32MP1 系列用的那个)为例,给你看看网卡节点怎么写:
ð1 {
status = "okay";
pinctrl-0 = <ð1_rgmii_pins_a>;
pinctrl-1 = <ð1_sleep_pins_a>;
pinctrl-names = "default", "sleep";
phy-mode = "rgmii-id";
phy-handle = <&phy0>;
max-speed = <1000>;
mdio0 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "snps,dwmac-mdio";
phy0: ethernet-phy@1 {
reg = <1>;
compatible = "ethernet-phy-ieee802.3-c22";
reset-gpios = <&gpiog 0 GPIO_ACTIVE_LOW>;
reset-assert-us = <10000>;
reset-deassert-us = <80000>;
};
};
};
这段代码里,我挑几个重点属性讲:
| 属性 | 含义 | 我在项目中踩过的坑 |
|---|---|---|
phy-mode |
PHY接口模式,如rgmii、rmii、mii | 有一次我写成了"rgmii",少了个i,内核直接报错 |
phy-handle |
指向PHY节点的引用 | 这个引用必须和mdio子节点里的标签一致 |
max-speed |
最大速率,单位Mbps | 不写的话默认1000,但有些PHY只支持100M |
pinctrl-* |
管脚复用配置 | 这个最容易忘,忘了网卡根本不通 |
我曾经在一个项目里,网卡驱动加载成功了,但死活ping不通。查了两天,最后发现是 phy-handle 写错了,指向了一个不存在的PHY节点。内核不会报错,它只是默默地把PHY当作不存在来处理。嗯,这种坑最要命。
还有一个容易被忽略的点:PHY节点的 compatible 属性。很多新手会写具体的PHY型号,比如 compatible = "realtek,rtl8211f"。其实更通用的写法是 compatible = "ethernet-phy-ieee802.3-c22",这样内核会自动用通用PHY驱动来匹配。除非你有特殊需求,否则我建议用通用写法。
4.3 platform_driver 与 platform_device 匹配
设备树写好了,内核怎么知道用哪个驱动来伺候它?这就涉及到 platform_driver 和 platform_device 的匹配机制了。
说白了,设备树里的每个节点,内核启动时会把它变成一个 platform_device。而驱动里注册的 platform_driver,会通过 compatible 属性来寻找自己对应的设备。
看一个网卡驱动的典型写法:
static const struct of_device_id stmmac_dt_ids[] = {
{ .compatible = "snps,dwmac-3.70a", .data = &dwmac370_ops },
{ .compatible = "snps,dwmac-4.00", .data = &dwmac400_ops },
{ .compatible = "snps,dwmac-4.10a", .data = &dwmac410_ops },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, stmmac_dt_ids);
static struct platform_driver stmmac_platform_driver = {
.probe = stmmac_platform_probe,
.remove = stmmac_platform_remove,
.driver = {
.name = "stmmac",
.of_match_table = stmmac_dt_ids,
.pm = &stmmac_platform_pm_ops,
},
};
module_platform_driver(stmmac_platform_driver);
匹配流程其实就三步:
- 内核解析设备树,生成
platform_device,其中dev.of_node指向设备树节点。 - 驱动注册时,内核遍历
of_match_table,和设备树节点的compatible属性做比较。 - 匹配成功,调用驱动的
probe函数,把platform_device传进去。
你可能会问:如果设备树里没有 compatible 属性怎么办?嗯,那就只能用传统的 platform_device.name 来匹配了。但说实话,现在新板子基本都用设备树,很少有人再写板级文件了。
这里有个小技巧:调试驱动匹配问题时,可以在内核启动参数里加 dyndbg="file drivers/base/platform.c +p",这样内核会打印出平台设备的匹配过程。我曾经靠这个定位过一个驱动死活不probe的问题,最后发现是设备树节点被 status = "disabled" 了。
4.4 知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
从这张图你可以看到,整个流程其实就是一个「描述-解析-匹配」的过程。设备树负责描述硬件,内核负责解析生成平台设备,驱动负责声明自己能处理哪些设备。三者配合好了,网卡就能正常工作。
我建议:刚开始学设备树时,不要试图一次性把所有属性都记住。先记住 compatible、reg、interrupts、status 这四个核心属性,其他的用到再查。我在实际项目中,80%的设备树问题都出在这四个属性上。
好了,这一节的内容就到这里。设备树和平台设备的匹配,说白了就是「对暗号」——驱动说一句暗号(compatible),设备树回一句暗号,对上了就握手成功。下次你遇到网卡驱动不工作,先检查这个暗号对不对得上。
公众号:蓝海资料掘金营,微信deep3321