news 2026/5/12 4:48:16

设备树在ARM64中的内存映射配置实战案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设备树在ARM64中的内存映射配置实战案例

设备树在ARM64中的内存映射配置实战案例

从一个真实问题说起:为什么DMA总是失败?

某天,你在调试一块基于飞腾FT-2000/4的嵌入式板卡,系统运行Linux 5.10,任务是实现高清音频回放。驱动写好了,DMA也配置完毕,但播放时总有杂音,偶尔还直接超时崩溃。

你检查了中断、时钟、寄存器设置,一切正常。最后用dmesg一查,发现关键线索:

[ 1.234567] cma: CMA allocation failed for size 8 MiB [ 1.234589] dma_alloc_coherent: failed to allocate 8 MiB

问题出在连续内存分配失败

这背后其实是一个典型的“硬件资源描述缺失”问题——内核不知道你要为音频DMA预留一大块物理上连续的内存区域。而解决它的钥匙,正是设备树(Device Tree)中对内存映射的精确配置


什么是设备树?它凭什么管到内存?

不再“硬编码”的时代

在早期嵌入式Linux开发中,每个平台都有自己的mach-*目录,里面一堆.c文件写着类似这样的代码:

static struct meminfo mem = { .nr_banks = 1, .banks[0] = { .start = 0x80000000, .size = SZ_1G, }, };

这意味着:每换一块板子,就得改一次内核源码,重新编译。维护成本极高。

ARM64来了之后,这个问题更突出:服务器级SoC动辄支持几十GB内存、多片DDR颗粒、NUMA结构、GPU专用显存、NPU保留区……靠硬编码根本玩不转。

于是,设备树成了标准答案。

一句话定义:设备树是一个描述硬件拓扑的数据结构,它让内核在启动时“读说明书”而不是“背课文”。

它由Bootloader(如U-Boot)加载并传递给内核,通过一个叫.dtb的二进制文件承载所有硬件信息,包括CPU核心数、外设地址、中断控制器,以及我们最关心的——内存布局


内存怎么被“说清楚”?看这两个节点

在ARM64系统中,内存相关的设备树配置主要集中在两个地方:

  • /memory:声明系统可用的主RAM。
  • /reserved-memory:声明不能被普通内存分配器使用的特殊区域。

它们共同构成了内核早期内存管理的基础——memblock子系统。

普通内存:告诉内核“哪里能用”

memory@0 { device_type = "memory"; reg = <0x00000000 0x80000000 /* 2GB at 0x0 */ 0x100000000 0x80000000>; /* 2GB at 4GB offset (PA) */ };

这段DTS的意思是:

  • 物理地址从0x00x80000000(即0–2GB)是一段可用内存;
  • 另一段从0x100000000(即4GB)开始,大小也是2GB。

为什么跳过了3–4GB?很可能那里被PCIe MMIO空间占用了。

🔍 内核会调用early_init_dt_add_memory_arch()来扫描这些reg值,并把它们加入memblock.memory链表,作为后续伙伴系统初始化的依据。

注意:reg<u64>类型,所以在32位DTS语法下需要用两个32位单元表示一个64位值。例如:

reg = <0x1 0x0 0x0 0x10000000>; /* 表示起始地址 4GB,长度 256MB */

保留内存:划出“禁区”,专供特定用途

有些内存不能随便分配出去,比如:

  • GPU要独占的一段显存;
  • 安全世界(TEE)保护的加密区域;
  • DMA需要的大块连续缓冲区;
  • 固件或协处理器固化的数据区。

这些就得靠/reserved-memory节点来声明:

reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; audio_dma_buffer: buffer@7fc000000 { compatible = "shared-dma-pool"; reusable; reg = <0x7fc000000 0x4000000>; /* 64MB at 512GB? 等等… */ alignment = <0x100000>; /* 1MB对齐 */ }; gpu_reserved: gpu-region@7e0000000 { no-map; reg = <0x7e0000000 0x20000000>; /* 512MB, not mapped to CPU */ }; };

别急着困惑地址怎么这么大,先拆解几个关键属性:

属性含义
#address-cells = <2>地址字段占64位(两个32位单元),必须加!否则高位会被截断
ranges允许子节点继承父节点的地址映射规则
compatible帮助驱动识别该保留区用途,可触发自动绑定
reusable标记为可被CMA回收使用(推荐用于DMA池)
no-map重点!表示这段内存不会映射到CPU虚拟地址空间,仅GPU等外设访问
关于那个“512GB”的疑问

0x7fc000000≈ 511.5 GB?听起来离谱,但在ARM64眼里很正常。

ARM64物理地址最多支持52位,理论上可达4PB物理内存空间。虽然你现在只有几GB DDR,但SoC可能把一些高端地址预留给各类保留区,避免和主内存冲突。

所以把DMA缓冲区放在4GB以上,反而是最佳实践:既不影响低端内存碎片化,又能保证大块连续性。


启动阶段发生了什么?一步步走进内核

当U-Boot把.dtb放好后,内核就开始解析设备树。整个过程像搭积木一样层层推进:

start_kernel() └── setup_arch() // 架构相关初始化 ├── paging_init() // 设置页表框架 │ └── bootmem_init() // 初始化早期内存分配器 │ ├── arm64_determine_memory_layout() ← 扫描 /memory 节点 │ ├── early_init_fdt_scan_reserved_mem() ← 扫描 /reserved-memory │ └── memblock_reserve() 将保留区标记为不可用 └── rest_init()

其中最关键的函数是:

  • early_init_dt_scan_memory():遍历所有/memory节点,调用memblock_add()加入可用内存池。
  • early_init_fdt_scan_reserved_mem():处理/reserved-memory下的所有子节点,调用memblock_remove()memblock_reserve()排除这些区域。

🧱memblock就像施工队的临时围栏,在真正的“物业管理”(buddy system)上线前,先把地盘划分清楚。

一旦进入mm_init()阶段,伙伴系统就会基于memblock的结果建立页帧管理结构,而那些被“划走”的保留区,将永远不会出现在kmalloc()get_free_pages()的分配范围内。


实战!修复你的DMA问题

回到开头那个音频DMA失败的问题。

现在你知道原因了:没有提前预留连续内存,等到运行时申请才发现已经碎成渣。

解决方案也很明确:在设备树里加上一个可重用的DMA内存池

第一步:添加保留内存节点

/* 在根节点下添加或修改 reserved-memory */ reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; /* 新增音频DMA专用池 */ audio_dma_pool: dma-pool@7f8000000 { compatible = "shared-dma-pool"; reusable; reg = <0x7f8000000 0x800000>; /* 起始地址 + 大小 = 8MB */ alignment = <0x100000>; /* 1MB对齐,利于CMA管理 */ }; };

第二步:启用CMA机制(确保内核配置正确)

检查你的内核配置是否包含:

CONFIG_CMA=y CONFIG_DMA_CMA=y CONFIG_CMA_SIZE_MBYTES=64

或者通过设备树指定该区域为默认CMA区:

audio_dma_pool: dma-pool@7f8000000 { compatible = "shared-dma-pool"; reusable; linux,cma-default; /* 关键!设为默认CMA池 */ reg = <0x7f8000000 0x800000>; alignment = <0x100000>; };

加上linux,cma-default后,CMA子系统会自动把这个区域纳入连续内存分配池。

第三步:驱动中使用DMA API

在音频驱动中,无需手动映射物理地址,直接使用标准DMA接口即可:

struct device *dev = &pdev->dev; void *vaddr; dma_addr_t paddr; /* 分配8MB一致内存(coherent memory) */ vaddr = dma_alloc_coherent(dev, 8 << 20, &paddr, GFP_KERNEL); if (!vaddr) { dev_err(dev, "Failed to allocate DMA buffer\n"); return -ENOMEM; } /* 自动从CMA池中分配,优先使用你定义的保留区 */

只要保留区足够大且未被占用,这次分配就会成功!


高阶技巧与避坑指南

❌ 坑点一:地址重叠导致内存“消失”

常见错误:

memory@0 { reg = <0x0 0x10000000>; /* 256MB RAM */ }; reserved-memory { crash_kernel: ram@10000000 { reg = <0x10000000 0x2000000>; /* 32MB @ 256MB */ }; };

看起来没问题?错!memoryreserved-memory的地址范围紧挨着是可以的,但如果写成:

reg = <0x0 0x12000000>; /* 把保留区包进去了 */

那就会发生重复添加,轻则警告,重则memblock崩溃。

秘籍:始终用工具验证内存布局。

推荐命令:

# 查看当前设备树内存节点 fdtdump your.dtb | grep -A5 -B2 "memory\|reg" # 启动后查看内核识别情况 cat /proc/iomem

输出应类似:

0000000000000000-000000007fffffff : System RAM 0000000000080000-0000000000ffffff : Kernel code 0000000001000000-000000007fffffff : Kernel data 7e0000000-7ffffffff : gpu-region@7e0000000 7f8000000-7f87fffff : dma-pool@7f8000000

如果看到保留区出现在System RAM内部,说明没隔离成功。


⚠️ 坑点二:忘了#address-cells导致高位地址丢失

这是新手最容易犯的错。

如果你只写了:

reserved-memory { audio_dma: buffer@7f8000000 { reg = <0x7f8000000 0x800000>; }; };

但没设置#address-cells = <2>;,那么0x7f8000000会被当作32位地址处理,实际变成0xf8000000,也就是256MB附近

结果就是:你想留高端内存,却误删了低端内存,系统直接起不来。

记住口诀:凡涉及64位地址,必配#address-cells = <2>#size-cells = <2>


💡 高效设计建议

  1. 优先使用标准 compatible 字符串
    -"shared-dma-pool"→ 被CMA识别
    -"linux,cma"→ 显式标记为CMA区域
    -"no-map"+"region"→ GPU/NPU专用

  2. 利用标签引用简化驱动绑定

dts &gpu { memory-region = <&gpu_reserved>; };

驱动中可用:

c of_parse_phandle(np, "memory-region", 0);

  1. 支持动态叠加(Overlay)扩展
    对于模块化硬件(如树莓派HAT),可通过firmware -> configfs动态注入设备树片段,实现热插拔内存设备的支持。

  2. 保留调试通道
    chosen节点中加入启动参数,方便排查:

dts chosen { bootargs = "console=ttyAMA0,115200 earlycon root=/dev/mmcblk0p2"; stdout-path = &uart0; };


写在最后:设备树不只是“配置文件”

掌握设备树中的内存映射配置,意味着你能做到:

  • 精确控制物理内存分布;
  • 为高性能I/O预留资源;
  • 实现零拷贝DMA、安全隔离、虚拟化内存透传;
  • 快速适配不同硬件版本而不改动内核;

它不仅是嵌入式工程师的基本功,更是通往系统级优化的第一道门槛。

未来,随着ARM64在数据中心、边缘计算、自动驾驶领域的深入应用,设备树还将承担更多职责:

  • 描述热插拔内存条;
  • 传递安全启动测量日志;
  • 支持KVM虚拟机内存热迁移;
  • 与ACPI共存,实现跨平台统一固件接口。

所以,请不要再把它当成“配一下reg就行”的简单文本。它是你与硬件之间的第一份契约


如果你正在移植一款国产ARM64平台(如鲲鹏、昇腾、瑞芯微RK35xx系列),不妨现在就打开你的.dts文件,检查一下:

“我的DMA内存真的够吗?GPU会不会踩到内核的地盘?”

也许,答案就在那一行regno-map之间。

欢迎在评论区分享你的设备树调试经历,我们一起填坑。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 10:36:19

2026物联网技术大专生,想找好工作需要考哪些证书?

&#x1f4a5;对大专生而言&#xff0c;学历不是短板&#xff0c;精准考取 “基础必备 核心进阶 拓展深耕” 的证书组合&#xff0c;尤其是以 CDA 数据分析师证书为核心的技能叠加&#xff0c;能快速构建差异化竞争力&#xff0c;直达物联网工程师、数据运营专员、平台开发助…

作者头像 李华
网站建设 2026/5/11 6:38:21

LangFlow部署指南:三步完成私有化AI工作流平台搭建

LangFlow 部署实战&#xff1a;十分钟搭建私有化 AI 工作流平台 在企业加速拥抱大模型的今天&#xff0c;一个现实问题摆在面前&#xff1a;如何让非算法背景的团队也能快速验证 AI 创意&#xff1f;传统的开发模式要求写代码、搭环境、调接口&#xff0c;周期动辄数周。而当业…

作者头像 李华
网站建设 2026/5/6 0:38:29

LangFlow绩效考核评语生成辅助

LangFlow绩效考核评语生成辅助 在企业人力资源管理中&#xff0c;每年一度的绩效考核总是伴随着大量重复而细致的文字工作。尤其是撰写员工评语——既要体现个性化表现&#xff0c;又要保持语言的专业与得体&#xff0c;还要确保跨团队评价标准的一致性。对于HR来说&#xff0c…

作者头像 李华
网站建设 2026/5/11 12:05:16

PCB生产流程中阻抗控制的设计配合要点

高速PCB设计的“生死线”&#xff1a;阻抗控制如何靠设计与制造协同落地&#xff1f;你有没有遇到过这样的场景&#xff1f;一块精心设计的高速主板&#xff0c;原理图无误、布局合理、电源干净&#xff0c;可回板后就是跑不起来PCIe链路&#xff0c;或者DDR始终无法稳定在标称…

作者头像 李华
网站建设 2026/5/12 0:42:10

Chrome Driver启动流程图解说明(驱动层视角)

Chrome Driver 启动流程深度解析&#xff1a;从驱动层看自动化控制的幕后机制你有没有遇到过这样的情况&#xff1f;明明代码写得没问题&#xff0c;Selenium脚本却卡在webdriver.Chrome()这一步&#xff0c;报出一串令人头大的错误&#xff1a;unknown error: cannot find Chr…

作者头像 李华
网站建设 2026/4/26 14:41:16

LangFlow如何帮助团队快速验证大模型应用场景?

LangFlow如何帮助团队快速验证大模型应用场景&#xff1f; 在智能客服系统频繁“答非所问”&#xff0c;或是内部知识库查询仍依赖人工翻阅的今天&#xff0c;许多企业已经意识到&#xff1a;大模型技术或许能解决这些问题。但真正动起手来&#xff0c;却往往卡在第一步——怎么…

作者头像 李华