第3章:设备树中的内存节点解析

设备树里的内存节点,说白了就是告诉内核:“嘿,你的物理内存长这样,哪些能用,哪些是保留的,哪些是给外设IO的。” 这事儿看着简单,但我在车载项目里见过太多次因为内存节点配错导致的诡异问题——系统启动一半挂掉、DMA踩到保留区、外设读写乱飞……

今天咱们就把这块彻底掰开揉碎。

3.1 标准内存节点(memory node)

先看最基础的。一个典型的内存节点长这样:

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

这里 reg 属性表示起始地址和大小。我习惯用两对 <地址 长度> 来表示64位地址空间。上面这个例子,起始地址是0x8000_0000,大小是2GB。

多块不连续内存怎么办?很简单,多写几组:

memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x40000000>,
          <0x1 0x00000000 0x0 0x40000000>;
};

第一块在0x8000_0000,1GB;第二块在0x1_0000_0000,又是1GB。内核启动时会把这些区域合并到伙伴系统里。

注意: device_type = "memory" 这个属性不能少。虽然新内核已经不太依赖它了,但很多bootloader和早期初始化代码还在用。我踩过这个坑——少写了这个属性,结果内核只认了512MB。

3.2 保留内存(reserved-memory)

这块才是重头戏。车载SoC里,很多内存区域是不能给Linux随便用的:

  • IPU/VPU的帧缓冲区
  • DSP的代码和数据段
  • 安全监控区的共享内存
  • 硬件看门狗的日志区

这些都得通过 reserved-memory 节点来声明。

3.2.1 基本用法

/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        /* 为IPU保留64MB */
        ipu_reserved: ipu_mem@90000000 {
            reg = <0x0 0x90000000 0x0 0x04000000>;
            no-map;
        };

        /* 为DSP保留32MB */
        dsp_reserved: dsp_mem@94000000 {
            reg = <0x0 0x94000000 0x0 0x02000000>;
            no-map;
        };

        /* 为安全监控保留4MB */
        secure_log: sec_log@96000000 {
            reg = <0x0 0x96000000 0x0 0x00400000>;
            no-map;
        };
    };
};

no-map; 这个属性很关键。加了它,内核就不会为这块区域建立页表映射。说白了,Linux的虚拟地址空间里完全看不到这块内存——只有通过ioremap或者DMA API才能访问。

我的习惯: 所有给硬件协处理器用的内存,一律加 no-map。否则万一哪个驱动不小心访问了,轻则数据错乱,重则系统崩溃。我在一个ADAS项目里就遇到过——VPU和CPU同时写一块没加 no-map 的缓冲区,结果图像花得一塌糊涂。

3.2.2 动态分配保留内存

有时候你不想硬编码地址,可以让内核自动分配:

shared_dma_pool: dma_pool@0 {
    compatible = "shared-dma-pool";
    size = <0x0 0x10000000>;  /* 256MB */
    alignment = <0x0 0x1000>;
    reusable;
};

reusable 表示这块内存在没人用时,内核可以临时借用。但一旦有设备要申请,内核得把内容清空再交出去。这个机制在内存紧张时很有用。

属性 含义 典型值
no-map 不建立页表映射 无值(布尔属性)
reusable 可复用,但需清空 无值(布尔属性)
alignment 对齐要求 0x1000(4KB)
alloc-ranges 限定分配范围 <0x0 0x80000000 0x0 0x20000000>

3.3 IO映射区域

外设的寄存器空间,在设备树里通过 reg 属性定义。但这里有个容易搞混的地方——reg 里的地址到底是物理地址还是总线地址?

答案是:取决于父节点的 ranges 属性。

soc {
    compatible = "simple-bus";
    #address-cells = <2>;
    #size-cells = <2>;
    ranges = <0x0 0x0 0x0 0xf0000000 0x0 0x10000000>;

    uart@1000 {
        compatible = "ns16550";
        reg = <0x0 0x1000 0x0 0x100>;
        interrupts = <0 33 4>;
    };
};

这里 ranges 做了地址翻译:子节点的 0x1000 对应物理地址 0xf000_1000。BSP驱动里调用 of_iomap() 时,内核会自动完成这个翻译。

关键点: 驱动里永远不要硬编码物理地址。用 platform_get_resource() 或 of_iomap() 从设备树获取。我见过有人直接写 ioremap(0xf0001000, 0x100),结果换了SoC版本后地址变了,驱动直接崩。

3.4 与BSP驱动的映射关系

设备树里的内存节点,最终要落到驱动代码里。我画了张图来说明这个流程:

设备树内存节点 → BSP驱动映射流程 设备树 memory/reserved-memory 内核 early_init_dt_scan_memory 伙伴系统 reserved-memory 节点 of_reserved_mem_device_init DMA池 外设 reg 属性 of_iomap / platform_get_resource ioremap BSP 驱动层 dma_alloc_coherent / ioremap / memremap 💡 驱动通过 of_ 系列API获取地址和大小,再调用 ioremap/dma_alloc_coherent 建立映射。 不要直接使用物理地址,否则设备树改了驱动也得跟着改。

驱动里怎么用?我给你看个实际例子:

static int my_ipu_driver_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct resource *res;
    void __iomem *regs;
    struct reserved_mem *rmem;

    /* 1. 获取寄存器IO空间 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(regs))
        return PTR_ERR(regs);

    /* 2. 获取保留内存 */
    rmem = of_reserved_mem_lookup(dev->of_node);
    if (rmem) {
        /* 用dma_alloc_coherent从保留区分配 */
        dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
        /* 或者直接memremap */
        void *vaddr = memremap(rmem->base, rmem->size, MEMREMAP_WC);
    }

    return 0;
}

避坑指南: 我曾经在 of_reserved_mem_lookup 返回后直接用了 rmem->base 做 ioremap,结果系统报错。后来才发现——加了 no-map 的保留区不能用 ioremap,得用 memremap 并指定 MEMREMAP_WC 或 MEMREMAP_WT。这两个的区别,说白了就是缓存策略不同。

3.5 常见问题与排查方法

最后总结几个我实际遇到过的坑:

问题现象 根因 排查方法
系统只识别了部分内存 reg 属性写错或 device_type 缺失 检查 /proc/iomem 和启动日志
DMA传输数据错乱 保留内存没加 no-map 查看 /sys/kernel/debug/dma-api/dump
外设寄存器读写异常 ranges 翻译错误或地址重叠 用 devmem2 直接读物理地址验证
驱动 probe 失败 reg 里地址位数与 #address-cells 不匹配 检查设备树编译警告

嗯,内存节点这块其实不难,但细节特别多。你想想看——一个 reg 属性写错一位,可能整个系统都起不来。我建议你在开发阶段,每次改完设备树都做三件事:

  1. 用 dtc -I dts -O dtb 编译,看有没有警告
  2. 启动后看 /proc/iomem 确认内存布局
  3. 用 devmem2 验证关键地址的读写

这三步走下来,90%的内存节点问题都能提前发现。