把一个按 TCM 地址链接的 .bin 下载到 DDR,并不会把它变成 DDR 版本。下载只是搬运字节;能否启动,还取决于向量表、程序实际所在的内存、A53/M7 的地址映射和启动代码的数据初始化。
本文用 NXP uboot-imx 的 lf_v2024.04 分支核对 bootaux 行为,用 MCUXpresso SDK 25.06 的 EVK-MIMX8MP 说明核对运行流程。其他 BSP,尤其开发板厂商修改版,必须重新检查同名命令和链接脚本。原文中的 ecspi_loopback、hello_world、AD7606_Init 保留为应用场景;函数名不代表本文实现了对应外设驱动。
1. 先区分四种“地址”
跳转到“1. 先区分四种“地址””| 名称 | 回答的问题 | 例子 |
|---|---|---|
| 文件偏移 | 字节在 BIN/ELF 文件的什么位置 | BIN 起点后第 4 字节是示例中的复位向量 |
| 暂存地址 | U-Boot 把文件读到哪里 | A53 可访问的 DDR 缓冲区 0x48000000 |
| 链接运行地址 VMA | 指令或变量在运行时使用哪个地址 | M7 ITCM 中的函数、DTCM 中的变量 |
| 加载地址 LMA / 总线映射 | 初始化字节先放在哪里,以及 A53 如何访问 M7 内存 | .data 初值紧随代码;M7 ITCM 对应 A53 的 TCM 窗口 |
LMA 与 A53 地址映射也不是同一个概念:LMA 描述链接镜像中的装载布局,地址映射描述不同总线主设备看见同一物理存储的方式。ELF 加载器可能按映射转换 LMA;普通文件下载不会自动做这件事。
2. 从 C 文件到 BIN
跳转到“2. 从 C 文件到 BIN”.c + 头文件 → 预处理 → 编译成汇编 → 汇编成 .o.o + 所需库 + 链接脚本 → 链接成 .elf → objcopy → .bin| 产物 | 保留什么 | 尚未解决什么 |
|---|---|---|
.i | 宏和头文件展开后的 C 源码 | 代码生成和地址布局 |
.s | 汇编指令与符号引用 | 汇编编码、跨文件符号和最终布局 |
.o | 机器码、节、符号表、重定位项 | 外部符号和部分地址需要链接器处理;文件内相对关系可能已确定 |
.elf | 可加载段、入口、节/符号和可选调试信息 | 是否能在目标硬件运行仍取决于布局和初始化 |
.bin | 按装载布局提取的原始字节 | 不再有 ELF 段表、入口元数据或通用自重定位能力 |
静态链接会解析符号并应用重定位,但不能概括成“每条指令都含绝对地址”。例如 Thumb BL 通常编码相对于 PC 的分支位移,链接器会修补对应重定位项,必要时也可能生成跳板。向量表中的函数指针、数据指针和一些常量池则可能保留固定地址。少数指令采用相对寻址,不等于整个固件可以任意搬移。
跨文件变量与函数
跳转到“跨文件变量与函数”头文件声明接口,单个 .c 文件提供对象定义:
#ifndef AD7606_H#define AD7606_H#include <stdint.h>extern volatile uint32_t g_sample_count;void AD7606_Init(void);#endif/* ad7606.c:仅用于演示符号链接,不操作真实 ADC。 */#include "ad7606.h"volatile uint32_t g_sample_count = 7;void AD7606_Init(void) { g_sample_count += 1; }#include "ad7606.h"volatile uint32_t g_seen;int main(void) { AD7606_Init(); g_seen = g_sample_count; return 0;}extern 声明本身不分配第二份变量。原应用中的 g_tAD7606 等结构体对象也遵循相同规则。SDK 的 PRINTF 可能是调试控制台宏或 SDK 函数,并不保证就是 C 库的 printf;需沿其头文件和实现查找依赖。
静态库 .a 是目标文件的集合。传统链接器根据未解析符号按需抽取目标文件成员;要进一步移除成员内部未使用函数,通常还需要函数/数据独立节及 --gc-sections 等配合。库顺序或循环依赖也可能造成 undefined reference。
3. 向量表里的入口是什么
跳转到“3. 向量表里的入口是什么”在这里讨论的 Cortex-M 固件布局中,镜像从向量表开始:
| 相对镜像起点 | 32 位条目 | 含义 |
|---|---|---|
0x00 | 初始 MSP | 初始主堆栈指针,通常设为已分配 RAM 栈区的顶部 |
0x04 | Reset Handler 指针 | 复位入口,最低位表示 Thumb 状态 |
0x08、0x0c | NMI、HardFault 指针 | 后续异常向量;完整表还包括外设中断 |
例如复位向量为 0x000004cd 时,指令地址是 0x000004cc,最低位 1 表示 Thumb 状态。不能把两字节指令画成从奇地址 0x4cd 开始。若初始 MSP 是 0x20020000,小端字节序是 00 00 02 20;00 00 20 20 实际表示 0x20200000。
向量表不会自行搬运程序。把这些字节下载到 DDR 后,复位条目仍然指向原来的 TCM 入口;如果 TCM 中没有对应代码,启动会失败,或者意外执行之前残留的内容。调试时应清楚区分“读到了入口”与“入口处已经有正确指令”。
4. i.MX8MP 的 A53 / M7 映射
跳转到“4. i.MX8MP 的 A53 / M7 映射”以下关系来自上述 NXP U-Boot 分支的 i.MX8MP host map,仅用于这个 SoC/实现的说明:
| M7 地址 | A53/U-Boot 地址 | 用途 |
|---|---|---|
0x00000000 | 0x007e0000 | ITCM 的 M7 指令别名起点,映射长度 0x20000 |
0x1ffe0000 | 0x007e0000 | TCM 的系统别名起点,映射长度 0x40000 |
0x20000000 | 0x00800000 | 上一行中 DTCM 所在部分 |
0x10000000 | 0x80000000 | DDR 代码别名窗口起点;窗口长度按 BSP 表核对 |
0x40000000 起的一段范围 | 同地址 | DDR 系统窗口,仍须确认实际装配内存与资源预留 |
DTCM 的换算是 0x007e0000 + (0x20000000 - 0x1ffe0000) = 0x00800000。原稿的 0x1f800000 不适用于这张映射表。TCM 与 DDR 中的 0x800000 和 0x80000000 也相差两个十六进制位,排查时应完整写出地址。
映射存在不代表当前时钟、权限和内存属性都允许访问。ELF 段长度也必须整个落在有效窗口内,不能只核对起点。
5. 链接脚本:代码与数据初值分开理解
跳转到“5. 链接脚本:代码与数据初值分开理解”NXP 的 MIMX8ML8 TCM 链接脚本通常把向量表放在 0x00000000,代码放在 0x00000400 之后,DTCM 从 0x20000000 开始;脚本还可能声明 DDR 数据区。以实际工程选择的文件为准,不能只根据文件名推断所有节的位置。
下面是本文静态编译检查用的最小布局,容量按 128 KiB ITCM 和 128 KiB DTCM 设置。它没有完整 SDK 内存属性、堆栈分配检查或外设启动逻辑,不能直接替代产品链接脚本。
ENTRY(Reset_Handler)MEMORY { IMAGE (rx) : ORIGIN = 0x00000000, LENGTH = 128K DATA (rw) : ORIGIN = 0x20000000, LENGTH = 128K}_estack = ORIGIN(DATA) + LENGTH(DATA);SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } > IMAGE .text ORIGIN(IMAGE) + 0x400 : { *(.text*) *(.rodata*) . = ALIGN(4); } > IMAGE .data : { _sdata = .; *(.data*) . = ALIGN(4); _edata = .; } > DATA AT> IMAGE _sidata = LOADADDR(.data); .bss (NOLOAD) : { _sbss = .; *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } > DATA /DISCARD/ : { *(.comment) *(.note*) *(.ARM.attributes) }}> DATA AT> IMAGE 表示 .data 运行在 DATA 区,但初始化字节存放在 IMAGE 装载区。启动代码要把 [_sidata, _sidata + (_edata - _sdata)) 复制到 [_sdata, _edata),再把 [_sbss, _ebss) 清零。如果仅把 .data 放到 DTCM 而未正确设置 LMA,转成 BIN 时可能在代码与数据间产生巨大空洞;只看 ELF 文件本身大小无法排除这个问题。
NOLOAD 的 .bss 不需要把全零初值存入 BIN;这不免除启动清零责任。栈顶还需满足 ABI 对齐,数据、BSS、堆和栈不可相互覆盖。
可重复的检查顺序
跳转到“可重复的检查顺序”在完整工程中,先用该工程的构建系统编译,然后检查:
arm-none-eabi-readelf -r main.oarm-none-eabi-readelf -l firmware.elfarm-none-eabi-objdump -h firmware.elfarm-none-eabi-nm -n firmware.elfarm-none-eabi-objdump -d firmware.elfarm-none-eabi-objcopy -O binary firmware.elf firmware.binreadelf -r 观察外部函数调用的重定位;-l 观察 ELF 可加载段;objdump -h 对比 VMA/LMA;nm 查实际符号地址;反汇编检查链接后的调用目标;BIN 再检查长度和前两个字。
若要观察编译阶段,可在工程相同的 CPU、ABI、宏和包含路径选项下分别使用 gcc -E、gcc -S、gcc -c。不要省略工程必要选项后比较两个并不等价的产物。增量构建也必须追踪头文件依赖,不能只重编修改过的 .c。
本文用 Arm GNU 13.3.1 实际编译了上面的三个 C 文件和一个仅用于检查的启动汇编,分别链接为 TCM 和 DDR 两种 IMAGE 起点;DATA 仍在 DTCM。结果如下:
| 检查项 | TCM 布局 | DDR 布局 |
|---|---|---|
| IMAGE 起点 | 0x00000000 | 0x80000000 |
| BIN 长度 | 1152 字节 | 1152 字节 |
| 初始 MSP | 0x20020000 | 0x20020000 |
| 复位向量 | 0x00000401 | 0x80000401 |
.data VMA | 0x20000000 | 0x20000000 |
.data LMA | 0x0000047c | 0x8000047c |
这验证了固定入口、Thumb 位、跨文件调用和紧凑数据初值布局。检查夹具只提供基本复位汇编和少量向量,未在 M7 上执行,也没有验证 SoC 时钟、缓存、MPU、VTOR、外设或 SDK 完整启动。
6. bootaux 实际负责什么
跳转到“6. bootaux 实际负责什么”在本文核对的 NXP 分支中,BIN 路径从给定地址读初始栈和复位向量,把它们写入 MCU 复位向量位置,处理数据缓存后请求平台固件启动辅助核。这条路径不会把整个 BIN 从 DDR 自动复制到 TCM。 A53 也不是直接对 M7 的 MSP/PC 寄存器赋值;M7 释放后按复位机制读取向量。
同一实现还有 ELF32 路径,按 PT_LOAD 与 host map 搬运段并处理段内零填充,入口取自 ELF;它还假定首个有文件内容的可加载段,其首个字就是初始栈值。因此普通格式有效的 ELF 也未必满足这套固件布局约定。这与 BIN 路径不同,且该简化加载器假定镜像有效;不能把“识别 ELF”当作完整安全验证。不同版本可能缺少此能力,或对 ELF 布局、入口和向量有不同假设。
命令打印 Starting auxiliary core 仅说明启动请求到达相关代码,不证明应用已正常执行。核已启动时,也不应假定再次运行 bootaux 会重载或复位它。
7. TCM 与 DDR 的加载流程
跳转到“7. TCM 与 DDR 的加载流程”首先确定本板 U-Boot/SDK 版本、固件大小、可用缓冲区和恢复方式。NXP EVK 文档还指出,部分较新 Linux BSP 要在启动 M 核前运行 prepare_mcore 环境脚本;应先用 printenv prepare_mcore 查本板定义,并核对对应 BSP 文档。不要把其他板的脚本内容或地址盲目复制过来。
TCM 固件:先暂存,再复制到实际 TCM
跳转到“TCM 固件:先暂存,再复制到实际 TCM”下面保留原文 mmc 2:2 和文件路径场景。它们是示例,必须替换为本板实际介质与路径;0x48000000 也必须已确认是可用 DDR 暂存区。目标应支持 hush、itest、cp.b 和 bootaux。
if ext4load mmc 2:2 0x48000000 /home/root/hello_world.bin; then setenv m7_size ${filesize} if itest.l ${m7_size} -ge 8 && itest.l ${m7_size} -le 0x20000; then if cp.b 0x48000000 0x007e0000 ${m7_size}; then bootaux 0x007e0000 else echo TCM copy failed fi else echo Image length outside this TCM layout fielse echo M7 image load failedfi这里的 0x20000 只对应本文从 ITCM 起点连续存放的最多 128 KiB 镜像布局,不能覆盖任意 SDK 链接方案。长度检查是必要的粗检查,还需检查镜像摘要、向量、链接图、数据初值和所有内存范围。cp.b 长度按字节解释;在任何后续文件加载前保存 ${filesize}。
ext4load 并非在命令层面“只能加载 DDR”。是否可以直接读入 TCM,取决于访问权限、时钟、缓存和传输实现;采用 DDR 暂存再复制,是官方流程常见且便于检查的办法。
DDR 固件:使用真正按 DDR 布局构建的文件
跳转到“DDR 固件:使用真正按 DDR 布局构建的文件”若固件向量、代码与初值已经链接到以 0x80000000 开始的布局,并且该范围已为 M7 预留,可以在加载成功、大小/向量/摘要全部核对后执行 bootaux 0x80000000。仅重命名文件为 _ddr.bin 或改变 ext4load 的目标地址不会改变链接结果。
DDR 代码与 DTCM 栈/数据可以同时存在,正如上面的检查夹具;“DDR 版本”并不规定栈一定在 DDR。真正决定位置的是链接脚本与启动代码。
Linux 随后启动时,设备树和驱动必须保留 M7 使用的内存、外设、时钟和通信资源。启动阶段可运行不代表 Linux 接管后仍安全。使用 remoteproc/RPMsg 的系统,还要统一固件生命周期和资源表约定,避免 Linux 与 U-Boot 各自独立启动同一核心。
8. 常见错误的定位顺序
跳转到“8. 常见错误的定位顺序”| 现象 | 检查方向 |
|---|---|
undefined reference | 对应定义是否编译/链接,条件宏和 C/C++ 符号是否一致,库顺序是否正确 |
multiple definition | 头文件是否放了外部对象定义,是否重复链接同一实现;把共享声明放头文件,单一定义放 .c |
| BIN 异常巨大 | .data 的 LMA、节间空洞、意外的可加载段,以及 objcopy 选择 |
| 向量指回 TCM 但只加载了 DDR | TCM 版本缺少复制,或错选链接脚本 |
| MSP/入口看起来明显越界 | 文件格式、偏移、端序、损坏和错误地址别名 |
| 有启动信息但没有输出 | M7 实际指令位置、数据初始化、VTOR/异常、UART 实例/时钟/引脚、权限和缓存 |
| Linux 启动后 M7 异常 | 预留内存、设备树资源、时钟与外设所有权、remoteproc 生命周期 |