MM32F5270程序从内部Flash迁移至QSPI Flash的完整指南

MM32F5270程序从内部Flash迁移至QSPI Flash的完整指南 1. 项目概述与核心挑战最近在做一个基于灵动微电子MM32F5270的项目内部Flash空间512KB被复杂的协议栈和图形库占得差不多了业务逻辑代码没地方放了。这时候板载的那颗8MB的QSPI Flash就成了“救星”。之前我们已经搞定了两件大事一是为MDKKeil制作了能直接下载代码到这颗外部Flash的下载算法Flash Loader二是写了一个放在片内Flash的二级引导程序2nd Bootloader负责上电后把程序从QSPI搬过来或者直接跳过去执行。但是这并不意味着你直接把原来在内部Flash跑的程序用下载算法烧进QSPI就能万事大吉。如果直接这么干大概率会卡死或者跑飞。核心原因在于程序在QSPI Flash中执行即XiP eXecute in Place与在内部Flash执行对微控制器来说是完全不同的两套“游戏规则”。你需要告诉编译链接器程序的新家在哪更要仔细检查代码确保没有在“搬家”后自毁长城比如不小心关闭了QSPI控制器。本文就以MindSDK中的hello_world工程为例手把手带你完成从内部Flash到QSPI Flash的完整迁移让你彻底掌握编译一个能在QSPI上正确运行的程序的所有要点。2. 工程迁移的核心思路与原理剖析2.1 为什么需要修改链接脚本程序编译链接后每一个函数、变量都有一个确定的地址。当芯片从内部Flash启动时它默认从0x08000000这个地址开始取指令。你的链接脚本Scatter File会告诉链接器“把所有代码和数据都放在以0x08000000开头、大小为512KB的这片区域里。”现在我们希望程序本体存放在QSPI Flash里而QSPI Flash被映射到了芯片的另一个地址空间对于MM32F5270这个基地址是0x90000000。芯片上电后通过我们预先烧录好的2nd Bootloader最终会跳转到0x90000000这个地址开始执行。因此我们必须修改链接脚本将程序的加载域Load Region和执行域Execution Region的基地址从内部Flash的0x08000000改为QSPI Flash映射的0x90000000。这样链接器生成的所有绝对地址引用比如函数调用地址、常量指针才会是基于0x90000000计算的确保CPU去QSPI的对应位置能找到正确的指令和数据。2.2 为什么必须检查并修改源代码这是迁移过程中最容易踩坑的地方原因在于代码执行的时序矛盾。时钟与复位初始化通常在main()函数之前或之初启动文件或板级支持包会初始化系统时钟并复位所有外设包括QSPI控制器到一个已知的默认状态。想象一下这个场景CPU正在从QSPI Flash地址0x90000000读取main()函数的指令但执行到的初始化代码里有一句“复位QSPI模块”。一旦这条指令被执行QSPI控制器被重置通信立即中断CPU无法再读取下一条指令程序必然“卡死”在当前位置。同理如果复位了QSPI所用到的GPIO引脚时钟这些引脚的功能也会失效导致通信失败。引脚复用配置QSPI需要占用6个引脚CLK, CS, IO0-IO3。如果你的应用程序初始化代码例如pin_init.c里将这些引脚重新配置为了普通GPIO、UART或其他功能那么QSPI物理链路就被破坏了程序同样无法继续执行。QSPI操作模式冲突QSPI控制器通常支持多种模式内存映射模式Memory-Mapped Mode和间接模式Indirect Mode。内存映射模式就是我们将QSPI Flash映射到0x90000000地址CPU像访问普通内存一样直接读取指令和数据这是XiP的基础。而间接模式则需要通过读写QSPI控制器的一系列寄存器来发起读写命令常用于擦除、编程Flash或读取Flash的ID等。关键在于这两种模式通常是互斥的。如果在XiP运行的应用程序中不小心插入了一段间接模式操作QSPI Flash的代码比如试图擦写自身所在的区域会切换控制器模式导致内存映射访问中断程序跑飞。所以修改源代码的核心原则是确保在程序从QSPI开始运行后任何代码都不会去破坏QSPI控制器及其引脚的工作状态也不要试图用间接模式去操作承载当前程序的Flash区域。3. 详细实操步骤修改链接脚本与工程配置3.1 获取与打开基础工程首先从灵动微电子官网或GitHub获取MindSDK开发套件。本文以MM32F5270平台的hello_world示例工程作为迁移对象。用MDKKeil uVision5或更新版本打开该工程。3.2 定位并修改Scatter File在MDK的Project窗口中右键点击Target选择Options for Target...或者直接点击工具栏的魔术棒图标。在弹出的对话框中切换到Linker选项卡。找到Scatter File一栏这里显示了当前工程使用的链接脚本文件例如mm32f5277e_flash.scf。点击其右侧的Edit...按钮MDK会在编辑区打开这个文件。现在我们需要修改这个文件中的存储器地址定义。关键是要找到定义ROM程序存储区域基地址和大小的宏。/*--------------------- Flash Configuration ---------------------------------- ; Flash Base Address 0x0-0xFFFFFFFF:8 ; Flash Size (in Bytes) 0x0-0xFFFFFFFF:8 ; *----------------------------------------------------------------------------*/ #define __ROM_BASE 0x08000000 // 默认是内部Flash起始地址 #define __ROM_SIZE 0x00080000 // 默认是512KB (0x80000) /*--------------------- RAM Configuration ------------------------------------ ; RAM Base Address 0x0-0xFFFFFFFF:8 ; RAM Size (in Bytes) 0x0-0xFFFFFFFF:8 ; *----------------------------------------------------------------------------*/ #define __RAM_BASE 0x20000000 #define __RAM_SIZE 0x00010000修改如下__ROM_BASE需要改为QSPI Flash在内存映射模式下的起始地址。查阅MM32F5270的用户手册《存储器映射》章节可以确认QSPI Flash的映射地址为0x90000000。__ROM_SIZE需要改为你板载QSPI Flash的实际大小。假设是一颗常见的8MB8 Megabyte Flash。计算过程8 MB 8 * 1024 * 1024 Bytes 8388608 Bytes。将其转换为十六进制8388608 0x800000。所以这里应改为0x00800000。注意务必根据实际板载Flash型号确认大小。常见的还有16MB0x01000000、32MB0x02000000等。设置过大超过实际容量会导致链接器分配超出范围的地址引发不可预知错误设置过小则无法充分利用Flash空间。修改后的关键部分如下#define __ROM_BASE 0x90000000 // 修改为QSPI Flash映射地址 #define __ROM_SIZE 0x00800000 // 修改为8MB大小保存这个scatter文件。3.3 在工程中添加QSPI Flash下载算法仅仅修改链接脚本编译出的程序只能“认为”自己住在0x90000000。我们还需要告诉MDK调试器如何把程序烧写到这个“房子”里。这就是之前制作的Flash Loader.FLM文件的作用。再次进入Options for Target...这次切换到Debug选项卡。在Use下拉菜单中选择你的调试器如J-Link。点击右侧的Settings按钮。在新弹出的窗口中切换到Flash Download选项卡。你会看到Download Function区域有一个列表默认可能只有内部Flash的算法。点击Add...按钮。浏览并找到你之前生成的MM32F5270_QSPI_FlashLoader.FLM文件通常位于MDK安装目录的ARM/Flash或工程目录下选中并点击Add。添加后确保该算法在列表中并且Programming Algorithm下的Start和Size自动填充正确Start应为0x90000000Size应为0x00800000。一个至关重要的细节 在Flash Download选项卡下方有一个RAM for Algorithm的设置。下载算法本身是一段需要在目标板RAM中运行的小程序这里定义了给它使用的RAM空间起始地址和大小。如果下载算法代码比较复杂默认的Size如0x1000可能不够会导致下载时出现“Cannot Load Flash Programming Algorithm”的错误。解决方案适当调大Size值。通常设置为0x20008KB或0x400016KB是安全的。你需要根据你的下载算法实际大小来调整如果遇到上述错误优先尝试增大这个值。4. 源代码审查与关键修改点完成了链接和下载配置下一步就是确保代码本身是“QSPI安全”的。我们需要系统性地审查工程源码。4.1 审查时钟初始化代码以clock_init.c为例打开board/clock_init.c或类似的系统时钟初始化文件。你需要寻找并注释掉或删除任何对QSPI外设时钟或GPIO时钟的复位操作。查找关键词RCC_AHBPeriph_QSPIRCC_AHBPeriph_GPIOx(x可能是QSPI所用到的端口如GPIOA,GPIOB等需根据原理图确认)RCC_AHBPeriphResetCmd()RCC_APBxPeriphResetCmd()(QSPI可能在APB总线)示例void BOARD_InitClock(void) { // ... 其他时钟配置 ... // 危险代码这会复位QSPI模块导致XiP程序崩溃。 // RCC_AHBPeriphResetCmd(RCC_AHBPeriph_QSPI, ENABLE); // RCC_AHBPeriphResetCmd(RCC_AHBPeriph_QSPI, DISABLE); // 危险代码这会复位QSPI所用GPIO端口的时钟。 // RCC_AHBPeriphResetCmd(RCC_AHBPeriph_GPIOA, ENABLE); // 假设QSPI CLK在PA0 // RCC_AHBPeriphResetCmd(RCC_AHBPeriph_GPIOA, DISABLE); // ... 其他代码 ... }实操心得最稳妥的方法不是简单注释而是使用条件编译。在时钟初始化函数中通过宏定义来区分程序运行的位置。// 在项目头文件中定义或在编译器预定义宏中添加 // #define RUN_FROM_QSPI 1 void BOARD_InitClock(void) { // ... 公共时钟配置 ... #ifndef RUN_FROM_QSPI // 只有当程序从内部Flash运行时才复位QSPI相关时钟 RCC_AHBPeriphResetCmd(RCC_AHBPeriph_QSPI, ENABLE); RCC_AHBPeriphResetCmd(RCC_AHBPeriph_QSPI, DISABLE); #endif // ... 其他代码 ... }这样当你编译QSPI版本时定义RUN_FROM_QSPI这段危险的复位代码就不会被编译进去。4.2 审查引脚初始化代码以pin_init.c为例打开board/pin_init.c查找所有QSPI相关引脚的初始化代码并确保它们没有被重新配置为其他功能。QSPI引脚通常包括QSPI_CLK,QSPI_CS,QSPI_IO0,QSPI_IO1,QSPI_IO2,QSPI_IO3。具体引脚号需查阅开发板原理图和芯片数据手册。查找并修改void BOARD_InitPins(void) { // ... 其他引脚初始化 ... // 错误的配置将QSPI_IO0和IO1假设是PA6, PA7配置成了UART引脚 // GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_7); // UART TX // GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_7); // UART RX // GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; // GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // GPIO_Init(GPIOA, GPIO_InitStructure); // 正确的做法要么完全删除这段配置要么同样用条件编译宏包裹。 #ifndef RUN_FROM_QSPI // 仅当不从QSPI运行时才配置这些引脚为其他功能 // ... (上述UART配置代码) ... #endif }注意有些板级支持包可能在pin_init.c里只初始化了用户LED、按键、串口等外设引脚并未涉及QSPI。但务必仔细检查确保没有任何地方修改了QSPI的6个引脚。4.3 审查应用程序中的QSPI间接访问代码在整个工程中搜索QSPI、QUADSPI等关键词。你需要警惕任何在main()函数或其后调用的函数中对QSPI Flash进行擦除Erase、编程Program、读取ID或状态寄存器的代码。这些操作通常通过间接模式完成会破坏内存映射模式。如果你的应用程序确实需要在运行时读写QSPI Flash的其他区域例如存储参数、日志你必须确保操作区域与程序本体区域无重叠仔细规划Flash的布局程序代码从0x90000000开始存放参数存储区放在后面的扇区。使用严格的中断保护在切换QSPI模式进行间接操作前关闭全局中断操作完成后立即恢复内存映射模式再打开中断。因为模式切换期间CPU无法取指。考虑将这部分代码放到内部RAM中执行这是更高级但更安全的方法写一段专门用于Flash操作的函数并将其链接到内部RAM中执行这样即使切换QSPI模式也不会影响正在RAM中运行的指令流。对于简单的hello_world示例通常不包含此类代码但这是复杂项目迁移时必须考虑的。5. 编译、下载与功能验证5.1 编译工程完成上述所有修改后点击MDK的Rebuild按钮。编译应该顺利通过。你可以通过查看生成的.map文件来验证链接地址是否正确。打开Options for Target - Listing勾选Linker Listing并指定生成.map文件。编译后查看.map文件搜索Load Region你应该看到类似下面的内容Load Region LR_IROM1 (Base: 0x90000000, Size: 0x00000xxx, Max: 0x00800000, ABSOLUTE)这确认了程序的加载地址确实是从0x90000000开始。5.2 下载与运行验证准备工作确保2nd Bootloader已正确烧录到芯片的内部Flash0x08000000起始地址。连接与下载使用调试器连接板子在MDK中点击Download按钮。MDK会使用我们添加的QSPI下载算法将hello_world程序直接烧写到QSPI Flash的0x90000000地址处。复位观察给板子复位。2nd Bootloader会启动完成必要的初始化后跳转到0x90000000执行。打开串口调试助手波特率与工程配置一致你应该能看到“hello, world”的输出。这是第一个里程碑证明程序能在QSPI中顺序执行。5.3 深入验证中断与调试顺序执行成功还不够嵌入式系统离不开中断。验证中断如SysTick 修改main.c添加一个简单的SysTick中断让LED闪烁或定时打印字符。volatile uint32_t systick_counter 0; void SysTick_Handler(void) { systick_counter; } int main(void) { BOARD_Init(); printf(System started from QSPI.\r\n); // 配置SysTick为1ms中断 SysTick_Config(SystemCoreClock / 1000); while (1) { if (systick_counter 1000) { // 每秒 systick_counter 0; printf(Tick from QSPI XiP!\r\n); // 或者翻转LED } } }重新编译下载并复位。如果串口能规律地每秒输出Tick from QSPI XiP!或者LED规律闪烁这就强有力地证明了在QSPI中运行的程序中断向量表的重映射、中断的响应和返回都是完全正常的。这是XiP功能可用的关键标志。调试器验证 在MDK中进入调试模式Start Debug Session。程序暂停后查看Disassembly窗口你会看到反汇编的指令地址是从0x9000xxxx开始的。查看Register窗口中的PC程序计数器值也应在0x9000xxxx范围内。这从调试器视角确认了CPU正在从QSPI地址空间取指执行。6. 常见问题排查与进阶技巧6.1 问题速查表问题现象可能原因排查步骤与解决方案下载失败提示“Cannot Load Flash Programming Algorithm”1. 下载算法.FLM文件路径错误或损坏。2.RAM for Algorithm的Size设置太小。3. 目标板RAM不足或算法所需RAM地址被占用。1. 确认.FLM文件存在且路径正确可尝试重新添加。2. 逐步增大Size值如0x2000, 0x4000。3. 检查算法配置的Start地址是否与目标板可用RAM区域冲突。程序下载成功但复位后无任何现象如串口无输出1.最常见源代码中未清除对QSPI或相关GPIO时钟的复位代码。2. Scatter File中__ROM_SIZE设置超过实际Flash大小。3. 2nd Bootloader跳转失败或初始化不正确。4. 中断向量表地址未正确设置如果用了中断。1.重点检查clock_init.c和pin_init.c确保所有QSPI相关初始化被禁用或条件编译。2. 核对板载QSPI Flash型号和容量修正__ROM_SIZE。3. 确认2nd Bootloader已烧录且其跳转地址为0x90000000。单步调试Bootloader。4. 在SystemInit或早期初始化代码中确认SCB-VTOR寄存器被设置为0x90000000。程序运行一段时间后死机1. 应用程序中包含了间接模式操作QSPI Flash的代码。2. 堆栈Stack设置不足导致溢出。QSPI访问延迟可能影响中断响应时序。1. 全局搜索QSPI驱动函数调用确保它们不会在XiP运行时被意外执行。2. 适当增大启动文件或链接脚本中定义的堆栈大小。考虑将频繁访问的代码或数据放到内部RAM。调试时无法命中断点在QSPI中执行代码时某些调试器对硬件断点的支持可能有限或者速度较慢。1. 尝试使用软件断点。2. 确保调试器配置正确能够访问QSPI地址空间。3. 对于复杂调试可以考虑将部分关键代码段加载到内部RAM中调试。6.2 进阶技巧与优化建议性能优化 - 启用缓存CacheMM32F5270的QSPI控制器可能支持指令缓存ICache或数据缓存DCache。在跳转到QSPI应用程序之前在2nd Bootloader中或应用程序初期使能这些缓存可以大幅提升XiP的执行效率减少因QSPI读取延迟带来的性能损失。查阅芯片参考手册找到相关寄存器并启用。性能优化 - 将关键代码/数据放入RAM即使有缓存对实时性要求极高的中断服务程序ISR或频繁调用的核心函数将其链接到内部RAM中执行能保证最稳定的性能。在MDK中可以通过__attribute__((section(.ram_code)))定义函数并在Scatter File中创建专门的执行域来实现。内存布局规划对于大容量QSPI Flash如8MB做好规划。前一部分例如1MB用于存放程序代码.text和只读数据.rodata。后面的部分可以划分为参数区、文件系统区如LittleFS、日志存储区等。在链接脚本中精确定义这些区域避免冲突。制作多位置启动工程利用MDK的Target Group功能可以创建一个工程同时生成内部Flash版本和QSPI Flash版本。通过不同的预定义宏如RUN_FROM_QSPI和不同的Scatter File来切换极大提高开发和测试效率。迁移程序到QSPI Flash上运行初看步骤不少但每一步都有其明确的物理意义和必要性。核心就是地址重定向和硬件状态保全。一旦你成功跑通第一个例程理解了背后的原理后续项目的迁移就会变得非常顺畅。这不仅能解决内部Flash空间不足的燃眉之急也为设计更复杂、功能更丰富的嵌入式产品打开了新的空间。