【WCH蓝牙系列芯片】-基于CH585开发板—SPI外设读写W25Q64
-------------------------------------------------------------------------------------------------------------------------------------
通常 SPI 接口由 4 个引脚组成:
SPI 片选引脚 SCS —————— 从设备使能信号,由主设备控制,一主多从时,SCS是从芯片是否被主芯片选中的控制信号,当片选信号为规定的使能信号时(高电位或低电位),只有被选中的从设备才会响应主机发送的数据和命令。
SPI 时钟引脚 SCK ——————传输时钟信号,用于主从设备的同步
SPI 串行数据引脚 MISO(主机输入/从机输出引脚)—————— 主模式时候:主机输入,从机输出 /从模式时候:主机输出。从机输入
SPI 串行数据引脚 MOSI(主机输出/从机输入引脚)—————— 主模式时候:从机输入,主机输出 / 从模式时候:从机输出。主机输入
在CH585芯片种SPI0 支持主机模式(Master)和从机模式(Slave),SPI1 只支持主机模式(Master);支持模式 0 和模式 3 数据传输 SPI时钟频率最高可达系统主频 Fsys 的一半。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
这次利用CH585的SPI0操作读写FLASH芯片W25Q64,有些情况下由于MCU的FLASH空间有限,在特殊使用场所中会存在FLASH存储不够使用的情况,W25Q64作为一种高性能的串行Flash存储器,就可以用于扩展MCU的存储空间。
W25Q64由每页256字节组成,每页的256字节用一次页编程指令即可完成。每次可以擦除16页(一个扇区),128页(32KB块),256页(64KB块)和全片擦除。 W25Q64的内存空间结构:一页256字节,4k(4096字节)为一个扇区,16个扇区为一块,总容量为8M字节,共有128个块即2048个扇区。SPI最高支持80MHZ,当使用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHZ,四倍输出时最高速率320MHZ。
1、W25Q64的硬件电路

2、W25Q64的引脚功能说明

3、W25Q64框图

4、SPI操作W25Q64程序说明
在进行任何数据操作前,需先完成 SPI 接口初始化和 Flash 芯片识别,确保硬件通信正常且芯片型号匹配。
1. SPI 硬件初始化(FLASH_Port_Init())
这里采用 SPI0 的 SCS(PA12)、SCK(PA13)、MOSI(PA14)配置为推挽输出,初始化为高电平;MISO(PA15)配置为上拉输入(接收 Flash 返回数据)。

-
SPI 控制器配置:通过
SPI0_MasterDefInit()初始化 SPI0 为主机模式,SPI0_DataMode(Mode3_HighBitINFront)设置为模式 3(时钟空闲时为高电平,下降沿采样),SPI0_CLKCfg(2)设置时钟分频(控制通信速率)

2. Flash 芯片识别(FLASH_IC_Check())
初始化完成后,需确认连接的芯片是否为目标型号(W25Q64),通过读取芯片 ID (FLASH_ReadID())实现:
-
拉低 CS 引脚(
PIN_FLASH_CS_LOW())选中芯片; -
发送 “读取 JEDEC ID” 指令(
CMD_FLASH_JEDEC_ID = 0x9F); -
连续读取 3 字节数据(制造商 ID、内存类型 ID、容量 ID),拼接为 32 位
Flash_ID; -
拉高 CS 引脚(
PIN_FLASH_CS_HIGH())释放芯片。
-
在
Main.c中,通过判断Flash_ID是否为W25Q64_FLASH_ID1 (0xEF4017)或W25Q64_FLASH_ID2 (0xEF6017)确认是否为 W25Q64 -
![image]()
二、擦除操作(以扇区擦除为例)
W25Q64 的擦除操作需一次擦除大小可以为16页(4KB)、128页(32KB)、256页(64KB)或者全擦除。(每个扇区 4096 字节,定义为SPI_FLASH_SectorSize),擦除后扇区内所有数据变为0xFF。
1. 擦除前准备:发送写使能
Flash 芯片默认禁止写入 / 擦除操作,需先发送 “写使能” 指令(CMD_FLASH_WREN = 0x06):
-
拉低 CS -> 发送
CMD_FLASH_WREN-> 拉高 CS(FLASH_WriteEnable()实现)。
2. 执行扇区擦除
-
拉低 CS 选中芯片;
-
发送 “扇区擦除” 指令(
CMD_FLASH_SECTOR_ERASE = 0x20); -
发送 24 位扇区地址(按高 8 位、中 8 位、低 8 位顺序发送,例如地址
0x000000对应第一个扇区); -
拉高 CS,触发擦除操作。
3. 等待擦除完成
擦除是耗时操作(毫秒级),需通过读取 “状态寄存器” 判断是否完成:
-
调用
FLASH_ReadStatusReg()读取状态寄存器(发送CMD_FLASH_RDSR = 0x05指令后读取 1 字节); -
检查状态寄存器的 bit0(忙标志位):bit0=1 表示正在擦除,bit0=0 表示擦除完成;
-
循环等待直到 bit0=0)。

三、写入操作
W25Q64 的写入需按 “页” 为单位(每页 256 字节,定义为SPI_FLASH_PageSize),且写入前必须确保目标区域已擦除(数据为0xFF)。
1. 写入前的扇区检查与处理
FLASH_Write()会自动处理跨扇区写入,并确保写入区域已擦除:
-
计算写入地址所在的扇区(
secpos = WriteAddr / 4096)、扇区内偏移(secoff = WriteAddr % 4096)和扇区剩余空间(secremain = 4096 - secoff); -
读取该扇区现有数据到缓冲SPI_FLASH_BUF,区检查目标写入区域是否全为0xFF;
-
若不是(有非
0xFF数据):先擦除该扇区(FLASH_Erase_Sector(secpos)),再将现有数据与新数据合并到缓冲区; -
若是:直接写入新数据。
-
2. 页写入(void W25XXX_WR_Page( uint8_t *pbuf, uint32_t address, uint32_t len ))
不管是否跨扇区,最终写入均以页为单位执行:
-
发送写使能(
FLASH_WriteEnable()); -
拉低 CS -> 发送 “页编程” 指令(
CMD_FLASH_BYTE_PROG = 0x02) -> 发送 24 位写入地址; -
连续发送数据(长度不超过 256 字节,若超过则截断);
-
拉高 CS,触发写入操作;
-
等待写入完成(通过状态寄存器 bit0 判断,同擦除流程)。
3. 跨页 / 跨扇区处理
若写入数据长度超过 1 页或 1 扇区,FLASH_Write()会自动拆分:
-
先写满当前页 / 扇区的剩余空间;
-
地址和数据指针偏移,重复写入下一页 / 扇区,直到所有数据写完。
四、读取操作
读取操作无需擦除或写使能,可直接按地址连续读取。

1. 启动读取
-
拉低 CS 选中芯片;
-
发送 “读取” 指令(
CMD_FLASH_READ = 0x03); -
发送 24 位读取起始地址(高 8 位、中 8 位、低 8 位)。
2. 连续读取数据
-
循环通过 SPI 读取数据(
SPI_FLASH_SendByte(0xFF),发送0xFF作为时钟填充,同时接收 Flash 返回的数据); -
数据存入缓冲区(
pBuffer),直到读取指定长度(size)。
3. 结束读取
-
拉高 CS 释放芯片(
PIN_FLASH_CS_HIGH())。
五、程序验证
在主函数中,进行“擦除 -> 写入 -> 读取验证的操作,
-
擦除块:循环调用
FLASH_Erase_Sector()擦除 2 个扇区(组成 8192 字节的块); -
写入块数据:通过
FLASH_Write()写入 8192 字节数据(前半部分为0xAA,后半部分为随机值); -
读取验证:调用
FLASH_Read()读取整个块数据,通过PrintBuffer()打印整快写入后的数据内容; -
扇区 / 页操作
// 测试参数定义 #define BLOCK_SIZE (2 * SPI_FLASH_SectorSize) // 测试块大小(2个扇区:8192字节) #define SECTOR_SIZE SPI_FLASH_SectorSize // 扇区大小(4096字节) #define PAGE_SIZE SPI_FLASH_PageSize // 页大小(256字节) #define TEST_ADDR 0x000000 // 测试起始地址 #define BLOCK_FILL_CHAR 0xAA // 块填充字符 #define SECTOR_FILL_CHAR 0x55 // 扇区填充字符 #define PAGE_FILL_CHAR 0x33 // 页填充字符 // 打印缓冲区数据(十六进制和ASCII),每行1024字节 void PrintBuffer(uint8_t *buf, uint16_t len, const char *desc) { uint16_t i; printf("\r\n===== %s (长度: %d字节) =====\r\n", desc, len); for(i = 0; i < len; i++) { // 打印当前字节的十六进制(格式:两位十六进制+空格,共3个字符) printf("%02X ", buf[i]); // 每1024字节换行,并打印对应ASCII字符 if((i + 1) % 1024 == 0) { printf(" "); // 十六进制与ASCII之间的分隔 // 打印当前1024字节的ASCII字符(可打印字符直接显示,不可打印用.代替) for(int j = i - 1023; j <= i; j++) { if(buf[j] >= 0x20 && buf[j] <= 0x7E) { printf("%c", buf[j]); } else { printf("."); } } printf("\r\n"); // 换行 // 每行结束时打印当前累计显示的字节数 printf("----- 当前已显示 %d 字节 -----\r\n", i + 1); } } // 处理最后一行不足1024字节的情况 if(i % 1024 != 0) { // 补充空格对齐(每个缺失字节对应3个空格,与十六进制格式"XX "匹配) for(int j = 0; j < 1024 - (i % 1024); j++) { printf(" "); } printf(" "); // 分隔符 // 打印最后一行的ASCII字符 for(int j = i - (i % 1024); j < i; j++) { if(buf[j] >= 0x20 && buf[j] <= 0x7E) { printf("%c", buf[j]); } else { printf("."); } } printf("\r\n"); } // 总字节数不是1024的倍数时,补充显示总长度 if(len % 1024 != 0) { printf("----- 总显示 %d 字节 -----\r\n", len); } printf("\r\n"); } void DebugInit(void) // SPI0与例程惯用串口打印引脚冲突,所以此例程改用UART1_输出打印 { GPIOA_SetBits(GPIO_Pin_9); GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeIN_PU); GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeOut_PP_5mA); UART1_DefInit(); } int main() { uint8_t block_buf[BLOCK_SIZE]; // 块缓冲区(8192字节) uint8_t sector_buf[SECTOR_SIZE]; // 扇区缓冲区(4096字节) uint8_t page_buf[PAGE_SIZE]; // 页缓冲区(256字节) uint8_t verify_ok = 1; uint16_t i; HSECFG_Capacitance(HSECap_18p); SetSysClock(SYSCLK_FREQ); // 初始化调试口 DebugInit(); PRINT("Start @ChipID=%02X\n", R8_CHIP_ID); printf("W25Q64 块/扇区/页操作测试开始...\r\n"); printf("块大小: %d字节, 扇区大小: %d字节, 页大小: %d字节\r\n", BLOCK_SIZE, SECTOR_SIZE, PAGE_SIZE); // 初始化SPI Flash FLASH_Port_Init(); printf("SPI Flash接口初始化完成\r\n"); // 检测Flash芯片 FLASH_IC_Check(); if(Flash_ID == W25Q64_FLASH_ID1 || Flash_ID == W25Q64_FLASH_ID2) { printf("检测到W25Q64 Flash, ID: 0x%08X\r\n", Flash_ID); } else { printf("未检测到W25Q64, ID: 0x%08X, 测试终止!\r\n", Flash_ID); while(1); } // 擦除整块区域并完整打印 printf("\r\n===== 步骤1: 擦除整块区域(0x%06X - 0x%06X) =====\r\n", TEST_ADDR, TEST_ADDR + BLOCK_SIZE - 1); for(i = 0; i < BLOCK_SIZE / SECTOR_SIZE; i++) { printf("擦除扇区 %d (地址:0x%06X)...\r\n", i, TEST_ADDR + i * SECTOR_SIZE); FLASH_Erase_Sector(TEST_ADDR + i * SECTOR_SIZE); } // 读取整块数据并完整打印 FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE); PrintBuffer(block_buf, BLOCK_SIZE, "整块擦除后的数据"); // 写入整块数据并完整打印 printf("\r\n===== 步骤2: 写入整块数据 =====\r\n"); // 填充块数据(前半部分用BLOCK_FILL_CHAR,后半部分用递增数据便于区分) for(i = 0; i < BLOCK_SIZE; i++) { block_buf[i] = (i < BLOCK_SIZE/2) ? BLOCK_FILL_CHAR : (i % 0xFF); } FLASH_Write(block_buf, TEST_ADDR, BLOCK_SIZE); // 读取整块数据并完整打印(验证写入效果) FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE); PrintBuffer(block_buf, BLOCK_SIZE, "整块写入后的数据"); // 擦除整块中的1个扇区并打印整块(观察变化) printf("\r\n===== 步骤3: 擦除整块中的第1个扇区(0x%06X - 0x%06X) =====\r\n", TEST_ADDR, TEST_ADDR + SECTOR_SIZE - 1); FLASH_Erase_Sector(TEST_ADDR); // 读取整块数据(第1个扇区应为0xFF,第2个扇区保持不变) FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE); PrintBuffer(block_buf, BLOCK_SIZE, "擦除1个扇区后的整块数据"); // 写入被擦除的扇区并打印整块 printf("\r\n===== 步骤4: 写入被擦除的扇区 =====\r\n"); // 填充扇区数据(用SECTOR_FILL_CHAR) for(i = 0; i < SECTOR_SIZE; i++) { sector_buf[i] = SECTOR_FILL_CHAR; } FLASH_Write(sector_buf, TEST_ADDR, SECTOR_SIZE); // 读取整块数据(验证扇区写入效果) FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE); PrintBuffer(block_buf, BLOCK_SIZE, "写入扇区后的整块数据"); // 擦除该扇区中的1页并打印整个扇区(观察变化) printf("\r\n===== 步骤5: 擦除扇区中的第1页(0x%06X - 0x%06X) =====\r\n", TEST_ADDR, TEST_ADDR + PAGE_SIZE - 1); // 备份扇区内非目标页数据 FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE); // 擦除整个扇区 FLASH_Erase_Sector(TEST_ADDR); // 恢复非目标页数据(只留第1页为0xFF) FLASH_Write(sector_buf + PAGE_SIZE, TEST_ADDR + PAGE_SIZE, SECTOR_SIZE - PAGE_SIZE); // 读取整个扇区数据(第1页应为0xFF,其他页保持) FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE); PrintBuffer(sector_buf, SECTOR_SIZE, "擦除1页后的扇区数据"); // 写入被擦除的页并打印整个扇区 printf("\r\n===== 步骤6: 写入被擦除的页 =====\r\n"); // 填充页数据(用PAGE_FILL_CHAR) for(i = 0; i < PAGE_SIZE; i++) { page_buf[i] = PAGE_FILL_CHAR; } FLASH_Write(page_buf, TEST_ADDR, PAGE_SIZE); // 读取整个扇区数据(验证页写入效果) FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE); PrintBuffer(sector_buf, SECTOR_SIZE, "写入页后的扇区数据"); // 最终验证 FLASH_Read(page_buf, TEST_ADDR, PAGE_SIZE); for(i = 0; i < PAGE_SIZE; i++) { if(page_buf[i] != PAGE_FILL_CHAR) { verify_ok = 0; break; } } if(verify_ok) { printf("\r\n******* 所有测试通过 *******\r\n"); } else { printf("\r\n******* 测试失败 *******\r\n"); } while(1); }






关键注意事项
1. CS 引脚控制:所有操作(读 / 写 / 擦除 / 读 ID)均需先拉低 CS 选中芯片,操作完成后拉高 CS 释放,否则指令无效;
2. 写使能:擦除和写入前必须发送CMD_FLASH_WREN,否则操作被忽略;
3. 忙等待:擦除和写入是耗时操作,必须通过状态寄存器的忙标志位等待完成,避免后续操作冲突;
4. 地址对齐:扇区擦除地址需对齐扇区起始(address % 4096 == 0),页写入地址需注意不跨页(或通过W25XXX_WR_Block()自动处理)。

浙公网安备 33010602011771号