链接脚本指南

本文档介绍 ARCS SDK 的链接脚本架构,包括内存布局、段管理、注入机制和代码重定位。

概述

嵌入式系统中,链接脚本决定了代码和数据在物理内存中的布局。ARCS SDK 采用 通用模板 + 片段注入 的架构,使组件和 SoC 可以声明式地注册自己的内存区域和段定义,而无需直接修改链接脚本。

CMake 注册阶段                    预处理阶段                链接阶段
┌─────────────────┐         ┌──────────────┐        ┌────────────┐
│ soc/CMakeLists   │         │ system.ld    │        │ linker.ld  │
│ boards/xxx       │ ──注册──►│ + snippets-* │─gcc -E─►│ (最终)     │
│ components/xxx   │         │ + autoconf.h │        └────────────┘
└─────────────────┘         └──────────────┘

链接脚本经历三个阶段生成最终的 linker.ld

  1. CMake 注册:各组件通过 CMake API 注册 linker 片段

  2. 片段收集:CMake 生成 snippets-*.ld 文件,包含排序后的 #include 指令

  3. 预处理:GCC 预处理器展开所有 #include#if CONFIG_* 条件,输出最终链接脚本

内存布局

通用模板定义了以下核心内存区域,地址和大小通过 Kconfig 参数化:

区域

别名

用途

条件

FLASH

ROM

代码和只读数据的存储区(XIP 执行或加载源)

始终存在

SRAM

RAM

主运行时内存,存放 .data.bss、堆栈

始终存在

PSRAM

外部伪静态 RAM,存放大体积数据和可执行代码

CONFIG_MEM_PSRAM_SIZE > 0

ILM

ITCM

指令紧耦合内存,用于低延迟代码执行

CONFIG_MEM_ILM_SIZE > 0

DLM

DTCM

数据紧耦合内存,用于低延迟数据访问

CONFIG_MEM_ILM_SIZE > 0

上述核心区域之外,SoC 和组件可通过注入机制注册额外的内存区域(如 WiFi RAM、BLE RAM、LUNA 共享内存等)。

内存区域的基地址和大小由 SoC 在 soc/Kconfig 中定义,反映硬件物理布局,应用层一般不需要修改。应用可调整的是栈和堆大小:

# prj.conf — 应用层可调整的内存参数
CONFIG_MEM_INTERRUPT_STACK_SIZE=0x2000
CONFIG_MEM_HEAP_SIZE=1024

段布局与注入槽

通用模板 soc/common/system.ld 定义了标准 ELF 段和 9 个注入槽(slot),组件通过 CMake API 向这些槽注入自定义段:

SECTIONS {
    ┌─ SECTIONS_START ────── 最低地址组件段(如 WiFi NOLOAD)
    │
    ├─ .init              ── 向量表 + 启动代码
    ├─ .scatab            ── 散加载/零填充表
    │
    ├─ COMPONENTS ─────── 组件专属段组(LUNA、WiFi LA dump 等)
    │
    ├─ .fast.text         ── SRAM 中执行的快速代码
    ├─ .itcm / .dtcm      ── 紧耦合内存段(条件编译)
    ├─ .fast.rodata/data/bss ─ SRAM 快速数据
    │
    ├─ ROM ───────────── 组件 ROM 段(设备注册表、SYS_INIT、Shell)
    │
    ├─ .psram.text        ── PSRAM 代码段(CONFIG_MEM_PSRAM_SIZE > 0 时)
    ├─ .text              ── 主代码段
    │
    ├─ POST_TEXT ─────── .text 之后的段(C++ runtime)
    │
    ├─ .psram.rodata      ── PSRAM 只读数据(CONFIG_MEM_PSRAM_SIZE > 0 时)
    ├─ .rodata            ── 只读数据
    │
    ├─ RAM ───────────── 组件 RAM 加载段(MAPI、Trace)
    │
    ├─ .data              ── 已初始化数据
    │
    ├─ POST_DATA ─────── .data 之后的段(IPC 共享内存)
    │
    ├─ .bss               ── 零初始化数据
    ├─ .noinit            ── 不初始化数据
    ├─ .psram.nocache_heap ─ PSRAM 非缓存堆(CONFIG_MEM_PSRAM_SIZE > 0 时)
    ├─ .heapstack         ── 堆和栈
    │
    └─ SECTIONS_END ────── 末尾组件段(BLE heap)
}

每个注入槽对应一个生成的文件 build/generated/snippets-<slot>.ld,包含按 SORT_KEY 排序的 #include 指令。

CMake 注入 API

注册内存区域

# 注册一个 MEMORY 区域片段
listenai_add_linker_memory(
    FILE ${CMAKE_CURRENT_SOURCE_DIR}/my-memory.ld
    SORT_KEY "10-soc"      # 可选,默认 "default"
)

片段文件示例:

/* my-memory.ld */
#if CONFIG_MY_FEATURE
    MY_RAM(rwx) : ORIGIN = 0x200C0000, LENGTH = 0x8000
#endif

注册段定义

# 注册一个段定义到指定 SLOT
listenai_add_linker_section(
    FILE ${CMAKE_CURRENT_SOURCE_DIR}/my-sections.ld
    SLOT COMPONENTS        # 必需:目标注入槽
    SORT_KEY "10-soc"      # 可选
)

可用的 SLOT 值:

SLOT

位置说明

MEMORY

MEMORY{} 块内,用于自定义内存区域

ALIASES

MEMORY{} 之后,REGION_ALIAS 声明

SECTIONS_START

SECTIONS{} 顶部,最低地址段

COMPONENTS

核心 PSRAM 段之后,.fast 段之前

ROM

.fast 段之后,.psram.text/.text 之前

POST_TEXT

.text 之后

RAM

.rodata 之后,.data 之前

POST_DATA

.data 之后

SECTIONS_END

SECTIONS{} 末尾

注册散加载表条目

# 注册需要从 ROM 复制到 RAM 的段
listenai_add_linker_scatter(SCATLOAD .my_section)

# 注册需要零填充的段
listenai_add_linker_scatter(SCATZERO .my_bss_section)

散加载表(scatter table)由启动代码在 main() 之前遍历,将段内容从 ROM 加载地址复制到 RAM 运行地址,或将 BSS 段清零。

SORT_KEY 约定

多个来源注册的片段通过 SORT_KEY 字典序排列,确保可预测的顺序:

SORT_KEY

来源

说明

10-soc

SoC 层

soc/${CHIP}/linker/ 下的片段

20-board

Board 层

boards/${BOARD}/linker/ 下的片段

30-component

SDK 组件

components/ 下的片段

50-app

应用层

应用项目注册的片段

default

默认

未指定 SORT_KEY 时的值

代码重定位

SDK 提供 listenai_code_relocate() API,将指定代码或数据从默认位置重定位到目标内存区域。典型场景:

  • 将中断处理函数放入 SRAM 以避免 Flash 等待周期

  • 将 GUI 库(如 LVGL)放入 PSRAM 以节省 Flash 空间

  • 将关键代码放入 ITCM 以获得最低延迟

按库重定位

# 将整个 LVGL 库重定位到 PSRAM
listenai_code_relocate(LIBRARY modules_lvgl LOCATION PSRAM)

按文件重定位

# 将 spiflash.c 的只读数据重定位到 SRAM
listenai_code_relocate(FILES spiflash.c LOCATION SRAM_RODATA)

按段名重定位

# 将中断处理代码重定位到 SRAM
listenai_code_relocate(
    SECTIONS .text.irq .text.xPortTaskSwitch
    LOCATION SRAM_TEXT
)

# 将电源管理代码放入 ITCM
listenai_code_relocate(SECTIONS .text.pm LOCATION ITCM)

可用的 LOCATION 值:

LOCATION

说明

PSRAM

聚合:展开为 PSRAM_TEXT + PSRAM_RODATA + PSRAM_DATA + PSRAM_BSS

SRAM

聚合:展开为 SRAM_TEXT + SRAM_RODATA + SRAM_DATA + SRAM_BSS

DTCM

聚合:展开为 DTCM_DATA + DTCM_BSS

PSRAM_TEXT

PSRAM 代码段

SRAM_TEXT

SRAM 代码段

ITCM

指令紧耦合内存

*_RODATA

各内存区域的只读数据

*_DATA

各内存区域的已初始化数据

*_BSS

各内存区域的零初始化数据

在 C 代码中使用段属性

也可以直接在 C 代码中使用 __attribute__((section())) 将函数或变量放入指定段:

#include <arcs_ap.h>

/* 将函数放入 SRAM 快速执行 */
_FAST_TEXT void my_irq_handler(void) {
    // 中断处理代码
}

/* 将数据放入 PSRAM */
__attribute__((section(".psram.data"))) uint8_t large_buffer[65536];

/* 将数据标记为不初始化(保留复位前的值) */
__attribute__((section(".noinit"))) uint32_t persistent_counter;

分层架构

链接脚本采用四层架构,每层可以注入片段但不需修改上层模板:

Layer 4: Application ── 应用层覆盖(可选)
         ↓ 注入片段
Layer 3: Board       ── 板级配置(boards/${BOARD}/linker/)
         ↓ 注入片段
Layer 2: SoC         ── SoC 专属段/内存(soc/${CHIP}/linker/)
         ↓ 注入片段
Layer 1: Template    ── SoC 通用模板(soc/common/system.ld)

Layer 1 — SoC 通用模板 soc/common/system.ld

定义标准 ELF 段骨架、MEMORY 框架、散加载表和所有注入槽。通过 Kconfig 参数化和条件编译,适配不同内存拓扑的 SoC。

Layer 2 — SoC 层 soc/${CHIP}/linker/

注册 SoC 专属的内存区域和段定义。当前 ARCS SoC 在此注册了 WiFi RAM、BLE RAM、IPC 共享内存、LUNA 共享内存、设备注册表、SYS_INIT 等段。

Layer 3 — Board 层 boards/${BOARD}/linker/ (可选)

板型可在此目录放置 *-memory.ld*-aliases.ld*-sections.ld 文件,自动以 SORT_KEY "20-board" 注册。适用于板级内存差异(如不同大小的外挂 PSRAM)。

Layer 4 — 应用层

应用的 CMakeLists.txt 可直接调用 listenai_add_linker_section() 等 API 注册片段,或使用 listenai_append_linker_script() 追加完整脚本。

SoC 链接脚本适配

新 SoC 接入时,链接脚本层面通常只需:

  1. 在 Kconfig 中配置内存参数:在 soc/Kconfig 中定义 CONFIG_MEM_* 系列配置项

  2. 创建 linker 片段目录:在 soc/<new_chip>/linker/ 下放置该 SoC 专属的内存和段定义

  3. 在 CMakeLists.txt 中注册片段:使用 listenai_add_linker_*() API 注册

通用模板通过条件编译自动适配——没有 PSRAM 的 SoC 不会生成 PSRAM 段,没有 TCM 的 SoC 不会生成 ITCM/DTCM 段。

只有在内存拓扑与通用模板差异极大时(如完全不同的段布局),才需要在 soc/<chip>/startup/system.ld 放置专属链接脚本覆盖通用模板。

调试与诊断

查看最终链接脚本

编译后可查看预处理展开的最终链接脚本:

# 查看最终链接脚本(所有条件编译和 #include 已展开)
cat build/linker.ld

查看注入片段

# 查看各 slot 收集到的片段
ls build/generated/snippets-*.ld

# 查看特定 slot 的内容(例如 COMPONENTS)
cat build/generated/snippets-components.ld

分析内存占用

# 查看段大小汇总
riscv-nuclei-elf-size build/<project>.elf

# 查看详细的段和符号分布
cat build/<project>.map | head -200

常见问题

链接错误:region ‘XXX’ overflowed

内存区域空间不足。通过 menuconfig 调大对应区域,或将大体积代码/数据重定位到 PSRAM。

未定义符号:__psram_xxx / __itcm_xxx

代码引用了 PSRAM 或 ITCM 符号,但对应内存区域未启用。检查 Kconfig 中 CONFIG_MEM_PSRAM_SIZECONFIG_MEM_ILM_SIZE 是否大于 0。

自定义段未出现在最终固件中

确认:(1) 通过 CMake API 注册了片段到正确的 SLOT;(2) 片段文件路径正确;(3) 段中使用了 KEEP() 防止被链接器垃圾回收。