SPI Flash基础知识点 (腾讯元宝版本)
好的,作为一名嵌入式软件工程师,深入理解 SPI Flash 的开发至关重要,因为它是嵌入式系统中最常用的存储器件之一。下面我将详细地为你梳理基础知识和关键注意点。
第一部分:SPI Flash 基础知识
1. 什么是 SPI Flash?
SPI Flash 是一种通过 SPI 接口进行通信的非易失性存储器。它结合了 SPI 接口的简单性和 Flash 存储器的断电数据保持特性,常用于存储固件、配置文件、日志数据等。
- SPI: 串行外设接口,一种全双工、同步的串行通信协议。主从模式,通常需要 4 条线:
- SCLK: 时钟信号,由主机产生。
- MOSI: 主设备数据输出,从设备数据输入。
- MISO: 主设备数据输入,从设备数据输出。
- CS/SS: 片选信号,由主机控制,用于选择从设备。
- (可选)WP#: 写保护。
- (可选)HOLD#: 保持信号,暂停通信而不取消片选。
- Flash: 闪存,属于 EEPROM 的一种,但擦除单位更大(扇区/块)。
2. 关键特性参数
- 容量: 通常从 512Kb 到 256Mb 甚至更大。注意单位是 bit,转换为字节要除以 8(例如,64Mbit = 8MB)。
- 页: 编程的最小单位。通常为 256 字节。一次可以写入一页内的任意字节,但不能跨页写入(除非地址连续)。
- 扇区: 擦除的最小单位之一。通常为 4KB。擦除后所有 bit 变为 1 (0xFF)。
- 块: 擦除的更大单位。通常为 64KB。用于快速擦除大块区域。
- 操作电压: 1.8V 或 3.3V,务必与你的 MCU 电平匹配。
3. 基本操作命令(重中之重)
所有操作都通过发送特定的命令字开始。命令字通常为 8 bit。
命令名称 | 命令字 | 描述 | 注意点 |
---|---|---|---|
Write Enable | 0x06 | 在执行任何写操作(编程或擦除)前,必须先发此命令。 | 这是一个易失性的状态位,断电或写失能后恢复。 |
Write Disable | 0x04 | 禁止写入。 | |
Read Data | 0x03 | 从指定地址开始读取数据。 | 最常用的读命令。可以连续读取,地址会自动增加。 |
Fast Read | 0x0B | 快速读取,比普通读更快。 | 需要在命令字和地址后跟一个哑字节,用于内部流水线准备。 |
Page Program | 0x02 | 向指定地址的一页内写入数据。 | 只能将 1 写成 0。写入前,目标区域必须已被擦除(全为 0xFF)。不能跨页。 |
Sector Erase | 0x20 | 擦除一个 4KB 扇区。 | 擦除后,该扇区所有数据变为 0xFF。 |
Block Erase | 0xD8 | 擦除一个 64KB 块。 | |
Chip Erase | 0xC7 / 0x60 | 擦除整个芯片。 | 非常耗时,谨慎使用! |
Read Status Register | 0x05 | 读取状态寄存器,检查设备是否忙。 | 这是轮询操作是否完成的关键! |
状态寄存器详解:
最重要的位是 BUSY/WIP 位。
- BIT0: WIP。1 表示设备正忙(正在执行编程、擦除或写状态寄存器操作);0 表示设备空闲。
- BIT1: WEL。1 表示写使能锁存器已置位(即已执行 Write Enable 命令);0 表示未使能。
在执行完 Page Program, Sector Erase 等命令后,必须不断读取状态寄存器,直到 WIP 位变为 0,才能进行下一步操作。
第二部分:软件开发流程与注意点
1. 驱动层开发
a) 硬件初始化
- 配置 MCU 的 SPI 外设:时钟极性、时钟相位、数据位顺序、波特率。
- 初始化 GPIO:正确配置 CS 引脚为输出,并初始化为高电平(不选中)。
b) 基本收发函数
- 实现
SPI_Transmit()
和SPI_Receive()
函数。对于全双工 SPI,收发通常可在一个函数内完成。
c) 核心命令函数封装
// 示例:读取器件ID
uint32_t SPI_FLASH_ReadID(void) {uint32_t ID = 0;CS_LOW(); // 选中器件SPI_Transmit(0x9F); // 读ID命令ID |= (SPI_Transmit(0xFF) << 16); // 发送哑数据,同时接收ID |= (SPI_Transmit(0xFF) << 8);ID |= SPI_Transmit(0xFF);CS_HIGH(); // 取消选中return ID;
}// 示例:等待设备就绪(轮询BUSY位)
void SPI_FLASH_WaitForReady(void) {uint8_t status;do {CS_LOW();SPI_Transmit(0x05); // 读状态寄存器命令status = SPI_Transmit(0xFF); // 发送哑数据,接收状态CS_HIGH();} while ((status & 0x01) == 1); // 检查BIT0 (WIP)
}
2. 应用层操作
a) 擦除操作流程
void SPI_FLASH_EraseSector(uint32_t sectorAddr) {// 1. 等待设备就绪SPI_FLASH_WaitForReady();// 2. 发送写使能命令SPI_FLASH_WriteEnable();// 3. 发送扇区擦除命令和地址CS_LOW();SPI_Transmit(0x20);SPI_Transmit((sectorAddr >> 16) & 0xFF); // 发送24位地址SPI_Transmit((sectorAddr >> 8) & 0xFF);SPI_Transmit(sectorAddr & 0xFF);CS_HIGH();// 4. 等待擦除完成(内部会自动开始,我们只需等待)SPI_FLASH_WaitForReady();
}
b) 写操作流程
void SPI_FLASH_WritePage(uint8_t* pBuffer, uint32_t writeAddr, uint16_t numByteToWrite) {// 1. 检查写入数量是否超出一页范围if (numByteToWrite > 256) return ERROR;// 2. 等待设备就绪SPI_FLASH_WaitForReady();// 3. 发送写使能SPI_FLASH_WriteEnable();// 4. 发送页编程命令和地址,然后发送数据CS_LOW();SPI_Transmit(0x02);SPI_Transmit((writeAddr >> 16) & 0xFF);SPI_Transmit((writeAddr >> 8) & 0xFF);SPI_Transmit(writeAddr & 0xFF);for (uint16_t i = 0; i < numByteToWrite; i++) {SPI_Transmit(pBuffer[i]);}CS_HIGH();// 5. 等待编程完成SPI_FLASH_WaitForReady();
}
注意:如果要写入的数据跨页,必须在第一页写满后,重新调用页编程命令从下一页的起始地址开始写。
c) 读操作流程
相对简单,无需写使能和等待。
void SPI_FLASH_ReadBuffer(uint8_t* pBuffer, uint32_t readAddr, uint16_t numByteToRead) {CS_LOW();SPI_Transmit(0x03); // 读数据命令SPI_Transmit((readAddr >> 16) & 0xFF);SPI_Transmit((readAddr >> 8) & 0xFF);SPI_Transmit(readAddr & 0xFF);for (uint16_t i = 0; i < numByteToRead; i++) {pBuffer[i] = SPI_Transmit(0xFF); // 发送哑数据,接收数据}CS_HIGH();
}
第三部分:关键注意点与踩坑指南(非常重要!)
1. 硬件相关
- 上拉电阻: SPI 总线的 CS、SCLK、MOSI 建议加上拉电阻(如 10K),确保初始状态稳定。
- 电源去耦: Flash 芯片的 VCC 和 GND 之间必须接一个 100nF 的陶瓷电容,并尽量靠近芯片引脚,以保证电源稳定。
- 电平匹配: 3.3V 的 Flash 不能直接与 5V 的 MCU 连接,需要电平转换电路。
- 布线: 高频下,SPI 线应尽量短,以减少信号反射和干扰。
2. 软件时序与协议
- 写使能是必须的: 任何改变存储内容的操作前,必须先发
0x06
命令。这个状态是易失的,一次写使能只对后续一次编程或擦除操作有效。 - 耐心等待 BUSY 位清除: 在发送编程、擦除命令后,绝对不能立即进行下一步操作。必须通过轮询状态寄存器的 BUSY 位来等待操作完成。使用延时函数
delay()
是极不推荐的,因为不同型号的 Flash 操作时间有差异,且浪费 CPU 资源。 - 跨页写入: 这是最常见的错误。如果写入数据的起始地址+长度会跨越页边界,超出部分会从该页的起始地址回绕覆盖,而不是写到下一页。软件必须处理页边界。
- 先擦后写: Flash 内存的特性是只能将 bit 从 1 改为 0,不能从 0 改为 1。擦除操作是将所有 bit 置 1。因此,在写入数据前,必须确保目标地址所在的扇区/块已经被擦除。尝试在未擦除的区域写入数据会导致数据错误。
3. 寿命与数据安全
- 擦写次数限制: NOR Flash 的典型擦写次数为 10 万次。频繁擦写同一区域会导致该区域损坏。
- 对策: 使用磨损均衡算法,将写操作分散到整个 Flash 的不同区域。对于文件系统或日志存储,建议使用现成的中间件如 LittleFS、SPIFFS,它们内置了磨损均衡和坏块管理。
- 数据损坏: 在写入过程中突然断电,可能导致数据错误或状态机混乱。
- 对策: 对于关键数据,使用事务机制或备份扇区。例如,先写备份区,验证无误后再设置有效标志。
- 读干扰: 虽然读操作不会磨损 Flash,但极度频繁地读取某一区域可能影响相邻单元的电荷,导致数据出错(现代 Flash 已较少见)。
- 对策: 必要时在固件中加入 ECC 校验。
4. 性能优化
- 使用 Quad SPI: 如果 MCU 和 Flash 都支持,可以启用四线模式,将数据线从 1 条增加到 4 条,极大提升读写速度。
- 使用 Fast Read: 优先使用
0x0B
命令而非0x03
,并合理设置 MCU SPI 时钟。 - DMA: 对于大数据量传输,使用 MCU 的 DMA 功能来搬运 SPI 数据,可以解放 CPU。
总结
开发 SPI Flash 驱动,核心是理解其 “命令-地址-数据” 的通信模型和 “先擦后写” 的基本原理。务必牢记两个黄金法则:
- 写之前先使能。
- 操作之后等忙完。
在实际项目中,建议将底层读写函数封装好,然后基于这些函数构建更高级的功能,如参数存储区或文件系统。仔细阅读你所使用型号的 Datasheet,它是解决一切疑难杂症的终极法宝。
好的,我们进行一次更深入、更全面的探讨。这不仅包括基础知识,还将涵盖高级主题、实战经验和行业最佳实践。
第零部分:核心概念再强化——像工程师一样思考
在深入细节前,务必建立两个核心思维模型:
- SPI Flash 是一个“状态机”:它永远不会“忘记”当前在做什么。你的驱动必须通过命令和状态寄存器来管理和跟踪它的状态。最严重的错误就是无视它的状态(比如在忙时发送新命令)。
- Flash 物理特性是根本约束:所有软件行为都源于其物理特性:“写”是充电(1→0),“擦除”是放电(全→1)。这导致了必须先擦后写、擦写寿命有限、读干扰等所有关键注意点。
第一部分:硬件层深入解析
1.1 SPI 模式与时钟极性/相位
这是嵌入式工程师最常踩的坑之一。SPI 模式由 CPOL 和 CPHA 决定。
模式 | CPOL | CPHA | 描述 |
---|---|---|---|
0 | 0 | 0 | 时钟空闲为低,数据在第一个时钟边沿(上升沿)采样 |
1 | 0 | 1 | 时钟空闲为低,数据在第二个时钟边沿(下降沿)采样 |
2 | 1 | 0 | 时钟空闲为高,数据在第一个时钟边沿(下降沿)采样 |
3 | 1 | 1 | 时钟空闲为高,数据在第二个时钟边沿(上升沿)采样 |
- 注意点:务必查阅 Flash 数据手册,确认它支持哪种模式。绝大多数 SPI Flash 支持 Mode 0 和 Mode 3。你的 MCU SPI 配置必须与 Flash 要求一致,否则通信完全失败。
1.2 高级 SPI 模式:提升性能的关键
- Dual SPI:将 MISO 和 MOSI 引脚都用作数据输出。命令阶段用标准 SPI,数据阶段可将读取速度提升一倍。相关命令如
0x3B
。 - Quad SPI:使用 4 条数据线进行数据传输。需要额外的 IO 引脚,但能极大提升读写速度。这是高性能应用的标配。相关命令如
0x6B
。启用 Quad 模式通常需要先配置状态寄存器中的特定位。 - QPI 模式:甚至命令和地址也通过 4 线传输,进一步提速。需要发送特定命令进入此模式。
1.3 硬件设计关键注意点
- PCB 布线:
- 等长布线:对于高时钟频率(>50MHz),SCK 到各 Flash 的走线应尽量等长,以减少时钟偏斜。
- 阻抗控制:高频信号线应做阻抗控制。
- 远离干扰源:SPI 走线应远离晶振、开关电源等噪声源。
- 电源质量:
- 去耦电容:除了 100nF 的陶瓷电容,在电源入口处最好并联一个 1-10uF 的钽电容或电解电容,以应对电流突变。
- 电源纹波:劣质 LDO 或 DCDC 的纹波可能导致 Flash 操作异常,尤其是在写入时。
第二部分:软件协议层深度剖析
2.1 命令序列详解(以 Winbond 为例)
1. 读数据流程优化:
- 标准读:
0x03
+ 24-bit addr。最可靠,但速度慢。 - 快速读:
0x0B
+ 24-bit addr + 8-bit dummy clocks。在 dummy clocks 期间,Flash 内部进行数据预取,从而允许主设备以更高时钟频率读取数据。这是最常用的读命令。 - 带模式的快速读:
0xEB
,用于 Quad 模式,dummy cycles 可能更多。
2. 写状态寄存器:
- 命令
0x01
。用于配置写保护、Quad 使能等。 - 注意:写状态寄存器也是一个“写操作”,需要先
Write Enable
,然后等待WIP
清除。
3. 释放掉电/深度掉电:
- 命令
0xAB
。有些 Flash 有超低功耗的深度掉电模式,唤醒需要特定的时序。
2.2 地址与数据格式
- 地址:通常是 24 位,可寻址 16MB。对于更大容量(>16MB)的 Flash,采用 32 位地址,命令字也不同(如
0x13
代替0x12
)。 - 数据:大端序。发送 24 位地址时,先发送最高位:
A23-A16
,A15-A8
,A7-A0
。
2.3 软件驱动架构设计
一个健壮的驱动应分为三层:
// 底层硬件抽象层
typedef struct {void (*CsLow)(void);void (*CsHigh)(void);uint8_t (*SpiTransfer)(uint8_t data);void (*DelayMs)(uint32_t ms);
} spi_flash_drv_t;// 中间命令层
static uint8_t SPI_Flash_ReadStatusReg(void) {drv->CsLow();drv->SpiTransfer(CMD_READ_STATUS_REG);uint8_t status = drv->SpiTransfer(0xFF);drv->CsHigh();return status;
}static void SPI_Flash_WriteEnable(void) {drv->CsLow();drv->SpiTransfer(CMD_WRITE_ENABLE);drv->CsHigh();
}// 上层应用接口
int SPI_Flash_Write(uint32_t addr, const uint8_t *data, uint32_t len) {// 参数检查if (addr + len > FLASH_SIZE) return -1;while (len > 0) {// 处理页边界uint32_t page_offset = addr % FLASH_PAGE_SIZE;uint32_t bytes_this_page = FLASH_PAGE_SIZE - page_offset;if (bytes_this_page > len) bytes_this_page = len;// 检查该扇区是否需要擦除(需自行维护擦除状态映射表)if (need_erase(addr, bytes_this_page)) {if (erase_sector_containing(addr) != 0) return -2;}// 页编程SPI_Flash_WaitForReady();SPI_Flash_WriteEnable();drv->CsLow();drv->SpiTransfer(CMD_PAGE_PROGRAM);drv->SpiTransfer((addr >> 16) & 0xFF);drv->SpiTransfer((addr >> 8) & 0xFF);drv->SpiTransfer(addr & 0xFF);for (uint32_t i = 0; i < bytes_this_page; i++) {drv->SpiTransfer(data[i]);}drv->CsHigh();SPI_Flash_WaitForReady();// 更新指针和长度addr += bytes_this_page;data += bytes_this_page;len -= bytes_this_page;}return 0;
}
第三部分:高级主题与实战经验
3.1 磨损均衡与坏块管理
当 Flash 用作文件系统或频繁更新数据时,必须考虑。
- 磨损均衡:
- 思想:将写操作平均分布到所有存储单元,避免某些扇区过早达到擦写上限。
- 实现:通常需要维护一个逻辑地址到物理地址的映射表。每次写入时,选择擦写次数最少的物理块。强烈建议使用成熟的文件系统,如 LittleFS、SPIFFS,它们内置了磨损均衡算法。
- 坏块管理:
- 原因:Flash 出厂时就可能有坏块,或在寿命期内产生新坏块。
- 管理:文件系统或驱动应能识别并跳过坏块。识别方法通常是读取操作后的状态寄存器,或对已写入数据进行 ECC 校验。
3.2 数据可靠性与掉电保护
- 掉电危险:在页编程或扇区擦除过程中掉电,可能导致:
- 数据写入不完整。
- 状态机混乱,芯片“死锁”。
- 保护策略:
- 事务设计:关键数据采用“准备-提交”机制。
- 准备阶段:将数据写入一个临时区域。
- 提交阶段:写入一个特殊的标志位到另一个安全位置,表示数据有效。
- 恢复阶段:上电后检查标志位,若有效则将临时数据复制到正式位置。
- 写保护引脚:硬件上使用 MCU 的 GPIO 控制 Flash 的
WP#
引脚。在非写操作期间拉低此引脚,硬件级防止误写。 - 监控电源:使用 MCU 的电压检测电路,在检测到电压跌落时,立即终止 Flash 操作并置位写保护。
- 事务设计:关键数据采用“准备-提交”机制。
3.3 性能优化技巧
- 使用 DMA:对于大数据量读写,配置 SPI 的 DMA 传输,解放 CPU。
- 启用 Quad 模式:这是最有效的提速手段。
- 缓存与批量操作:尽量减少小的、随机的写操作。在 RAM 中缓存数据,达到一定量后再一次性写入 Flash。
- 后台擦除:在系统空闲时,提前擦除一些空闲扇区,这样在需要写入时可以直接进行,避免等待擦除的时间。
3.4 启动加载与 XIP
- XIP:片内执行。当 Flash 连接到 MCU 的内存映射区域时,CPU 可以像读取内存一样直接读取 Flash 中的代码并执行,无需先加载到 RAM。
- 条件:需要支持 Memory-Mapped 模式的控制器(如 STM32 的 Quad-SPI 外设)。Flash 本身也需要支持对应的快速读命令。
- 优势:极大节省 RAM,加速启动。
第四部分:调试与测试指南
4.1 常见问题与排查
问题现象 | 可能原因 | 排查方法 |
---|---|---|
完全无法通信 | 1. 电源问题 2. SPI 模式配置错误 3. CS 引脚控制错误 4. 硬件连接问题 | 1. 测量电压 2. 用逻辑分析仪抓取 SPI 波形,看时钟和数据是否符合预期模式 3. 检查 CS 引脚时序 |
能读 ID,但无法写入 | 1. 未发送 Write Enable 2. 写保护生效(状态寄存器/WP#引脚) 3. 目标地址未擦除 | 1. 检查代码是否在写操作前发送了 0x06 2. 读取状态寄存器,检查 WEL 和 BP 位3. 先执行擦除操作 |
写入的数据部分错误 | 1. 跨页写入未处理 2. 时钟频率过高,信号质量差 3. 电源纹波大 | 1. 检查驱动代码的页边界处理逻辑 2. 降低 SPI 时钟频率,用示波器查看信号完整性 3. 检查电源波形 |
操作一段时间后失效 | 1. 未正确等待 WIP 位清除 2. 频繁擦写导致局部坏块 | 1. 在每次操作后加强 WIP 轮询,并加入超时判断2. 引入磨损均衡机制 |
4.2 必备工具
- 逻辑分析仪:如 Saleae,是调试 SPI 协议的神器。可以清晰看到命令、地址、数据的每一位。
- 示波器:用于检查信号质量和电源纹波。
- 万用表:检查电源和基本连通性。
总结
成为一名精通 SPI Flash 的嵌入式工程师,需要:
- 理解物理本质:从硅的物理特性理解所有约束。
- 掌握协议细节:熟练运用命令集和状态机。
- 重视硬件基础:良好的硬件设计是稳定运行的基石。
- 设计健壮软件:考虑边界条件、错误处理、掉电保护。
- 善用高级特性:通过磨损均衡、XIP 等提升产品竞争力。
- 熟练使用工具:逻辑分析仪和示波器是延伸的眼睛。
最终,数据手册是你的圣经。不同厂商、不同型号的 Flash 在命令字、dummy cycles 等细节上可能存在差异,开发前务必仔细阅读对应型号的 Datasheet。
将外部 SPI Flash 映射为 GD32 MCU 的可执行内存空间(XIP,eXecute In Place)是一个高级应用。这需要硬件和软件的双重配合。下面我详细说明实现步骤。
硬件要求
1. MCU 要求
必须使用带有 QSPI 内存映射模式的 GD32 型号,例如:
- GD32F4xx 系列(如 GD32F450、GD32F470)
- GD32E5xx 系列
- GD32F3xx 系列的部分型号
检查要点:查看数据手册,确认 MCU 的 QSPI 外设支持 Memory Mapped Mode。
2. 硬件连接
必须使用 QSPI 接口,而不仅仅是标准 SPI:
GD32 MCU ↔ SPI Flash
QSPI_CLK → CLK
QSPI_CS → CS#
QSPI_IO0 → DI/IO0 (MOSI)
QSPI_IO1 → DO/IO1 (MISO)
QSPI_IO2 → WP#/IO2
QSPI_IO3 → HOLD#/IO3
注意:必须连接全部 4 条数据线(IO0-IO3)才能实现内存映射。
软件实现步骤
步骤 1:配置 QSPI 外设
#include "gd32f4xx.h"void QSPI_Configuration(void)
{/* 启用时钟 */rcu_periph_clock_enable(RCU_QSPI);/* 配置 QSPI 引脚 */gpio_af_set(GPIOA, GPIO_AF_10, GPIO_PIN_6); // QSPI_CLKgpio_af_set(GPIOA, GPIO_AF_9, GPIO_PIN_7); // QSPI_CSgpio_af_set(GPIOF, GPIO_AF_9, GPIO_PIN_6); // QSPI_IO0gpio_af_set(GPIOF, GPIO_AF_9, GPIO_PIN_7); // QSPI_IO1gpio_af_set(GPIOF, GPIO_AF_9, GPIO_PIN_8); // QSPI_IO2gpio_af_set(GPIOF, GPIO_AF_9, GPIO_PIN_9); // QSPI_IO3gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);// ... 类似配置其他引脚/* QSPI 外设配置 */qspi_parameter_struct qspi_struct;qspi_struct.prescaler = 2; // 时钟分频qspi_struct.fifo_threshold = 4; // FIFO 阈值qspi_struct.sample_shift = QSPI_SAMPLE_SHIFT_HALF; // 采样时机qspi_struct.cs_high_time = QSPI_CS_HIGHTIME_2CYCLE; // CS 高电平时间qspi_init(QSPI, &qspi_struct);
}
步骤 2:初始化 SPI Flash 并进入 Quad 模式
void QSPI_Flash_Init(void)
{/* 1. 标准 SPI 模式初始化 Flash */QSPI_SoftwareMode_Enable(); // 先进入软件模式/* 读取 ID,确认通信正常 */uint8_t id_buf[3];QSPI_Flash_ReadID(id_buf);/* 2. 使能 Quad 模式 */QSPI_Flash_WriteEnable();QSPI_Flash_WriteStatusReg(0x40); // 设置 Quad 使能位(具体看 Flash 手册)QSPI_Flash_WaitForReady();/* 3. 验证 Quad 模式 */uint8_t status = QSPI_Flash_ReadStatusReg();if((status & 0x40) == 0) {// Quad 模式使能失败,需要处理错误}
}
步骤 3:配置内存映射模式
void QSPI_Enable_MemoryMappedMode(void)
{qspi_command_struct cmd_struct;/* 配置 QSPI 命令 */qspi_command_struct_init(&cmd_struct);cmd_struct.instruction = 0xEB; // Quad 快速读命令cmd_struct.address_size = QSPI_ADDRESS_24_BITS; // 24位地址cmd_struct.instruction_mode = QSPI_INS_MODE_SINGLE; // 指令单线cmd_struct.address_mode = QSPI_ADDRESS_MODE_SINGLE; // 地址单线cmd_struct.alt_bytes_mode = QSPI_ALT_BYTES_MODE_NONE; // 无交替字节cmd_struct.dummy_cycles = 6; // 哑周期数(根据 Flash 手册调整)cmd_struct.data_mode = QSPI_DATA_MODE_QUAD; // 数据阶段使用 4 线cmd_struct.functional_mode = QSPI_FUNC_MODE_MM; // 关键:内存映射模式/* 应用配置 */qspi_command_config(QSPI, &cmd_struct);/* 使能内存映射模式 */qspi_memory_mapped_mode_enable(QSPI);
}
步骤 4:修改链接脚本(分散加载文件)
这是最关键的一步,告诉链接器将代码段放到外部 Flash 的地址空间。
GD32 的内存映射通常为:
- 内部 Flash:
0x08000000 - 0x08FFFFFF
- 内部 SRAM:
0x20000000 - 0x2007FFFF
- QSPI 内存映射区域:
0x90000000 - 0x9FFFFFFF
修改链接脚本示例(GCC):
MEMORY
{FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K /* 内部 Flash */RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K /* 内部 RAM */QSPI (rx) : ORIGIN = 0x90000000, LENGTH = 8M /* 外部 QSPI Flash */
}SECTIONS
{/* 中断向量表必须放在内部 Flash 以确保启动速度 */.isr_vector :{. = ALIGN(4);KEEP(*(.isr_vector)). = ALIGN(4);} >FLASH/* 将部分代码段放到外部 QSPI */.qsi_text :{. = ALIGN(4);*qspi_code.o(.text .text.*) /* 指定目标文件 */*lib_gui.a:(.text .text.*) /* 图形库等大体积代码 */*(.qspi_text .qspi_text.*) /* 特殊段名 */. = ALIGN(4);} >QSPI AT>FLASH /* 在 QSPI 执行,但存储在内部 Flash 用于初始化 *//* 内部 Flash 的文本段 */.text :{. = ALIGN(4);*(.text .text.*). = ALIGN(4);} >FLASH
}
步骤 5:代码分区和初始化
1. 标记需要放在外部 Flash 的函数:
/* 定义特殊段属性 */
#define QSPI_FUNC __attribute__((section(".qspi_text")))/* 被标记的函数将被链接到外部 Flash */
QSPI_FUNC void GUI_DrawBitmap(uint32_t x, uint32_t y, const uint8_t *bitmap)
{// 图形绘制函数,体积较大// ...
}QSPI_FUNC void DataProcessing_Algorithm(float *input, float *output)
{// 复杂算法函数// ...
}
2. 启动时初始化 QSPI 内存映射:
int main(void)
{/* 硬件初始化 */system_init();QSPI_Configuration();QSPI_Flash_Init();/* 在调用外部 Flash 中的函数前,启用内存映射 */QSPI_Enable_MemoryMappedMode();/* 现在可以安全调用外部 Flash 中的函数 */GUI_DrawBitmap(0, 0, my_bitmap);while(1) {// 主循环}
}
关键注意事项和陷阱
1. 性能优化
- 启用缓存:GD32 的 Flash 访问通常有缓存机制,确保启用。
- 指令预取:配置合适的预取缓冲区大小。
- 时钟速度:QSPI 时钟可以设置较高,但要确保 Flash 支持。
2. 中断延迟
- 外部 Flash 的读取速度比内部 Flash 慢,可能增加中断响应时间。
- 关键中断服务函数务必放在内部 Flash。
3. 初始化顺序陷阱
// 错误示例:在启用内存映射前调用外部函数
void function_in_internal_flash(void)
{external_function(); // 错误!此时内存映射还未建立QSPI_Enable_MemoryMappedMode();
}// 正确示例
void function_in_internal_flash(void)
{QSPI_Enable_MemoryMappedMode(); // 先建立映射external_function(); // 再调用
}
4. 调试技巧
- 硬故障处理:在 HardFault_Handler 中检查是否因访问未初始化的 QSPI 区域引起。
- 性能分析:比较函数在内部和外部 Flash 的执行时间。
5. 启动加载器(Bootloader)考虑
如果使用 bootloader,需要确保 bootloader 也能正确初始化 QSPI,或者应用程序自己负责初始化。
验证步骤
- 编译检查:查看 map 文件,确认函数被正确分配到
0x90000000
区域。 - 硬件测试:单步调试,在调用外部 Flash 函数前后设置断点。
- 性能测试:测量函数执行时间,与内部 Flash 对比。
总结
实现 GD32 MCU 的 QSPI 内存映射需要:
- 硬件:支持 QSPI 的 MCU + 正确连接
- 驱动:正确配置 QSPI 外设和 Flash
- 链接:修改链接脚本,合理分配代码段
- 初始化:确保在调用外部函数前建立内存映射
这种技术非常适合存储大容量代码(如图形库、文件系统、算法库),可以极大扩展可用存储空间。