1. GD32F10x与GD25Q128硬件连接基础
第一次接触GD32F10x的SPI接口驱动Flash时,我对着原理图发呆了半小时——PA5、PA6、PA7三个引脚怎么就能同时收发数据?后来才明白SPI是全双工通信的典型代表。GD32F10x的SPI0接口与GD25Q128的连接其实非常简单:
- SCK(时钟线):PA5,配置为复用推挽输出
- MISO(主入从出):PA6,浮空输入模式
- MOSI(主出从入):PA7,复用推挽输出
- CS(片选):通常用普通GPIO如PE3,推挽输出
硬件设计时有个坑要注意:如果板子上有多个SPI设备,记得每个设备都要独立片选线。我曾因为偷懒共用片选,结果数据全乱套了。另外,CLK线记得加22-100Ω的串联电阻,能有效抑制振铃现象。
2. SPI初始化关键配置解析
配置SPI就像调教一辆手动挡汽车,每个参数都要恰到好处。下面是初始化代码的核心片段:
spi_parameter_struct spi_init_struct; spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 全双工模式 spi_init_struct.device_mode = SPI_MASTER; // 主机模式 spi_init_struct.nss = SPI_NSS_SOFT; // 软件控制片选 spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据帧 spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; // 模式0 spi_init_struct.prescale = SPI_PSC_8; // 时钟预分频 spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位在前 spi_init(SPI0, &spi_init_struct);时钟相位和极性是新手最容易栽跟头的地方。GD25Q128支持模式0(CPOL=0, CPHA=0)和模式3(CPOL=1, CPHA=1)。我习惯用模式0,因为时钟空闲时为低电平,用逻辑分析仪抓取信号时更直观。
预分频值需要根据系统时钟计算。假设PCLK2为72MHz:
- SPI_PSC_2 → 36MHz
- SPI_PSC_4 → 18MHz
- SPI_PSC_8 → 9MHz (推荐初始值)
- SPI_PSC_256 → 281kHz (低速调试用)
3. Flash读写操作实战技巧
3.1 基础读写函数实现
先来看最核心的字节读写函数,这是所有高级操作的基础:
uint8_t SPI_FLASH_SendByte(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); // 返回接收数据 }这个函数实现了SPI全双工通信的精髓:发送和接收同时进行。我曾试图拆分成单独发送和接收函数,结果通信效率直接减半。
3.2 重要Flash指令封装
GD25Q128有几十种指令,但常用的就这几个:
#define WRITE_ENABLE 0x06 #define READ_DATA 0x03 #define PAGE_PROGRAM 0x02 #define SECTOR_ERASE 0x20 #define READ_STATUS_REG_1 0x05写使能是修改Flash的前提,每次写入前都必须调用:
void FLASH_WriteEnable(void) { SPI0_CS_LOW(); SPI_FLASH_SendByte(WRITE_ENABLE); SPI0_CS_HIGH(); }3.3 扇区擦除实战
Flash写入前必须先擦除,就像黑板写字前要先擦干净。GD25Q128的最小擦除单位是4KB扇区:
void FLASH_SectorErase(uint32_t addr) { FLASH_WriteEnable(); // 必须步骤! SPI0_CS_LOW(); SPI_FLASH_SendByte(SECTOR_ERASE); SPI_FLASH_SendByte((addr >> 16) & 0xFF); // 24位地址分三次发送 SPI_FLASH_SendByte((addr >> 8) & 0xFF); SPI_FLASH_SendByte(addr & 0xFF); SPI0_CS_HIGH(); FLASH_WaitForWriteEnd(); // 等待擦除完成 }注意地址要对齐到4KB边界(addr%4096==0)。我有次传入0x1234地址,结果整个扇区数据全飞了。
4. 性能优化关键策略
4.1 时钟速度优化
GD25Q128最高支持104MHz时钟,但实际速度受以下因素限制:
- GD32F10x的SPI时钟最大为PCLK/2
- PCB布线质量
- 电源稳定性
推荐优化步骤:
- 初始用SPI_PSC_8(如PCLK=72MHz则SPI时钟=9MHz)
- 逐步提高分频系数,测试到SPI_PSC_2(36MHz)
- 用逻辑分析仪观察波形是否畸变
4.2 DMA传输优化
大数据量传输一定要用DMA。以256字节页写入为例:
void FLASH_PageWrite_DMA(uint8_t *buf, uint32_t addr) { FLASH_WriteEnable(); SPI0_CS_LOW(); SPI_FLASH_SendByte(PAGE_PROGRAM); // 发送地址... dma_channel_enable(DMA0, DMA_CH3); // 启用DMA发送 while(dma_flag_get(DMA0, DMA_CH3, DMA_FLAG_FTF)==RESET); SPI0_CS_HIGH(); FLASH_WaitForWriteEnd(); }配置DMA时注意:
- 设置内存到外设模式
- 内存地址递增,外设地址固定
- 开启DMA中断可进一步优化
4.3 双缓冲技术
对于实时数据采集等场景,可以创建两个缓冲区:
uint8_t bufA[256], bufB[256]; volatile uint8_t *activeBuf = bufA; // 当activeBuf写满时: FLASH_PageWrite_DMA(activeBuf, addr); addr += 256; activeBuf = (activeBuf == bufA) ? bufB : bufA; // 切换缓冲区5. 常见问题排查指南
5.1 读取全为0xFF
- 检查硬件连接,特别是CS线
- 确认SPI模式(CPOL/CPHA)
- 测量CLK信号是否正常
5.2 写入失败
- 是否先执行了写使能(WRITE_ENABLE)
- 是否等待了足够擦除时间(典型值:扇区擦除400ms)
- 电源电压是否稳定(建议3.3V±5%)
5.3 数据偶尔错误
- 降低SPI时钟速度测试
- 检查PCB布局,缩短走线长度
- 在SCK和MOSI上加33pF对地电容
6. 高级功能扩展
6.1 写保护实现
GD25Q128支持硬件和软件写保护。通过状态寄存器配置:
void FLASH_EnableWriteProtect(void) { FLASH_WriteEnable(); SPI0_CS_LOW(); SPI_FLASH_SendByte(WRITE_STATUS_REG); SPI_FLASH_SendByte(0x7C); // 块保护位 SPI0_CS_HIGH(); }6.2 低功耗管理
深度睡眠前调用:
void FLASH_Sleep(void) { SPI0_CS_LOW(); SPI_FLASH_SendByte(0xB9); // POWER_DOWN指令 SPI0_CS_HIGH(); }唤醒时需要发送0xAB指令并等待3us。
7. 真实项目经验分享
去年在智能电表项目中,我们需要每5分钟存储一次用电数据。最初方案是每次直接写入,结果Flash寿命很快耗尽。后来改进为:
- 建立环形缓冲区(16个扇区)
- 数据先写入RAM缓冲区
- 缓冲区满或断电时批量写入
- 磨损均衡算法分散写入区域
这使Flash擦写次数从每天288次降到每周1次,寿命提升2000倍。关键代码如下:
#define SECTOR_COUNT 16 uint32_t currentSector = 0; void SaveEnergyData(void) { static uint8_t pageBuffer[256]; static int bufferPos = 0; // 填充buffer... if(++bufferPos >= 16) { // 攒够16页=4KB FLASH_SectorErase(currentSector * 4096); for(int i=0; i<16; i++) { FLASH_PageWrite(&pageBuffer[i*16], currentSector*4096 + i*256); } currentSector = (currentSector + 1) % SECTOR_COUNT; bufferPos = 0; } }