STM32 进阶封神之路(三十二):SPI 通信深度实战 —— 硬件 SPI 驱动 W25Q64 闪存(底层时序 + 寄存器配置 + 读写封装)
STM32 进阶封神之路三十二SPI 通信深度实战 —— 硬件 SPI 驱动 W25Q64 闪存底层时序 寄存器配置 读写封装上一篇我们掌握了 STM32 硬件 IIC 的底层原理与 BH1750 驱动这一篇聚焦嵌入式高速同步通信协议 ——SPISerial Peripheral Interface。SPI 以 “全双工、高速率、主从架构” 的优势广泛应用于闪存、触摸屏、ADC/DAC、无线模块等外设而 W25Q64 作为 SPI 接口的 NOR Flash是存储日志、固件、配置参数的核心器件。本文基于参考文档中的 SPI 协议与 W25Q64 核心知识点从 SPI 协议底层原理、STM32 硬件 SPI 配置、W25Q64 存储架构、指令集解析到完整的读写擦除实战全程超详细拆解帮你彻底掌握 SPI 通信与 Flash 存储的工业级应用一、SPI 协议核心认知为什么它是高速通信首选1. SPI 协议核心特性与应用场景SPI 是 Motorola 提出的同步串行通信协议核心特点的是全双工、高速率、主从架构、三线 / 四线通信相比 IIC 和 UART优势极为突出速率高支持 Mbps 级传输W25Q64 最高支持 80MHz远超 IIC 的 400Kbps全双工同一时钟周期内可同时发送和接收数据通信效率高结构简单仅需 4 根线NSS、SCK、MOSI、MISO支持一主多从稳定性强同步时钟驱动时序精度高抗干扰能力优于异步通信如 UART。典型应用场景存储设备SPI FlashW25Q64、W25Q128、EEPROM显示设备OLED 屏幕SSD1306 的 SPI 模式、触摸屏XPT2046传感器加速度传感器MPU6050 的 SPI 模式、ADC 芯片ADS1115无线模块WiFiESP8266 的 SPI 模式、蓝牙模块。2. SPI 协议核心概念必掌握1总线组成四线制参考文档物理层知识点表格引脚名称功能描述主机配置从机配置NSSCS片选信号低电平选中从设备输出模式控制选中哪个从机输入模式接收主机片选信号SCKSerial Clock同步时钟由主机生成输出模式输入模式MOSIMaster Out Slave In主机输出从机输入输出模式输入模式MISOMaster In Slave Out主机输入从机输出输入模式输出模式关键说明NSS 为低电平时从设备被选中仅选中的从设备会响应 SPI 通信一主多从场景下主机通过不同 NSS 引脚控制多个从设备SCK、MOSI、MISO 三线共用部分场景可省略 NSS 引脚软件片选但硬件片选更稳定推荐使用。2四种工作模式核心时序差异SPI 的工作模式由时钟极性CPOL和时钟相位CPHA决定共四种组合参考文档时序知识点表格模式CPOL时钟极性CPHA时钟相位核心时序适用场景模式 0默认0空闲时 SCK 为低电平0第一个时钟跳变沿采样上升沿采样下降沿发送多数 SPI 设备W25Q64 默认支持模式 10空闲时 SCK 为低电平1第二个时钟跳变沿采样下降沿采样上升沿发送部分传感器模式 21空闲时 SCK 为高电平0第一个时钟跳变沿采样下降沿采样上升沿发送工业控制设备模式 31空闲时 SCK 为高电平1第二个时钟跳变沿采样上升沿采样下降沿发送高速通信场景时序核心采样接收方读取数据的时刻需与发送方的发送时刻同步发送发送方更新数据到数据线的时刻W25Q64 支持模式 0 和模式 3实战中推荐使用模式 0兼容性最好。3数据传输规则高位优先MSB First默认传输顺序数据从最高位开始逐位传输低位优先LSB First需手动配置部分设备支持同步传输每一个 SCK 时钟周期传输 1 位数据MOSI 和 MISO 的数据同步变化。3. STM32F103 SPI 外设资源参考文档 STM32 SPI 知识点STM32F103 系列内置 2 个硬件 SPI 外设SPI1、SPI2核心参数如下SPI1挂载在 APB2 总线最高 72MHz支持高速传输引脚为 PA4NSS、PA5SCK、PA6MISO、PA7MOSISPI2挂载在 APB1 总线最高 36MHz引脚为 PB12NSS、PB13SCK、PB14MISO、PB15MOSI支持主模式、从模式支持 8 位 / 16 位数据宽度支持软件片选NSS 引脚不用由软件控制和硬件片选支持中断模式和 DMA 模式传输。二、STM32 硬件 SPI 外设配置底层寄存器 库函数本节基于 SPI1PA4~PA7配置主机模式驱动 W25Q64核心流程为 “时钟使能→GPIO 复用配置→SPI 参数配置→SPI 使能”。1. 硬件接线SPI1 W25Q64表格STM32 引脚SPI 功能W25Q64 引脚备注PA4SPI1_NSSNSS片选CS低电平选中硬件片选PA5SPI1_SCKSCK时钟SCK主机生成时钟PA6SPI1_MISOMISO主机输入SO从机输出数据PA7SPI1_MOSIMOSI主机输出SI主机输出数据3.3VVCCVCCW25Q64 工作电压 3.3VGNDGNDGND共地3.3VWPWP写保护引脚接高电平禁用写保护3.3VHOLDHOLD保持引脚接高电平正常工作2. 库函数配置步骤c运行#include stm32f10x.h #include delay.h // SPI1引脚定义 #define SPI1_NSS_PIN GPIO_Pin_4 #define SPI1_SCK_PIN GPIO_Pin_5 #define SPI1_MISO_PIN GPIO_Pin_6 #define SPI1_MOSI_PIN GPIO_Pin_7 #define SPI1_GPIO_PORT GPIOA #define SPI1_GPIO_CLK RCC_APB2Periph_GPIOA #define SPI1_CLK RCC_APB2Periph_SPI1 // W25Q64片选控制宏 #define W25Q64_CS_HIGH() GPIO_SetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN) // 取消选中 #define W25Q64_CS_LOW() GPIO_ResetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN) // 选中 // SPI1初始化主机模式模式08位数据10MHz速率 void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 1. 使能GPIO和SPI1时钟 RCC_APB2PeriphClockCmd(SPI1_GPIO_CLK | SPI1_CLK, ENABLE); // 2. 配置GPIO为复用功能 // NSSPA4推挽输出硬件片选主机控制 GPIO_InitStruct.GPIO_Pin SPI1_NSS_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(SPI1_GPIO_PORT, GPIO_InitStruct); // SCKPA5、MOSIPA7复用推挽输出 GPIO_InitStruct.GPIO_Pin SPI1_SCK_PIN | SPI1_MOSI_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(SPI1_GPIO_PORT, GPIO_InitStruct); // MISOPA6浮空输入 GPIO_InitStruct.GPIO_Pin SPI1_MISO_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(SPI1_GPIO_PORT, GPIO_InitStruct); // 3. 配置SPI1参数 SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; // 全双工模式 SPI_InitStruct.SPI_Mode SPI_Mode_Master; // 主机模式 SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; // 8位数据宽度 SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; // 时钟极性0空闲低电平 SPI_InitStruct.SPI_CPHA SPI_CPHA_1Edge; // 时钟相位0第一个跳变沿采样 SPI_InitStruct.SPI_NSS SPI_NSS_Soft; // 软件片选或SPI_NSS_Hard硬件片选 SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 波特率分频系数8 // APB2时钟72MHz分频后速率72MHz/89MHzW25Q64支持最高80MHz SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; // 高位优先 SPI_InitStruct.SPI_CRCPolynomial 7; // CRC校验多项式默认7禁用CRC时无影响 // 4. 初始化SPI1 SPI_Init(SPI1, SPI_InitStruct); // 5. 禁用CRC校验多数场景无需CRC SPI_Cmd(SPI1, DISABLE); SPI_CRCConfig(SPI1, DISABLE); SPI_Cmd(SPI1, ENABLE); // 6. 初始状态取消选中W25Q64 W25Q64_CS_HIGH(); }3. 关键配置说明参考文档 STM32 SPI 知识点GPIO 模式NSS硬件片选时配置为推挽输出软件片选时可配置为普通 GPIOSCK/MOSI复用推挽输出AF_PP因为需要 SPI 外设控制引脚电平MISO浮空输入IN_FLOATING避免外部电平干扰。波特率分频SPI1 挂载在 APB2 总线72MHz分频系数可选 2、4、8、16 等速率 APB2 时钟 / 分频系数W25Q64 支持最高 80MHz实战中推荐 9MHz稳定或 18MHz高速。片选模式硬件片选SPI_NSS_HardNSS 引脚由 SPI 外设自动控制主模式下发送数据时自动拉低软件片选SPI_NSS_SoftNSS 引脚由用户手动控制推荐灵活性更高。工作模式W25Q64 默认支持模式 0因此 CPOLLowCPHA1Edge。4. SPI 核心通信函数发送 / 接收 1 字节c运行// SPI1发送1字节数据返回接收的字节全双工同步传输 uint8_t SPI1_Send_Byte(uint8_t data) { // 等待发送缓冲区为空 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); // 发送数据 SPI_I2S_SendData(SPI1, data); // 等待接收缓冲区非空 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); // 返回接收的数据全双工发送的同时会接收数据 return SPI_I2S_ReceiveData(SPI1); } // SPI1接收1字节数据通过发送0xFF触发接收 uint8_t SPI1_Receive_Byte(void) { return SPI1_Send_Byte(0xFF); // 发送0xFF接收从机返回的数据 } // SPI1发送多字节数据 void SPI1_Send_Buffer(uint8_t *buf, uint16_t len) { if (buf NULL || len 0) return; for (uint16_t i 0; i len; i) { SPI1_Send_Byte(buf[i]); } } // SPI1接收多字节数据 void SPI1_Receive_Buffer(uint8_t *buf, uint16_t len) { if (buf NULL || len 0) return; for (uint16_t i 0; i len; i) { buf[i] SPI1_Receive_Byte(); } }关键说明SPI 是全双工通信发送数据的同时必然会接收数据接收数据时也需要发送一个 “dummy 数据”如 0xFF触发时钟这是 SPI 的核心特性。三、W25Q64 闪存核心解析参考文档 W25Q64 知识点W25Q64 是 Winbond华邦推出的 8MB 容量 SPI NOR Flash支持高速 SPI 通信是嵌入式系统中存储数据的常用器件。1. W25Q64 核心参数容量8MB64Mbit分为 128 个扇区每个扇区 64KB每个扇区分为 16 个页每个页 4KB通信接口SPI支持模式 0/3最高速率 80MHz擦除方式扇区擦除64KB、块擦除32KB/64KB、全片擦除编程方式页编程最多 4KB / 页使用寿命10 万次擦写数据保留 100 年工作电压2.7V~3.6V推荐 3.3V。2. W25Q64 存储布局参考文档容量布局表格存储单元大小数量地址范围示例用途页Page4KB20480x00000~0x00FFF第 0 页单次编程最小单元扇区Sector64KB1280x00000~0x0FFFF第 0 扇区单次擦除最小单元块Block32KB/64KB256/1280x00000~0x07FFF32KB 块批量擦除单元全片Chip8MB10x000000~0x7FFFFF整个闪存关键规则编程前必须先擦除Flash 特性只能将 1 改为 0擦除可将 0 改为 1页编程时超过 4KB 的数据会循环覆盖当前页的起始地址扇区擦除会将该扇区内的所有数据置为 0xFF。3. W25Q64 核心指令集参考文档指令操作W25Q64 通过 SPI 接收指令实现读写擦除常用核心指令如下表格指令代码指令名称功能描述指令长度备注0x9F读取 ID读取制造商 ID、设备 ID1 字节指令 3 字节数据验证通信是否正常0x06写使能允许后续的编程 / 擦除操作1 字节指令编程 / 擦除前必须执行0x05读状态寄存器 1读取闪存状态如是否忙1 字节指令 1 字节数据0x00 空闲0x01 忙0x02页编程向指定地址写入数据≤4KB1 字节指令 3 字节地址 N 字节数据地址需对齐到页起始0x20扇区擦除擦除指定扇区64KB1 字节指令 3 字节地址擦除时间约 40ms0x03读数据从指定地址读取数据1 字节指令 3 字节地址 N 字节数据支持任意长度读取0xC7全片擦除擦除整个闪存1 字节指令擦除时间约 10 秒慎用四、W25Q64 驱动实现硬件 SPI 指令封装基于 STM32 SPI1 和 W25Q64 指令集封装初始化、读 ID、擦除、编程、读取等核心函数实现完整的 Flash 操作。1. W25Q64 驱动头文件定义c运行#ifndef __W25Q64_H__ #define __W25Q64_H__ #include stm32f10x.h // W25Q64容量定义8MB64Mbit #define W25Q64_FLASH_SIZE 0x800000UL // 8MB #define W25Q64_SECTOR_SIZE 0x10000UL // 64KB/扇区 #define W25Q64_PAGE_SIZE 0x1000UL // 4KB/页 // W25Q64 ID定义制造商ID0xEF设备ID0x4017 #define W25Q64_MANUFACTURER_ID 0xEF #define W25Q64_DEVICE_ID 0x4017 // W25Q64指令定义 #define W25Q64_CMD_READ_ID 0x9F #define W25Q64_CMD_WRITE_EN 0x06 #define W25Q64_CMD_READ_SR1 0x05 #define W25Q64_CMD_PAGE_PROG 0x02 #define W25Q64_CMD_SECTOR_ERASE 0x20 #define W25Q64_CMD_READ_DATA 0x03 #define W25Q64_CMD_CHIP_ERASE 0xC7 // 函数声明 void W25Q64_Init(void); uint32_t W25Q64_ReadID(void); void W25Q64_WaitBusy(void); void W25Q64_WriteEnable(void); void W25Q64_SectorErase(uint32_t addr); void W25Q64_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len); void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len); void W25Q64_ChipErase(void); #endif2. W25Q64 核心驱动函数实现c运行#include w25q64.h #include spi.h #include delay.h // W25Q64初始化初始化SPI1 void W25Q64_Init(void) { SPI1_Init(); W25Q64_CS_HIGH(); // 初始取消选中 delay_ms(10); } // 读取W25Q64 ID制造商ID设备ID uint32_t W25Q64_ReadID(void) { uint32_t id 0; W25Q64_CS_LOW(); // 选中W25Q64 delay_us(10); // 发送读ID指令0x9F SPI1_Send_Byte(W25Q64_CMD_READ_ID); // 读取3字节ID制造商ID1字节 设备ID2字节 id | (uint32_t)SPI1_Receive_Byte() 16; // 制造商ID0xEF id | (uint32_t)SPI1_Receive_Byte() 8; // 设备ID高字节0x40 id | (uint32_t)SPI1_Receive_Byte(); // 设备ID低字节0x17 W25Q64_CS_HIGH(); // 取消选中 delay_us(10); return id; } // 等待W25Q64空闲擦除/编程完成 void W25Q64_WaitBusy(void) { uint8_t sr1 0; W25Q64_CS_LOW(); delay_us(10); // 发送读状态寄存器1指令 SPI1_Send_Byte(W25Q64_CMD_READ_SR1); // 循环读取状态直到Bit0为0空闲 do { sr1 SPI1_Receive_Byte(); } while ((sr1 0x01) 0x01); // 0x01忙0x00空闲 W25Q64_CS_HIGH(); delay_us(10); } // 写使能编程/擦除前必须执行 void W25Q64_WriteEnable(void) { W25Q64_CS_LOW(); delay_us(10); SPI1_Send_Byte(W25Q64_CMD_WRITE_EN); W25Q64_CS_HIGH(); delay_us(10); } // 扇区擦除64KB void W25Q64_SectorErase(uint32_t addr) { // 地址校验不能超过Flash容量 if (addr W25Q64_FLASH_SIZE) return; // 写使能 W25Q64_WriteEnable(); W25Q64_CS_LOW(); delay_us(10); // 发送扇区擦除指令 SPI1_Send_Byte(W25Q64_CMD_SECTOR_ERASE); // 发送3字节地址A23~A0 SPI1_Send_Byte((addr 16) 0xFF); // 高字节 SPI1_Send_Byte((addr 8) 0xFF); // 中字节 SPI1_Send_Byte(addr 0xFF); // 低字节 W25Q64_CS_HIGH(); delay_us(10); // 等待擦除完成约40ms W25Q64_WaitBusy(); } // 页编程最多4KB void W25Q64_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len) { if (addr W25Q64_FLASH_SIZE || buf NULL || len 0) return; // 限制单次编程长度≤4KB if (len W25Q64_PAGE_SIZE) len W25Q64_PAGE_SIZE; // 写使能 W25Q64_WriteEnable(); W25Q64_CS_LOW(); delay_us(10); // 发送页编程指令 SPI1_Send_Byte(W25Q64_CMD_PAGE_PROG); // 发送3字节地址 SPI1_Send_Byte((addr 16) 0xFF); SPI1_Send_Byte((addr 8) 0xFF); SPI1_Send_Byte(addr 0xFF); // 发送数据 SPI1_Send_Buffer(buf, len); W25Q64_CS_HIGH(); delay_us(10); // 等待编程完成约1ms W25Q64_WaitBusy(); } // 读数据任意长度 void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { if (addr W25Q64_FLASH_SIZE || buf NULL || len 0) return; W25Q64_CS_LOW(); delay_us(10); // 发送读数据指令 SPI1_Send_Byte(W25Q64_CMD_READ_DATA); // 发送3字节地址 SPI1_Send_Byte((addr 16) 0xFF); SPI1_Send_Byte((addr 8) 0xFF); SPI1_Send_Byte(addr 0xFF); // 接收数据 SPI1_Receive_Buffer(buf, len); W25Q64_CS_HIGH(); delay_us(10); } // 全片擦除慎用约10秒 void W25Q64_ChipErase(void) { // 写使能 W25Q64_WriteEnable(); W25Q64_CS_LOW(); delay_us(10); // 发送全片擦除指令 SPI1_Send_Byte(W25Q64_CMD_CHIP_ERASE); W25Q64_CS_HIGH(); delay_us(10); // 等待擦除完成约10秒 W25Q64_WaitBusy(); }3. 多页数据写入封装跨页处理W25Q64 的页编程限制单次写入≤4KB若要写入超过 4KB 的数据需手动处理跨页逻辑c运行// 多页写入自动处理跨页 void W25Q64_MultiPageWrite(uint32_t addr, uint8_t *buf, uint32_t len) { uint16_t page_remain 0; // 当前页剩余空间 uint32_t write_len 0; // 本次写入长度 uint32_t current_addr addr; // 当前写入地址 uint32_t current_len len; // 当前剩余长度 uint8_t *current_buf buf; // 当前写入缓冲区指针 while (current_len 0) { // 计算当前页剩余空间地址对齐到页起始 page_remain W25Q64_PAGE_SIZE - (current_addr % W25Q64_PAGE_SIZE); // 确定本次写入长度取剩余长度和页剩余空间的最小值 write_len (current_len page_remain) ? page_remain : current_len; // 页编程 W25Q64_PageWrite(current_addr, current_buf, write_len); // 更新参数 current_addr write_len; current_buf write_len; current_len - write_len; } }五、实战整合W25Q64 读写擦除测试1. 主函数实现c运行#include stm32f10x.h #include usart.h #include w25q64.h #include delay.h // 测试数据定义 #define TEST_ADDR 0x000000UL // 测试地址第0扇区第0页 #define TEST_LEN 5000UL // 测试数据长度5KB跨1个页 uint8_t write_buf[TEST_LEN]; // 写入缓冲区 uint8_t read_buf[TEST_LEN]; // 读取缓冲区 int main(void) { uint32_t id 0; uint8_t err_flag 0; // 初始化系统时钟72MHz SystemInit(); // 初始化串口1115200bps打印调试信息 USART1_Init(115200); // 初始化W25Q64 W25Q64_Init(); printf(STM32硬件SPIW25Q64实战测试\r\n); printf(\r\n\r\n); // 1. 读取W25Q64 ID id W25Q64_ReadID(); printf(W25Q64 ID0x%06X\r\n, id); printf(制造商ID0x%02X预期0xEF\r\n, (id 16) 0xFF); printf(设备ID0x%04X预期0x4017\r\n, id 0xFFFF); if (id ((W25Q64_MANUFACTURER_ID 16) | W25Q64_DEVICE_ID)) { printf(W25Q64通信正常\r\n); } else { printf(W25Q64通信异常\r\n); while (1); } printf(---------------------------------------\r\n\r\n); // 2. 填充测试数据0x00~0x1387 for (uint32_t i 0; i TEST_LEN; i) { write_buf[i] i 0xFF; // 填充0x00~0xFF循环数据 } // 3. 扇区擦除测试地址所在扇区 printf(开始擦除扇区地址0x%06X...\r\n, TEST_ADDR); W25Q64_SectorErase(TEST_ADDR); printf(扇区擦除完成\r\n); printf(---------------------------------------\r\n\r\n); // 4. 多页写入测试数据5KB跨2个页 printf(开始写入数据长度%d字节...\r\n, TEST_LEN); W25Q64_MultiPageWrite(TEST_ADDR, write_buf, TEST_LEN); printf(数据写入完成\r\n); printf(---------------------------------------\r\n\r\n); // 5. 读取数据 printf(开始读取数据地址0x%06X长度%d字节...\r\n, TEST_ADDR, TEST_LEN); W25Q64_ReadData(TEST_ADDR, read_buf, TEST_LEN); printf(数据读取完成\r\n); printf(---------------------------------------\r\n\r\n); // 6. 校验数据对比写入和读取的数据 printf(开始校验数据...\r\n); for (uint32_t i 0; i TEST_LEN; i) { if (read_buf[i] ! write_buf[i]) { printf(数据校验失败地址0x%06X预期0x%02X实际0x%02X\r\n, TEST_ADDR i, write_buf[i], read_buf[i]); err_flag 1; break; } } if (err_flag 0) { printf(数据校验成功所有数据一致\r\n); } printf(\r\n); printf(W25Q64读写擦除测试完成\r\n); while (1) { delay_ms(1000); } }2. 串口打印效果plaintextSTM32硬件SPIW25Q64实战测试 W25Q64 ID0xEF4017 制造商ID0xEF预期0xEF 设备ID0x4017预期0x4017 W25Q64通信正常 --------------------------------------- 开始擦除扇区地址0x000000... 扇区擦除完成 --------------------------------------- 开始写入数据长度5000字节... 数据写入完成 --------------------------------------- 开始读取数据地址0x000000长度5000字节... 数据读取完成 --------------------------------------- 开始校验数据... 数据校验成功所有数据一致 W25Q64读写擦除测试完成六、SPIW25Q64 常见问题与避坑指南参考文档注意事项1. 读取 ID 失败返回 0x000000 或 0xFFFFFF原因 1硬件接线错误SCK/MOSI/MISO 接反、未共地解决重新检查接线确保 SCK 接 PA5、MOSI 接 PA7、MISO 接 PA6必须共地原因 2SPI 工作模式不匹配W25Q64 默认模式 0代码配置为其他模式解决确认 SPI 配置为 CPOLLow、CPHA1Edge模式 0原因 3片选信号未正确控制始终选中或未选中解决初始化时 W25Q64_CS_HIGH取消选中通信时拉低选中原因 4SPI 速率过高超过 W25Q64 支持的 80MHz解决降低 SPI 速率如分频系数 89MHz。2. 编程 / 擦除失败状态寄存器始终为忙原因 1未执行写使能指令0x06解决编程 / 擦除前必须调用W25Q64_WriteEnable()原因 2地址超出 Flash 容量解决确认地址≤0x7FFFFF8MB原因 3写保护引脚WP接低电平启用写保护解决WP 引脚接 3.3V禁用写保护原因 4SPI 通信异常数据传输错误解决先通过读 ID 验证通信确保 SPI 发送 / 接收函数正常。3. 数据校验失败写入与读取的数据不一致原因 1编程前未擦除扇区Flash 只能写 0不能写 1解决编程前必须擦除对应的扇区原因 2页编程超过 4KB导致数据覆盖解决使用W25Q64_MultiPageWrite函数自动处理跨页原因 3SPI 数据宽度配置错误如配置为 16 位解决确保SPI_DataSize SPI_DataSize_8b原因 4地址发送顺序错误应为高字节→低字节解决确认地址发送顺序为(addr 16) 0xFF高、(addr 8) 0xFF中、addr 0xFF低。4. SPI 通信不稳定偶尔成功偶尔失败原因 1GPIO 速率配置过低如 GPIO_Speed_2MHz解决将 SPI 引脚的 GPIO 速率配置为GPIO_Speed_50MHz原因 2未添加延时片选切换后未稳定解决片选拉低 / 拉高后添加 10~20μs 延时原因 3电源纹波干扰W25Q64 供电不稳定解决在 W25Q64 的 VCC 和 GND 之间并联 0.1μF 陶瓷电容原因 4SPI 时钟极性 / 相位配置错误接近工作临界点解决严格配置为模式 0CPOLLowCPHA1Edge。七、SPI 协议进阶软件 SPI 与硬件 SPI 选型指南1. 软件 SPI 实现参考文档软件 SPI 知识点软件 SPI 通过普通 GPIO 模拟 SPI 时序无需依赖硬件 SPI 外设灵活性更高核心代码示例c运行// 软件SPI引脚定义任意GPIO #define SOFT_SPI_SCK_PIN GPIO_Pin_0 #define SOFT_SPI_MOSI_PIN GPIO_Pin_1 #define SOFT_SPI_MISO_PIN GPIO_Pin_2 #define SOFT_SPI_NSS_PIN GPIO_Pin_3 #define SOFT_SPI_GPIO_PORT GPIOB // 软件SPI引脚操作宏 #define SOFT_SPI_SCK_HIGH() GPIO_SetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_SCK_PIN) #define SOFT_SPI_SCK_LOW() GPIO_ResetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_SCK_PIN) #define SOFT_SPI_MOSI_HIGH() GPIO_SetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_MOSI_PIN) #define SOFT_SPI_MOSI_LOW() GPIO_ResetBits(SOFT_SPI_GPIO_PORT, SOFT_SPI_MOSI_PIN) #define SOFT_SPI_MISO_READ() GPIO_ReadInputDataBit(SOFT_SPI_GPIO_PORT, SOFT_SPI_MISO_PIN) // 软件SPI发送1字节模式0 uint8_t Soft_SPI_SendByte(uint8_t data) { uint8_t i, recv_data 0; for (i 0; i 8; i) { // 高位优先 if (data 0x80) { SOFT_SPI_MOSI_HIGH(); } else { SOFT_SPI_MOSI_LOW(); } data 1; // 上升沿采样 SOFT_SPI_SCK_HIGH(); recv_data 1; if (SOFT_SPI_MISO_READ()) { recv_data | 0x01; } delay_us(1); // 下降沿准备下一位 SOFT_SPI_SCK_LOW(); delay_us(1); } return recv_data; }2. 软件 SPI 与硬件 SPI 对比参考文档 2.9 节表格对比维度软件 SPI硬件 SPI实现方式普通 GPIO 模拟时序硬件外设自动生成时序CPU 占用高需 CPU 全程参与低配置后自动传输速率低最高约 1MHz高最高 80MHz引脚限制无任意 GPIO有必须使用指定 SPI 引脚代码复杂度低手动编写时序中配置外设寄存器稳定性一般依赖延时精度高硬件时序精准适用场景低速、简单场景高速、批量数据传输3. 选型建议优先选择硬件 SPIW25Q64、OLED 屏幕、高速传感器等需要批量传输数据的场景优先选择软件 SPI引脚资源紧张、低速通信、快速原型开发场景兼容性考虑硬件 SPI 的引脚可能与其他外设冲突如 JTAG需注意引脚复用。八、总结SPIW25Q64 核心要点与进阶方向1. 核心要点回顾SPI 协议核心四线制、全双工、同步时钟、四种工作模式模式 0 为默认W25Q64 核心8MB 容量、扇区擦除 页编程、编程前必须写使能、Flash 只能写 0 擦 1驱动关键SPI 参数配置匹配模式 0、高位优先、8 位数据、指令时序正确、地址发送顺序高→低避坑核心通信前读 ID 验证、编程前擦除、写使能不可少、跨页处理、电源稳定。2. 进阶学习方向DMA 模式硬件 SPI 支持 DMA 传输进一步降低 CPU 占用适合大批量数据读写中断模式实现 SPI 通信中断避免查询模式阻塞主循环多从设备在同一 SPI 总线上挂载多个设备如 W25Q64OLED通过 NSS 引脚切换低功耗优化W25Q64 支持掉电模式结合 STM32 低功耗延长电池续航应用扩展实现固件升级将新固件存储到 W25Q64再写入 STM32 Flash、日志存储、配置参数保存。掌握 SPIW25Q64 后你已具备嵌入式高速通信与数据存储的核心能力可应用于工业控制、物联网设备、智能硬件等场景。下一篇我们将学习 STM32 的 CAN 总线通信聚焦工业级设备间的高可靠性、实时性数据传输