.. _linker_script: ================ 链接脚本指南 ================ 本文档介绍 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 参数化: .. list-table:: :widths: 15 15 50 20 :header-rows: 1 * - 区域 - 别名 - 用途 - 条件 * - ``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`` 中定义,反映硬件物理布局,应用层一般不需要修改。应用可调整的是栈和堆大小: .. code-block:: 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-.ld``,包含按 SORT_KEY 排序的 ``#include`` 指令。 CMake 注入 API =============== 注册内存区域 ------------- .. code-block:: cmake # 注册一个 MEMORY 区域片段 listenai_add_linker_memory( FILE ${CMAKE_CURRENT_SOURCE_DIR}/my-memory.ld SORT_KEY "10-soc" # 可选,默认 "default" ) 片段文件示例: .. code-block:: c /* my-memory.ld */ #if CONFIG_MY_FEATURE MY_RAM(rwx) : ORIGIN = 0x200C0000, LENGTH = 0x8000 #endif 注册段定义 ----------- .. code-block:: cmake # 注册一个段定义到指定 SLOT listenai_add_linker_section( FILE ${CMAKE_CURRENT_SOURCE_DIR}/my-sections.ld SLOT COMPONENTS # 必需:目标注入槽 SORT_KEY "10-soc" # 可选 ) 可用的 SLOT 值: .. list-table:: :widths: 25 75 :header-rows: 1 * - 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{} 末尾 注册散加载表条目 ----------------- .. code-block:: cmake # 注册需要从 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 字典序排列,确保可预测的顺序: .. list-table:: :widths: 25 25 50 :header-rows: 1 * - 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 以获得最低延迟 按库重定位 ----------- .. code-block:: cmake # 将整个 LVGL 库重定位到 PSRAM listenai_code_relocate(LIBRARY modules_lvgl LOCATION PSRAM) 按文件重定位 ------------- .. code-block:: cmake # 将 spiflash.c 的只读数据重定位到 SRAM listenai_code_relocate(FILES spiflash.c LOCATION SRAM_RODATA) 按段名重定位 ------------- .. code-block:: cmake # 将中断处理代码重定位到 SRAM listenai_code_relocate( SECTIONS .text.irq .text.xPortTaskSwitch LOCATION SRAM_TEXT ) # 将电源管理代码放入 ITCM listenai_code_relocate(SECTIONS .text.pm LOCATION ITCM) 可用的 LOCATION 值: .. list-table:: :widths: 25 75 :header-rows: 1 * - 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()))`` 将函数或变量放入指定段: .. code-block:: c #include /* 将函数放入 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//linker/`` 下放置该 SoC 专属的内存和段定义 3. **在 CMakeLists.txt 中注册片段**:使用 ``listenai_add_linker_*()`` API 注册 通用模板通过条件编译自动适配——没有 PSRAM 的 SoC 不会生成 PSRAM 段,没有 TCM 的 SoC 不会生成 ITCM/DTCM 段。 只有在内存拓扑与通用模板差异极大时(如完全不同的段布局),才需要在 ``soc//startup/system.ld`` 放置专属链接脚本覆盖通用模板。 调试与诊断 ========== 查看最终链接脚本 ----------------- 编译后可查看预处理展开的最终链接脚本: .. code-block:: bash # 查看最终链接脚本(所有条件编译和 #include 已展开) cat build/linker.ld 查看注入片段 ------------- .. code-block:: bash # 查看各 slot 收集到的片段 ls build/generated/snippets-*.ld # 查看特定 slot 的内容(例如 COMPONENTS) cat build/generated/snippets-components.ld 分析内存占用 ------------- .. code-block:: bash # 查看段大小汇总 riscv-nuclei-elf-size build/.elf # 查看详细的段和符号分布 cat build/.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()`` 防止被链接器垃圾回收。