STM32CubeMX实战:手把手教你用SPI驱动W25Q64 Flash存储数据(附完整代码)
STM32CubeMX实战:从零构建SPI Flash数据记录系统
在嵌入式开发中,外部Flash存储器常被用于扩展存储容量或保存关键数据。W25Q64作为一款常见的SPI接口Flash芯片,具有8MB容量、低功耗和较高性价比等特点,非常适合用于数据记录、固件存储等场景。本文将手把手带你完成一个完整的SPI Flash数据记录系统开发,涵盖CubeMX配置、驱动封装到应用实现的全部流程。
1. 环境准备与CubeMX基础配置
1.1 硬件选型与连接
W25Q64采用标准的SPI接口与MCU通信,典型连接方式如下:
| MCU引脚 | W25Q64引脚 | 功能说明 |
|---|---|---|
| PA5 | CLK | 时钟信号 |
| PA6 | MISO | 主入从出数据线 |
| PA7 | MOSI | 主出从入数据线 |
| PC0 | CS | 片选信号(自定义) |
提示:虽然STM32的SPI1硬件NSS引脚是PA4,但实际开发中更推荐使用普通GPIO手动控制片选,这样更灵活且避免硬件NSS的复杂配置。
1.2 CubeMX工程创建
打开STM32CubeMX,选择对应型号MCU(如STM32F103C8T6)
配置时钟树,确保系统时钟为72MHz(SPI1挂在APB2总线上)
在Connectivity中启用SPI1,配置为全双工主模式:
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; // 软件控制NSS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 18MHz将PC0配置为GPIO_Output,初始电平设为高(片选默认不选中)
生成MDK-ARM工程,选择"为每个外设生成单独的.c/.h文件"
2. W25Q64驱动层实现
2.1 基础通信函数封装
首先封装SPI收发的基础函数,后续所有操作都基于这些底层接口:
// SPI阻塞式发送 HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size) { return HAL_SPI_Transmit(&hspi1, send_buf, size, 100); } // SPI阻塞式接收 HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size) { return HAL_SPI_Receive(&hspi1, recv_buf, size, 100); } // 带片选控制的复合传输 HAL_StatusTypeDef SPI_TransmitReceiveCS(uint8_t* tx, uint8_t* rx, uint16_t size) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&hspi1, tx, rx, size, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return status; }2.2 设备识别与状态管理
W25Q64的每个操作都需要检查设备状态,避免在忙时发送命令:
#define W25Q_READ_STATUS_REG1 0x05 #define W25Q_BUSY_MASK 0x01 uint8_t W25Q_ReadStatusReg(uint8_t reg) { uint8_t cmd[] = {reg, 0x00}; uint8_t status = 0; SPI_TransmitReceiveCS(cmd, &status, 2); return status; } void W25Q_WaitBusy(void) { while(W25Q_ReadStatusReg(W25Q_READ_STATUS_REG1) & W25Q_BUSY_MASK); }设备识别函数可验证硬件连接是否正确:
uint32_t W25Q_ReadJEDECID(void) { uint8_t cmd = 0x9F; uint8_t id[3] = {0}; SPI_TransmitReceiveCS(&cmd, id, 4); // 发送1字节,接收3字节 return (id[0]<<16)|(id[1]<<8)|id[2]; }2.3 存储单元操作实现
W25Q64的基本存储操作包括擦除、编程和读取:
扇区擦除(4KB)
void W25Q_EraseSector(uint32_t addr) { uint8_t cmd[4] = {0x20, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF}; W25Q_WriteEnable(); SPI_TransmitReceiveCS(cmd, NULL, 4); W25Q_WaitBusy(); }页编程(256字节)
void W25Q_PageProgram(uint32_t addr, uint8_t* data, uint16_t len) { uint8_t cmd[4] = {0x02, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF}; W25Q_WriteEnable(); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, data, len, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); W25Q_WaitBusy(); }数据读取
void W25Q_ReadData(uint32_t addr, uint8_t* buf, uint32_t len) { uint8_t cmd[4] = {0x03, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, buf, len, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }3. 数据记录仪应用实现
3.1 存储结构设计
为实现高效的数据管理,设计如下存储结构:
| 4B Magic | 4B DataCount | 4B NextAddr | 512B Data... | |----------|--------------|-------------|--------------| | 0x55AA55AA | 累计记录数 | 下条记录地址 | 实际传感器数据 |对应的数据结构体:
#pragma pack(push, 1) typedef struct { uint32_t magic; uint32_t count; uint32_t next_addr; uint8_t data[512]; } DataRecord_t; #pragma pack(pop)3.2 循环存储策略
为避免频繁擦除,实现循环写入算法:
#define RECORD_SIZE sizeof(DataRecord_t) #define SECTOR_SIZE 4096 #define RECORDS_PER_SECTOR (SECTOR_SIZE/RECORD_SIZE) uint32_t FindLastRecord(void) { uint32_t addr = 0; DataRecord_t rec; while(addr < W25Q_TOTAL_SIZE) { W25Q_ReadData(addr, (uint8_t*)&rec, sizeof(rec.magic)); if(rec.magic != 0x55AA55AA) break; addr = rec.next_addr; } return (addr > 0) ? (addr - RECORD_SIZE) : 0; } void WriteNewRecord(DataRecord_t* rec) { uint32_t last_addr = FindLastRecord(); uint32_t new_addr = last_addr + RECORD_SIZE; // 需要擦除新扇区 if((new_addr / SECTOR_SIZE) != (last_addr / SECTOR_SIZE)) { W25Q_EraseSector(new_addr); } rec->magic = 0x55AA55AA; rec->next_addr = new_addr + RECORD_SIZE; W25Q_PageProgram(new_addr, (uint8_t*)rec, RECORD_SIZE); }3.3 数据验证与恢复
添加CRC校验提高数据可靠性:
uint16_t CalcCRC16(uint8_t* data, uint32_t len) { uint16_t crc = 0xFFFF; for(uint32_t i=0; i<len; i++) { crc ^= data[i]; for(uint8_t j=0; j<8; j++) { if(crc & 0x0001) crc = (crc>>1) ^ 0xA001; else crc >>= 1; } } return crc; } int VerifyRecord(uint32_t addr) { DataRecord_t rec; W25Q_ReadData(addr, (uint8_t*)&rec, RECORD_SIZE); uint16_t calc_crc = CalcCRC16(rec.data, sizeof(rec.data)); uint16_t stored_crc = *(uint16_t*)&rec.data[sizeof(rec.data)-2]; return (calc_crc == stored_crc) ? 1 : 0; }4. 性能优化与高级功能
4.1 双缓冲写入技术
为提高写入效率,实现双缓冲机制:
#define BUF_SIZE 512 uint8_t bufA[BUF_SIZE], bufB[BUF_SIZE]; uint8_t* activeBuf = bufA; uint16_t bufPos = 0; void FlushBuffer(uint8_t* buf) { DataRecord_t rec; memcpy(rec.data, buf, BUF_SIZE); rec.data[BUF_SIZE-2] = CalcCRC16(buf, BUF_SIZE-2) & 0xFF; rec.data[BUF_SIZE-1] = CalcCRC16(buf, BUF_SIZE-2) >> 8; WriteNewRecord(&rec); } void WriteToBuffer(uint8_t* data, uint16_t len) { if(bufPos + len > BUF_SIZE) { FlushBuffer(activeBuf); activeBuf = (activeBuf == bufA) ? bufB : bufA; bufPos = 0; } memcpy(&activeBuf[bufPos], data, len); bufPos += len; }4.2 磨损均衡策略
延长Flash寿命的写入算法:
uint32_t wear_count[W25Q_SECTOR_COUNT]; uint32_t GetNextWriteSector(void) { static uint32_t current_sector = 0; uint32_t least_worn = 0xFFFFFFFF; uint32_t candidate = current_sector; // 查找磨损最少的扇区 for(uint32_t i=0; i<W25Q_SECTOR_COUNT; i++) { if(wear_count[i] < least_worn) { least_worn = wear_count[i]; candidate = i; } } wear_count[candidate]++; current_sector = (candidate + 1) % W25Q_SECTOR_COUNT; return candidate * SECTOR_SIZE; }4.3 掉电保护机制
应对意外断电的数据保护:
void WriteTransactionBegin(uint32_t meta_addr) { uint8_t marker = 0xA5; W25Q_PageProgram(meta_addr, &marker, 1); // 写入开始标记 } void WriteTransactionEnd(uint32_t meta_addr) { uint8_t marker = 0x00; W25Q_PageProgram(meta_addr, &marker, 1); // 清除开始标记 } int IsRecoveryNeeded(uint32_t meta_addr) { uint8_t marker; W25Q_ReadData(meta_addr, &marker, 1); return (marker == 0xA5) ? 1 : 0; }5. 实际应用示例
5.1 温湿度记录仪实现
结合DHT11传感器的完整示例:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); // 初始化Flash if(W25Q_ReadJEDECID() != 0xEF4017) { printf("Flash init failed!\r\n"); while(1); } // 恢复未完成的事务 if(IsRecoveryNeeded(RECOVERY_MARKER_ADDR)) { printf("Recovering interrupted write...\r\n"); // 实现恢复逻辑 } while(1) { // 读取DHT11数据 float temp, humid; if(DHT11_Read(&temp, &humid) == DHT11_OK) { SensorData_t data; data.timestamp = HAL_GetTick(); data.temperature = temp; data.humidity = humid; WriteTransactionBegin(RECOVERY_MARKER_ADDR); WriteToBuffer((uint8_t*)&data, sizeof(data)); WriteTransactionEnd(RECOVERY_MARKER_ADDR); } HAL_Delay(5000); // 每5秒记录一次 } }5.2 数据导出工具
通过串口导出存储的数据:
void ExportAllRecords(void) { uint32_t addr = 0; DataRecord_t rec; printf("timestamp,temperature,humidity\r\n"); while(addr < W25Q_TOTAL_SIZE) { W25Q_ReadData(addr, (uint8_t*)&rec, sizeof(rec.magic)); if(rec.magic != 0x55AA55AA) break; W25Q_ReadData(addr+12, (uint8_t*)&rec.data, sizeof(rec.data)); SensorData_t* data = (SensorData_t*)rec.data; printf("%lu,%.1f,%.1f\r\n", data->timestamp, data->temperature, data->humidity); addr = rec.next_addr; } }