第一次接触STM32的SPI外设和W25Q64 Flash芯片时,我踩了不少坑。从CubeMX的参数配置到驱动代码的编写,每一步都可能隐藏着意想不到的问题。这篇文章将带你完整走一遍配置和开发的流程,重点解决那些容易出错的关键环节。
SPI(Serial Peripheral Interface)是一种高速全双工同步串行通信协议,在嵌入式系统中广泛应用。W25Q64是华邦电子推出的一款8MB容量NOR Flash存储器,采用SPI接口通信。
SPI通信的核心参数:
W25Q64的存储结构如下:
| 层级 | 大小 | 数量 | 备注 |
|---|---|---|---|
| 全片 | 8MB | 1 | 总容量 |
| 块(Block) | 64KB | 128 | 块擦除单位 |
| 扇区(Sector) | 4KB | 16/块 | 最小擦除单位 |
| 页(Page) | 256B | 16/扇区 | 编程单位 |
特别注意:W25Q64必须在擦除后才能写入数据,擦除后所有位变为1。未擦除直接写入会导致数据异常。
在CubeMX中配置SPI1为全双工主模式:
c复制/* SPI1 parameter settings */
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 4分频
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
关键点说明:
除了SPI引脚(SCK/MISO/MOSI),还需要配置一个GPIO作为片选信号(CS):
c复制#define W25Q_CS_Pin GPIO_PIN_0
#define W25Q_CS_Port GPIOC
void W25Q_CS_Level(uint8_t level) {
HAL_GPIO_WritePin(W25Q_CS_Port, W25Q_CS_Pin,
level ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
首先实现SPI收发的基础函数:
c复制static HAL_StatusTypeDef W25Q64_Transmit(uint8_t *pData, uint16_t Size) {
return HAL_SPI_Transmit(&hspi1, pData, Size, HAL_MAX_DELAY);
}
static HAL_StatusTypeDef W25Q64_Receive(uint8_t *pData, uint16_t Size) {
return HAL_SPI_Receive(&hspi1, pData, Size, HAL_MAX_DELAY);
}
写使能/禁止:
c复制#define W25Q_WRITE_ENABLE 0x06
#define W25Q_WRITE_DISABLE 0x04
HAL_StatusTypeDef W25Q64_WriteEnable(uint8_t enable) {
uint8_t cmd = enable ? W25Q_WRITE_ENABLE : W25Q_WRITE_DISABLE;
HAL_StatusTypeDef status;
W25Q_CS_Level(0);
status = W25Q64_Transmit(&cmd, 1);
W25Q_CS_Level(1);
return status;
}
读取状态寄存器:
c复制#define W25Q_READ_STATUS_REG1 0x05
uint8_t W25Q64_ReadStatusReg(void) {
uint8_t cmd = W25Q_READ_STATUS_REG1;
uint8_t status = 0;
W25Q_CS_Level(0);
W25Q64_Transmit(&cmd, 1);
W25Q64_Receive(&status, 1);
W25Q_CS_Level(1);
return status;
}
void W25Q64_WaitBusy(void) {
while(W25Q64_ReadStatusReg() & 0x01); // 检查BUSY位
}
扇区擦除(4KB):
c复制#define W25Q_SECTOR_ERASE 0x20
HAL_StatusTypeDef W25Q64_SectorErase(uint32_t sectorAddr) {
uint8_t cmd[4];
HAL_StatusTypeDef status;
// 转换为字节地址
sectorAddr *= 4096;
// 构造命令
cmd[0] = W25Q_SECTOR_ERASE;
cmd[1] = (sectorAddr >> 16) & 0xFF;
cmd[2] = (sectorAddr >> 8) & 0xFF;
cmd[3] = sectorAddr & 0xFF;
W25Q64_WriteEnable(1);
W25Q_CS_Level(0);
status = W25Q64_Transmit(cmd, 4);
W25Q_CS_Level(1);
W25Q64_WaitBusy();
return status;
}
页编程(最大256字节):
c复制#define W25Q_PAGE_PROGRAM 0x02
HAL_StatusTypeDef W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) {
uint8_t cmd[4];
HAL_StatusTypeDef status;
// 构造命令
cmd[0] = W25Q_PAGE_PROGRAM;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25Q64_WriteEnable(1);
W25Q_CS_Level(0);
status = W25Q64_Transmit(cmd, 4);
if(status == HAL_OK) {
status = W25Q64_Transmit(data, len);
}
W25Q_CS_Level(1);
W25Q64_WaitBusy();
return status;
}
读取数据:
c复制#define W25Q_READ_DATA 0x03
HAL_StatusTypeDef W25Q64_ReadData(uint32_t addr, uint8_t *data, uint16_t len) {
uint8_t cmd[4];
HAL_StatusTypeDef status;
// 构造命令
cmd[0] = W25Q_READ_DATA;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25Q_CS_Level(0);
status = W25Q64_Transmit(cmd, 4);
if(status == HAL_OK) {
status = W25Q64_Receive(data, len);
}
W25Q_CS_Level(1);
return status;
}
检查硬件连接:
验证SPI配置:
基础功能测试:
问题1:写入数据后读取全为0xFF
可能原因:
解决方案:
问题2:读取数据不稳定
可能原因:
解决方案:
合理规划存储结构:
批量操作优化:
缓存机制:
c复制#define SECTOR_SIZE 4096
uint8_t sectorBuffer[SECTOR_SIZE];
void W25Q64_UpdateSector(uint32_t sectorAddr, uint16_t offset, uint8_t *data, uint16_t len) {
// 1. 读取整个扇区到缓冲区
W25Q64_ReadData(sectorAddr * SECTOR_SIZE, sectorBuffer, SECTOR_SIZE);
// 2. 修改缓冲区数据
memcpy(sectorBuffer + offset, data, len);
// 3. 擦除扇区
W25Q64_SectorErase(sectorAddr);
// 4. 写回整个扇区
W25Q64_PageProgram(sectorAddr * SECTOR_SIZE, sectorBuffer, SECTOR_SIZE);
}
以下是整合后的W25Q64驱动头文件示例:
c复制// w25q64.h
#ifndef __W25Q64_H
#define __W25Q64_H
#include "stm32f1xx_hal.h"
// 引脚定义
#define W25Q_CS_PIN GPIO_PIN_0
#define W25Q_CS_PORT GPIOC
// 指令定义
#define W25Q_WRITE_ENABLE 0x06
#define W25Q_WRITE_DISABLE 0x04
#define W25Q_READ_STATUS_REG1 0x05
#define W25Q_PAGE_PROGRAM 0x02
#define W25Q_SECTOR_ERASE 0x20
#define W25Q_BLOCK_ERASE 0xD8
#define W25Q_CHIP_ERASE 0xC7
#define W25Q_READ_DATA 0x03
#define W25Q_READ_JEDEC_ID 0x9F
// 函数声明
void W25Q64_Init(SPI_HandleTypeDef *hspi);
uint32_t W25Q64_ReadID(void);
void W25Q64_WriteEnable(uint8_t enable);
uint8_t W25Q64_ReadStatusReg(void);
void W25Q64_WaitBusy(void);
HAL_StatusTypeDef W25Q64_SectorErase(uint32_t sectorAddr);
HAL_StatusTypeDef W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len);
HAL_StatusTypeDef W25Q64_ReadData(uint32_t addr, uint8_t *data, uint16_t len);
#endif
对应的源文件实现:
c复制// w25q64.c
#include "w25q64.h"
#include <string.h>
static SPI_HandleTypeDef *hspi_w25q;
void W25Q64_Init(SPI_HandleTypeDef *hspi) {
hspi_w25q = hspi;
// CS引脚初始化为高电平
HAL_GPIO_WritePin(W25Q_CS_PORT, W25Q_CS_PIN, GPIO_PIN_SET);
}
static void W25Q_CS_Level(uint8_t level) {
HAL_GPIO_WritePin(W25Q_CS_PORT, W25Q_CS_PIN,
level ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
static HAL_StatusTypeDef W25Q64_Transmit(uint8_t *pData, uint16_t Size) {
return HAL_SPI_Transmit(hspi_w25q, pData, Size, HAL_MAX_DELAY);
}
static HAL_StatusTypeDef W25Q64_Receive(uint8_t *pData, uint16_t Size) {
return HAL_SPI_Receive(hspi_w25q, pData, Size, HAL_MAX_DELAY);
}
uint32_t W25Q64_ReadID(void) {
uint8_t cmd = W25Q_READ_JEDEC_ID;
uint8_t id[3] = {0};
W25Q_CS_Level(0);
W25Q64_Transmit(&cmd, 1);
W25Q64_Receive(id, 3);
W25Q_CS_Level(1);
return (id[0] << 16) | (id[1] << 8) | id[2];
}
// 其他函数实现参考前面章节...
实际项目中,我发现最常遇到的问题是不按规范操作顺序导致的。比如忘记写使能、未等待芯片就绪就进行下一步操作等。建议在每个关键操作后都添加状态检查,确保操作序列的正确执行。