固件开发 - 启动方式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的“总设计师”,它决定了编译后的代码和数据的最终归宿。关键定义如下:
定义内存区域(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表示可读、写、执行。分配段(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
24SECTIONS
{
/* 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.s或crt0.S)是芯片上电后执行的第一段程序,负责在进入main()函数前完成关键的“数据搬运”工作。
- 设置栈指针(Stack Pointer, sp):初始化栈指针,为后续的函数调用做好准备。
- 搬运
.data段:将从_sidata(Flash)开始的数据,复制到_sdata(RAM)开始的区域,直到_edata。 - 清零
.bss段:将从_sbss到_ebss的RAM区域全部清零。 - 跳转到
main():完成上述初始化后,跳转到主函数main(),开始执行用户的应用程序。
以下是搬运.data段的汇编逻辑示意:
1 | /* 伪代码,实际实现取决于具体汇编器 */ |
⚙️ 第三步:配置编译选项
在编译时需要指定正确的架构、链接脚本和启动文件。例如,使用RISC-V GNU工具链时:
1 | # 1. 编译:使用启动代码startup.s,编译成目标文件 |
🧪 实践案例:基于GD32VF103系列MCU
以广泛使用的GD32VF103系列RISC-V MCU为例,使用Nuclei SDK可以快速上手XIP。
- 开发板与模式:Nuclei SDK为GD32VF103系列开发板(如RV-STAR、Longan Nano)预设了
flashxip下载模式。 - 项目构建:通过指定
SOC和BOARD变量即可编译、烧录和调试。1
2make 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架构的魅力所在。