固件开发 - 启动方式XIP和RISCV实例

固件开发 - 启动方式XIP和RISCV实例

1. XIP概要

1. 什么是 XIP

XIP(Execute-In-Place,芯片内执行) 是指 CPU 无需将代码加载到内部 RAM,而是直接在外部或片上的 NOR Flash 存储介质中读取并执行程序代码的技术。在“大 Flash(如 128KB)、小 RAM(如 4KB)”的嵌入式系统中,这是保证程序正常运行的核心基石。


2. 硬件实现基础

要实现 CPU 随机访问 SPI Flash 并直接执行代码,需要满足以下硬件与机制要求:

  • 存储介质必须为 NOR Flash:NOR Flash 支持按字节随机寻址,CPU 可通过地址总线直接访问任意指令地址;而 NAND Flash 以块(Block)为单位读写,无法支持 XIP。
  • 内存映射(Memory Mapping):MCU 内部集成了 XIP 控制器,将 SPI Flash 的一段物理地址空间映射到系统的统一编址内存空间中。当 CPU 访问该段地址时,硬件自动将其转化为 SPI 总线上的读取时序。
  • XIP 读取模式:普通 SPI Flash 读取需发送“指令 + 地址”,而 XIP 模式下,控制器在初始化后保持“读取态”,CPU 仅需提供目标地址,Flash 即可直接返回数据,显著降低随机访问的指令开销。

3. Cache(高速缓存)的关键作用

由于 SPI Flash 的读取速率(通常几十至上百 MHz)远低于 CPU 内核运行速率(数百 MHz),Cache 是不可或缺的加速桥梁

  • 指令预取与缓冲:Cache 自动缓存 CPU 最近访问过的 Flash 地址附近的指令代码。当 CPU 顺序执行或循环执行时,绝大多数指令可直接从极高速的 Cache 中获取,避免 CPU 频繁进入“等待(Wait-State)”状态。
  • 性能平衡:Cache 的存在使 XIP 的平均执行速度大幅提升,但在面对长跳转或大规模随机分支时,Cache 命中率下降,执行速度会暂时变慢。

4. 大 Flash + 小 RAM 下的代码与数据分工

在 RAM 极度紧张(如仅 4KB)的场景下,代码与数据必须严格分层:

存储介质 核心职责 具体内容
Flash(128KB) 存储与执行代码 绝大部分固件代码(.text 段)、只读常量(.rodata 段)均存放于此。CPU 通过 XIP 机制直接在 Flash 中取指执行。
RAM(4KB) 存储动态数据 存放可读写的全局变量/静态变量(.data/.bss 段)、堆(Heap)栈(Stack)。栈空间用于函数调用、局部变量及中断上下文保存,是程序运行的必要“工作台”。

系统启动流程:芯片上电复位后,程序计数器(PC)硬件自动指向 Flash 映射地址中的复位向量,CPU 随即开始从 Flash 取指执行,完成堆栈指针初始化、变量搬移(如 .data 从 Flash 复制到 RAM)等操作后,进入主循环。


5. RAM 的“加速”与“特殊任务”

尽管代码主要在 Flash 运行,但仅有的 4KB RAM 还需承担以下关键加速任务:

  • 运行关键中断服务程序(ISR):对于响应时间要求极苛刻的中断,可将 ISR 代码通过链接脚本放置于 RAM 中运行,避免 Cache Miss 带来的延迟。
  • 运行 Flash 擦写函数(IAP/OTA):当需要执行 Flash 扇区擦除或编程操作时,必须将这部分函数复制到 RAM 中执行。因为 Flash 在擦写期间无法响应读取请求,若 CPU 仍在 Flash 取指,将导致总线挂起(Hang)或指令异常。
  • 临时数据缓冲:作为通信外设(如 UART、SPI)DMA 传输的临时缓冲区。

6. 保障系统稳定的关键技术

为了在 XIP 模式下保证程序的正确性和可靠性,编译与链接阶段需引入特殊技术:

  • 位置无关代码(PIC,Position-Independent Code):生成与绝对地址无关的指令码。这使得代码在 Flash 中即使因为固件升级(IAP)而发生地址偏移,程序依然能够正常跳转和运行。
  • 覆盖(Overlay)技术:将功能互斥(不同时运行)的大型函数段分配到同一块 RAM 地址空间。需要时,从 Flash 将对应函数动态加载到该 RAM 区域执行,有效复用紧缺的 RAM 资源。
  • 中断向量表重映射:将中断向量表放置在 Flash 起始地址或通过重映射寄存器指向 RAM,确保中断响应路径正确。

7. 重要注意事项

  • 并发操作限制(读-写-擦冲突):在 XIP 模式下,若 CPU 一边从 Flash 取指执行,一边通过 SPI 接口对该 Flash 发起擦写操作,会引发总线冲突。高级 MCU 支持暂停(Suspend)/恢复(Resume) 机制,或通过硬件仲裁临时挂起 CPU 取指请求。
  • 只读属性:映射到内存空间的 XIP 地址区域通常为只读属性。若程序指针意外尝试写入该区域,将触发硬件总线错误(HardFault)异常。
  • 性能权衡:XIP 适合顺序执行和循环密集的代码,而涉及大量随机跳转或频繁访问全局数据的算法,放在 RAM 中执行效率更高,需根据实际 CPU 主频和 Flash 等待周期进行测算。

8. 核心要点总结

Flash 负责“存”和“执行”(XIP),RAM 负责“存”和“处理”(数据与关键加速)。

通过 NOR Flash 的随机寻址特性 + MCU 的 XIP 内存映射控制器 + 高速 Cache 的缓冲加速,使得在极小 RAM(如 4KB)的 MCU 上运行百 KB 级别的固件成为可能。开发人员需通过合理的链接脚本(Linker Script)划分代码与数据段,并针对中断、IAP 函数进行特殊的 RAM 搬移处理,以充分发挥 XIP 技术的优势。

RISC-V环境中实践XIP

在RISC-V环境中实践XIP(就地执行),核心在于精确控制代码和数据的存放位置,并在启动时完成必要的数据搬运。这主要通过链接脚本(Linker Script)启动代码(Startup Code) 的配合来实现。

以下是基于裸机或RTOS环境下的具体实践步骤和关键点。

🎯 第一步:编写链接脚本(Linker Script)—— 定义内存布局

链接脚本是XIP的“总设计师”,它决定了编译后的代码和数据的最终归宿。关键定义如下:

  1. 定义内存区域(MEMORY):明确划分Flash(ROM)和RAM的起始地址与大小。

    1
    2
    3
    4
    5
    6
    7
    8
    /* 在链接脚本中定义内存区域 */
    MEMORY
    {
    /* Flash (ROM) 用于存放代码和只读数据,XIP在此执行 */
    rom (rx) : ORIGIN = 0x08000000, LENGTH = 128K /* 1. 定义只读、可执行的Flash区域 */
    /* RAM 用于存放可读写数据 */
    ram (rwx) : ORIGIN = 0x20000000, LENGTH = 4K /* 2. 定义可读、写、执行的RAM区域 */
    }

    这是一个典型的定义。rx表示只读、可执行,rwx表示可读、写、执行。

  2. 分配段(SECTIONS):精确控制各段在内存中的位置。

    • .text.rodata:包含代码和只读数据,它们的虚拟地址(VMA)加载地址(LMA) 都指向Flash (rom)区域,这是实现XIP的关键。
    • .data:包含已初始化的全局变量。它的VMA在RAM,但LMA在Flash。这意味着程序的初始值存放在Flash中,但运行时需要在RAM中操作。
    • .bss:包含未初始化的全局变量。它的VMA在RAM,LMA紧随.data段之后,没有实际的初始化数据。
    • 堆(Heap)和栈(Stack):在RAM中分配空间。

    一个简单的段分配示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    SECTIONS
    {
    /* 1. .text, .rodata 等只读段,VMA和LMA都在ROM */
    .text : {
    KEEP(*(.vectors)) /* 确保中断向量表不被优化掉 */
    *(.text*)
    *(.rodata*)
    } > rom

    /* 2. .data 段:VMA在RAM,LMA在ROM,启动时需复制 */
    _sidata = LOADADDR(.data); /* 3. 记录.data段在Flash中的加载地址 */
    .data : AT ( _sidata ) {
    _sdata = .; /* 4. 标记.data段在RAM中的起始地址 */
    *(.data*)
    _edata = .; /* 5. 标记.data段在RAM中的结束地址 */
    } > ram

    /* 6. .bss 段:VMA在RAM,无需加载 */
    .bss : {
    _sbss = .; /* 7. 标记.bss段在RAM中的起始地址 */
    *(.bss*)
    _ebss = .; /* 8. 标记.bss段在RAM中的结束地址 */
    } > ram
    }

    注意,_sidata_sdata等符号,是供后续启动代码使用的。

🚀 第二步:编写启动代码(Startup Code)—— 准备执行环境

启动代码(通常是startup.scrt0.S)是芯片上电后执行的第一段程序,负责在进入main()函数前完成关键的“数据搬运”工作。

  1. 设置栈指针(Stack Pointer, sp):初始化栈指针,为后续的函数调用做好准备。
  2. 搬运.data:将从_sidata(Flash)开始的数据,复制到_sdata(RAM)开始的区域,直到_edata
  3. 清零.bss:将从_sbss_ebss的RAM区域全部清零。
  4. 跳转到main():完成上述初始化后,跳转到主函数main(),开始执行用户的应用程序。

以下是搬运.data段的汇编逻辑示意:

1
2
3
4
5
6
7
8
9
10
11
12
    /* 伪代码,实际实现取决于具体汇编器 */
la a0, _sdata /* 1. 加载RAM起始地址 */
la a1, _edata /* 2. 加载RAM结束地址 */
la a2, _sidata /* 3. 加载Flash中的源地址 */
copy_data_loop:
beq a0, a1, copy_data_done
lw t0, 0(a2) /* 从Flash加载 */
sw t0, 0(a0) /* 存储到RAM */
addi a0, a0, 4
addi a2, a2, 4
j copy_data_loop
copy_data_done:

⚙️ 第三步:配置编译选项

在编译时需要指定正确的架构、链接脚本和启动文件。例如,使用RISC-V GNU工具链时:

1
2
3
4
5
6
# 1. 编译:使用启动代码startup.s,编译成目标文件
riscv32-unknown-elf-gcc -march=rv32i -mabi=ilp32 -c startup.s -o startup.o
# 2. 链接:指定链接脚本 gcc_flashxip.ld,生成ELF文件
riscv32-unknown-elf-gcc -T gcc_flashxip.ld *.o -o result.elf
# 3. 生成映射文件,用于调试
riscv32-unknown-elf-objdump -h result.elf > project.map

🧪 实践案例:基于GD32VF103系列MCU

以广泛使用的GD32VF103系列RISC-V MCU为例,使用Nuclei SDK可以快速上手XIP。

  • 开发板与模式:Nuclei SDK为GD32VF103系列开发板(如RV-STAR、Longan Nano)预设了flashxip下载模式。
  • 项目构建:通过指定SOCBOARD变量即可编译、烧录和调试。
    1
    2
    make SOC=gd32vf103 BOARD=gd32vf103v_rvstar all   # 编译
    make SOC=gd32vf103 BOARD=gd32vf103v_rvstar upload # 烧录
  • 链接脚本位置:对应的链接脚本(如gcc_gd32vf103_flashxip.ld)位于SDK的SoC/gd32vf103/Board/.../Source/GCC/目录下,可以直接参考或修改。

⚠️ 关键注意事项

  • KEEP()指令:在链接脚本中,务必使用KEEP()指令包裹中断向量表等关键段,防止链接器在优化时将其误删。
  • 数据一致性:确保启动代码中使用的_sidata_sdata等符号与链接脚本中定义的完全一致。
  • Flash擦写:如果需要IAP(在线升级),擦写Flash的代码必须在RAM中运行。因为Flash在擦写时无法响应取指请求,若CPU仍在Flash取指会导致崩溃。

总而言之,RISC-V环境下的XIP实现,就是通过链接脚本精确规划内存,再通过启动代码将“执行环境”准备就绪的过程。掌握这两个核心,就能顺利在RISC-V平台上实践XIP技术。

HBird-e200的启动方式(最佳实践)

背景:hbird-e200 RISCV有ITCM空间,有基于此RISCV核的芯片产品,bootloader启动时将SPI Flash的code拷贝到此ITCM再执行,并不是XIP方式。

以蜂鸟E203(hbird-e200)为代表的这类RISC-V芯片,确实提供了一种“非XIP”的启动方式:上电后,先将程序从Flash搬运到ITCM(指令紧耦合内存,Instruction Tightly Coupled Memory)中,再执行

这并非对XIP的否定,而是在极致的性能、功耗与设计简洁性之间做出的另一种权衡。蜂鸟E200系列本身就支持包括XIP在内的多种启动模式。

⚙️ 蜂鸟E203的三种启动模式

根据其SDK设计,蜂鸟E203支持三种不同的程序运行方式:

模式 描述 特点
flashxip (XIP模式) 程序直接在SPI Flash中执行 非易失:代码掉电不丢失。
执行较慢:需从外部Flash取指。
itcm (ILM模式) 程序直接下载到ITCM中执行 执行极快:直接从高速ITCM取指。
易失:掉电程序丢失,适合调试。
flash (拷贝执行模式) 程序烧录在Flash中,上电后由启动代码搬运至ITCM再执行 兼具两者优点:代码固化在Flash,但获得ITCM的高速执行性能。

你提到的正是第三种 flash(拷贝执行)模式

🚀 “拷贝到ITCM执行”方案的优势与代价

这种设计是为了在特定场景下追求最优解。

核心优势:极致的性能与功耗

  • 性能飞跃:ITCM通过专用的、高带宽的紧耦合总线与CPU核心直连,其访问速度和内部SRAM相当,远快于通过SPI外设读取外部Flash。程序在ITCM中运行,没有XIP模式下SPI总线和Flash访问的延迟,能达到CPU的满速运行。
  • 功耗优化:程序在ITCM中运行期间,可以将外部SPI Flash置于低功耗或掉电状态,这对于电池供电的IoT设备至关重要。
  • 设计简化:这种模式规避了XIP模式下复杂的Cache一致性、Flash擦写时总线阻塞等问题,简化了芯片设计和软件开发的复杂度。

代价:需要额外的“搬运”开销

  • 启动时间:每次上电都需要执行一段“搬运”代码(通常是BootROM),将程序从Flash复制到ITCM,这会增加启动时间。
  • ITCM容量限制:程序大小受限于ITCM的物理容量。如果程序超过ITCM大小,则无法使用此模式。

🤔 为什么此类芯片会采用这种模式?

你看到的这种“拷贝执行”方案,常见于以下场景:

  • 追求极致性能的MCU:对于需要进行大量计算或实时控制的场景(如电机控制、音频处理),程序在ITCM中运行能提供确定性的、极致的高速响应。
  • 对功耗极端敏感的IoT设备:通过让程序在ITCM运行,可以长时间关闭外部Flash,从而降低系统功耗。
  • FPGA原型验证或仿真阶段:在开发初期,为了快速验证和调试,开发者常将程序直接加载到ITCM中运行。芯来科技(Nuclei)的MCU开发板上,也支持这种直接从ITCM启动的调试方式。
  • 作为XIP的补充:许多芯片会同时提供XIP和ITCM执行两种模式。开发者可以根据应用需求,在“追求极致性能”和“追求大代码容量”之间灵活选择。例如,可以将核心算法放在ITCM中运行,而将非关键的业务逻辑放在Flash中XIP执行。

💎 总结

你观察到的“拷贝到ITCM执行”方案,是蜂鸟E203等RISC-V芯片为了追求极致性能而提供的一种与XIP并行的可选路径

它并非要取代XIP,而是通过**“用启动时间的微小增加,换取运行时的极致性能与功耗优化”**的策略,为开发者提供了另一种强有力的工具。这种灵活性,正是RISC-V架构的魅力所在。