MM32F5270程序从内部Flash迁移至QSPI Flash的完整指南
2026/5/22 21:03:54 网站建设 项目流程

1. 项目概述与核心挑战

最近在做一个基于灵动微电子MM32F5270的项目,内部Flash空间(512KB)被复杂的协议栈和图形库占得差不多了,业务逻辑代码没地方放了。这时候,板载的那颗8MB的QSPI Flash就成了“救星”。之前我们已经搞定了两件大事:一是为MDK(Keil)制作了能直接下载代码到这颗外部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 为什么必须检查并修改源代码?

这是迁移过程中最容易踩坑的地方,原因在于代码执行的时序矛盾

  1. 时钟与复位初始化:通常,在main()函数之前或之初,启动文件或板级支持包会初始化系统时钟,并复位所有外设(包括QSPI控制器)到一个已知的默认状态。想象一下这个场景:CPU正在从QSPI Flash(地址0x90000000)读取main()函数的指令,但执行到的初始化代码里有一句“复位QSPI模块”。一旦这条指令被执行,QSPI控制器被重置,通信立即中断,CPU无法再读取下一条指令,程序必然“卡死”在当前位置。同理,如果复位了QSPI所用到的GPIO引脚时钟,这些引脚的功能也会失效,导致通信失败。

  2. 引脚复用配置:QSPI需要占用6个引脚(CLK, CS, IO0-IO3)。如果你的应用程序初始化代码(例如pin_init.c)里,将这些引脚重新配置为了普通GPIO、UART或其他功能,那么QSPI物理链路就被破坏了,程序同样无法继续执行。

  3. 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示例工程作为迁移对象。用MDK(Keil uVision5或更新版本)打开该工程。

3.2 定位并修改Scatter File

  1. 在MDK的Project窗口中,右键点击Target,选择Options for Target...,或者直接点击工具栏的魔术棒图标。
  2. 在弹出的对话框中,切换到Linker选项卡。
  3. 找到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的实际大小。假设是一颗常见的8MB(8 Megabyte) Flash。计算过程:8 MB = 8 * 1024 * 1024 Bytes = 8388608 Bytes。将其转换为十六进制:8388608 = 0x800000。所以这里应改为0x00800000

注意:务必根据实际板载Flash型号确认大小。常见的还有16MB(0x01000000)、32MB(0x02000000)等。设置过大(超过实际容量)会导致链接器分配超出范围的地址,引发不可预知错误;设置过小则无法充分利用Flash空间。

修改后的关键部分如下:

#define __ROM_BASE 0x90000000 // 修改为QSPI Flash映射地址 #define __ROM_SIZE 0x00800000 // 修改为8MB大小

保存这个scatter文件。

3.3 在工程中添加QSPI Flash下载算法

仅仅修改链接脚本,编译出的程序只能“认为”自己住在0x90000000。我们还需要告诉MDK调试器,如何把程序烧写到这个“房子”里。这就是之前制作的Flash Loader.FLM文件)的作用。

  1. 再次进入Options for Target...,这次切换到Debug选项卡。
  2. 在Use下拉菜单中,选择你的调试器(如J-Link)。
  3. 点击右侧的Settings按钮。
  4. 在新弹出的窗口中,切换到Flash Download选项卡。
  5. 你会看到Download Function区域有一个列表,默认可能只有内部Flash的算法。点击Add...按钮。
  6. 浏览并找到你之前生成的MM32F5270_QSPI_FlashLoader.FLM文件(通常位于MDK安装目录的ARM/Flash或工程目录下),选中并点击Add
  7. 添加后,确保该算法在列表中,并且Programming Algorithm下的StartSize自动填充正确(Start应为0x90000000,Size应为0x00800000)。

一个至关重要的细节: 在Flash Download选项卡下方,有一个RAM for Algorithm的设置。下载算法本身是一段需要在目标板RAM中运行的小程序,这里定义了给它使用的RAM空间起始地址和大小。如果下载算法代码比较复杂,默认的Size(如0x1000)可能不够,会导致下载时出现“Cannot Load Flash Programming Algorithm”的错误。

解决方案:适当调大Size值。通常设置为0x2000(8KB)或0x4000(16KB)是安全的。你需要根据你的下载算法实际大小来调整,如果遇到上述错误,优先尝试增大这个值。

4. 源代码审查与关键修改点

完成了链接和下载配置,下一步就是确保代码本身是“QSPI安全”的。我们需要系统性地审查工程源码。

4.1 审查时钟初始化代码(以clock_init.c为例)

打开board/clock_init.c或类似的系统时钟初始化文件。你需要寻找并注释掉或删除任何对QSPI外设时钟或GPIO时钟的复位操作。

查找关键词

  • RCC_AHBPeriph_QSPI
  • RCC_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间接访问代码

在整个工程中搜索QSPIQUADSPI等关键词。你需要警惕任何在main()函数或其后调用的函数中,对QSPI Flash进行擦除(Erase)、编程(Program)、读取ID或状态寄存器的代码。

这些操作通常通过间接模式完成,会破坏内存映射模式。如果你的应用程序确实需要在运行时读写QSPI Flash的其他区域(例如存储参数、日志),你必须:

  1. 确保操作区域与程序本体区域无重叠:仔细规划Flash的布局,程序代码从0x90000000开始存放,参数存储区放在后面的扇区。
  2. 使用严格的中断保护:在切换QSPI模式进行间接操作前,关闭全局中断;操作完成后,立即恢复内存映射模式,再打开中断。因为模式切换期间CPU无法取指。
  3. 考虑将这部分代码放到内部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 下载与运行验证

  1. 准备工作:确保2nd Bootloader已正确烧录到芯片的内部Flash(0x08000000起始地址)。
  2. 连接与下载:使用调试器连接板子,在MDK中点击Download按钮。MDK会使用我们添加的QSPI下载算法,将hello_world程序直接烧写到QSPI Flash的0x90000000地址处。
  3. 复位观察:给板子复位。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.cpin_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 进阶技巧与优化建议

  1. 性能优化 - 启用缓存(Cache):MM32F5270的QSPI控制器可能支持指令缓存(ICache)或数据缓存(DCache)。在跳转到QSPI应用程序之前(在2nd Bootloader中)或应用程序初期,使能这些缓存可以大幅提升XiP的执行效率,减少因QSPI读取延迟带来的性能损失。查阅芯片参考手册,找到相关寄存器并启用。
  2. 性能优化 - 将关键代码/数据放入RAM:即使有缓存,对实时性要求极高的中断服务程序(ISR)或频繁调用的核心函数,将其链接到内部RAM中执行能保证最稳定的性能。在MDK中,可以通过__attribute__((section(".ram_code")))定义函数,并在Scatter File中创建专门的执行域来实现。
  3. 内存布局规划:对于大容量QSPI Flash(如8MB),做好规划。前一部分(例如1MB)用于存放程序代码(.text)和只读数据(.rodata)。后面的部分可以划分为参数区、文件系统区(如LittleFS)、日志存储区等。在链接脚本中精确定义这些区域,避免冲突。
  4. 制作多位置启动工程:利用MDK的Target Group功能,可以创建一个工程,同时生成内部Flash版本和QSPI Flash版本。通过不同的预定义宏(如RUN_FROM_QSPI)和不同的Scatter File来切换,极大提高开发和测试效率。

迁移程序到QSPI Flash上运行,初看步骤不少,但每一步都有其明确的物理意义和必要性。核心就是地址重定向硬件状态保全。一旦你成功跑通第一个例程,理解了背后的原理,后续项目的迁移就会变得非常顺畅。这不仅能解决内部Flash空间不足的燃眉之急,也为设计更复杂、功能更丰富的嵌入式产品打开了新的空间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询