GD32F103驱动GD25Q128 SPI Flash:硬件连接、软件驱动与调试避坑指南
1. 项目概述:为什么是GD32F10x与GD25Q128的组合?
在嵌入式开发中,外部存储是扩展系统数据容量的关键。当你手头的STM32系列芯片缺货或成本压力增大时,国产的GD32系列MCU就成了一个非常靠谱的替代选择。我最近在一个数据采集项目里,就用GD32F103C8T6这颗“国民MCU”去驱动一颗128Mbit(16MB)的SPI Flash芯片GD25Q128,用来存储设备运行日志和配置参数。这个组合性价比极高,GD32F10x系列完全兼容STM32F10x的库函数和开发环境,而GD25Q128又是Winbond W25Q128的常见替代品,生态成熟。但实际操作起来,从引脚配置、SPI模式选择到具体的读写擦除时序,每一步都有细节需要注意,直接照搬STM32的代码可能会在GD32上遇到一些意想不到的坑。这篇文章,我就把从硬件连接到软件驱动,再到实际调试中踩过的雷和总结的技巧,完整地梳理一遍。
2. 硬件设计与连接要点
2.1 核心器件选型与引脚功能解析
我使用的MCU是GD32F103C8T6,它属于GD32F10x系列的中等容量产品,拥有3个SPI接口(SPI0, SPI1, SPI2)。这里选择SPI0,是因为它的引脚(PA5/PA6/PA7)通常默认作为主SPI功能,且不与常用的调试接口(如SWD)冲突。GD25Q128是一颗3.3V供电的SPI NOR Flash,容量为16MB,支持标准的SPI、Dual SPI和Quad SPI模式,我们初期只使用标准SPI模式以保证兼容性和简单性。
引脚连接表如下:
| GD32F103C8T6 引脚 | GD25Q128 引脚 | 功能说明 | 备注 |
|---|---|---|---|
| PA5 (SPI0_SCK) | CLK | 时钟信号 | 主设备输出,需配置为上拉或推挽输出 |
| PA6 (SPI0_MISO) | DO (IO1) | 主设备输入,从设备输出 | 接收Flash数据,配置为浮空输入或上拉输入 |
| PA7 (SPI0_MOSI) | DI (IO0) | 主设备输出,从设备输入 | 发送指令和数据给Flash,配置为推挽输出 |
| PA4 (自定义CS) | CS# | 片选信号 | 低电平有效,任何GPIO均可,配置为推挽输出,初始置高 |
| 3.3V | VCC | 电源 | 务必同电压域 |
| GND | GND | 地 | |
| 3.3V | HOLD# / WP# | 写保护/保持 | 通常上拉到VCC,禁用保护功能 |
| 不连接 | NC | 未连接 |
注意:这里的
PA4作为片选(CS)是我手动指定的一个GPIO,并非SPI0硬件自带的NSS引脚。我更喜欢用软件控制GPIO来管理片选,这样时序控制更灵活,也避免了硬件NSS模式可能带来的复杂配置问题。HOLD#和WP#引脚必须上拉到高电平,否则Flash会处于写保护或挂起状态,无法进行写操作。
2.2 电源与去耦设计的心得
虽然电路简单,但电源稳定性决定了Flash工作的可靠性,尤其是进行页编程(Page Program)和扇区擦除(Sector Erase)时,电流会有瞬间波动。我的做法是,在GD32和GD25Q128的VCC引脚附近,各放置一个0.1μF的陶瓷电容到地,用于滤除高频噪声。同时,在整板的3.3V电源入口处,增加一个10μF的钽电容,以应对可能的低频波动。如果布线空间允许,在SPI的时钟线SCK和数据线MOSI、MISO上串联一个22Ω到33Ω的小电阻,可以有效抑制信号过冲和反射,尤其在主频较高或走线较长时,这个细节能大幅提升通信稳定性。
3. 软件驱动层实现详解
3.1 SPI0外设初始化配置
GD32的标准外设库(GD32F10x Firmware Library)用起来和STM32的StdPeriph库很像,这降低了迁移成本。但初始化时仍有几个关键参数需要仔细考量。
/** * @brief 初始化SPI0为主机模式,用于驱动GD25Q128 * @param 无 * @retval 无 */ void SPI0_Init(void) { spi_parameter_struct spi_init_struct; /* 使能时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI0); /* 配置SPI引脚: PA5-SCK, PA6-MISO, PA7-MOSI */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7); // SCK, MOSI 推挽复用输出 gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6); // MISO 浮空输入 /* 配置自定义片选引脚PA4为推挽输出 */ gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); GPIO_BC(GPIOA) = GPIO_PIN_4; // 初始置高,不选中 /* SPI参数配置 */ spi_struct_para_init(&spi_init_struct); spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 全双工 spi_init_struct.device_mode = SPI_MASTER; // 主机模式 spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据帧 spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; // CPOL=0, CPHA=1 (模式1) spi_init_struct.nss = SPI_NSS_SOFT; // 软件控制NSS(即我们用GPIO控制CS) spi_init_struct.prescale = SPI_PSC_8; // 预分频,APB2时钟72MHz / 8 = 9MHz spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位先行 spi_init(SPI0, &spi_init_struct); /* 使能SPI0 */ spi_enable(SPI0); }关键配置解析:
- 时钟极性与相位(CPOL & CPHA):我设置为
SPI_CK_PL_LOW_PH_1EDGE,即模式1。这是查阅GD25Q128数据手册后确认的,它在“标准SPI”模式下,要求在时钟空闲时为低电平(CPOL=0),在第二个边沿(即上升沿)采样数据(CPHA=1)。务必与Flash芯片要求严格一致,否则读出的全是0xFF或乱码。 - 片选(NSS)管理:使用
SPI_NSS_SOFT软件模式,这样我们就可以用GPIO(PA4)自由控制片选时序。硬件NSS模式在多从机或复杂时序下反而麻烦。 - 波特率预分频(prescale):GD32F103的APB2总线时钟是72MHz。我选择8分频,得到9MHz的SCK频率。对于GD25Q128,在3.3V电压下,最高支持104MHz(Fast Read)甚至133MHz(Quad I/O),但初期调试建议先用较低频率(如9MHz或18MHz),确保通信稳定后再逐步提高。过高的频率对PCB布线质量要求很高。
- 数据帧大小与端序:Flash指令和地址都是8位传输,MSB先行,这是标准配置。
3.2 基础读写函数封装
SPI底层收发函数是驱动的基础。这里需要处理一个关键细节:GD32的SPI数据寄存器(SPI_DATA)在发送和接收时是同一个,发送一个字节的同时也会接收一个字节。
/** * @brief 通过SPI0交换一个字节(发送并接收) * @param byte: 要发送的字节 * @retval 接收到的字节 */ static uint8_t SPI0_ReadWriteByte(uint8_t byte) { /* 等待发送缓冲区空 */ while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); /* 发送数据 */ spi_i2s_data_transmit(SPI0, byte); /* 等待接收缓冲区非空 */ while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); /* 返回接收到的数据 */ return spi_i2s_data_receive(SPI0); } /** * @brief 设置片选信号(CS)为低电平,选中Flash */ static void GD25Q128_CS_Low(void) { GPIO_BC(GPIOA) = GPIO_PIN_4; // 使用位清除寄存器置低,操作速度比GPIO_WriteBit快且更安全 } /** * @brief 设置片选信号(CS)为高电平,取消选中Flash */ static void GD25Q128_CS_High(void) { GPIO_BOP(GPIOA) = GPIO_PIN_4; // 使用位设置寄存器置高 }实操心得:在
SPI0_ReadWriteByte函数中,务必先检查发送缓冲区空(TBE)再写入数据,然后检查接收缓冲区非空(RBNE)再读取。这个顺序不能错。另外,直接操作GPIO_BC(位清除)和GPIO_BOP(位置位)寄存器来控制CS引脚,比使用gpio_bit_write函数效率更高,能产生更干净利落的片选沿,对于高速SPI通信有益。
4. GD25Q128指令集与核心操作实现
4.1 基本指令封装与器件识别
任何操作开始前,必须确保MCU能与Flash正确对话。首先实现几个最基本的指令函数,并通过读取制造商和设备ID来验证连接。
/* GD25Q128 常用指令定义 */ #define GD25Q128_CMD_WRITE_ENABLE 0x06 #define GD25Q128_CMD_WRITE_DISABLE 0x04 #define GD25Q128_CMD_READ_STATUS_REG1 0x05 #define GD25Q128_CMD_READ_STATUS_REG2 0x35 #define GD25Q128_CMD_WRITE_STATUS_REG 0x01 #define GD25Q128_CMD_PAGE_PROGRAM 0x02 #define GD25Q128_CMD_SECTOR_ERASE_4K 0x20 #define GD25Q128_CMD_BLOCK_ERASE_32K 0x52 #define GD25Q128_CMD_BLOCK_ERASE_64K 0xD8 #define GD25Q128_CMD_CHIP_ERASE 0xC7 #define GD25Q128_CMD_READ_DATA 0x03 #define GD25Q128_CMD_FAST_READ 0x0B #define GD25Q128_CMD_READ_JEDEC_ID 0x9F /** * @brief 向GD25Q128发送单字节指令 * @param cmd: 指令码 */ static void GD25Q128_SendCmd(uint8_t cmd) { GD25Q128_CS_Low(); SPI0_ReadWriteByte(cmd); GD25Q128_CS_High(); } /** * @brief 读取GD25Q128的JEDEC ID(制造商、内存类型、容量) * @param manufacturer_id: 制造商ID指针 * @param device_id: 设备ID指针 * @retval 无 */ void GD25Q128_ReadID(uint8_t *manufacturer_id, uint16_t *device_id) { uint8_t id_buffer[3] = {0}; GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_READ_JEDEC_ID); id_buffer[0] = SPI0_ReadWriteByte(0xFF); // 制造商ID,GD对应0xC8 id_buffer[1] = SPI0_ReadWriteByte(0xFF); // 内存类型 id_buffer[2] = SPI0_ReadWriteByte(0xFF); // 容量ID GD25Q128_CS_High(); *manufacturer_id = id_buffer[0]; *device_id = (id_buffer[1] << 8) | id_buffer[2]; }调用GD25Q128_ReadID后,正确的返回值应该是:manufacturer_id = 0xC8(兆易创新),device_id = 0x4018(其中0x40代表类型,0x18代表128Mbit容量)。这是验证硬件连接和SPI配置是否正确的第一步。如果读出来是0xFF或0x00,请立即检查电源、地线、CS引脚电平以及SPI的CPOL/CPHA设置。
4.2 状态寄存器管理与写使能
SPI Flash在进行写操作(编程或擦除)前,必须先发送写使能(Write Enable)指令,并且要等待该指令执行完成。这通过轮询状态寄存器1(Status Register 1)的BUSY位和WEL位来实现。
/** * @brief 等待Flash内部操作完成(BUSY位清零) */ void GD25Q128_WaitBusy(void) { uint8_t status; do { GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_READ_STATUS_REG1); status = SPI0_ReadWriteByte(0xFF); GD25Q128_CS_High(); } while(status & 0x01); // 检查Status Register 1的BIT0 (BUSY) } /** * @brief 发送写使能指令,并确认WEL位置位 * @retval 成功返回0,失败返回非0 */ uint8_t GD25Q128_WriteEnable(void) { uint8_t status; uint16_t timeout = 10000; // 超时计数器 /* 发送写使能指令 */ GD25Q128_SendCmd(GD25Q128_CMD_WRITE_ENABLE); /* 短暂延时,等待指令生效 */ delay_us(10); /* 轮询状态寄存器,确认WEL位(BIT1)被置1 */ do { GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_READ_STATUS_REG1); status = SPI0_ReadWriteByte(0xFF); GD25Q128_CS_High(); if(--timeout == 0) { return 1; // 超时,写使能失败 } delay_us(10); } while((status & 0x02) == 0); // 检查BIT1 (WEL) return 0; // 成功 }踩坑记录:这里最容易出问题的是时序。发送
WRITE_ENABLE指令后,必须等待一小段时间(数据手册要求典型值t_WEL,通常小于10us)再读取状态寄存器。我最初没有加delay_us(10),在高速MCU上偶尔会出现轮询超时,因为芯片内部状态更新需要时间。另外,任何擦除或编程操作后,BUSY位会自动置1,此时除了读状态寄存器和暂停指令,发送其他任何指令都是无效的。所以GD25Q128_WaitBusy()是写操作后必须调用的。
4.3 扇区擦除与页编程操作
Flash的写操作必须遵循“先擦后写”的原则,因为只能把bit从1变成0,擦除操作(将整个扇区/块/芯片置1)是必不可少的。
/** * @brief 擦除指定地址开始的4K字节扇区 * @param addr: 扇区内的任意地址(24位地址) * @retval 成功返回0,失败返回非0 */ uint8_t GD25Q128_EraseSector4K(uint32_t addr) { /* 1. 发送写使能 */ if(GD25Q128_WriteEnable() != 0) { return 1; } /* 2. 发送扇区擦除指令及24位地址 */ GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_SECTOR_ERASE_4K); SPI0_ReadWriteByte((addr >> 16) & 0xFF); // 地址高8位 SPI0_ReadWriteByte((addr >> 8) & 0xFF); // 地址中8位 SPI0_ReadWriteByte(addr & 0xFF); // 地址低8位 GD25Q128_CS_High(); /* 3. 等待擦除操作完成 */ GD25Q128_WaitBusy(); return 0; } /** * @brief 向指定地址写入一页数据(最多256字节) * @param pBuffer: 待写入数据缓冲区指针 * @param addr: 写入起始地址(24位地址) * @param size: 写入数据字节数(1-256) * @retval 成功返回0,失败返回非0 */ uint8_t GD25Q128_WritePage(uint8_t* pBuffer, uint32_t addr, uint16_t size) { uint16_t i; if(size == 0 || size > 256) return 1; // 页编程不能跨页 /* 1. 发送写使能 */ if(GD25Q128_WriteEnable() != 0) { return 2; } /* 2. 发送页编程指令、地址及数据 */ GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_PAGE_PROGRAM); SPI0_ReadWriteByte((addr >> 16) & 0xFF); SPI0_ReadWriteByte((addr >> 8) & 0xFF); SPI0_ReadWriteByte(addr & 0xFF); for(i = 0; i < size; i++) { SPI0_ReadWriteByte(pBuffer[i]); } GD25Q128_CS_High(); /* 3. 等待编程操作完成 */ GD25Q128_WaitBusy(); return 0; }关键细节解析:
- 地址对齐:
GD25Q128_EraseSector4K的地址参数只要是目标4K扇区内的任意地址即可,芯片会自动对齐到扇区起始边界。但GD25Q128_WritePage的地址和大小必须保证本次写入不跨越256字节的页边界。例如,从地址250开始写10个字节,就会跨越页边界(250-255, 256-259),这是不允许的,会导致数据写入错误。软件中必须处理页边界拆分。 - 擦除时间:4K扇区擦除时间典型值为50ms,最大300ms。
GD25Q128_WaitBusy()函数会阻塞等待。在实际应用中,如果系统实时性要求高,可以考虑在擦除指令发出后,利用状态寄存器查询而非死等,让MCU去处理其他任务。 - 写保护位:除了
WEL位,状态寄存器中的块保护位(BP2, BP1, BP0)和状态寄存器2中的QE位等,也可能影响擦写。默认出厂状态通常全扇区可写,但如果你的操作失败,记得检查一下这些位是否被意外设置。
4.4 数据读取函数实现
读取操作相对简单,但为了提高速度,可以使用快速读(Fast Read)指令,它在发送地址后需要一个额外的“哑元”(Dummy)时钟周期。
/** * @brief 从指定地址读取数据 * @param pBuffer: 读取数据存储缓冲区指针 * @param addr: 读取起始地址 * @param size: 要读取的字节数 * @retval 无 */ void GD25Q128_ReadData(uint8_t* pBuffer, uint32_t addr, uint32_t size) { uint32_t i; GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_READ_DATA); // 标准读指令,0x03 SPI0_ReadWriteByte((addr >> 16) & 0xFF); SPI0_ReadWriteByte((addr >> 8) & 0xFF); SPI0_ReadWriteByte(addr & 0xFF); for(i = 0; i < size; i++) { pBuffer[i] = SPI0_ReadWriteByte(0xFF); } GD25Q128_CS_High(); } /** * @brief 从指定地址快速读取数据(需要额外Dummy Clock) * @param pBuffer: 读取数据存储缓冲区指针 * @param addr: 读取起始地址 * @param size: 要读取的字节数 * @retval 无 */ void GD25Q128_FastReadData(uint8_t* pBuffer, uint32_t addr, uint32_t size) { uint32_t i; GD25Q128_CS_Low(); SPI0_ReadWriteByte(GD25Q128_CMD_FAST_READ); // 快速读指令,0x0B SPI0_ReadWriteByte((addr >> 16) & 0xFF); SPI0_ReadWriteByte((addr >> 8) & 0xFF); SPI0_ReadWriteByte(addr & 0xFF); SPI0_ReadWriteByte(0xFF); // 必需的哑元时钟周期 for(i = 0; i < size; i++) { pBuffer[i] = SPI0_ReadWriteByte(0xFF); } GD25Q128_CS_High(); }标准读(0x03)指令在地址发送完后立即输出数据。而快速读(0x0B)在地址后需要额外8个SCK周期(发送一个哑元字节,如0xFF)才会输出数据,但这个指令允许SCK时钟频率更高,从而提升读取吞吐量。在初始化SPI时,如果预分频设置得较小(如SPI_PSC_2得到36MHz),务必使用快速读指令,否则可能无法正确读取数据。
5. 高级功能与文件系统适配思考
5.1 擦写均衡与坏块管理初步
虽然NOR Flash不像NAND那样有严格的坏块问题,但频繁擦写同一扇区仍会导致该区域提前失效。对于需要频繁更新数据的应用(如日志存储),实现简单的擦写均衡(Wear Leveling)能极大延长Flash寿命。
一个简单的思路是使用“循环队列”存储。例如,将多个4K扇区虚拟成一个大的循环缓冲区。每次写入新数据时,找到当前已写满的扇区后的第一个空闲扇区。当所有扇区都写满后,再回过头来擦除最早的那个扇区并写入。这样,擦除操作就被均匀分布到所有扇区上。关键是需要一个固定的“元数据区”(例如,使用最后的两个扇区)来存储当前写指针、扇区状态表等信息。每次上电后,先读取元数据,恢复出当前的写入位置。
5.2 与FatFs等文件系统的结合
如果需要在PC上方便地读取Flash内的数据,移植一个轻量级文件系统如FatFs是理想选择。这时,你需要将GD25Q128的底层驱动对接给FatFs的磁盘I/O层(disk_read,disk_write,disk_ioctl)。
核心工作是实现这几个接口函数:
disk_read: 调用GD25Q128_ReadData。disk_write: 需要先调用GD25Q128_EraseSector4K擦除对应扇区,再调用GD25Q128_WritePage写入数据。这里要特别注意,FatFs的disk_write接口要求按扇区(通常是512字节)写入,而GD25Q128的擦除单位是4K,编程单位是256字节。你需要做地址转换和缓冲区管理,可能还需要一个RAM缓冲区来凑齐一个4K扇区再进行擦写,这会引入复杂度并影响性能。disk_ioctl: 需要实现GET_SECTOR_SIZE(返回512)、GET_SECTOR_COUNT(返回总容量/512)、CTRL_SYNC(刷新缓冲区)等命令。
我个人建议,对于简单的参数存储,直接使用上面实现的读写函数,自己定义二进制结构体来管理更高效。对于复杂的、需要与电脑交换大量文件的应用,再考虑上FatFs。
5.3 功耗管理与深度省电模式
在电池供电设备中,GD25Q128的功耗也需要关注。它支持掉电模式(Power-down),通过指令0xB9进入,此时功耗可低至1μA。在进入低功耗前,发送掉电指令;在需要操作前,发送释放掉电/器件ID指令0xAB唤醒它。注意,唤醒需要一定的时间(t_RES1,典型值30us)。在驱动层可以封装GD25Q128_EnterPowerDown()和GD25Q128_ReleasePowerDown()两个函数,在系统进入休眠前调用。
6. 调试实战与常见问题排查
6.1 调试工具与方法
- 逻辑分析仪是神器:连接SCK、MOSI、MISO、CS四根线,设置好SPI协议解码(模式1,MSB First)。可以清晰看到每次通信发送的指令、地址和数据,是排查通信问题的终极手段。
- 万用表与示波器:首先用万用表确认电源3.3V稳定,CS引脚默认高电平。然后用示波器看SCK波形是否干净,频率是否符合预期,CS拉低和拉高的时序是否正常。
- 软件打印:在关键函数(如ReadID、WriteEnable)前后,通过串口打印状态和返回值,是最基础的调试方法。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 读取ID全是0xFF或0x00 | 1. 物理连接问题(虚焊、线断) 2. CS片选信号异常(常高或常低) 3. SPI模式(CPOL/CPHA)设置错误 4. Flash未上电或损坏 | 1. 检查焊接,测量电压。 2. 用示波器看CS引脚波形。 3. 确认CPOL/CPHA为模式1(0,1)。 4. 换一片Flash试试。 |
| 能读ID但不能读写数据 | 1. 写使能(WEL)未成功 2. 地址发送错误(字节顺序) 3. 操作后未等待BUSY结束 | 1. 单步调试GD25Q128_WriteEnable,检查状态寄存器WEL位。2. 用逻辑分析仪抓取完整指令、地址序列。 3. 在每次擦/写操作后调用 GD25Q128_WaitBusy。 |
| 写入的数据读出来不对 | 1. 未擦除就直接写入 2. 写入跨页了 3. 电源噪声导致写入错误 | 1. 确保先执行扇区擦除。 2. 检查写入地址和长度,确保不跨越256字节边界。 3. 加强电源去耦,在编程期间避免大电流负载变化。 |
| 高速时通信失败 | 1. SPI时钟频率过高 2. PCB走线过长,信号质量差 3. 未使用快速读指令 | 1. 降低SPI波特率预分频。 2. 检查走线,缩短长度,必要时串联小电阻。 3. 高频读取时换用 GD25Q128_FastReadData。 |
| 偶尔操作失败 | 1. 状态轮询超时时间不足 2. 中断打断了SPI时序 3. 片选时序不严格 | 1. 增加GD25Q128_WaitBusy中的超时判断值。2. 在关键的SPI连续通信段(指令+地址+数据)关闭全局中断。 3. 确保CS在指令间有足够的高电平时间(t_SHSL)。 |
6.3 我的避坑技巧
- 上电初始化后加延时:GD25Q128从上电到接受第一条指令有一个
t_PU(Power-up time)时间,最大约300ms。虽然通常不需要等这么久,但在SPI0_Init()之后和首次读ID之前,加一个delay_ms(10)是良好的习惯。 - 封装统一的错误码:为所有驱动函数设计一个统一的返回类型(如
GD25Q128_StatusTypeDef),定义诸如GD25Q128_OK、GD25Q128_ERR_TIMEOUT、GD25Q128_ERR_WRITE等错误码,便于上层应用排查问题。 - 编写一个完整的测试函数:在项目初期,编写一个
GD25Q128_Test(void)函数,顺序执行:读ID -> 擦除一个测试扇区 -> 写入特定模式数据(如0xAA, 0x55交替)-> 回读校验。每次硬件改动或软件升级后跑一遍,能快速验证整个存储系统是否工作正常。 - 注意跨页写入:这是最隐蔽的bug之一。我现在的做法是在
GD25Q128_Write函数内部做判断,如果发现addr % 256 + size > 256,就自动拆分成两次页编程操作。虽然牺牲了一点效率,但保证了鲁棒性。
通过以上步骤,你应该能稳稳地驱动起GD25Q128这颗Flash。从最底层的引脚配置、SPI初始化,到中间的指令封装、擦写函数,再到上层的均衡策略和文件系统思考,整个链路打通后,这颗16MB的外部存储就能为你的GD32项目提供可靠的数据存储空间了。实际项目中,根据需求对驱动进行裁剪和优化,比如增加DMA传输、使能Quad SPI模式进一步提升速度,那又是另一个层次的玩法了。
