本文首先简单介绍XIP是什么,STM32H7S78-DK开发板主控MCU的片上存储器资源,以及开发板上的外部存储器信息;接着,介绍XIP项目如何使用,最后对XIP项目的源码进行分析。从源码层面解释了XIP项目是如何实现将程序下载到STM32H7S78-DK开发板的外部NOR Flash上的,以及从外部NOR Flash上的程序时如何运行起来的。
一、背景简介
1.1 XIP简介
XIP是,Execute in place 的英文简写,字面意思为”原地执行“,它的实际含义通常是指——允许代码直接在外部存储器中执行,而无需将代码复制到内部存储器中。例如,本文介绍的STM32H7S78-DK的主控制器就是支持直接从片外的Flash或者PSRAM执行代码的,而无需将外部代码拷贝到片上SRAM中执行。
1.2 STM32H7S7L8的存储器资源
STM32H7S78-DK开发板的主控制器为STM32H7S7L8H6H,其片上存储器资源包括:
它的片上Flash仅有64 KB,比片上SRAM还要小。对于稍微复杂的项目,64KB无法存储整个项目的二进制代码,需要借助它的外部存储器接口和XIP能力,实现直接执行外部存储器上的代码。
STM32H7R/S系列外部存储器接口和控制器包括:
- 2个SD/SDIO/MMC接口;
- 2个 Octo-SPI 存储器接口,或者1个Octo-SPI + 1个 Hexa-SPI接口,支持XIP;
- 灵活存储器控制器(FMC),可以支持到32比特位宽,支持SRAM, PSRAM, FRAM, SDR/LPSDR SDRAM, NOR/NAND存储器;
1.3 STM32H7S78-DK的外部存储器
STM32H7S78-DK开发板上搭载了两个外部存储器,分别是:
- 256 Mbit Octo-SPI PSRAM
- 1 Gbit Octo-SPI NOR Flash
两个外部存储器的具体型号为:
- 256 Mbit PSRAM: APS256XXN-OBR-BG
- 1 Gbit NOR Flash: MX66UW1G45GXDI00
开发板PSRAM部分原理图:
开发板NOR Flash部分原理图:
二、XiP项目使用说明
STM32CubeH7RS软件包中提供了XIP项目模板,项目专有文件位于Projects\STM32H7S78-DK\Templates\Template_XIP
子目录。
以下内容来自该目录内的README文档(以及机器翻译)。
2.1 Templates_XIP 示例描述
-
This project provides a reference template based on the STM32Cube HAL API that can be used to build any firmware application to execute from external Flash (Sub-project Appli). It boots from internal Flash and jumps to the application code in external Flash (Sub-project Boot).
本项目提供了一个基于 STM32Cube HAL API 的参考模板,可用于构建任何固件应用程序以从外部闪存执行(子项目应用程序)。它从内部 Flash 启动,并跳转到外部 Flash 中的应用程序代码(子项目启动)。
-
This project is targeted to run on STM32H7S7xx device on STM32H7S78-DK board from STMicroelectronics. At the beginning of the main program, the HAL_Init() function is called to reset all the peripherals and initialize the systick used as 1ms HAL timebase.
该项目的目标是在 STMicroelectronics 的 STM32H7S78-DK 板上的 STM32H7S7xx 器件上运行。在主程序开始时,调用 HAL_Init() 函数来重置所有外围设备并初始化用作 1ms HAL 时基的系统。
-
This project runs from the external Flash memory. It is launched from a first boot stage and inherits from this boot project configuration (caches, MPU regions [regions 0, 1 and 2 here], system clock at 600 MHz and external memory interface at the highest speed). Note that the boot part is automatically downloaded from the IDE environment via the board project Templates/Template_XIP/Binary/Boot_XIP.hex file.
此项目从外部 Flash 存储器运行。它从第一个引导阶段启动,并继承自此引导项目配置(缓存、MPU 区域 [此处为区域 0、1 和 2]、600 MHz 的系统时钟和最高速度的外部存储器接口)。请注意,引导部件是通过板项目 Templates/Template_XIP/Binary/Boot_XIP.hex 文件从 IDE 环境自动下载的。
-
The template project calls also SCB_EnableICache() and SCB_EnableDCache() functions in order to enable the Layer 1 Core Instruction and Data Caches. This is provided as template implementation that the User may integrate in his application in order to enhance the performance.
模板项目还调用 SCB_EnableICache() 和 SCB_EnableDCache() 函数,以启用第 1 层内核指令和数据缓存。这是作为模板实现提供的,用户可以将其集成到其应用程序中以提高性能。
Notes 注意
- Care must be taken when using HAL_Delay(), this function provides accurate delay (in milliseconds) based on variable incremented in SysTick ISR. This implies that if HAL_Delay() is called from a peripheral ISR process, then the SysTick interrupt must have higher priority (numerically lower) than the peripheral interrupt. Otherwise the caller ISR process will be blocked. To change the SysTick interrupt priority you have to use HAL_NVIC_SetPriority() function.
使用 HAL_Delay() 时必须小心,此函数根据 SysTick ISR 中递增的变量提供准确的延迟(以毫秒为单位)。这意味着,如果 HAL_Delay() 是从外围 ISR 进程调用的,则 SysTick 中断必须具有比外围中断更高的优先级(数值上更低)。否则,调用方 ISR 进程将被阻止。要更改 SysTick 中断优先级,您必须使用 HAL_NVIC_SetPriority() 函数。
- The application needs to ensure that the SysTick time base is always set to 1 millisecond to have correct HAL operation.
应用程序需要确保 SysTick 时基始终设置为 1 毫秒,才能执行正确的 HAL 操作。
- Whenever the application is using ITCM/DTCM memories (@0x0000000 / @0x20000000: not cacheable and only accessible by the Cortex M7 and the GPDMA/HPDMA), there is no need for cache maintenance. If the application needs to put DMA buffers in AXI SRAM (starting from @0x24000000), the user has to:
每当应用程序使用 ITCM/DTCM 内存(@0x0000000 / @0x20000000:不可缓存,只能由 Cortex M7 和 GPDMA/HPDMA 访问)时,无需缓存维护。如果应用程序需要将 DMA 缓冲区放入 AXI SRAM 中(从 @0x24000000 开始),用户必须:
- either define a non-cacheable region in the MPU and linker configuration file to locate DMA buffers (a proposed noncacheable_buffer section is available from CMSIS Device linker template file and its size must be adapted to the application requirements)
在 MPU 和链接器配置文件中定义不可缓存区域以查找 DMA 缓冲区(CMSIS 设备链接器模板文件中提供了建议的 noncacheable_buffer 部分,其大小必须适应应用程序要求)
- or to ensure cache maintenance operations to ensure the cache coherence between the CPU and the DMAs. This is true also for any other data buffers accessed by the CPU and other masters (DMA2D, LTDC). The addresses and the size of cacheable buffers (shared between CPU and other masters) must be properly defined to be aligned to data cache line size (32 bytes) and of a size of being multiple of this cache line size. Please refer to the AN4838 “Managing memory protection unit (MPU) in STM32 MCUs”. Please refer to the AN4839 “Level 1 cache on STM32F7 Series”.
或确保缓存维护操作,以确保 CPU 和 DMA 之间的缓存一致性。这也适用于 CPU 和其他主站(DMA2D、LTDC)访问的任何其他数据缓冲区。地址和可缓存缓冲区的大小(在 CPU 和其他 masters之间共享)必须正确定义,以便与数据缓存行大小(32 字节)保持一致,并且大小是此缓存行大小的倍数。请参阅 AN4838“在 STM32 MCU 中管理内存保护单元 (MPU)”。请参阅 AN4839 “STM32F7 系列上的 1 级缓存”。
2.2 如何使用它 ?
这里仅摘录STM32CubeIDE的部分:
STM32CubeIDE
- Open your toolchain 打开您的工具链
- Open Multi-projects workspace file .project
打开多项目工作区文件 .project
Optional: 自选:
- Select the “Template_XIP_Boot” project
选择 “Template_XIP_Boot” 项目
- Build the project 生成项目
- If the project is not compiled, Appli debugging will manage its compilation for debugging
如果项目没有编译,Appli 调试会管理它的编译进行调试
- With the debug icon select the configuration “Template_XIP_Boot Debug”. This operation loads the boot in internal Flash.
使用调试图标选择配置“Template_XIP_Boot Debug”。此操作在内部 Flash 中加载引导。
- Select the “Template_XIP_Appli” project
选择 “Template_XIP_Appli” 项目
- Build the project 生成项目
- With the Debug icon select the configuration “Template_XIP_Appli Debug”. This sub-project will first load the Boot binary in internal Flash, then load the Appli binary in External memory available on STM32H7S78-DK board
使用 Debug 图标选择配置 “Template_XIP_Appli Debug”。此子项目将首先将 Boot 二进制文件加载到内部 Flash 中,然后将 Appli 二进制文件加载到 STM32H7S78-DK 板上可用的外部存储器中
- Run the example 运行示例
完整介绍见网页:Templates_XIP Example Description
这个示例编译、下载之后,如无意外,可以顺利闪烁绿色LED灯。
三、XIP项目源码分析
接下来分析这个项目的源码,并进行简单的原理介绍。
3.1 固件下载命令
经过我的调试,固件下载阶段STM32CubeIDE界面操作实际执行的后台命令是:
D:\ST\STM32CubeIDE_1.16.0\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.win32_2.1.400.202404281720\tools\bin\STM32_Programmer_CLI.exe --connect port=SWD speed=fast freq=21000 ap=1 mode=UR reset=hwRst --extload D:\ST\STM32CubeIDE_1.16.0\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.win32_2.1.400.202404281720\tools\bin\ExternalLoader\Template_XIP_ExtMemLoader.stldr --download C:\Users\xu\AppData\Local\Temp\ST-LINK_GDB_server_a45164.srec --log C:\Users\xu\AppData\Local\Temp\STM32CubeProgrammer_a45164.log
从这个命令可以看到:
- STM32CubeIDE的安装目录是
D:\ST\STM32CubeIDE_1.16.0\STM32CubeIDE
;
- 使用了STM32_Programmer_CLI.exe进行固件下载;
- 命令行参数
--extload
指定的文件Template_XIP_ExtMemLoader.stldr
是由ExtMemLoader子项目编译生成的(elf文件重命名得到的);
- 命令行参数
--download
指定的文件C:\Users\xu\AppData\Local\Temp\ST-LINK_GDB_server_a45164.srec
是由Appli子项目编译生成的(objcopy传-O srec
可以生成srec格式的文件);
- 命令行参数
--log
指定的文件用于记录整个执行过程的详细日志(包括传输的数据)。
3.2 固件下载过程
固件下载过程,可以简单理解为以下步骤:
- PC上STM32_Programmer_CLI程序将extloader代码发送给MCU,MCU接收这部分代码并写入片上SRAM;
- MCU运行extloader代码,并继续接收STM32_Programmer_CLI发送来的外部存储器地址和数据;
- MCU按照收到外部存储器写入地址和数据,操作外部存储器进行擦除和写入;
这个过程由PC和MCU配合进行。
注意:以上内容是我根据STM32_Programmer_CLI执行日志分析和理解的,并未找到相应的官方文档,具体流程和细节上未必准确,仅供参考(如果有哪位读者清楚上述过程在哪篇官方文档中有介绍,欢迎留言告诉我,感谢!)。
3.2 ExtMemLoader源码分析
本节分析ExtMemLoader子项目的源代码,根据前面的下载过程介绍,可以知道,ExtMemLoader的主要作用是接收STM32_Programmer_CLI发来的数据并写入外部存储器。因此,ExtMemLoader源码分析的主要目标是——分析具体写入的外部存储器是哪个?是SRAM还是NOR Flash?
3.2.1 ExtMemLoader的链接脚本
首先从链接脚本开始,链接脚本文件:STM32CubeIDE\ExtMemLoader\STM32H7S7L8HXH_FLASH.ld
大概看一眼链接脚本代码:
可以看到入口函数是Reset_Handler,ExtMemLoader代码会放到片上SRAM中执行。
接着看入口函数Reset_Handler,也就是启动代码,这个函数是用汇编写的。
3.2.2 ExtMemLoader的启动代码
启动代码: STM32CubeIDE\ExtMemLoader\Application\User\Startup\startup_stm32h7s7l8hxh.s
这段汇编代码中,包含几个关键的注释,分别为:
- set stack pointer,设置栈指针;
- Call the clock system initialization function,调用时钟系统初始化函数;
- Copy the data segment initializers from flash to SRAM,从flash中拷贝数据段到SRAM;
- Zero fill the bss segment,向bss段填充0;
- Call static constructors,调用静态构造函数;
- Call the application's entry point,调用应用程序的入口main函数;
这些注释将整个Reset_Handler汇编代码的功能已经说明的非常清楚,不需要额外解释了。
3.2.3 ExtMemLoader的main函数
ExtMemLoader的main函数,代码文件位于: \Middlewares\ST\STM32_ExtMem_Loader\STM32Cube\stm32_loader_api.c
具体代码内容为:
这个main函数是一个状态机的写法,并且从DEBUG_STATE_WAIT
下方的注释可以看到——exec
是可以被外部更新的。
main函数的状态机可以根据exec执行4种操作:
- 初始化外部存储器,调用 Init 函数实现;
- 写入数据到外部存储器,调用Write函数实现;
- 擦除整个外部存储器,调用MassErase函数实现;
- 擦除外部存储器的部分分区,调用SectorErase函数实现;
3.2.4 ExtMemLoader的Init函数
下面仅以Init函数为例进行分析,代码文件还是刚刚的stm32_loader_api.c
。
可以看到,Init函数中调用到了extmemloader_Init()
,文件路径为: ExtMemLoader\Src\extmemloader_init.c
;
这里可以看到:
- 134行,
extmemloader_Init
函数调用了MX_XSPI2_Init()
;
- 139行,
extmemloader_Init
函数调用了MX_EXTMEM_MANAGER_Init()
;
MX_XSPI2_Init函数的具体代码为:
这里初始化了XSPI2的一些参数;
MX_EXTMEM_MANAGER_Init函数的定义位于文件:ExtMemLoader\Src\extmem_manager.c
具体实现代码为:
这里可以看到初始化的是8线NOR Flash。
3.2.5 ExtMemLoader的XSPI2引脚配置
查看ioc文件中,XSPI2相关引脚配置,可以看到:
和前面的NOR Flash原理图部分可以完全对应的上。
类似的,我们可以分析ExtMemLoader的main函数的其他几个函数调用,最终都可以对应到开发板的NOR Flash设备的操作上。
3.3 Boot源码分析
本节分析Boot子项目的源码。
3.3.1 Boot的链接脚本
首先从链接脚本开始,链接脚本文件:STM32CubeIDE\Boot\STM32H7S7L8HXH_FLASH.ld
大概看一眼链接脚本代码:
可以看到入口函数是Reset_Handler,ExtMemLoader代码会放到片上Flash中执行。
接着看入口函数Reset_Handler,也就是启动代码,这个函数是用汇编写的。
3.3.2 Boot的启动代码
Boot子项目的启动代码文件位于: STM32CubeIDE\Boot\Application\Startup\startup_stm32h7s7xx.s
这段汇编代码和前面的ExtMemLoader的完全一样,同样包含几个关键的注释,分别为:
- set stack pointer,设置栈指针;
- Call the clock system initialization function,调用时钟系统初始化函数;
- Copy the data segment initializers from flash to SRAM,从flash中拷贝数据段到SRAM;
- Zero fill the bss segment,向bss段填充0;
- Call static constructors,调用静态构造函数;
- Call the application's entry point,调用应用程序的入口main函数;
这些注释将整个Reset_Handler汇编代码的功能已经说明的非常清楚,不需要额外解释了。
3.3.3 Boot的main函数
ExtMemLoader的main函数,代码文件位于: Boot\Src\main.c
具体代码内容为:
main函数中,比较重要的两个函数调用是:
- MX_EXTMEM_MANAGER_Init() 实现了外部存储器的初始化;
- BOOT_Application() 实现了控制逻辑的跳转,这一步非常关键;
3.3.4 Boot的MX_EXTMEM_MANAGER_Init函数
Boot的MX_EXTMEM_MANAGER_Init函数,实现位于文件: Boot\Src\extmem_manager.c
具体代码为:
可以看到,这里对NOR Flash和PSRAM两个外部存储器均进行了初始化,注册的枚举对应关系为:
- EXTMEMORY_1,使用XSPI2接口,控制NOR Flash存储器;
- EXTMEMORY_2,使用XSPI1接口,控制PSRAM存储器;
3.3.5 Boot的BOOT_Application函数
Boot的BOOT_Application函数,实现位于文件: \Middlewares\ST\STM32_ExtMem_Manager\boot\stm32_boot_xip.c
具体代码为:
可以看到,这里分别调用了MapMemory和JumpToApplication两个函数;这两个函数均位于同一个文件内;下面分别分析。
MapMemory函数具体代码:
这里会执行两个外部存储器的内存映射动作,具体映射的地址范围手册中Memory map有说明:
接下来就是重头戏JumpToApplication函数,具体代码为:
这个函数代码中已经有一些注释了,除此之外,比较重要的是:
- 123行,调用EXTMEM_GetMapAddress函数,获取外部存储器映射的内存地址;
- 142和145行,计算应用程序入口地址;
- 144行,重新设置中断向量表地址;
- 154行,重新设置主栈指针(MSP)地址;
- 159行,跳转到新的程序入口地址并执行;
其中,123行函数调用的参数EXTMEM_MEMORY_BOOTXIP宏的定义位于:Boot\Inc\stm32_extmem_conf.h
文件
具体代码为:
通过回看前面的MX_EXTMEM_MANAGER_Init函数实现,可以知道EXTMEMORY_1对应的正是NOR Flash设备。
好了,到这里就完成了Boot的全部功能,程序执行流程已经成功跳转到了外部NOR Flash上。
四、参考链接
- XIP技术总结: https://zhuanlan.zhihu.com/p/368276428
- STM32H7S78-DK开发板原理图: https://www.st.com.cn/resource/en/schematic_pack/mb1736-h7s7l8-d01-schematic.pdf
- STM32H7S78-DK开发板PSRAM存储器数据手册: APS256XXN-OBR-BG.pdf
- STM32H7S78-DK开发板NOR Flash存储器数据手册:MX66U1G45G.pdf
- STM32H7Rx/Sx 系列MCU的Cube软件包: https://github.com/STMicroelectronics/STM32CubeH7RS
写的好详细,给大佬点赞👍