25. GD32E230驱动W25Q64 SPI FLASH:从硬件SPI配置到读写擦除实战

25. GD32E230驱动W25Q64 SPI FLASH:从硬件SPI配置到读写擦除实战 GD32E230驱动W25Q64 SPI FLASH从硬件SPI配置到读写擦除实战很多刚开始玩GD32的朋友都会遇到一个需求单片机自带的Flash不够用想存点大文件或者记录一些数据这时候就需要外挂一个Flash芯片。W25Q64这颗8MB的SPI Flash价格便宜、容量够用是很多项目的首选。今天我就带大家手把手用GD32E230的硬件SPI把它驱动起来实现完整的读写擦除功能。咱们的目标很明确让GD32E230通过SPI接口和W25Q64“说上话”能正确读取它的ID能往里面存数据也能把数据读出来。我会把每一步的原理、代码和容易踩的坑都讲清楚保证你跟着做一遍就能掌握。1. 认识我们的“存储伙伴”W25Q64在动手接线写代码之前咱们先花几分钟了解一下要驱动的对象——W25Q64。这就像交朋友得先知道对方的基本情况。W25Q64是一颗非常常见的串行SPIFlash存储器。简单来说它就是一个通过SPI接口一种简单的四线制通信协议来访问的“小硬盘”。它的容量是64M-bit也就是8M-Byte对于存储一些配置文件、字库、图片或者日志数据来说完全足够了。它有几个关键特性你需要记住接口标准SPI接口支持标准、双线和四线模式咱们今天用最基础的标准SPI。工作电压2.7V到3.6V和GD32E230的3.3V供电完美匹配。时钟频率最高支持104MHz甚至133MHz速度很快。内部结构它的存储空间像一本书被划分成很多“页”和“扇区”。理解这个对正确操作它至关重要页Page最小的编程单位一次最多可以写入256个字节。扇区Sector最小的擦除单位大小为4KB4096字节。在写入新数据前对应的扇区必须是擦除状态所有位为1即0xFF。块Block由16个扇区组成大小为64KB是更大的擦除单位。注意Flash存储器的写入编程和擦除是两种不同的操作并且有“只能从1写0不能从0写1”的特性。所以如果你想改写某个地址的数据必须先擦除整个包含该地址的扇区使其全变为0xFF然后再写入新数据。2. 硬件连接与引脚配置要让GD32和W25Q64通信首先得把它们正确地连起来。我们使用GD32E230C8T6的硬件SPI0外设这样通信效率高不占用CPU太多时间。2.1 接线图根据GD32E230的数据手册SPI0的引脚可以复用在PA4到PA7上。我们这样连接GD32E230引脚引脚功能连接至W25Q64引脚作用说明PA4GPIO输出 (软件片选)CS (Pin 1)片选信号低电平有效。选择要通信的从设备。PA5SPI0_SCKCLK (Pin 6)时钟信号由主设备GD32产生。PA6SPI0_MISODO (Pin 2)主设备输入从设备输出。W25Q64通过此线发送数据给GD32。PA7SPI0_MOSIDI (Pin 5)主设备输出从设备输入。GD32通过此线发送数据给W25Q64。另外别忘了给W25Q64接上电源VCC 3.3V和地GND。它的HOLD#和WP#引脚如果不用可以直接上拉到3.3V通常通过10K电阻。2.2 初始化GPIO和SPI外设接线完成后就要在代码里告诉GD32这些引脚是用来做什么的。下面是配置代码我加了详细注释void W25Q64_Config(void) { // 1. 开启GPIOA和SPI0的时钟 // 时钟就像电源开关不开时钟外设是无法工作的 rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI0); // 2. 配置SPI功能引脚 (PA5:SCK, PA6:MISO, PA7:MOSI) // 先将引脚复用功能设置为SPI0 gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_5); // SCK gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_6); // MISO gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_7); // MOSI // 设置引脚模式为复用功能模式 gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_5); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7); // 设置输出选项推挽输出高速50MHz gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7); // 3. 配置片选(CS)引脚PA4为普通推挽输出 // 注意这里我们使用软件控制CS所以配置为普通GPIO输出模式而不是复用模式 // GPIOA时钟上面已经开启过了这里无需重复开启 gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_4); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); // 初始状态置高不选中W25Q64 gpio_bit_write(GPIOA, GPIO_PIN_4, SET); // 4. 配置SPI0外设的工作参数 spi_parameter_struct spi_init_struct; spi_init_struct.trans_mode SPI_TRANSMODE_FULLDUPLEX; // 全双工模式可同时收發 spi_init_struct.device_mode SPI_MASTER; // 主机模式我们GD32是主机 spi_init_struct.frame_size SPI_FRAMESIZE_8BIT; // 数据帧8位 spi_init_struct.clock_polarity_phase SPI_CK_PL_HIGH_PH_2EDGE; // CPOL1, CPHA1 (模式3) spi_init_struct.nss SPI_NSS_SOFT; // 软件管理片选信号 spi_init_struct.prescale SPI_PSC_2; // 时钟2分频SPI时钟36MHz spi_init_struct.endian SPI_ENDIAN_MSB; // 高位在前 // 将配置参数初始化到SPI0 spi_init(SPI0, spi_init_struct); // 最后使能SPI0 spi_enable(SPI0); }关键点解析软件片选 (SPI_NSS_SOFT)硬件SPI自带一个硬件NSS片选引脚但一个SPI主机通常只能固定控制一个从机。我们选择“软件片选”就是把一个普通的GPIO这里用PA4当作片选线来控制。这样非常灵活一个SPI接口可以挂多个从设备用不同的GPIO去片选它们就行。SPI模式 (SPI_CK_PL_HIGH_PH_2EDGE)这是最容易出错的地方之一。SPI有4种模式CPOL和CPHA的组合。根据W25Q64数据手册它支持模式0 (CPOL0, CPHA0)和模式3 (CPOL1, CPHA1)。这里我们选择了模式3。务必保证主从设备模式一致否则通信不上。时钟分频 (SPI_PSC_2)GD32的APB2总线时钟是72MHz2分频后SPI时钟为36MHz。W25Q64最高支持104MHz以上所以36MHz完全没问题。初期调试如果担心速度太快可以加大分频比如8分频、16分频来降低通信速率。3. 编写SPI底层收发函数SPI外设配置好后我们需要一个最基础的字节收发函数。这个函数是后续所有高级命令读ID、写数据等的基石。// SPI读写一个字节函数 // 参数dat要发送出去的一个字节数据 // 返回值接收到的一个字节数据 uint8_t spi_read_write_byte(uint8_t dat) { // 等待发送缓冲区为空TBE标志置位表示可以写入新数据了 while(RESET spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); // 写入要发送的数据到SPI数据寄存器硬件会自动发送 spi_i2s_data_transmit(SPI0, dat); // 等待接收缓冲区非空RBNE标志置位表示有数据可读了 while(RESET spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); // 读取SPI数据寄存器获取接收到的数据 return spi_i2s_data_receive(SPI0); }这个函数实现了全双工通信在发送一个字节的同时也会接收到一个字节。对于W25Q64的很多命令我们发送指令后需要“假装”接收一些时钟来获取它的回复这时发送的数据可以是任意值常用0xFF我们只关心接收到的数据。为了方便操作片选引脚我们在头文件里定义一个宏// W25Q64.h 中定义 #define W25QXX_CS_ON(x) gpio_bit_write(GPIOA, GPIO_PIN_4, x?SET:RESET) // 当x0时拉低CS选中芯片 // 当x1时拉高CS取消选中4. 实战第一步读取芯片ID通信链路打通后第一件要做的事就是“验明正身”——读取芯片的ID。这不仅能确认硬件连接和SPI配置是否正确还能知道Flash的具体型号是W25Q64还是W25Q128等。根据W25Q64数据手册读取ID的命令是0x90后面需要跟一个24位的地址这里用0x000000然后芯片会返回两个字节制造商IDManufacturer ID和设备IDDevice ID。// 读取W25Q64的ID // 返回值高8位是制造商ID如0xEF低8位是设备ID uint16_t W25Q64_readID(void) { uint16_t temp 0; // 1. 拉低CS开始一次SPI通信 W25QXX_CS_ON(0); // 2. 发送读取ID命令 0x90 spi_read_write_byte(0x90); // 3. 发送24位地址 0x000000 spi_read_write_byte(0x00); spi_read_write_byte(0x00); spi_read_write_byte(0x00); // 4. 读取制造商ID第一个字节 // 发送0xFF产生时钟同时读取返回的数据 temp spi_read_write_byte(0xFF) 8; // 5. 读取设备ID第二个字节 temp | spi_read_write_byte(0xFF); // 6. 拉高CS结束本次通信 W25QXX_CS_ON(1); return temp; }如何判断成功在main函数里调用这个函数并打印结果uint16_t flash_id W25Q64_readID(); printf(Flash ID: 0x%04X\r\n, flash_id);如果看到输出是0xEF16那么恭喜你这表示制造商是Winbond0xEF设备是W25Q640x16。如果读出来是0xFFFF或0x0000那就要回头检查硬件连接和SPI配置了。5. 深入核心Flash的写入与擦除机制在真正写数据之前必须理解Flash存储器的特性否则数据永远写不进去。Flash的写入编程特性你可以把Flash的一个存储位想象成一个只能从1变成0的小开关。初始状态擦除后所有位都是10xFF。写入数据时如果某位需要是0就把开关拨下去变成0如果需要是1就保持不动本来就是1。关键来了你无法把已经变成0的位再单独变回1想把0变回1唯一的办法就是执行擦除Erase操作它会将整个扇区或块的所有位一次性全部重置为1。所以完整的写入流程必须是擦除目标扇区确保该区域全为0xFF。写使能解除芯片的写保护。页编程写入数据。等待操作完成检查忙状态。5.1 写使能Write Enable在每次擦除或写入操作前都必须先发送写使能命令0x06。// 发送写使能命令 void W25Q64_write_enable(void) { W25QXX_CS_ON(0); spi_read_write_byte(0x06); // 写使能命令 W25QXX_CS_ON(1); }5.2 等待芯片空闲Wait Busy擦除和写入操作需要时间几毫秒到几十毫秒。芯片在执行这些内部操作时会置忙状态寄存器Status Register 1的BUSY位bit0为1。我们必须等待它变为0后才能进行下一步操作。// 等待W25Q64内部操作完成忙等待 void W25Q64_wait_busy(void) { uint8_t status; do { W25QXX_CS_ON(0); spi_read_write_byte(0x05); // 读状态寄存器1命令 status spi_read_write_byte(0xFF); // 读取状态值 W25QXX_CS_ON(1); } while ((status 0x01) 0x01); // 判断BUSY位是否为1是则循环等待 }5.3 扇区擦除Sector Erase擦除是以扇区4KB为最小单位的。擦除命令是0x20后面要紧跟要擦除扇区的24位起始地址。/** * brief 擦除一个扇区 * param sector_num: 扇区号 (0~2047)对于8MB的W25Q64共有2048个扇区 * note 擦除操作耗时较长操作后需等待 */ void W25Q64_erase_sector(uint32_t sector_num) { // 将扇区号转换为24位的起始字节地址 uint32_t addr sector_num * 4096; // 每个扇区4096字节 W25Q64_write_enable(); // 第一步写使能 W25Q64_wait_busy(); // 等待写使能完成 W25QXX_CS_ON(0); spi_read_write_byte(0x20); // 扇区擦除命令 // 发送24位地址先发高字节 spi_read_write_byte((uint8_t)((addr) 16)); spi_read_write_byte((uint8_t)((addr) 8)); spi_read_write_byte((uint8_t)addr); W25QXX_CS_ON(1); W25Q64_wait_busy(); // 等待擦除操作完成 }重要提示擦除操作是不可逆的会清空整个扇区的数据。在执行前请确保该扇区没有需要保留的数据。5.4 页编程Page Program写入数据擦除完成后就可以向“干净”的扇区写入数据了。写入命令是0x02后面跟24位起始地址然后是你要写入的数据流。注意一次页编程不能跨页256字节写入。如果写入长度超过一页超出的部分会从该页的起始地址“卷绕”覆盖。/** * brief 向W25Q64写入数据 * param buffer: 要写入的数据缓冲区指针 * param addr: 写入的起始地址字节地址 * param numbyte: 要写入的字节数 * note 函数内部会先擦除目标地址所在的整个扇区请谨慎使用 */ void W25Q64_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte) { uint16_t i; // 注意这个示例函数在写入前自动擦除了整个扇区。 // 实际项目中你可能需要更精细的管理避免频繁擦写。 W25Q64_erase_sector(addr / 4096); // 计算并擦除目标扇区 W25Q64_write_enable(); // 写使能 W25Q64_wait_busy(); // 等待 W25QXX_CS_ON(0); spi_read_write_byte(0x02); // 页编程命令 // 发送24位地址 spi_read_write_byte((uint8_t)((addr) 16)); spi_read_write_byte((uint8_t)((addr) 8)); spi_read_write_byte((uint8_t)addr); // 连续写入数据 for(i 0; i numbyte; i) { spi_read_write_byte(buffer[i]); } W25QXX_CS_ON(1); // 注意原文代码这里是W25QXX_CS_ON(0)应为笔误应拉高CS W25Q64_wait_busy(); // 等待写入完成 }6. 读取数据读取操作就简单多了不需要擦除也不需要写使能。读取命令是0x03后面跟24位起始地址然后就可以连续读取数据了。读取操作不会影响Flash中存储的内容。/** * brief 从W25Q64读取数据 * param buffer: 存储读取数据的缓冲区指针 * param read_addr: 读取的起始地址字节地址 * param read_length: 要读取的字节长度 */ void W25Q64_read(uint8_t* buffer, uint32_t read_addr, uint16_t read_length) { uint16_t i; W25QXX_CS_ON(0); spi_read_write_byte(0x03); // 读数据命令 // 发送24位地址 spi_read_write_byte((uint8_t)((read_addr) 16)); spi_read_write_byte((uint8_t)((read_addr) 8)); spi_read_write_byte((uint8_t)read_addr); // 连续读取数据 for(i 0; i read_length; i) { // 发送0xFF产生时钟同时读取数据 buffer[i] spi_read_write_byte(0xFF); } W25QXX_CS_ON(1); }7. 完整功能验证把上面所有的函数整合到W25Q64.c和W25Q64.h文件中。最后我们在main函数里写一个简单的测试流程来验证整个驱动是否工作正常。#include gd32e23x.h #include systick.h #include stdio.h #include W25Q64.h #include bsp_usart.h // 假设你有串口初始化代码 int main(void) { uint8_t read_buffer[20] {0}; // 读取缓冲区 systick_config(); // 配置系统滴答定时器 usart_gpio_config(9600U); // 初始化串口用于打印信息 W25Q64_Config(); // 初始化W25Q64的SPI和GPIO printf(GD32E230 SPI Flash Test Start...\r\n); // 1. 读取芯片ID uint16_t id W25Q64_readID(); printf(Flash ID: 0x%04X\r\n, id); // 2. 首次读取地址0的数据应该是全FF或随机值 W25Q64_read(read_buffer, 0, 7); printf(Data before write: %s\r\n, read_buffer); // 3. 向地址0写入字符串“Hello!” // 注意这会擦除第0扇区地址0~4095的所有数据 W25Q64_write((uint8_t *)Hello!, 0, 7); // 写入7个字节包含结束符\0 // 4. 再次从地址0读取数据 // 先将缓冲区清零以便观察 for(int i0; i20; i) read_buffer[i] 0; W25Q64_read(read_buffer, 0, 7); printf(Data after write: %s\r\n, read_buffer); while(1) { // 主循环 } }预期结果串口助手应该依次打印出Flash ID: 0xEF16证明芯片识别成功Data before write:可能为空或乱码因为Flash初始可能是0xFFData after write: Hello!证明写入和读取都成功了如果最后读出的数据是Hello!那么恭喜你GD32E230驱动W25Q64 SPI Flash就大功告成了最后几个小提示寿命问题Flash有擦写次数限制通常10万次左右。在项目中不要频繁地对同一个扇区进行擦写可以采用“磨损均衡”的策略。数据管理对于需要频繁修改的小数据可以考虑先缓存到RAM攒够一定量再一次性写入Flash。跨页写入如果需要写入的数据超过256字节你需要自己处理分页逻辑分多次调用页编程命令。代码优化本文的W25Q64_write函数每次写入都先擦除整个扇区实际应用中可以优化先判断该扇区是否需要擦除。希望这篇教程能帮你顺利点亮外部的SPI Flash。遇到问题别着急多检查接线、SPI模式、片选信号和命令序列一步步调试肯定能成功。