当前位置: 首页 > news >正文

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工程创建

  1. 打开STM32CubeMX,选择对应型号MCU(如STM32F103C8T6)

  2. 配置时钟树,确保系统时钟为72MHz(SPI1挂在APB2总线上)

  3. 在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
  4. 将PC0配置为GPIO_Output,初始电平设为高(片选默认不选中)

  5. 生成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; } }
http://www.cnnetsun.cn/news/2465729.html

相关文章:

  • Android11 热点超时机制深度解析:从源码到自定义配置
  • 图灵架构与实时光线追踪:从硬件原理到混合渲染实践
  • OpenCasCade(OCCT) 7.7.0 坐标系统实战:从世界坐标到交互转换(C#/C++ CLI)
  • 从仿真到实战:我的第一个毫米波雷达干涉测角MATLAB项目(附76GHz频段完整代码)
  • 嵌入式Linux驱动开发进阶:设备树与按键驱动的实战解析
  • ARMv9地址转换与内存屏障技术解析
  • 告别Sass除法弃用警告:从Deprecation Warning到math.div的平滑迁移实战
  • 从零到一:vue-print-nb插件在Vue项目中的实战打印方案
  • VSCode集成ModelSim调试Verilog时遭遇vlog-7报错:深入解析modelsim.ini文件路径配置
  • 博图编程实战☞P_TRIG:捕捉RLO信号跳变的工业逻辑
  • UE4/UE5 虚幻引擎,Pawn碰撞体设置与根组件绑定,彻底解决移动穿透问题
  • 从Listen到Spell:LAS模型如何重塑端到端语音识别——技术演进与实践解析
  • 荔枝派Zero V3s开发板:手把手教你编译和烧录主线U-Boot(含SPI Flash启动配置)
  • 深入理解rkmedia数据流:从VI、RGA到VO的模块化绑定与性能调优实战
  • 生化危机4:重制版+修改器2026最新官方正版免费下载 一键转存 永久更新 (看到速转存 资源随时走丢)
  • SPM数据预处理保姆级避坑指南:从DICOM到平滑,手把手教你搞定fMRI分析
  • Ubuntu 20.04 + RTX 3090 保姆级教程:从零搞定BEVFusion环境(附CUDA 11.3/PyTorch 1.10配置清单)
  • 量子能量隐形传态与W态纠缠技术解析
  • 高级部署指南:Cartographer ROS在Docker环境中的完整配置方案
  • CANN/cannbot-skills npugraph_ex DFX 分诊
  • MAA智能辅助工具:解放双手的明日方舟自动化助手终极指南
  • Perplexity医生信息搜索:5步精准定位最新诊疗指南与真实世界证据
  • C51编译器枚举类型检查机制与优化实践
  • Perplexity提示工程精要(2024权威认证版):覆盖92%高频场景的12类黄金模板
  • 保姆级教程:用HackRF One复现汽车钥匙重放攻击(附完整命令与避坑点)
  • CANN asc-devkit矢量广播矩阵函数
  • Perplexity图标搜索突然失效?紧急修复手册(含Chrome DevTools实时调试+CDN缓存穿透方案)
  • 别再只问ChatGPT答案了!试试这个Prompt技巧,让大模型把解题思路‘说’给你听
  • NCE外汇:服务体验与平台稳定性的协同提升
  • CANN/asc-devkit InitStartBufHandle函数说明