第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驱动的映射关系
设备树里的内存节点,最终要落到驱动代码里。我画了张图来说明这个流程:
驱动里怎么用?我给你看个实际例子:
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 属性写错一位,可能整个系统都起不来。我建议你在开发阶段,每次改完设备树都做三件事:
- 用
dtc -I dts -O dtb编译,看有没有警告 - 启动后看
/proc/iomem确认内存布局 - 用
devmem2验证关键地址的读写
这三步走下来,90%的内存节点问题都能提前发现。