第2章:多平台挑战:为什么需要复用?不同 SoC 间设备树差异分析
说实话,我刚入行那会儿,觉得设备树就是个配置文件。
不就是描述硬件嘛,一个板子一个 .dts,搞定收工。直到我接手了一个项目——同一个产品线,要同时支持三款不同厂商的 SoC。嗯,那才叫真正的「噩梦」开始。
2.1 为什么非要复用?
你想想看,一个产品卖得好,老板肯定想快速出衍生版。换 SoC、改外设、调 pinmux,这些事几乎每周都在发生。
如果每次都要从头写一个设备树,那维护成本会爆炸。我见过一个团队,维护了 40 多个几乎一模一样的 .dts 文件,改一个 GPIO 编号要改 40 个地方。你说累不累?
核心痛点:
- 代码冗余:同一份硬件描述,在不同 SoC 间反复拷贝
- 维护困难:改一处遗漏十处,编译不报错,跑起来就挂
- 验证成本高:每个变体都要重新测试设备树绑定
- 升级痛苦:内核版本升级,设备树语法变化,全量重写
复用,说白了就是让「公共部分」只写一次。SoC 特有的部分单独拎出来,板级差异用覆盖机制处理。这样你改一个 i2c 控制器,所有用这个 SoC 的板子自动生效。
2.2 不同 SoC 间的差异到底在哪?
我做过一个对比分析,拿三款主流 SoC 来举例:TI AM335x、NXP i.MX6、Allwinner H3。它们都是 ARM Cortex-A 系列,但设备树的写法天差地别。
| 差异维度 | TI AM335x | NXP i.MX6 | Allwinner H3 |
|---|---|---|---|
| 兼容属性 | ti,am335x-xxx | fsl,imx6q-xxx | allwinner,sun8i-h3 |
| 时钟模型 | clkctrl + l4_wkup | ccm 时钟控制器 | ccu 时钟驱动 |
| 中断控制器 | ti,omap2-intc | arm,gic-400 | allwinner,sunxi-nmi |
| pinmux 方式 | pinctrl-single | pinctrl-imx6 | pinctrl-sunxi |
| DMA 引擎 | ti,edma3 | fsl,imx6-dma | sun8i-h3-dma |
看到没?光是 compatible 字符串就完全不同。更别说底层的寄存器地址、时钟树结构、中断路由方式了。
2.3 差异的根源:SoC 架构设计哲学
为什么会这样?我个人的理解是:每家芯片公司的硬件架构思路不一样。
- TI 的风格:喜欢用「层级总线」结构。L3、L4 总线层层嵌套,外设挂在不同的 interconnect 上。设备树里你得写一堆 ranges 和 reg 映射。
- NXP 的风格:偏向「统一编址」。外设地址空间相对平坦,但时钟和电源管理特别复杂。一个外设可能要关联 3-4 个时钟源。
- Allwinner 的风格:比较「精简」。寄存器少,功能简单,但 pinmux 的复用规则很绕。同一个引脚可能有 8 种功能,你得对着 datasheet 一个一个数。
我记得有一次,把 AM335x 的 uart 节点直接复制到 i.MX6 上,编译通过了,但串口死活不工作。查了两天才发现——AM335x 的 uart 时钟来自 l4_wkup,而 i.MX6 的 uart 时钟来自 ccm。时钟路径不一样,驱动根本不知道去哪找时钟。
避坑指南:千万不要以为 compatible 字符串改了就能通吃。时钟、中断、复位、电源域这些「基础设施」的差异,才是真正的大坑。我曾经因为漏配一个时钟父节点,导致整个 mmc 控制器在休眠后无法唤醒。
2.4 复用策略的核心思路
既然差异这么大,怎么复用?我总结了一个三层模型:
- SoC 层(.dtsi):描述 SoC 内部的所有外设、时钟、中断、pinmux。这部分是芯片厂商提供的,原则上不动。
- 板级层(.dts):描述板子上的外设连接、GPIO 用途、电源管理策略。这部分是产品工程师写的。
- 覆盖层(.dts overlay):用于运行时修改或扩展设备树。适合做产品变体、扩展板支持。
说白了,就是「把不变的锁死,把可变的暴露出来」。SoC 内部的东西,比如 uart 控制器的寄存器地址、中断号,这些是固定的,写在 .dtsi 里。板子上的东西,比如 uart 用哪个引脚、波特率多少,这些是灵活的,写在 .dts 里。
我的习惯:在 SoC 的 .dtsi 里,我会把外设节点写成「禁用」状态(status = "disabled")。板级 .dts 里再按需启用。这样既保留了 SoC 的完整描述,又避免了未使用外设浪费资源。
2.5 一个真实的差异案例
拿 GPIO 中断来说。同样是按键中断,三款 SoC 的写法完全不同:
// TI AM335x:使用 gpio-keys 驱动,中断直接挂到 gpio 控制器
gpio-keys {
compatible = "gpio-keys";
pinctrl-0 = &key_pins;
button@0 {
label = "KEY1";
linux,code = <KEY_ENTER>;
gpios = <&gpio0 4 GPIO_ACTIVE_LOW>;
};
};
// NXP i.MX6:使用 gpio-keys,但需要指定中断父节点
gpio-keys {
compatible = "gpio-keys";
pinctrl-0 = <&pinctrl_key>;
interrupt-parent = <&gpio1>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
button {
label = "KEY1";
linux,code = <KEY_ENTER>;
};
};
// Allwinner H3:中断号需要查 datasheet,而且 pinmux 要单独配
gpio-keys {
compatible = "gpio-keys";
pinctrl-0 = <&key_pin_h3>;
button@0 {
label = "KEY1";
linux,code = <KEY_ENTER>;
gpios = <&pio 0 4 GPIO_ACTIVE_LOW>;
};
};
你看,同样是按键,TI 用 gpios 属性直接指定,NXP 用 interrupt-parent + interrupts,Allwinner 的 gpio 控制器叫 pio 而不是 gpio0。这些细节,就是复用时必须处理掉的差异。
2.6 知识体系总览
下面这张图,是我自己画的多平台设备树复用知识体系。你可以看到,核心就是「分层 + 覆盖」:
这张图我画了好几个版本,最后发现还是三层结构最清晰。SoC 层在最底下,板级层在中间,覆盖层在最上面。差异主要集中在 SoC 层和板级层的接口处——也就是 pinmux、时钟、中断这些「粘合剂」。
2.7 小结
不同 SoC 间的设备树差异,本质上是芯片架构设计哲学的差异。复用不是「复制粘贴」,而是通过分层、覆盖、抽象,把公共部分提取出来,把差异部分隔离掉。
我个人建议,刚开始做多平台支持时,先花一周时间把三款 SoC 的 .dtsi 通读一遍。不用全看懂,但至少要知道它们的时钟树长什么样、中断控制器怎么配、pinmux 的写法习惯。这些基本功,比任何技巧都重要。
一句话总结:设备树复用,不是让所有 SoC 长得一样,而是让它们「看起来」一样——通过合理的分层和覆盖,把差异藏起来,把共性露出来。
可可精品资料库