Skip to content

第 4 章 设备绑定:从「协议」到「方言」

有一类 bug 调起来特别让人抓狂。现象很典型:设备明明挂在总线上,驱动也加载了,日志里显示 probe 成功,但数据就是死活出不来。你查寄存器、查时钟、查电源电压,一切硬件信号看起来都正常,但内核就是收不到一个字节。

这种时候,问题往往不在代码里,而在「契约」里。设备树本质上是一份契约,它规定了硬件该怎么向内核描述自己,以及内核该依据哪些字段来驱动硬件。如果硬件工程师写设备树时随手填了一个属性,而驱动工程师写代码时假定的是另一个属性——虽然文件格式没错,编译也没报错,但内核和硬件在执行的是两份完全不同的逻辑协议。

这就是为什么我们需要 Device Bindings(设备绑定)。这不是一份「建议参考」的文档,而是法律条文。它定义了特定类别的设备(比如串口、网卡)必须在设备树里包含哪些属性、这些属性叫什么名字、值的类型是什么。compatible 属性就是这份契约的身份证号——内核通过它找到对应的 Binding 文档,然后按图索骥地去解析节点里的其他属性。这一章,我们把这些 Binding 拆开来看,你会发现它们有的设计得非常直观,有的则带着浓重的历史包袱——但这正是工程世界的真实面貌。


4.1 制定规则:如何创造一种设备描述

在我们要为一种新设备写 Binding 之前,得先搞清楚 Linux 社区(以及 devicetree.org)认可的「行文规范」。你不能随便造个词就往上写,否则这棵树就长成了杂草。创造一个新 Binding,核心原则就一句话:描述必须完整。你定义的属性集合,必须足以让驱动程序获取到运行所需的全部信息,缺一项驱动就跑不起来或者得靠硬编码——那是绝对禁止的。

具体来说,社区推荐的实践流程是这样的。首先定义 compatible 字符串,这是设备的唯一标识,必须遵循 "manufacturer,model" 的命名规则——这是驱动匹配设备的第一把钥匙。然后复用标准属性,永远不要重新发明轮子。如果标准属性里有能用的(比如 reg, interrupts, clocks),一定要直接用,这不仅是为了省事,更是为了让通用工具能读懂你的设备树。

接着看看你的设备是不是属于某种已知的设备类别(比如串口类或网络类)。如果是,必须遵循该类别的通用规范。如果有些常用属性不在你的类别里但确实通用(比如 clock-frequency),去查查杂项属性列表。如果必须发明新属性,实在没办法必须造一个新属性名时,请遵循 "<company>,<property-name>" 的格式。

千万别用 my-gpio 这种名字。十年后维护代码的人看到这个名字,根本不知道它是哪家公司的定义,更不知道为什么要有它。正确的做法是加上公司前缀,比如 "fsl,my-gpio" 或者 "brcm,my-gpio"。这里的 <company> 最好是用 OUI(Organizationally Unique Identifier)或者股票代码那种全球唯一的短字符串,这是为了防止命名空间冲突——你不想两家公司都定义了 status 属性但含义完全不同吧?

有些属性太通用了,通用到几乎任何设备都可能用得上,但又不足以单独成为一个类别。我们把它们放在这里作为标准化的工具箱。clock-frequency 是一个 <prop-encoded-array> 类型的属性用来描述时钟频率,单位是 Hz。它有两种形式:一个 32 位整数(<u32>)或者一个 64 位整数(<u64>)。虽然表格里写得很宽泛,但在实际工程中你会发现它几乎总是用于串口、定时器这类外设。如果驱动程序需要根据硬件时钟计算波特率或分频系数,这个属性就是必填的。

reg-shift 是一个非常有意思的属性,类型是 <u32>。它的作用是解决「布局兼容但步进不同」的问题。有些设备本质上是一样的(比如都是 16550 UART),但在不同的总线映射上,寄存器之间的间隔可能不同。有的寄存器是紧密排列的地址间隔 1 字节,有的为了地址对齐方便,寄存器之间隔了 4 字节(0x0, 0x4, 0x8...)。这时候你不能为了这就去写一套新驱动,reg-shift 属性指定了寄存器之间的字节间隔。

计算公式是:实际寄存器地址 = 基址 + (寄存器索引 << reg-shift)。如果你的 16550 UART 寄存器位于 0x0, 0x4, 0x8... 这种间隔为 4 字节的布局上,你就应该设置 reg-shift = <2>,因为左移 2 位等于乘以 4。如果不指定这个属性,默认值是 0,驱动就会按紧密排列去访问,结果就是读写错位——大概率会写到不可预知的内存地址上,直接 Kernel Panic。

label 是一个 <string> 类型。它的定义很简单:人类可读的字符串,但它的具体含义是由具体的 Binding 文档定义的。有时候它是用来在控制台显示设备名(比如 "Serial0"),有时候只是作为调试时的辅助信息。别把它和 name 属性搞混——name 是节点名,label 更多是给「人」看的备注。


4.2 串口设备:通信的起点

串口是嵌入式系统的「嘴」,没有它你甚至看不到 Kernel Panic 的报错。串口设备这个类别包括了各种点对点的串行线路设备,典型的就是 8250 UART16550 UART,还有 HDLC、BISYNC 这些。只要兼容 RS-232 标准的硬件,通常都属于这一类。

I2C 和 SPI 也是串行的,但绝不是串口设备。它们有各自独立的总线表示方法,千万不要把 I2C 设备挂在串口类下,否则驱动模型会完全乱套。对于串口类设备,有两个属性极其重要。

clock-frequency 特指波特率发生器的输入时钟频率。驱动程序要想设置 115200 的波特率,必须知道「我输入的时钟是多少 Hz」。这个值通常由硬件原理图决定,是固定的。current-speed 是当前波特率,类型 <u32> 单位是 bps(bits per second)。这个属性描述的是「当前这个串口正在以什么速度运行」。注意这通常不是给硬件看的,而是给操作系统看的。如果 Bootloader 已经初始化了串口,它应该把这个属性设为初始化后的值,这样内核启动时就不需要重新初始化,可以直接接着用。

这是世界上最常见的 UART 之一。如果你的板子上用了一颗兼容 NS16550 的芯片,或者 SoC 内部集成了这样的 IP,就必须遵守下面的规则。compatible 必须包含 "ns16550",通常为了兼容性可能会写成 "ns16550a""nvidia,tegra20-uart" 等具体型号,但 "ns16550" 必须在列表里。clock-frequency 是波特率发生器输入时钟频率,没有这个驱动算不出波特率。current-speed 是当前波特率,虽然标了 Optional Recommended,但建议如果 Bootloader 设了就写上。reg 是寄存器物理地址,这是标准属性没它驱动没法映射内存。interrupts 是中断号,串口如果不配中断只能用轮询模式效率极低,强烈建议配置。reg-shift 见前面的说明,如果寄存器间隔不是 1 字节必须填这个。virtual-reg 是一个特殊属性,如果这个节点被用作系统控制台,这个属性是必须的。它指定了 reg 首地址映射后的有效虚拟地址,这在内核极早期(MMU 刚开启但还没完全跑平设备树解析时)用来打印 log 非常关键。


4.3 网络设备:数据链路层的抽象

网络设备是数据包导向的通信设备。按照 OSI 七层模型,这一类设备通常工作在数据链路层(Layer 2),并且使用 MAC 地址来寻址。常见的例子:Ethernet(以太网)、802.11 无线网卡、FDDI、Token-Ring(令牌环)。

这一类设备有一些通用的属性用来描述地址和物理层特性。address-bits 指定了 MAC 地址的位数,默认值是 48 位(标准的以太网 MAC)。如果你的设备比较奇葩用的是 64 位 MAC 或者其他长度的地址,就必须显式指定这个属性。local-mac-address 指定了硬件原本分配给这个设备的 MAC 地址,这通常是从 OTP、EEPROM 或者 Fuse 里读出来的出厂地址。mac-address 则是 Bootloader 最后使用的 MAC 地址

为什么要搞两个?有时候 Bootloader 可能会通过命令行参数强制覆盖 MAC 地址(比如为了做无盘启动)。如果 Bootloader 修改后的地址和 local-mac-address 不一样,就应该用 mac-address 来记录修改后的值。规则是:只有当它与 local-mac-address 不同时,才应该使用这个属性。

max-frame-size 描述了物理接口能收发的最大数据包长度,单位是字节。标准以太网帧是 1518 字节,但如果你用了 Jumbo Frame(巨型帧),这个值就会变大。驱动需要根据这个值来配置 DMA 缓冲区的大小,不然大包一来就溢出了。

以太网(Ethernet, IEEE 802.3)虽然属于网络类,但它有自己的特殊需求——主要因为它太复杂了,涉及 PHY(物理层芯片)的各种配置。除了通用的网络属性,以太网设备还常用以下属性。

max-speed 告诉驱动,这块板子上的硬件设计最高支持多少带宽。比如虽然 PHY 芯片支持 1000M,但板子走线只按 100M 设计的,或者 MagJack(网口变压器)只买了 100M 的,这里就要限制一下,防止驱动协商出错误的速率。phy-connection-type 必须准确描述硬件连接方式。以太网控制器(MAC)和 PHY 芯片之间的接口有很多种标准,一定要查原理图!常见的推荐值包括 "mii"(最基础的 Media Independent Interface)、"rmii"(Reduced MII,少几根线成本低)、"rgmii"(Reduced Gigabit MII,跑千兆常用)、"rgmii-id"(RGMII with internal delay,内部延时)。

特别注意:很多 PHY 需要 MAC 端提供延时,如果你的 MAC 硬件没做延时 PCB 走线,软件里选这个 id 模式通常能救回来。还有 "gmii", "sgmii", "qsgmii" 各种高速接口。phy-handle 是连接 MAC 和 PHY 的关键,以太网设备节点里必须有一个指针指向对应的 PHY 节点。PHY 节点通常挂在 MDIO 总线下。


4.4 Power ISA Open PIC 中断控制器

中断控制器是整个系统的神经系统。Open PIC(Open Programmable Interrupt Controller)是一种架构,最早由 AMD 和 Cyrix 联合开发,广泛应用于 PowerPC 和 Power ISA 系统中。在设备树里描述一个 Open PIC 中断控制器,有一套严格的规矩。

Open PIC 的中断域里,中断描述符由 2 个 cell(单元格) 组成。Cell 0 是中断号,Cell 1 是触发类型和电平信息。这第二个 cell 的编码格式是固定的千万别搞反了:0 是低到高边沿触发,1 是低电平触发,2 是高电平触发,3 是高到低边沿触发。

compatible 必须包含 "open-pic"reg 是寄存器物理地址。interrupt-controller 是空属性,它标记这个节点是一个中断控制器——这是告诉内核「嘿,这里有别的设备可以来注册中断」。#interrupt-cells 必须是 2,因为上面说了它用 2 个 cell 描述一个中断。#address-cells 必须是 0,中断控制器不需要地址。


4.5 simple-bus:把内存直接暴露出来

SoC(片上系统)内部经常有一种总线,它很简单,上面挂的设备没法被软件「探测」。不像 PCI 总线可以扫描配置空间,这种总线上的设备纯粹靠物理地址连接。你只需要告诉内核:「这段地址空间里,全是外设,直接按地址访问就行。」这种总线我们用 compatible = "simple-bus" 来表示。

compatible 必须包含 "simple-bus"ranges 是最关键的属性,它定义了「父总线地址 -> 子总线地址」的映射关系。如果没有这个,内核不知道子节点的寄存器地址怎么加到总线地址上。

nonposted-mmio 是一个可选的空属性。通常 CPU 写内存时,写操作会被放入 Write Buffer(写缓冲),CPU 不等待实际写完成就继续执行下一行代码,这叫 Posted Write。但对于某些硬件寄存器,写操作不仅是为了存数据,更是为了触发动作(比如「清中断」)。如果写操作被缓冲了,CPU 还没真正清完中断就去读状态,可能读到错误的值。nonposted-mmio 属性告诉内核:这根总线上的设备,必须保证每次写操作都真正到达硬件后才能返回。不能搞合并,不能搞缓冲。

虽然表格里写它是 Optional,但在某些高性能 ARM SoC 的特定外设总线上,如果不加这个,你会发现中断莫名其妙清除不掉,或者配置寄存器写入无效——那就是踩到 Posted Write 的坑里了。


本章回响

现在我们知道了如何用标准化的语言描述具体的设备。回头看开头那个问题:为什么驱动加载了却没反应?现在答案更清晰了。因为驱动加载只是匹配了 compatible 字符串,这只是第一步。如果 Binding 里规定的某个关键属性——比如 phy-handle 或者 clock-frequency——在设备树里缺失或填错了,驱动程序在尝试进行 I/O 操作时就会失败,或者操作了错误的地址。

Binding 的存在就是为了消除这种歧义。它强制硬件设计者把所有的隐含假设(时钟多少?MAC 地址在哪?中断触发方式是什么?)全部写在台面上。下一章,我们将进入更深水区:中断。那是设备树里最容易让初学者崩溃,也是设计最精妙的部分。我们会看到 interrupt-parent 是如何把整个树串联成一个有机体的。

Built with VitePress