SPI(Serial Peripheral Interface)是一种高速全双工的同步串行通信协议,由摩托罗拉公司提出。与I2C相比,SPI最大的特点是通信速率高、协议简单,特别适合需要快速传输数据的场景。在STM32项目中,我们常用SPI接口来驱动各类外设,其中W25Qxx系列FLASH芯片就是典型代表。
W25Qxx是Winbond公司推出的SPI接口NOR Flash存储器,常见型号有W25Q16(16Mbit)、W25Q32(32Mbit)、W25Q64(64Mbit)和W25Q128(128Mbit)。这类芯片具有以下特点:
在实际项目中,我经常用W25Qxx来存储系统配置参数、日志数据或者固件备份。比如在工业控制设备中,需要保存PID参数;在物联网终端中,需要缓存传感器数据。这些场景都要求数据在断电后不丢失,W25Qxx正好能满足需求。
STM32F429有多个SPI外设,我习惯用SPI5,因为它的引脚分布在PF6-PF9,布线比较方便。下面是硬件连接示意图:
| W25Qxx引脚 | STM32F429引脚 | 功能说明 |
|---|---|---|
| CS | PF6 | 片选信号 |
| CLK | PF7 | 时钟信号 |
| DO | PF8 | 数据输出 |
| DI | PF9 | 数据输入 |
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
初始化代码需要注意几个关键点:
c复制void SPI5_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 使能GPIOF和SPI5时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI5, ENABLE);
// 配置PF7,8,9为SPI复用功能
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOF, &GPIO_InitStruct);
// 配置PF6为普通输出(CS)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_Init(GPIOF, &GPIO_InitStruct);
// SPI5参数配置
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI5, &SPI_InitStruct);
SPI_Cmd(SPI5, ENABLE);
}
W25Qxx支持SPI模式0和模式3,我推荐使用模式3(CPOL=1, CPHA=1),因为:
波特率分频系数要根据实际需求选择。STM32F429的APB2时钟为90MHz,如果选择4分频,理论速率可达22.5Mbps。但在长线传输时,建议降低速率到5-10Mbps以提高稳定性。
先实现最基础的字节收发函数,这是所有操作的基础:
c复制uint8_t SPI5_ReadWriteByte(uint8_t data)
{
while(SPI_I2S_GetFlagStatus(SPI5, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI5, data);
while(SPI_I2S_GetFlagStatus(SPI5, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI5);
}
然后是芯片识别函数,通过读取JEDEC ID来确认芯片型号:
c复制uint32_t W25Q_ReadID(void)
{
uint32_t temp = 0;
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x9F); // JEDEC ID指令
temp |= SPI5_ReadWriteByte(0xFF) << 16;
temp |= SPI5_ReadWriteByte(0xFF) << 8;
temp |= SPI5_ReadWriteByte(0xFF);
W25Q_CS_HIGH();
return temp;
}
W25Qxx在执行写操作前必须先发送写使能指令,并且要检查BUSY状态:
c复制void W25Q_WaitBusy(void)
{
while((W25Q_ReadSR() & 0x01) == 0x01);
}
void W25Q_WriteEnable(void)
{
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x06); // 写使能指令
W25Q_CS_HIGH();
}
uint8_t W25Q_ReadSR(void)
{
uint8_t sr;
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x05); // 读状态寄存器指令
sr = SPI5_ReadWriteByte(0xFF);
W25Q_CS_HIGH();
return sr;
}
FLASH存储器必须先擦除才能写入,擦除的最小单位是扇区(4KB):
c复制void W25Q_SectorErase(uint32_t addr)
{
W25Q_WriteEnable();
W25Q_WaitBusy();
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x20); // 扇区擦除指令
SPI5_ReadWriteByte((addr >> 16) & 0xFF);
SPI5_ReadWriteByte((addr >> 8) & 0xFF);
SPI5_ReadWriteByte(addr & 0xFF);
W25Q_CS_HIGH();
W25Q_WaitBusy();
}
写入数据使用页编程指令,单次最多写入256字节:
c复制void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
uint16_t i;
W25Q_WriteEnable();
W25Q_WaitBusy();
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x02); // 页编程指令
SPI5_ReadWriteByte((addr >> 16) & 0xFF);
SPI5_ReadWriteByte((addr >> 8) & 0xFF);
SPI5_ReadWriteByte(addr & 0xFF);
for(i=0; i<len; i++)
SPI5_ReadWriteByte(data[i]);
W25Q_CS_HIGH();
W25Q_WaitBusy();
}
读取数据相对简单,没有长度限制:
c复制void W25Q_ReadData(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint32_t i;
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x03); // 读数据指令
SPI5_ReadWriteByte((addr >> 16) & 0xFF);
SPI5_ReadWriteByte((addr >> 8) & 0xFF);
SPI5_ReadWriteByte(addr & 0xFF);
for(i=0; i<len; i++)
buf[i] = SPI5_ReadWriteByte(0xFF);
W25Q_CS_HIGH();
}
实际项目中经常需要写入超过256字节的数据,这时候需要处理跨页情况:
c复制void W25Q_WriteBuffer(uint32_t addr, uint8_t *data, uint32_t len)
{
uint32_t page_remain;
uint32_t write_len;
uint32_t write_addr = addr;
while(len > 0)
{
page_remain = 256 - (write_addr % 256);
write_len = (len > page_remain) ? page_remain : len;
W25Q_PageProgram(write_addr, data, write_len);
write_addr += write_len;
data += write_len;
len -= write_len;
}
}
为了保证数据可靠性,建议增加校验机制。这里展示简单的CRC8校验:
c复制uint8_t Calc_CRC8(uint8_t *data, uint32_t len)
{
uint8_t crc = 0xFF;
uint32_t i, j;
for(i=0; i<len; i++)
{
crc ^= data[i];
for(j=0; j<8; j++)
{
if(crc & 0x80)
crc = (crc << 1) ^ 0x31;
else
crc <<= 1;
}
}
return crc;
}
void W25Q_WriteWithCRC(uint32_t addr, uint8_t *data, uint32_t len)
{
uint8_t crc = Calc_CRC8(data, len);
W25Q_WriteBuffer(addr, data, len);
W25Q_WriteBuffer(addr + len, &crc, 1);
}
uint8_t W25Q_VerifyCRC(uint32_t addr, uint8_t *data, uint32_t len)
{
uint8_t crc_read, crc_calc;
W25Q_ReadData(addr + len, &crc_read, 1);
crc_calc = Calc_CRC8(data, len);
return (crc_read == crc_calc);
}
对于复杂应用,可以在W25Qxx上实现简单的文件系统:
c复制typedef struct {
uint32_t start_addr;
uint32_t end_addr;
uint32_t file_size;
uint8_t file_name[16];
uint8_t file_type;
uint32_t create_time;
} File_Header;
#define FILE_SYSTEM_BASE 0x000000
#define MAX_FILE_NUM 32
#define FILE_HEADER_SIZE sizeof(File_Header)
void FS_Format(void)
{
W25Q_SectorErase(FILE_SYSTEM_BASE);
}
uint8_t FS_CreateFile(File_Header *header)
{
uint32_t addr = FILE_SYSTEM_BASE;
File_Header temp;
// 查找空闲位置
while(addr < FILE_SYSTEM_BASE + 4096)
{
W25Q_ReadData(addr, (uint8_t*)&temp, FILE_HEADER_SIZE);
if(temp.file_type == 0xFF) // 空闲位置
{
header->start_addr = addr + FILE_HEADER_SIZE;
header->end_addr = header->start_addr + header->file_size;
W25Q_WriteBuffer(addr, (uint8_t*)header, FILE_HEADER_SIZE);
return 1;
}
addr += FILE_HEADER_SIZE + temp.file_size;
}
return 0;
}
uint8_t FS_ReadFile(File_Header *header, uint8_t *buf)
{
if(header->file_type == 0xFF)
return 0;
W25Q_ReadData(header->start_addr, buf, header->file_size);
return 1;
}
在工业控制器中,我们需要保存PID参数、校准数据等:
c复制typedef struct {
float Kp;
float Ki;
float Kd;
uint32_t serial_num;
uint8_t calib_date[11];
} System_Params;
void Save_Params(System_Params *params)
{
W25Q_SectorErase(0x10000);
W25Q_WriteWithCRC(0x10000, (uint8_t*)params, sizeof(System_Params));
}
uint8_t Load_Params(System_Params *params)
{
uint8_t status;
W25Q_ReadData(0x10000, (uint8_t*)params, sizeof(System_Params));
status = W25Q_VerifyCRC(0x10000, (uint8_t*)params, sizeof(System_Params));
return status;
}
对于物联网设备,需要记录传感器数据:
c复制typedef struct {
uint32_t timestamp;
float temperature;
float humidity;
uint16_t battery;
} Sensor_Data;
#define LOG_START_ADDR 0x20000
#define LOG_MAX_NUM 1024
void Log_Write(Sensor_Data *data)
{
static uint32_t log_index = 0;
uint32_t addr;
if(log_index >= LOG_MAX_NUM)
{
// 循环覆盖最早的数据
log_index = 0;
}
addr = LOG_START_ADDR + log_index * sizeof(Sensor_Data);
W25Q_WriteBuffer(addr, (uint8_t*)data, sizeof(Sensor_Data));
log_index++;
}
void Log_Read(uint32_t index, Sensor_Data *data)
{
uint32_t addr = LOG_START_ADDR + index * sizeof(Sensor_Data);
W25Q_ReadData(addr, (uint8_t*)data, sizeof(Sensor_Data));
}
实现固件双备份机制,提高系统可靠性:
c复制#define FW_MAIN_ADDR 0x40000
#define FW_BACKUP_ADDR 0xC0000
#define FW_MAX_SIZE 0x80000
uint8_t FW_Update(uint8_t *fw_data, uint32_t fw_size)
{
if(fw_size > FW_MAX_SIZE)
return 0;
// 先写入备份区
for(uint32_t i=0; i<fw_size; i+=4096)
{
W25Q_SectorErase(FW_BACKUP_ADDR + i);
W25Q_WriteBuffer(FW_BACKUP_ADDR + i, fw_data + i,
(fw_size-i)>4096 ? 4096 : (fw_size-i));
}
// 验证备份固件
if(!FW_Verify(FW_BACKUP_ADDR, fw_data, fw_size))
return 0;
// 更新主固件区
for(uint32_t i=0; i<fw_size; i+=4096)
{
W25Q_SectorErase(FW_MAIN_ADDR + i);
W25Q_WriteBuffer(FW_MAIN_ADDR + i, fw_data + i,
(fw_size-i)>4096 ? 4096 : (fw_size-i));
}
return 1;
}
uint8_t FW_Verify(uint32_t addr, uint8_t *fw_data, uint32_t fw_size)
{
uint8_t buf[256];
uint32_t i, len;
for(i=0; i<fw_size; i+=256)
{
len = (fw_size-i)>256 ? 256 : (fw_size-i);
W25Q_ReadData(addr + i, buf, len);
if(memcmp(buf, fw_data + i, len) != 0)
return 0;
}
return 1;
}
对于大数据量传输,可以启用SPI DMA:
c复制void SPI5_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStruct;
// 使能DMA2时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
// 配置DMA发送通道
DMA_InitStruct.DMA_Channel = DMA_Channel_2;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI5->DR;
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)0;
DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStruct.DMA_BufferSize = 0;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream3, &DMA_InitStruct);
// 配置DMA接收通道
DMA_InitStruct.DMA_Channel = DMA_Channel_2;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI5->DR;
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)0;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStruct.DMA_BufferSize = 0;
DMA_Init(DMA2_Stream2, &DMA_InitStruct);
// 使能SPI DMA请求
SPI_I2S_DMACmd(SPI5, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE);
}
void W25Q_DMA_Read(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint8_t cmd[4] = {0x03, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
W25Q_CS_LOW();
// 发送读命令和地址
DMA2_Stream3->M0AR = (uint32_t)cmd;
DMA2_Stream3->NDTR = 4;
DMA_Cmd(DMA2_Stream3, ENABLE);
while(DMA_GetFlagStatus(DMA2_Stream3, DMA_FLAG_TCIF3) == RESET);
DMA_Cmd(DMA2_Stream3, DISABLE);
// 接收数据
DMA2_Stream2->M0AR = (uint32_t)buf;
DMA2_Stream2->NDTR = len;
DMA_Cmd(DMA2_Stream2, ENABLE);
while(DMA_GetFlagStatus(DMA2_Stream2, DMA_FLAG_TCIF2) == RESET);
DMA_Cmd(DMA2_Stream2, DISABLE);
W25Q_CS_HIGH();
}
对于实时数据采集系统,可以采用双缓冲技术:
c复制#define BUF_SIZE 2048
uint8_t buf1[BUF_SIZE];
uint8_t buf2[BUF_SIZE];
uint8_t *active_buf = buf1;
uint8_t *ready_buf = buf2;
uint32_t buf_pos = 0;
void Data_Collector_ISR(void)
{
// 采集数据存入active_buf
active_buf[buf_pos++] = Read_Sensor();
if(buf_pos >= BUF_SIZE)
{
// 切换缓冲区
uint8_t *temp = active_buf;
active_buf = ready_buf;
ready_buf = temp;
buf_pos = 0;
// 触发存储任务
Start_Store_Task();
}
}
void Store_Task(void)
{
static uint32_t store_addr = 0x30000;
W25Q_SectorErase(store_addr);
W25Q_WriteBuffer(store_addr, ready_buf, BUF_SIZE);
store_addr += BUF_SIZE;
if(store_addr >= 0x40000)
store_addr = 0x30000;
}
延长FLASH使用寿命的磨损均衡实现:
c复制#define WEAR_LEVELING_SECTORS 16
#define WEAR_COUNT_ADDR 0x50000
uint32_t wear_count[WEAR_LEVELING_SECTORS];
uint32_t current_sector = 0;
void Wear_Leveling_Init(void)
{
// 从FLASH加载磨损计数
W25Q_ReadData(WEAR_COUNT_ADDR, (uint8_t*)wear_count, sizeof(wear_count));
// 查找使用最少的扇区
uint32_t min_count = 0xFFFFFFFF;
for(uint32_t i=0; i<WEAR_LEVELING_SECTORS; i++)
{
if(wear_count[i] < min_count)
{
min_count = wear_count[i];
current_sector = i;
}
}
}
uint32_t Get_Next_Sector(void)
{
// 更新磨损计数
wear_count[current_sector]++;
W25Q_WriteBuffer(WEAR_COUNT_ADDR, (uint8_t*)wear_count, sizeof(wear_count));
// 选择下一个扇区
current_sector = (current_sector + 1) % WEAR_LEVELING_SECTORS;
return 0x60000 + current_sector * 4096;
}
检查硬件连接
验证SPI基本功能
测试芯片识别
检查时序参数
现象:读取的数据与写入的不一致
现象:偶尔数据错误
现象:写入速度很慢
c复制void W25Q_PowerDown(void)
{
W25Q_CS_LOW();
SPI5_ReadWriteByte(0xB9); // 进入低功耗模式
W25Q_CS_HIGH();
}
void W25Q_ReleasePowerDown(void)
{
W25Q_CS_LOW();
SPI5_ReadWriteByte(0xAB); // 退出低功耗模式
W25Q_CS_HIGH();
Delay(3); // 等待唤醒
}
c复制void W25Q_BulkErase(void)
{
W25Q_WriteEnable();
W25Q_WaitBusy();
W25Q_CS_LOW();
SPI5_ReadWriteByte(0xC7); // 整片擦除指令
W25Q_CS_HIGH();
W25Q_WaitBusy(); // 等待擦除完成,约需20-30秒
}
c复制void W25Q_UnprotectAll(void)
{
W25Q_WriteEnable();
W25Q_CS_LOW();
SPI5_ReadWriteByte(0x01); // 写状态寄存器指令
SPI5_ReadWriteByte(0x00); // 取消所有保护
W25Q_CS_HIGH();
W25Q_WaitBusy();
}