news 2026/5/1 10:34:26

设备树在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/1 5:51:18

LangFlow图像生成工作流搭建实录

LangFlow图像生成工作流搭建实录 在AI内容创作日益普及的今天&#xff0c;一个常见的挑战摆在设计师和产品经理面前&#xff1a;如何让“脑海中的画面”快速变成真正可看的图像&#xff1f;传统流程往往需要先写提示词、调模型、再拼接结果——每一步都依赖技术背景&#xff0c…

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

LangFlow支持导出为Python代码,便于生产环境迁移

LangFlow&#xff1a;从可视化设计到生产级代码的无缝跃迁 在大语言模型&#xff08;LLM&#xff09;应用爆发式增长的今天&#xff0c;开发者面临一个现实困境&#xff1a;如何在快速验证创意的同时&#xff0c;确保最终系统具备工程上的可维护性与可部署性&#xff1f;传统的…

作者头像 李华
网站建设 2026/5/1 6:55:32

Centos7安装Node.js环境

1、使用 NodeSource 仓库 设置 NodeSource 仓库 curl -sL https://rpm.nodesource.com/setup_16.x | sudo bash -上面的命令会安装 Node.js 16.x 版本。如果你想安装其他版本&#xff0c;可以替换 16.x 为其他版本号&#xff0c;例如 14.x 或 18.x。 安装 Node.js 和 npm sudo …

作者头像 李华
网站建设 2026/5/1 7:22:55

LangFlow直播带货话术生成助手

LangFlow直播带货话术生成助手 在直播电商竞争白热化的今天&#xff0c;一场成功的带货直播背后&#xff0c;往往不是靠主播临场发挥的“即兴表演”&#xff0c;而是高度结构化、数据驱动的内容策略。如何在短时间内为上百款商品快速生成风格统一、情绪饱满、转化导向明确的话术…

作者头像 李华
网站建设 2026/5/1 7:24:01

LangFlow用户评价回复生成器

LangFlow&#xff1a;让AI工作流“看得见、摸得着”的开发革命 在今天&#xff0c;构建一个能自动回复用户评论的客服助手&#xff0c;已经不再需要从零开始写几十行Python代码。你不需要精通LangChain API&#xff0c;也不必反复调试提示词逻辑——只需要打开浏览器&#xff0…

作者头像 李华
网站建设 2026/5/1 7:18:25

手把手教程:使用Kibana搭建elasticsearch可视化工具实战案例

手把手教你用 Kibana 搭出真正好用的 Elasticsearch 可视化监控系统你有没有遇到过这样的场景&#xff1f;服务器日志堆成山&#xff0c;运维说看不过来&#xff1b;业务方天天要报表&#xff0c;开发只能手动查 ES 写 PPT&#xff1b;线上突然 500 错误飙升&#xff0c;却没人…

作者头像 李华