第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 复用策略的核心思路

既然差异这么大,怎么复用?我总结了一个三层模型:

  1. SoC 层(.dtsi):描述 SoC 内部的所有外设、时钟、中断、pinmux。这部分是芯片厂商提供的,原则上不动。
  2. 板级层(.dts):描述板子上的外设连接、GPIO 用途、电源管理策略。这部分是产品工程师写的。
  3. 覆盖层(.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 层(.dtsi) 描述 SoC 内部所有外设、时钟、中断、pinmux 由芯片厂商提供,原则上不动 板级层(.dts) 描述板子上的外设连接、GPIO 用途、电源管理 由产品工程师编写,按需启用 SoC 外设 覆盖层(.dtso) 运行时修改或扩展设备树 适合产品变体、扩展板支持 差异集中区 复用策略 核心思想:把不变的锁死,把可变的暴露出来

这张图我画了好几个版本,最后发现还是三层结构最清晰。SoC 层在最底下,板级层在中间,覆盖层在最上面。差异主要集中在 SoC 层和板级层的接口处——也就是 pinmux、时钟、中断这些「粘合剂」。

2.7 小结

不同 SoC 间的设备树差异,本质上是芯片架构设计哲学的差异。复用不是「复制粘贴」,而是通过分层、覆盖、抽象,把公共部分提取出来,把差异部分隔离掉。

我个人建议,刚开始做多平台支持时,先花一周时间把三款 SoC 的 .dtsi 通读一遍。不用全看懂,但至少要知道它们的时钟树长什么样、中断控制器怎么配、pinmux 的写法习惯。这些基本功,比任何技巧都重要。

一句话总结:设备树复用,不是让所有 SoC 长得一样,而是让它们「看起来」一样——通过合理的分层和覆盖,把差异藏起来,把共性露出来。

可可精品资料库