SPI(Serial Peripheral Interface)是一种高速、全双工的同步串行通信协议,由摩托罗拉公司在上世纪80年代推出。它凭借简单的硬件设计和高效的传输速率,成为嵌入式系统中使用最广泛的通信接口之一。我第一次接触SPI是在做一个传感器数据采集项目时,当时需要高速读取加速度计的数据,I2C的速率已经无法满足需求,转而使用SPI后数据传输立刻流畅了许多。
SPI协议使用四根信号线进行通信:
与I2C相比,SPI有几点显著优势:首先是速度,SPI可以轻松达到几十MHz的传输速率;其次是全双工特性,可以同时收发数据;最后是没有复杂的仲裁机制,主从通信更加直接。不过SPI也有缺点,比如没有硬件应答机制,需要软件来保证数据可靠性。
在实际项目中,我遇到过SPI时钟相位配置错误导致通信失败的情况。当时调试了半天才发现是模式设置不对,这个教训让我深刻理解了SPI四种工作时序模式的重要性。
W25Q64是Winbond公司推出的一款64Mbit(8MB)串行Flash存储器,采用SPI接口通信。这款芯片在我参与的多个嵌入式存储项目中表现出色,特别是它的擦写寿命可以达到10万次,数据保存期限长达20年。
芯片的主要特性包括:
记得第一次使用W25Q64时,我犯了个典型错误 - 没有先擦除就直接写入数据,结果发现数据写入异常。后来仔细阅读手册才明白,Flash存储器的每个bit只能从1变为0,要想写入新数据必须先擦除整个扇区(将bit全部置1)。
芯片的引脚定义也很简单:
在设计STM32与W25Q64的硬件连接时,有几个关键点需要注意。根据我的项目经验,合理的硬件设计可以避免很多后期调试的麻烦。
基本连接方式:
几个实用建议:
我曾经在一个高速数据采集项目中遇到过SPI通信不稳定的问题,后来发现是PCB布局不合理导致信号完整性受损。重新设计PCB时,我特别注意了以下几点:
在STM32标准库中配置SPI接口需要以下几个步骤,这里以SPI1为例:
c复制void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 配置SPI引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置CS引脚(PA4)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4); // 初始置高
// SPI参数配置
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18MHz @72MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
这里有几个容易出错的地方:
实现W25Q64的驱动需要先完成几个基础函数:
c复制// 读取芯片ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
SPI_CS_LOW(); // 片选使能
SPI_ReadWriteByte(W25Q64_JEDEC_ID); // 发送读取ID指令
*MID = SPI_ReadWriteByte(0xFF); // 读取厂商ID
*DID = SPI_ReadWriteByte(0xFF); // 读取设备ID高字节
*DID <<= 8;
*DID |= SPI_ReadWriteByte(0xFF); // 读取设备ID低字节
SPI_CS_HIGH(); // 片选禁用
}
// 写使能
void W25Q64_WriteEnable(void)
{
SPI_CS_LOW();
SPI_ReadWriteByte(W25Q64_WRITE_ENABLE);
SPI_CS_HIGH();
}
// 等待忙状态结束
void W25Q64_WaitBusy(void)
{
while(1)
{
SPI_CS_LOW();
SPI_ReadWriteByte(W25Q64_READ_STATUS_REGISTER_1);
if((SPI_ReadWriteByte(0xFF) & 0x01) == 0)
break;
SPI_CS_HIGH();
Delay_us(100);
}
SPI_CS_HIGH();
}
Flash存储器的写入有其特殊性,必须遵循正确的操作流程:
c复制// 页编程(写入数据)
void W25Q64_PageProgram(uint32_t addr, uint8_t *buf, uint16_t len)
{
W25Q64_WriteEnable(); // 必须先写使能
SPI_CS_LOW();
SPI_ReadWriteByte(W25Q64_PAGE_PROGRAM); // 页编程指令
SPI_ReadWriteByte((addr >> 16) & 0xFF); // 地址高字节
SPI_ReadWriteByte((addr >> 8) & 0xFF); // 地址中字节
SPI_ReadWriteByte(addr & 0xFF); // 地址低字节
while(len--)
{
SPI_ReadWriteByte(*buf++); // 写入数据
}
SPI_CS_HIGH();
W25Q64_WaitBusy(); // 等待写入完成
}
// 扇区擦除(4KB)
void W25Q64_SectorErase(uint32_t addr)
{
W25Q64_WriteEnable(); // 必须先写使能
SPI_CS_LOW();
SPI_ReadWriteByte(W25Q64_SECTOR_ERASE_4KB); // 扇区擦除指令
SPI_ReadWriteByte((addr >> 16) & 0xFF); // 地址高字节
SPI_ReadWriteByte((addr >> 8) & 0xFF); // 地址中字节
SPI_ReadWriteByte(addr & 0xFF); // 地址低字节
SPI_CS_HIGH();
W25Q64_WaitBusy(); // 等待擦除完成
}
读取操作相对简单,不需要提前擦除:
c复制// 读取数据
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len)
{
SPI_CS_LOW();
SPI_ReadWriteByte(W25Q64_READ_DATA); // 读取指令
SPI_ReadWriteByte((addr >> 16) & 0xFF); // 地址高字节
SPI_ReadWriteByte((addr >> 8) & 0xFF); // 地址中字节
SPI_ReadWriteByte(addr & 0xFF); // 地址低字节
while(len--)
{
*buf++ = SPI_ReadWriteByte(0xFF); // 读取数据
}
SPI_CS_HIGH();
}
在实际项目中使用W25Q64时,我总结了一些宝贵的经验教训:
1. 写前必须擦除
Flash存储器的一个关键特性是只能将bit从1改为0,不能从0改为1。因此写入新数据前必须先擦除相应区域。我曾经因为没有擦除就直接写入,导致数据异常,调试了很久才发现问题。
2. 注意地址对齐
3. 忙状态检查
每次写入或擦除操作后,芯片会进入忙状态。在这期间任何读写操作都会被忽略。我建议在驱动中加入忙状态检查函数,并在每次操作后调用。
4. 寿命管理
虽然W25Q64标称有10万次擦写寿命,但实际使用时建议:
5. 电源稳定性
Flash对电源波动很敏感,突然断电可能导致数据损坏。在关键应用中建议:
经过多个项目的实践,我总结出一些提升W25Q64使用效率的技巧:
1. 批量操作
尽量批量读写数据,减少单独操作次数。比如需要存储多个参数时,可以先将它们打包成一个结构体,然后一次性写入。
2. 缓存机制
在RAM中建立缓存区,将频繁修改的数据先在RAM中累积,达到一定量后再一次性写入Flash。
3. 四线模式
如果硬件支持,可以使用QSPI四线模式,将传输速率提升4倍。不过这会增加代码复杂度,需要权衡使用。
4. DMA传输
对于大数据量传输,可以使用SPI的DMA功能,减轻CPU负担。配置示例:
c复制void SPI1_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置TX DMA
DMA_DeInit(DMA1_Channel3);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)0; // 使用时设置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 0; // 使用时设置
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
// 配置RX DMA
DMA_DeInit(DMA1_Channel2);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)0; // 使用时设置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE);
}
5. 中断管理
合理使用中断可以提高系统响应速度,比如:
下面是一个使用W25Q64存储传感器数据的完整示例:
c复制// 数据结构定义
typedef struct {
uint32_t timestamp;
float temperature;
float humidity;
uint16_t pressure;
uint8_t checksum;
} SensorData;
// 存储传感器数据
void SaveSensorData(uint32_t addr, SensorData *data)
{
// 计算校验和
uint8_t *p = (uint8_t*)data;
data->checksum = 0;
for(int i=0; i<sizeof(SensorData)-1; i++)
{
data->checksum += p[i];
}
// 先擦除扇区
W25Q64_SectorErase(addr & 0xFFF000);
// 写入数据
W25Q64_PageProgram(addr, (uint8_t*)data, sizeof(SensorData));
}
// 读取传感器数据
uint8_t ReadSensorData(uint32_t addr, SensorData *data)
{
uint8_t checksum = 0;
// 读取数据
W25Q64_ReadData(addr, (uint8_t*)data, sizeof(SensorData));
// 验证校验和
uint8_t *p = (uint8_t*)data;
for(int i=0; i<sizeof(SensorData)-1; i++)
{
checksum += p[i];
}
return (checksum == data->checksum);
}
int main(void)
{
SensorData currentData;
uint32_t storageAddr = 0x001000; // 存储起始地址
// 初始化
SPI1_Init();
W25Q64_Init();
Sensor_Init();
while(1)
{
// 采集传感器数据
currentData.timestamp = GetTimestamp();
currentData.temperature = ReadTemperature();
currentData.humidity = ReadHumidity();
currentData.pressure = ReadPressure();
// 存储数据
SaveSensorData(storageAddr, ¤tData);
// 更新存储地址
storageAddr += sizeof(SensorData);
if(storageAddr >= 0x00800000) // 到达芯片末尾
{
storageAddr = 0x001000; // 循环使用
}
Delay_ms(1000); // 每秒存储一次
}
}
这个示例展示了如何将传感器数据安全地存储在W25Q64中,包括:
在实际项目中,我还建议增加以下功能: