4、链接脚本(Linker Script)详解:如何定义内存区域、段(Section)的分配与重定位

在STM32H7系列MCU的嵌入式开发中,链接脚本(通常以.ld或.lcf为后缀)是决定最终固件如何映射到物理内存的“施工图纸”。对于传感器数据分类任务而言,模型参数、特征缓冲区、中间计算结果和实时采集数据往往具有截然不同的生命周期与访问频率。如果仅依赖默认链接脚本,极易出现RAM空间不足、DMA缓冲区对齐错误或关键数据被意外初始化覆盖等问题。本章将深入剖析GNU LD链接脚本的核心语法,结合STM32H7的物理内存布局,讲解如何通过自定义内存区域与段分配规则,实现精确的内存分区与重定位。

STM32H743 物理内存 FLASH (0x08000000) 2MB · 只读 · 存放代码与常量 DTCM RAM (0x20000000) 128KB · 紧耦合 · 零等待 适合栈、关键变量 AXI SRAM (0x24000000) 512KB · 主RAM · 支持DMA 适合传感器数据缓冲区 SRAM1/2/3 (0x30000000) 共288KB · 总线矩阵访问 适合大块数据缓存 MEMORY 命令 链接脚本段定义 (SECTIONS) .isr_vector → FLASH 中断向量表 · 起始地址 .text → FLASH 代码段 · 只读常量 .data → 加载在FLASH 运行在DTCM · 启动时拷贝 .bss → AXI SRAM 零初始化 · 传感器缓冲区 .sensor_buf → SRAM1 自定义段 · 16字节对齐 重定位机制 • 加载地址(LMA):FLASH • 运行地址(VMA):RAM • 启动代码负责从LMA 拷贝到VMA • AT> 指定加载区域 • > 指定运行区域 • 符号 _sdata/_edata 用于拷贝循环边界 • 对齐属性 ALIGN(16) 保证DMA缓冲区安全

上图展示了STM32H7中链接脚本的核心作用:左侧为芯片物理内存资源,右侧为脚本中定义的逻辑段,中间通过MEMORY和SECTIONS命令建立映射关系。理解这张图是掌握链接脚本的关键起点。

4.1 核心原理:MEMORY命令与物理内存区域定义

链接脚本的第一项核心任务是通过MEMORY命令描述目标芯片可用的物理内存范围。STM32H7系列的内存布局比传统M3/M4内核复杂得多,它拥有多个物理上独立的RAM块,各自具有不同的总线接口、访问速度和DMA支持能力。以STM32H743为例,其主要内存资源包括:DTCM RAM(0x20000000,128KB,紧耦合,CPU零等待访问)、ITCM RAM(0x00000000,64KB,指令紧耦合)、AXI SRAM(0x24000000,512KB,主系统RAM,支持DMA1/DMA2和MDMA访问)、SRAM1/2/3(0x30000000起,共288KB,挂在AHB总线矩阵上)以及备份SRAM(0x38800000,4KB)。

在链接脚本中,我们需要为每个物理区域定义一个逻辑名称,并指定其起始地址、长度和可选属性。典型的定义如下:

MEMORY
{
  FLASH   (rx)  : ORIGIN = 0x08000000, LENGTH = 2048K
  DTCM    (rw)  : ORIGIN = 0x20000000, LENGTH = 128K
  AXI_SRAM (rw) : ORIGIN = 0x24000000, LENGTH = 512K
  SRAM1   (rw)  : ORIGIN = 0x30000000, LENGTH = 128K
  SRAM2   (rw)  : ORIGIN = 0x30020000, LENGTH = 128K
  SRAM3   (rw)  : ORIGIN = 0x30040000, LENGTH = 32K
}

属性标记r(只读)、w(可写)、x(可执行)用于在链接阶段检查段与区域的兼容性。例如,将代码段放入仅标记为rw的RAM区域会触发警告。对于传感器数据分类任务,建议将实时性要求最高的栈空间放在DTCM,将DMA采集缓冲区放在AXI SRAM,将模型中间计算结果放在SRAM1/2/3,以充分利用各总线的并行访问能力。

经验提醒:STM32H7的DTCM RAM不支持DMA访问,如果传感器数据通过DMA传输,绝对不能将DMA目标缓冲区定义在DTCM中。这是新手最容易犯的错误,会导致数据始终无法更新。

4.2 段(Section)的分配与重定位机制

段是链接脚本中管理输入文件(.o目标文件)中各类数据的最小逻辑单元。GCC编译器默认会生成.text(代码)、.data(已初始化全局变量)、.bss(零初始化全局变量)、.rodata(只读常量)等标准段。链接脚本的SECTIONS命令负责将这些输入段重新组合、排序并分配到指定的内存区域。

重定位是链接脚本最强大的功能之一。它允许我们为某个段指定两个不同的地址:加载地址(LMA)和运行地址(VMA)。LMA是段内容在烧录时存放的位置(通常为FLASH),VMA是程序运行时该段在内存中的实际位置。对于.data段,其初始值必须保存在FLASH中(LMA),但运行时需要被拷贝到RAM中(VMA)以便读写。链接脚本通过AT>指令指定LMA,通过>指令指定VMA:

SECTIONS
{
  .text :
  {
    KEEP(*(.isr_vector))
    *(.text*)
    *(.rodata*)
    _etext = .;
  } > FLASH

  .data :
  {
    _sdata = .;
    *(.data*)
    _edata = .;
  } > DTCM AT> FLASH

  .bss :
  {
    _sbss = .;
    *(.bss*)
    *(COMMON)
    _ebss = .;
  } > AXI_SRAM
}

上述脚本中,_sdata、_edata、_sbss、_ebss是链接器自动生成的符号,启动文件(如startup_stm32h743xx.s)中的汇编代码会利用这些符号完成.data的拷贝和.bss的零初始化。理解这一机制对于排查“变量初始值丢失”或“全局变量未清零”的问题至关重要。

4.3 自定义段与传感器数据分区实战

在传感器数据分类任务中,我们通常需要定义一些自定义段,以满足特殊的内存对齐要求或实现缓存策略。例如,ADC采集的DMA缓冲区要求32字节对齐(STM32H7的DMA支持8/16/32字节突发传输),而神经网络模型的特征图缓冲区可能要求64字节对齐以配合MDMA。我们可以通过__attribute__((section))将变量强制放入自定义段:

#define ALIGN_32 __attribute__((aligned(32)))
#define SECTION_SENSOR __attribute__((section(".sensor_buf")))

ALIGN_32 SECTION_SENSOR uint8_t adc_dma_buffer[4096];
ALIGN_32 SECTION_SENSOR float feature_map[128][32];

然后在链接脚本中为.sensor_buf段分配独立的区域,并指定对齐属性:

  .sensor_buf (NOLOAD) :
  {
    . = ALIGN(32);
    *(.sensor_buf)
    . = ALIGN(32);
  } > SRAM1

使用NOLOAD关键字表示该段不需要在启动时初始化,因为DMA缓冲区的内容完全由硬件填充,而特征图缓冲区在每次推理前会被显式清零。这样既节省了FLASH空间(不需要存储初始值),又避免了启动时不必要的拷贝操作。

内存区域 典型用途 访问特性 对齐要求
DTCM (0x20000000) 栈、RTOS内核变量、高频中断上下文 CPU零等待,不支持DMA 8字节(AAPCS)
AXI SRAM (0x24000000) DMA缓冲区、传感器原始数据队列 支持DMA1/2、MDMA,带宽高 32字节(突发传输)
SRAM1/2/3 (0x30000000) 模型权重、中间特征图、分类结果缓存 AHB总线矩阵,支持并行访问 16字节(浮点运算)
FLASH (0x08000000) 代码、只读常量、启动向量 执行时零等待(带缓存) 32字节(编程粒度)

4.4 常见误区与排障复盘

误区一:忽略LMA与VMA的区别。当程序运行后发现.data段变量初始值正确,但修改后重启又恢复原值,通常是因为启动代码未正确拷贝,或链接脚本中AT>缺失导致LMA被错误地设置为与VMA相同。检查_sdata和_edata符号是否被启动文件正确引用。

误区二:DMA缓冲区跨内存边界。STM32H7的DMA支持突发传输,但要求缓冲区不能跨越1KB边界(部分外设要求更严格)。如果缓冲区被链接器放置在两个内存区域的交界处,可能导致DMA传输错误。解决方案是在链接脚本中为关键缓冲区段添加SUBALIGN或使用NOLOAD段并手动指定起始地址。

误区三:栈溢出检测失效。当栈段被放置在DTCM中时,如果链接脚本未在栈底放置一个已知的填充模式(如0xDEADBEEF),则无法通过检查栈指针是否越界来检测溢出。建议在.stack段末尾预留4字节的哨兵值。

风险提示:在修改链接脚本时,务必确认_estack(栈顶地址)与启动文件中的定义一致。STM32H7的栈顶地址通常被放置在RAM区域的最高地址,如果链接脚本调整了RAM区域大小,必须同步更新启动文件中的栈顶初始化值,否则上电后栈指针指向非法地址,导致硬件错误(HardFault)。

案例复盘:在某振动传感器数据采集项目中,工程师发现当启用DMA双缓冲模式后,采集到的数据每隔一段时间会出现一段全零数据。通过查看MAP文件,发现两个DMA缓冲区被链接器分配到了AXI SRAM的末尾,其中一个缓冲区跨越了0x2407FFFF与0x24080000的边界(实际上AXI SRAM只有512KB,0x24080000已超出范围)。进一步检查发现,链接脚本中AXI SRAM的LENGTH被误写为1024K,导致链接器认为有1MB空间可用。修正LENGTH为512K,并在.dma_buf段上添加. = ALIGN(1024);确保缓冲区不跨1KB边界后,问题彻底解决。这个案例说明,链接脚本中的每一个数字都必须与芯片手册严格对应,任何夸大内存容量的行为都会引发难以排查的随机故障。

通过本章的学习,我们掌握了链接脚本中MEMORY和SECTIONS命令的核心用法,理解了LMA与VMA的重定位机制,并学会了如何为传感器数据分类任务自定义内存分区。在实际工程中,建议每次修改链接脚本后都生成MAP文件,并仔细检查各段的起始地址、结束地址和长度是否符合预期,这是确保系统稳定运行的最后一道防线。