ARM64启动过程中ATAGS与Device Tree对比

AI助手已提取文章相关产品:

ARM64启动机制的进化之路:从ATAGS到设备树的彻底重构

在嵌入式系统的世界里,每一次芯片上电都是一次“创世”——内核从零开始重建整个软件宇宙。而这场重启仪式的核心,就是如何让操作系统“认识”自己运行在哪块硬件上。

你有没有想过,为什么现代ARM开发板不再需要为每一块电路板单独编译内核?为什么树莓派4B和NVIDIA Jetson Xavier可以共用同一个Linux镜像?这一切的背后,其实藏着一个鲜为人知但至关重要的技术革命: 设备树(Device Tree)取代ATAGS

这不仅仅是一个参数传递方式的变更,而是一场关于“硬件描述权”的根本性转移。它改变了我们编写驱动、调试板卡、发布固件的方式。今天,我们就来深入这场变革的底层逻辑,看看ARM64架构是如何通过一次优雅的设计跃迁,彻底解决困扰嵌入式行业二十年的老问题。


一、ATAGS的时代:简单却脆弱的标签链

回到2000年代初,那时的SoC还很“单纯”。一块三星S3C2410芯片配上32MB内存、一个串口、一张NAND闪存,就能撑起一台PDA或工控终端。在这种环境下,一种名为 ATAGS(Attribute Tags) 的机制应运而生。

它的设计哲学非常朴素: 用一段连续的内存块,装下所有硬件信息 。Bootloader像写简历一样,把CPU型号、内存大小、命令行参数一个个填进去,然后告诉内核:“嘿,你的配置在这里。”

📦 ATAGS长什么样?

想象一下,你在纸上列清单:

[开始]
→ 内存:从0x30000000开始,32MB
→ 命令行:console=ttySAC0,115200 root=/dev/mtdblock2
→ 序列号:123456789ABCDEF
[结束]

这就是ATAGS的本质——一个由 struct tag 组成的线性链表,每个tag都有类型和长度字段。内核启动时只需遍历这个列表,提取关键信息即可。

struct tag_header {
    u32 size;   // 单位是word(4字节)
    u32 tag;    // 标签类型,如ATAG_MEM=0x54410002
};

struct tag {
    struct tag_header hdr;
    union {
        struct tag_core       core;
        struct tag_mem32      mem;
        struct tag_cmdline    cmdline;
        // ... 其他类型
    } u;
};

典型的内存布局如下图所示(虽然不能画图,但我们可以在脑中构建):
- 起始地址 0x80000100
- 第一个tag是 ATAG_CORE ,表示结构体开始
- 接着是 ATAG_MEM 描述内存区域
- 然后是 ATAG_CMDLINE 传入启动参数
- 最后以 ATAG_NONE 结束

整个结构必须对齐到4字节边界,总长度不超过16KB。看起来简洁明了,不是吗?

⚙️ Bootloader怎么构造它?

以U-Boot为例,开发者通常会写一段类似这样的代码:

void setup_atags(void) {
    struct tag *t = (struct tag *)0x80000100;

    // 设置核心信息
    t->hdr.tag = ATAG_CORE;
    t->hdr.size = tag_size(tag_core);
    t->u.core.pagesize = 4096;

    t = tag_next(t);

    // 添加内存
    t->hdr.tag = ATAG_MEM;
    t->hdr.size = tag_size(tag_mem32);
    t->u.mem.start = 0x80000000;
    t->u.mem.size = 0x20000000; /* 512MB */

    t = tag_next(t);

    // 设置命令行
    t->hdr.tag = ATAG_CMDLINE;
    strcpy(t->u.cmdline.cmdline, "console=ttyAMA0,115200");
    t->hdr.size = (strlen(t->u.cmdline.cmdline) + 1 + 3) / 4 + 1;

    t = tag_next(t);
    t->hdr.tag = ATAG_NONE;
    t->hdr.size = 0;
}

这段代码的问题在哪?
👉 它完全 硬编码 !一旦换了主板、改了内存容量、加了个I2C触摸屏……你就得重新编译Bootloader!

更糟的是,如果厂商想添加自定义设备支持(比如LCD面板),只能自己定义私有tag值,比如 0x54410010 。不同厂家可能用了相同的数值代表完全不同含义,这就埋下了兼容性地雷。

❌ 实际使用中的三大痛点

外设类型 是否支持 维护难度
UART串口 ✅ 是
LCD显示 ⚠️ 部分(私有tag) 极差
I2C设备 ❌ 否 不可维护
SPI Flash ❌ 否 不可维护
GPIO配置 ❌ 否 完全不行

你会发现,ATAGS只适合最基础的信息传递。对于复杂的外设拓扑,它无能为力。没有层次结构、无法表达依赖关系、也不能动态扩展。


二、为什么ATAGS必须被淘汰?

进入ARM64时代后,SoC变得越来越“全能”。一颗芯片内部集成了双核A78 + 四核A55、GICv3中断控制器、多个电源域、十几条I2C/SPI总线、PCIe接口……这时候再用ATAGS去描述这些硬件,简直就像用算盘处理大数据。

🔧 痛点1:灵活性为零

假设你是一家厂商,同一款SoC用于三种产品:
- 智能门锁:带指纹模块 + 蓝牙
- 工业网关:多路RS485 + CAN总线
- 医疗设备:高精度ADC + 实时时钟

按照ATAGS模式,你得准备三套不同的Bootloader镜像 + 三套内核补丁。每次更新都要测试三次,发布流程复杂到令人发指。

而设备树只需要三个 .dtb 文件,内核镜像完全通用。换一句话说: 一次编译,处处部署

🔀 痛点2:扩展成本极高

你想加个温控传感器?好啊,请按以下步骤操作:
1. 定义新的tag ID;
2. 修改Bootloader源码;
3. 修改内核添加解析函数;
4. 更新文档并通知上下游团队。

这个过程动辄数周,严重拖慢研发节奏。

而在设备树中呢?只需写几行DTS:

i2c1: i2c@1c234000 {
    temperature-sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};

重新编译DTB,搞定。全程不碰固件也不改内核。

💸 痛点3:维护地狱

Linus Torvalds 曾公开批评ARM32时代的板级代码是“垃圾合并”(crap merge)。确实,在 arch/arm/mach-* 目录下,你能看到成百上千个几乎重复的初始化文件,仅仅因为ATAGS无法承载足够的硬件信息,导致大量配置被迫固化进内核。

ARM64吸取教训,直接规定: 所有平台必须使用设备树,禁止新增板级初始化代码 。从此, arch/arm64/kernel/ 下的代码变得异常干净,差异全部下沉到DTB中管理。


三、设备树登场:一种全新的硬件描述语言

如果说ATAGS是“简历”,那设备树就是一份完整的“工程图纸”。

它采用树状结构精确刻画系统的物理布局:

/
├── cpus
│   ├── cpu@0
│   └── cpu@1
├── memory@80000000
├── soc
│   ├── timer@2c00000
│   ├── uart@7e201000
│   └── i2c@7e804000
│       └── eeprom@50
└── chosen
    └── bootargs

这种层级化表达能力,使得我们可以清晰地描述“哪个设备接在哪条总线上”、“谁依赖谁的时钟”、“中断如何路由”等复杂关系。

🛠️ DTS → DTB:文本到二进制的蜕变

设备树开发围绕三个核心元素展开:

名称 类型 作用
DTS 文本文件 人类可读的源码, .dts 结尾
DTC 编译器 将DTS转为DTB
DTB 二进制Blob 运行时加载到内存

一个典型的DTS片段如下:

/dts-v1/;
#include "skeleton.dtsi"

/ {
    model = "My ARM64 Board";
    compatible = "vendor,myboard", "simple-bus";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a53";
            reg = <0x0>;
            clock-frequency = <1800000000>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x80000000>; /* 2GB */
    };
};

通过命令行编译:

dtc -I dts -O dtb -o board.dtb board.dts

生成的DTB包含四个部分:
1. Header :魔数、总大小、结构偏移等元信息
2. Structure Block :节点与属性的扁平化存储
3. Strings Block :属性名称字符串池
4. Memory Reservation Map :保留内存区域(如CRAMFS位置)

有趣的是,DTB是可以反向还原的!你可以用:

dtc -I dtb -O dts -o recovered.dts board.dtb

这对调试和逆向分析极为有用。


四、设备树的灵魂:compatible 字符串匹配机制

如果说设备树有一个“心脏”,那就是 compatible 属性。

它是驱动与设备之间的“婚介所”——决定哪个驱动能绑定到哪个设备。

看这个例子:

i2c@7e804000 {
    compatible = "brcm,bcm2835-i2c", "snps,designware-i2c";
    reg = <0x7e804000 0x1000>;
    interrupts = <0x5c>;
};

当内核扫描到该节点时,会依次尝试匹配:
1. 先找叫 brcm,bcm2835-i2c 的驱动;
2. 找不到就退而求其次,匹配 snps,designware-i2c

这就实现了“特化 → 通用”的匹配策略。既能让特定厂商做优化,又能利用成熟的通用驱动降低开发成本。

💡 小知识:所有合法的 compatible 字符串都必须在 Documentation/devicetree/bindings/ 中注册,否则会被视为非法设备。


五、ARM64启动全流程拆解:设备树如何接管系统

现在让我们走进ARM64开机的第一秒,看看设备树在整个启动流程中扮演的角色。

🚀 第一步:U-Boot加载DTB

主流Bootloader如U-Boot早已原生支持设备树。典型启动脚本如下:

setenv bootargs console=ttyAMA0,115200 root=/dev/mmcblk0p2 rw
load mmc 0:1 $kernel_addr_r Image
load mmc 0:1 $fdt_addr_r board.dtb
booti $kernel_addr_r - $fdt_addr_r

其中 $fdt_addr_r 是DTB加载地址(建议避开内核解压区,如 0x87f00000 )。 booti 命令会验证DTB魔数( 0xd00dfeed ),确认无误后将地址存入 x0 寄存器并跳转至内核入口。

值得一提的是,U-Boot甚至允许运行时修改DTB:

=> fdt addr $fdt_addr_r
=> fdt set /memory/reg <0x80000000 0x40000000>

这条命令可以直接更改内存声明,无需重启!这是ATAGS完全做不到的动态能力。

🔍 第二步:内核早期验证DTB

内核第一条有效指令位于 arch/arm64/kernel/head.S _text 入口。此时仍是汇编环境,但很快就会调用:

int __init early_init_dt_verify(void *params)
{
    if (!params)
        return -EINVAL;

    if (be32_to_cpu(((struct fdt_header *)params)->magic) != FDT_MAGIC)
        return -ENODEV;

    initial_boot_params = params;
    return 0;
}

只有魔数正确才会继续执行。否则打印错误日志,可能尝试fallback路径(如有CONFIG_ARM64_ATAGS_COMPAT启用)。

🌲 第三步:展开设备树为运行时结构

接下来调用 unflatten_device_tree() ,将扁平化的DTB转换为内核可用的 struct device_node 链表:

void __init unflatten_device_tree(void)
{
    __unflatten_device_tree(initial_boot_params, NULL,
                            &of_root,
                            early_init_dt_alloc_memory_arch,
                            false);
}

生成的 of_root 成为全局根节点,后续所有设备探测都将基于此树进行。

🔎 第四步:early_init_dt_scan 扫描关键节点

在完全展开前,内核已通过 of_scan_flat_dt 快速提取必要信息:

函数 功能
early_init_dt_scan_memory() 解析内存节点,加入memblock
early_init_dt_scan_chosen() 获取bootargs作为命令行
early_init_dt_scan_cpus() 枚举CPU数量与MPIDR
early_init_dt_scan_root() 提取#address-cells等元数据

例如内存扫描逻辑:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
                                     int depth, void *data)
{
    if (depth == 1 && !strcmp(uname, "memory")) {
        const __be32 *reg;
        int len;

        reg = of_get_flat_dt_prop(node, "reg", &len);
        if (reg == NULL) return 0;

        early_init_dt_add_memory_arch(
            be64_to_cpup((__be64*)reg),
            be64_to_cpup((__be64*)(reg + 2)));
    }
    return 0;
}

注意这里用的是 of_get_flat_dt_prop —— 直接从原始DTB块中读取,避免依赖尚未建立的完整结构。这是一种巧妙的“懒加载”优化。

直到 device_tree_init() 完成,完整的 of_allnodes 链表才正式对外暴露。


六、实战技巧:写出高质量的设备树

好的设备树不只是语法正确,更要具备可维护性、可移植性和健壮性。以下是工程师必须掌握的最佳实践。

✅ CPU、内存、中断控制器的标准写法

CPU节点
cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a72";
        reg = <0x0>;
        enable-method = "psci";
    };

    cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a72";
        reg = <0x1>;
        enable-method = "psci";
    };
};

⚠️ 注意: reg 值应与 MPIDR_EL1 中CPU ID一致。

内存节点
memory@80000000 {
    device_type = "memory";
    reg = <0x80000000 0x80000000>; /* 2GB */
};
GICv3中断控制器
intc: interrupt-controller@2f00000 {
    compatible = "arm,gic-v3";
    reg = <0x0 0x2f00000 0x0 0x10000>,
           <0x0 0x2f10000 0x0 0x10000>;
    interrupt-controller;
    #interrupt-cells = <3>;
    ranges;
};

#interrupt-cells = <3> 表示每个中断描述符占三个u32:类型、编号、触发方式。


🔗 使用phandle处理设备间引用

phandle是设备树中实现“对象引用”的关键机制。通过label和 <&> 语法,避免硬编码地址。

clk: clock@100 {
    #clock-cells = <0>;
    compatible = "fixed-clock";
    clock-frequency = <24000000>;
};

uart0: serial@9000000 {
    compatible = "ns16550a";
    reg = <0x9000000 0x1000>;
    interrupts = <0x30>;
    clocks = <&clk>;
    status = "okay";
};

内核可通过 of_clk_get() 自动解析 clocks = <&clk> 并完成时钟使能。即使Clock节点地址变更,链接依然有效。

技巧 说明
使用label而非绝对地址 增强可移植性
避免重复定义相同设备 复用已有节点
设置 status = "disabled" 临时关闭设备 调试利器
利用 __overlay__ 支持运行时动态加载 适用于FPGA或热插拔

七、迁移策略与性能优化:平稳过渡的艺术

尽管ARM64强制要求设备树,但在实际项目中,我们仍可能遇到旧平台迁移的需求。

🔄 过渡方案:CONFIG_ARM64_ATAGS_COMPAT

Linux提供了兼容选项 CONFIG_ARM64_ATAGS_COMPAT ,允许内核解析ATAGS信息块:

// arch/arm64/kernel/atags_compat.c
static int __init parse_atags(struct setup_data *setup)
{
    struct tag *tags = (struct tag *)phys_to_virt(setup->data);
    if (tags->hdr.tag != ATAG_CORE)
        return -EINVAL;

    pr_info("ATAGS compatibility mode enabled\n");
    save_atags(tags);
    arm64_boot_args[1] = (u64)tags;
    return 0;
}

但这只是临时手段!常见陷阱包括:
- ATAGS未正确终止(缺少ATAG_NONE)
- 物理地址转换失败(页表未建立)
- 与DTB参数冲突导致资源重复注册

建议尽快完成DTS迁移,并通过串口日志监控 [ATAGS] 提示信息。


⚡ 性能调优:让启动更快一点

随着设备树规模增长,其解析时间不容忽视。以下是一些实战优化技巧:

1. 减小DTB体积
  • 删除未焊接外设的节点
  • 合并冗余属性
  • 使用 /delete-node/ 清理通用dtsi中的无用部分

Makefile优化示例:

quiet_cmd_dtb_slim = DTB     $@
      cmd_dtb_slim = $(CPP) -nostdinc -x assembler-with-cpp \
                     -I$(srctree)/arch/$(SRCARCH)/boot/dts \
                     $(DTC_FLAGS) -o $@.tmp $< && \
                     $(DTC) -O dtb -o $@ $@.tmp && \
                     rm $@.tmp
2. 调整节点顺序提升early init效率

确保关键节点靠前:

/ {
    chosen {
        bootargs = "console=ttyAMA0,115200";
    };

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x80000000>;
    };

    cpus { /* ... */ };
    soc { /* ... */ };
};

这样能让 early_init_dt_scan_*() 更快命中目标。

3. 关键寄存器提前映射

对于GIC、UART等高频访问模块,可在设备树中标记 reg ,配合 early_ioremap 或静态映射减少TLB压力。

设备类型 是否建议静态映射 映射大小
GIC中断控制器 ✅ 是 4KB–64KB
UART调试串口 ✅ 是 4KB
DMA控制器 ⚠️ 视情况 8KB
GPIO模块 ❌ 否 动态映射

八、总结:一场静默的技术革命

从ATAGS到设备树,看似只是参数传递方式的改变,实则是嵌入式系统设计理念的一次重大跃迁。

对比维度 ATAGS Device Tree
数据结构 线性链表 树形拓扑
可扩展性 极差 极强
是否需改内核
支持动态更新 是(overlay)
多平台共用内核 ❌ 不可能 ✅ 完美支持
调试便利性 好(可反编译)

设备树不仅解决了历史难题,更为未来留下了足够空间。如今,Yocto Project、Android Treble、OpenWrt等主流嵌入式框架都深度依赖设备树实现硬件抽象。就连RISC-V架构也直接采用了这一模型,足见其普适价值。

当你下次按下开发板电源键时,不妨想想:就在那一瞬间,一个精心构造的设备树正在默默引导内核,唤醒整个数字世界。而这背后,是一群工程师用二十年时间打磨出的智慧结晶。🛠️💡

“真正的技术进步,往往藏在看不见的地方。”
—— 致敬每一位深耕底层的系统开发者 🙇‍♂️

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

基于TROPOMI高光谱遥感仪器获取的大气成分观测资料,本研究聚焦于大气污染物一氧化氮(NO₂)的空间分布浓度定量反演问题。NO₂作为影响空气质量的关键指标,其精确监测对环境保护大气科学研究具有显著价值。当前,利用卫星遥感数据结合先进算法实现NO₂浓度的高精度反演已成为该领域的重要研究方向。 本研究构建了一套以深度学习为核心的技术框架,整合了来自TROPOMI仪器的光谱辐射信息、观测几何参数以及辅助气象数据,形成多维度特征数据集。该数据集充分融合了不同来源的观测信息,为深入解析大气中NO₂的时空变化规律提供了数据基础,有助于提升反演模型的准确性环境预测的可靠性。 在模型架构方面,项目设计了一种多分支神经网络,用于分别处理光谱特征气象特征等多模态数据。各分支通过独立学习提取代表性特征,并在深层网络中进行特征融合,从而综合利用不同数据的互补信息,显著提高了NO₂浓度反演的整体精度。这种多源信息融合策略有效增强了模型对复杂大气环境的表征能力。 研究过程涵盖了系统的数据处理流程。前期预处理包括辐射定标、噪声抑制及数据标准化等步骤,以保障输入特征的质量一致性;后期处理则涉及模型输出的物理量转换结果验证,确保反演结果符合实际大气浓度范围,提升数据的实用价值。 此外,本研究进一步对不同功能区域(如城市建成区、工业带、郊区及自然背景区)的NO₂浓度分布进行了对比分析,揭示了人类活动污染物空间格局的关联性。相关结论可为区域环境规划、污染管控政策的制定提供科学依据,助力大气环境治理公共健康保护。 综上所述,本研究通过融合TROPOMI高光谱数据多模态特征深度学习技术,发展了一套高效、准确的大气NO₂浓度遥感反演方法,不仅提升了卫星大气监测的技术水平,也为环境管理决策支持提供了重要的技术工具。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值