ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SPI NOR Flash实战:GD25Q80E命令时序与STM32 QSPI配置详解

SPI NOR Flash实战:GD25Q80E命令时序与STM32 QSPI配置详解 1. 为什么我要认真聊聊 SPI NOR Flash 这件事搞嵌入式的人迟早会跟 SPI NOR Flash 打交道。你可能只是想存个配置参数、放几张图片、做个字库或者跑个 OTA 升级结果一翻手册几十条命令、一堆时序图、四种工作模式直接劝退。更别提 STM32 的 QSPI 外设寄存器又多又绕很多人调了几天连 JEDEC ID 都读不出来。我自己第一次用 GD25Q80E 的时候就踩了个经典坑命令发出去数据读回来全是 0xFF查了半天以为是焊接问题最后发现是 QSPI 的 dummy cycles 没配对。这种问题手册上写得明明白白但你不踩一次根本不会去注意。这篇内容就是把我这些年用 SPI NOR Flash 的经验完整梳理一遍。从 GD25Q80E 这颗具体芯片的命令时序讲起到 STM32 QSPI 外设的配置逻辑再到实际读写擦除的完整代码实现最后把常见问题和排查思路整理成速查表。不管你是刚接触 Flash 的新手还是已经用过但总觉得没吃透的老手应该都能从里面找到有用的东西。GD25Q80E 是兆易创新的一款 8Mbit1MBSPI NOR Flash支持标准 SPI、Dual SPI、Quad SPI 三种模式工作电压 2.7V~3.6V100000 次擦写寿命数据保存 20 年。这个规格在嵌入式项目里非常典型学会了它换到 W25Q 系列、IS25 系列基本是平移的事。2. 先搞懂 SPI NOR Flash 的底层逻辑2.1 NOR Flash 和 NAND Flash 到底差在哪很多人知道 Flash 分 NOR 和 NAND但说不清楚区别。我用一个生活化的类比来解释NOR Flash 像图书馆的书架每本书每个字节都有固定位置你可以直接走到任何一本书前面拿下来看不需要从头翻。NAND Flash 像一卷胶卷你要看第 500 帧得从头快进过去。这个差异来自它们的内部结构。NOR Flash 的每个存储单元并联在 bit line 上支持随机访问所以可以直接按地址读取任意字节。NAND Flash 的存储单元串联在一起必须按页page读取、按块block擦除。实际项目中这意味着什么NOR Flash 适合存代码、存配置、做 XIPExecute In Place因为读取速度快、随机访问方便。NAND Flash 适合存大容量数据比如文件系统、音视频因为单位成本低、容量大。你不可能用 NOR Flash 存几个 GB 的视频也不应该用 NAND Flash 直接跑代码。还有一个关键区别是可靠性。NOR Flash 的坏块率极低基本出厂就是全好块不需要做坏块管理。NAND Flash 出厂就可能有坏块用久了还会新增坏块必须做 ECC 校验和坏块管理。这也是为什么小容量存储场景大家更愿意用 NOR Flash省心。2.2 GD25Q80E 的存储组织结构GD25Q80E 的 8Mbit 容量是这样组织的层级大小数量说明页Page256 字节4096 页最小写入单位扇区Sector4KB256 个扇区最小擦除单位4KB擦除块Block64KB16 个块支持 32KB/64KB 块擦除整片1MB1 片支持整片擦除这里有个非常重要的概念Flash 的写入只能把 1 变成 0不能把 0 变成 1。擦除操作才是把整个扇区恢复成 0xFF全 1。所以你要修改一个字节流程必须是读出整个扇区到 RAM → 在 RAM 里修改 → 擦除扇区 → 把修改后的数据写回去。这个读-改-写流程是 Flash 操作的核心模式后面讲代码的时候会反复用到。注意擦除操作的最小单位是扇区4KB不是字节。你不能只擦一个字节。这是新手最容易犯的错误之一。2.3 SPI 通信模式Mode 0/1/2/3 的选择SPI 有四种通信模式由 CPOL时钟极性和 CPHA时钟相位组合决定模式CPOLCPHA空闲时钟电平采样边沿Mode 000低上升沿Mode 101低下降沿Mode 210高下降沿Mode 311高上升沿GD25Q80E 支持 Mode 0 和 Mode 3。实际用的时候选哪个我一般用 Mode 0因为大部分 MCU 的 SPI 外设默认就是 Mode 0配置简单。但如果你发现读出来的数据不稳定可以试试 Mode 3有些板子的信号完整性在 Mode 3 下更好。这里有个经验SPI 时钟频率越高信号完整性越重要。GD25Q80E 标准 SPI 模式下最高支持 133MHz快速读命令但实际能跑多快取决于你的 PCB 走线、上拉电阻、电源质量。我一般先在低速比如 1MHz下调通功能再逐步提高频率测试稳定性。3. GD25Q80E 命令时序深度拆解3.1 命令集概览哪些命令必须掌握GD25Q80E 的命令有几十条但常用的就那么十来条。我把它们分成几类识别类命令0x9F读 JEDEC ID返回 3 字节厂商 ID 存储类型 容量0x90读制造商 ID 和设备 ID0xAB释放掉电/读设备 ID状态类命令0x05读状态寄存器 1BUSY 位、WEL 位、BP 位0x35读状态寄存器 2QE 位、SRP 位0x01写状态寄存器0x06写使能Write Enable0x04写禁止Write Disable读取类命令0x03标准读最高 50MHz0x0B快速读最高 133MHz需要 dummy cycle0x3B双线快速读0x6B四线快速读0xEB四线快速读需要 6 个 dummy cycle编程类命令0x02页编程最多 256 字节0x32四线页编程0x204KB 扇区擦除0x5232KB 块擦除0xD864KB 块擦除0xC7/0x60整片擦除配置类命令0x31写状态寄存器 2设置 QE 位0x15读状态寄存器 3这些命令里0x06写使能是最容易被忽略但最关键的。每次写操作页编程、擦除、写状态寄存器之前都必须先发 0x06否则操作不会执行。我见过太多人调了半天写不进去最后发现是忘了发写使能。3.2 读 JEDEC ID验证通信是否正常的第一步读 JEDEC ID 是调试 SPI Flash 的第一步因为它不需要擦除、不需要写使能只要通信正常就能读到固定值。时序是这样的CS 拉低发送 0x9F1 字节读取 3 字节数据CS 拉高GD25Q80E 的 JEDEC ID 是 0xC8 0x40 0x140xC8厂商 ID兆易创新0x40存储类型SPI NOR0x14容量代码8Mbit 1MB如果你读出来是 0x00 0x00 0x00 或者 0xFF 0xFF 0xFF说明通信有问题。0xFF 通常是 MISO 线没接好或者 Flash 没供电0x00 通常是 MOSI 或 CLK 有问题。实操心得读 JEDEC ID 的时候SPI 时钟先设到 1MHz 以下确保信号稳定。等确认能读到了再逐步提高频率。3.3 读状态寄存器判断忙闲和写使能状态状态寄存器 1 的位定义Bit名称说明7SRP0状态寄存器保护位6BP2块保护位 25BP1块保护位 14BP0块保护位 03TB顶部/底部保护2SEC扇区/块保护1WEL写使能锁存0BUSY忙标志BUSY 位Bit 0是最常用的。擦除和编程操作需要时间4KB 扇区擦除典型 45ms页编程典型 0.7ms。在这段时间内Flash 不响应新命令你必须轮询 BUSY 位直到它变成 0。WEL 位Bit 1用来确认写使能是否生效。发完 0x06 之后读状态寄存器如果 WEL 是 1说明写使能成功如果是 0说明写使能没生效后面的写操作肯定失败。读状态寄存器的时序CS 拉低发送 0x05读取 1 字节可以连续读但一般读一次就够CS 拉高3.4 页编程时序256 字节的边界陷阱页编程命令 0x02 的时序CS 拉低发送 0x02发送 3 字节地址24 位发送最多 256 字节数据CS 拉高等待 BUSY 位变 0这里有个大坑页编程不能跨页。如果你从地址 0x0000F0 开始写 32 字节会写到 0x0000F0~0x00010F但 0x000100 是下一页的起始地址数据会回卷到 0x000000 开始覆盖。这不是 bug是 Flash 的设计特性。所以写数据的时候必须做页对齐处理。我的做法是把要写的数据按页边界切分每段不超过 256 字节且不跨页。void flash_write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t page_remain; while (len 0) { page_remain 256 - (addr % 256); uint32_t write_len (len page_remain) ? len : page_remain; flash_page_program(addr, data, write_len); addr write_len; data write_len; len - write_len; } }3.5 扇区擦除时序4KB 对齐是硬性要求扇区擦除命令 0x20 的时序发送 0x06写使能CS 拉低发送 0x20发送 3 字节地址CS 拉高等待 BUSY 位变 0典型 45ms最大 400ms地址必须是 4KB 对齐的也就是说地址的低 12 位必须是 0。如果你传了个 0x00001000那没问题如果传 0x00001001芯片会忽略低 12 位实际擦除的还是 0x00001000 开始的 4KB。注意擦除时间比较长轮询 BUSY 位的时候要加超时机制。我一般设 1 秒超时超过就报错。正常不会超过 400ms但如果电源不稳或者芯片有问题可能会卡住。4. STM32 QSPI 外设配置实战4.1 为什么用 QSPI 而不是普通 SPI普通 SPI 外设只能单线传输MOSI 和 MISO 各一根线而 QSPI 支持四线并行传输IO0~IO3 四根数据线。理论上 QSPI 的吞吐量是普通 SPI 的四倍。但 QSPI 的优势不只是速度。STM32 的 QSPI 外设支持内存映射模式Memory-Mapped Mode可以把 Flash 映射到 MCU 的地址空间像读内存一样直接读 Flash 数据。这对 XIP 执行代码或者直接读取字库、图片非常方便。STM32 的 QSPI 外设还支持自动轮询、命令序列自定义等功能可以灵活适配各种 Flash 芯片。不过灵活性也带来了配置复杂度寄存器多、参数多第一次配容易懵。4.2 硬件连接哪些引脚必须接以 STM32F4/F7/H7 系列的 QSPI 为例典型连接QSPI 信号STM32 引脚Flash 引脚说明CLKPB2CLK时钟CSPB6CS#片选IO0PC9DI/IO0数据线 0IO1PC10DO/IO1数据线 1IO2PE2WP#/IO2数据线 2 / 写保护IO3PD13HOLD#/IO3数据线 3 / 保持注意 IO2 和 IO3 在标准 SPI 模式下是 WP# 和 HOLD# 功能在 Quad 模式下才作为数据线使用。如果你只用标准 SPI 模式这两个引脚可以不接但建议接上并上拉到 VCC防止误触发写保护。实操心得CLK 和 CS 线上建议串 22Ω~33Ω 的电阻可以改善信号完整性。特别是 CLK 频率超过 50MHz 的时候这个电阻能明显减少过冲和振铃。4.3 QSPI 初始化从时钟到参数STM32 QSPI 的初始化分几步第一步配置 GPIO 复用功能GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); // CLK gpio.Pin GPIO_PIN_2; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF9_QUADSPI; HAL_GPIO_Init(GPIOB, gpio); // CS gpio.Pin GPIO_PIN_6; HAL_GPIO_Init(GPIOB, gpio); // IO0, IO1 gpio.Pin GPIO_PIN_9 | GPIO_PIN_10; HAL_GPIO_Init(GPIOC, gpio); // IO3 gpio.Pin GPIO_PIN_13; HAL_GPIO_Init(GPIOD, gpio); // IO2 gpio.Pin GPIO_PIN_2; HAL_GPIO_Init(GPIOE, gpio);第二步配置 QSPI 外设参数QSPI_HandleTypeDef hqspi {0}; hqspi.Instance QUADSPI; hqspi.Init.ClockPrescaler 1; // 时钟分频1 表示 2 分频 hqspi.Init.FifoThreshold 4; // FIFO 阈值 hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize 22; // 2^22 4MB 地址空间 hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_4_CYCLE; hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; hqspi.Init.FlashID QSPI_FLASH_ID_1; hqspi.Init.DualFlash QSPI_DUALFLASH_DISABLE; HAL_QSPI_Init(hqspi);这里几个参数需要解释ClockPrescalerQSPI 时钟 AHB 时钟 / (Prescaler 1)。如果 AHB 是 200MHzPrescaler 设 1QSPI 时钟就是 100MHz。GD25Q80E 在 Quad 模式下最高支持 133MHz所以 100MHz 是安全的。SampleShifting采样偏移。QSPI_SAMPLE_SHIFTING_HALFCYCLE 表示在时钟边沿后半个周期采样可以改善高速下的信号完整性。如果读数据不稳定可以试试改成 QSPI_SAMPLE_SHIFTING_NONE。FlashSizeFlash 容量对应的地址位数。8Mbit 1MB 2^20所以应该设 20。但很多人设 22 也没问题因为地址空间大一点不影响只要不超过 32 位就行。ChipSelectHighTimeCS 拉高后的保持时间。GD25Q80E 要求最小 50nsQSPI_CS_HIGH_TIME_4_CYCLE 在 100MHz 下是 40ns稍微不够建议设 QSPI_CS_HIGH_TIME_6_CYCLE 或更高。4.4 命令序列配置QSPI 的核心机制STM32 QSPI 最核心的概念是命令序列。每条操作读 ID、读数据、写数据、擦除都对应一个命令序列包含以下阶段阶段说明可配置项Instruction发送命令码模式无/单线/双线/四线、值Address发送地址模式、大小8/16/24/32位、值Alternate Bytes发送交替字节模式、大小、值Dummy Cycles空周期数量Data传输数据模式、长度每个阶段的模式可以是QSPI_CCR_IMODE_NONE跳过该阶段QSPI_CCR_IMODE_1_LINE单线QSPI_CCR_IMODE_2_LINES双线QSPI_CCR_IMODE_4_LINES四线以读 JEDEC ID 为例命令序列配置QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x9F; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 3; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; uint8_t id[3]; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, id, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);读数据快速读0x0B的命令序列cmd.Instruction 0x0B; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Address addr; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 8; // 0x0B 需要 8 个 dummy cycles cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData len;Quad 读0xEB的命令序列cmd.Instruction 0xEB; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Address addr; cmd.AddressMode QSPI_ADDRESS_4_LINES; // 地址四线传输 cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_4_LINES; cmd.AlternateBytes 0x00; // 模式字节 cmd.AlternateBytesSize QSPI_ALTERNATE_BYTES_8_BITS; cmd.DummyCycles 6; // 0xEB 需要 6 个 dummy cycles cmd.DataMode QSPI_DATA_4_LINES; // 数据四线传输 cmd.NbData len;注意0xEB 命令的 dummy cycles 是 6 个不是 8 个。这个参数如果设错读出来的数据会整体偏移表现为第一个字节丢失或者多出垃圾数据。我踩过这个坑查了一整天才发现是 dummy cycles 的问题。5. 完整读写擦除代码实现5.1 写使能与等待忙闲void qspi_write_enable(void) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x06; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_NONE; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); } uint8_t qspi_read_status(void) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x05; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; uint8_t status 0; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, status, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); return status; } void qspi_wait_busy(void) { uint32_t timeout 1000000; while ((qspi_read_status() 0x01) timeout--); if (timeout 0) { // 超时处理 } }5.2 扇区擦除实现void qspi_sector_erase(uint32_t addr) { qspi_write_enable(); qspi_wait_busy(); QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x20; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Address addr; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_NONE; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }5.3 页编程实现void qspi_page_program(uint32_t addr, uint8_t *data, uint16_t len) { if (len 256) len 256; qspi_write_enable(); qspi_wait_busy(); QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x02; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Address addr; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData len; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, data, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }5.4 数据读取实现void qspi_read(uint32_t addr, uint8_t *buf, uint32_t len) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x0B; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.Address addr; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 8; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData len; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, buf, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); }5.5 使能 Quad 模式GD25Q80E 默认是标准 SPI 模式要用 Quad 模式必须先设置状态寄存器 2 的 QE 位Bit 1。void qspi_enable_quad_mode(void) { qspi_write_enable(); qspi_wait_busy(); // 读状态寄存器 2 QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x35; cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; uint8_t sr2 0; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, sr2, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); // 设置 QE 位 sr2 | 0x02; // 写状态寄存器 2 qspi_write_enable(); cmd.Instruction 0x31; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 1; HAL_QSPI_Command(hqspi, cmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, sr2, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); qspi_wait_busy(); }6. 常见问题与排查技巧实录6.1 读出来全是 0xFF 或 0x00这是最常见的问题排查思路如下现象可能原因排查方法全 0xFFMISO 未连接、Flash 未供电、CS 未拉低万用表测电压、示波器看波形全 0x00MOSI 或 CLK 未连接、GPIO 复用配置错误检查 GPIO 配置、示波器看 CLK部分正确时序问题、dummy cycles 错误降低时钟频率、检查 dummy cycles随机数据信号完整性问题、电源噪声加去耦电容、串匹配电阻我的经验是先用示波器看 CLK、CS、MOSI、MISO 四根线的波形。如果 CLK 没有输出说明 QSPI 外设没使能或者 GPIO 复用没配对。如果 CS 一直是高说明片选逻辑有问题。如果 MOSI 有数据但 MISO 没反应说明 Flash 没工作或者 MISO 线断了。6.2 写不进去数据写操作失败通常有这几个原因忘记发写使能0x06这是最常见的。每次页编程或擦除之前都必须发 0x06而且发完之后要确认 WEL 位是 1。目标地址没擦除Flash 只能把 1 写成 0如果目标地址之前写过数据有 0直接写新数据会失败。必须先擦除。状态寄存器保护位被设置BP0~BP2 位如果被设置对应区域的写操作会被禁止。读状态寄存器确认这些位是 0。写使能后发了其他命令写使能是一个锁存状态发完 0x06 之后如果发了其他命令除了写操作WEL 位会被清除。所以 0x06 之后要紧接着发写命令。6.3 Quad 模式读数据错位用 0xEB 命令读数据时如果 dummy cycles 设错数据会整体偏移。GD25Q80E 的 0xEB 命令需要 6 个 dummy cycles不是 8 个。如果你设成 8 个读出来的数据会多两个字节的垃圾后面的数据全部后移。排查方法读一个已知内容比如 JEDEC ID 或者之前写入的固定数据对比读出来的内容看偏移量是多少。如果偏移了 2 个字节说明 dummy cycles 多了 2 个。6.4 擦除时间过长或超时4KB 扇区擦除典型 45ms最大 400ms。如果你发现擦除经常超时可能是电源电压偏低导致擦除效率下降Flash 芯片老化擦写次数接近上限轮询 BUSY 位的代码有问题比如读状态寄存器时没正确拉 CS实操心得擦除和编程操作期间尽量不要频繁读状态寄存器。我一般每 1ms 读一次既能及时检测到完成又不会占用太多总线时间。6.5 QSPI 内存映射模式读数据异常内存映射模式Memory-Mapped Mode配置好之后可以直接用指针读 Flash 数据。但有几个坑地址对齐映射后的基地址是 0x90000000对于 QSPI Bank 1但 Flash 内部的地址偏移要加上去。比如读 Flash 地址 0x1000 的数据指针应该是 0x90000000 0x1000。Cache 问题如果 MCU 有 Cache映射区域的读数据可能被缓存。修改 Flash 内容后需要失效对应的 Cache 行才能读到新数据。Dummy cycles 配置内存映射模式也需要配置 dummy cycles而且这个配置在进入映射模式之前就要设好进入之后不能改。7. 性能优化与进阶技巧7.1 提高读取速度的几种手段用 Quad 模式0xEB 命令比 0x0B 快四倍数据线从 1 根变 4 根。但需要先使能 QE 位。提高时钟频率GD25Q80E 在 Quad 模式下最高 133MHz。STM32 QSPI 的时钟源是 AHB通过分频得到。如果 AHB 是 200MHz分频系数设 1 得到 100MHz设 0 得到 200MHz可能超过 Flash 上限。用内存映射模式省去每次发命令的开销直接读地址。适合频繁读取的场景。开启 QSPI 的 DDR 模式双沿采样理论速度翻倍。但 GD25Q80E 不支持 DDR这个功能用不上。7.2 擦写寿命管理与磨损均衡GD25Q80E 的擦写寿命是 100000 次。如果你频繁写同一个扇区很快就会坏。简单的磨损均衡思路把配置数据分成两份交替写入两个扇区每次写入时记录一个计数器写到一定次数后切换到另一个扇区读取时比较两个扇区的计数器取最新的那份这个方案实现简单能把寿命翻倍。如果数据量不大还可以用更多扇区做轮询。7.3 OTA 升级中的 Flash 分区设计做 OTA 升级时Flash 通常分成几个区域区域大小说明Bootloader32KB引导程序负责跳转和升级App A200KB应用程序 AApp B200KB应用程序 B备份/升级目标Config4KB配置参数Flag4KB升级标志、版本号升级流程Bootloader 检查 Flag 区如果有升级标志把新固件从 App B 拷贝到 App A清除标志跳转到 App A。如果没有升级标志直接跳转到 App A。这个设计的关键是 Flag 区的读写要可靠。我一般用两个扇区做冗余每个扇区存两份数据加 CRC 校验确保掉电不会丢标志。8. 我踩过的那些坑和最后的小技巧调 SPI NOR Flash 这些年踩过的坑真不少。有一次用 GD25Q80E 做字库存储读出来的汉字全是乱码查了两天发现是地址算错了——字库文件的偏移地址没加上 Flash 的基地址。还有一次做 OTA升级完重启后程序跑飞最后发现是 Bootloader 跳转前没关中断中断向量表还指向旧程序。几个我觉得比较实用的小技巧第一调试初期一定要用逻辑分析仪或者示波器看波形。光靠读代码很难发现时序问题看到实际波形就一目了然了。第二写操作之前先读一遍目标区域。如果读出来不是 0xFF说明没擦除直接写会失败。这个检查能省很多调试时间。第三状态寄存器的 BUSY 位轮询要加超时。我见过 Flash 因为电源问题卡在 BUSY 状态不出来的情况没有超时机制的话程序就死在那里了。第四Quad 模式使能后最好读一次状态寄存器确认。有些批次的芯片 QE 位写入后需要额外的时间才能生效读一次确认更稳妥。第五PCB 布局时 Flash 尽量靠近 MCU。SPI 时钟线走线过长会导致信号质量下降特别是频率超过 50MHz 的时候。如果实在要拉远记得加匹配电阻和去耦电容。GD25Q80E 这颗芯片我用过很多项目从简单的参数存储到复杂的 OTA 升级稳定性一直不错。关键是命令时序要吃透QSPI 配置要仔细剩下的就是多调多试。希望这篇内容能帮你少走一些弯路。
返回列表