STM32CubeMX实战从零构建SPI Flash数据记录系统在嵌入式开发中外部Flash存储器常被用于扩展存储容量或保存关键数据。W25Q64作为一款常见的SPI接口Flash芯片具有8MB容量、低功耗和较高性价比等特点非常适合用于数据记录、固件存储等场景。本文将手把手带你完成一个完整的SPI Flash数据记录系统开发涵盖CubeMX配置、驱动封装到应用实现的全部流程。1. 环境准备与CubeMX基础配置1.1 硬件选型与连接W25Q64采用标准的SPI接口与MCU通信典型连接方式如下MCU引脚W25Q64引脚功能说明PA5CLK时钟信号PA6MISO主入从出数据线PA7MOSI主出从入数据线PC0CS片选信号(自定义)提示虽然STM32的SPI1硬件NSS引脚是PA4但实际开发中更推荐使用普通GPIO手动控制片选这样更灵活且避免硬件NSS的复杂配置。1.2 CubeMX工程创建打开STM32CubeMX选择对应型号MCU如STM32F103C8T6配置时钟树确保系统时钟为72MHzSPI1挂在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; // CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0 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的基本存储操作包括擦除、编程和读取扇区擦除4KBvoid W25Q_EraseSector(uint32_t addr) { uint8_t cmd[4] {0x20, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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 i0; ilen; i) { crc ^ data[i]; for(uint8_t j0; j8; j) { if(crc 0x0001) crc (crc1) ^ 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 i0; iW25Q_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(addr12, (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; } }
STM32CubeMX实战:手把手教你用SPI驱动W25Q64 Flash存储数据(附完整代码)
STM32CubeMX实战从零构建SPI Flash数据记录系统在嵌入式开发中外部Flash存储器常被用于扩展存储容量或保存关键数据。W25Q64作为一款常见的SPI接口Flash芯片具有8MB容量、低功耗和较高性价比等特点非常适合用于数据记录、固件存储等场景。本文将手把手带你完成一个完整的SPI Flash数据记录系统开发涵盖CubeMX配置、驱动封装到应用实现的全部流程。1. 环境准备与CubeMX基础配置1.1 硬件选型与连接W25Q64采用标准的SPI接口与MCU通信典型连接方式如下MCU引脚W25Q64引脚功能说明PA5CLK时钟信号PA6MISO主入从出数据线PA7MOSI主出从入数据线PC0CS片选信号(自定义)提示虽然STM32的SPI1硬件NSS引脚是PA4但实际开发中更推荐使用普通GPIO手动控制片选这样更灵活且避免硬件NSS的复杂配置。1.2 CubeMX工程创建打开STM32CubeMX选择对应型号MCU如STM32F103C8T6配置时钟树确保系统时钟为72MHzSPI1挂在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; // CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0 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的基本存储操作包括擦除、编程和读取扇区擦除4KBvoid W25Q_EraseSector(uint32_t addr) { uint8_t cmd[4] {0x20, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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, (addr16)0xFF, (addr8)0xFF, addr0xFF}; 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 i0; ilen; i) { crc ^ data[i]; for(uint8_t j0; j8; j) { if(crc 0x0001) crc (crc1) ^ 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 i0; iW25Q_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(addr12, (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; } }