链接脚本指南
本文档介绍 ARCS SDK 的链接脚本架构,包括内存布局、段管理、注入机制和代码重定位。
概述
嵌入式系统中,链接脚本决定了代码和数据在物理内存中的布局。ARCS SDK 采用 通用模板 + 片段注入 的架构,使组件和 SoC 可以声明式地注册自己的内存区域和段定义,而无需直接修改链接脚本。
CMake 注册阶段 预处理阶段 链接阶段
┌─────────────────┐ ┌──────────────┐ ┌────────────┐
│ soc/CMakeLists │ │ system.ld │ │ linker.ld │
│ boards/xxx │ ──注册──►│ + snippets-* │─gcc -E─►│ (最终) │
│ components/xxx │ │ + autoconf.h │ └────────────┘
└─────────────────┘ └──────────────┘
链接脚本经历三个阶段生成最终的 linker.ld:
CMake 注册:各组件通过 CMake API 注册 linker 片段
片段收集:CMake 生成
snippets-*.ld文件,包含排序后的#include指令预处理:GCC 预处理器展开所有
#include和#if CONFIG_*条件,输出最终链接脚本
内存布局
通用模板定义了以下核心内存区域,地址和大小通过 Kconfig 参数化:
区域 |
别名 |
用途 |
条件 |
|---|---|---|---|
|
|
代码和只读数据的存储区(XIP 执行或加载源) |
始终存在 |
|
|
主运行时内存,存放 |
始终存在 |
|
— |
外部伪静态 RAM,存放大体积数据和可执行代码 |
|
|
|
指令紧耦合内存,用于低延迟代码执行 |
|
|
|
数据紧耦合内存,用于低延迟数据访问 |
|
上述核心区域之外,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{} 之后,REGION_ALIAS 声明 |
|
SECTIONS{} 顶部,最低地址段 |
|
核心 PSRAM 段之后,.fast 段之前 |
|
.fast 段之后,.psram.text/.text 之前 |
|
.text 之后 |
|
.rodata 之后,.data 之前 |
|
.data 之后 |
|
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 |
来源 |
说明 |
|---|---|---|
|
SoC 层 |
|
|
Board 层 |
|
|
SDK 组件 |
|
|
应用层 |
应用项目注册的片段 |
|
默认 |
未指定 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_TEXT + PSRAM_RODATA + PSRAM_DATA + PSRAM_BSS |
|
聚合:展开为 SRAM_TEXT + SRAM_RODATA + SRAM_DATA + SRAM_BSS |
|
聚合:展开为 DTCM_DATA + DTCM_BSS |
|
PSRAM 代码段 |
|
SRAM 代码段 |
|
指令紧耦合内存 |
|
各内存区域的只读数据 |
|
各内存区域的已初始化数据 |
|
各内存区域的零初始化数据 |
在 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 接入时,链接脚本层面通常只需:
在 Kconfig 中配置内存参数:在
soc/Kconfig中定义CONFIG_MEM_*系列配置项创建 linker 片段目录:在
soc/<new_chip>/linker/下放置该 SoC 专属的内存和段定义在 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_SIZE 和 CONFIG_MEM_ILM_SIZE 是否大于 0。
自定义段未出现在最终固件中
确认:(1) 通过 CMake API 注册了片段到正确的 SLOT;(2) 片段文件路径正确;(3) 段中使用了 KEEP() 防止被链接器垃圾回收。