手把手教你用STM32F103和W25Q64做个‘固态U盘’:SPI Flash挂载FATFS实战指南

张开发
2026/4/18 7:52:20 15 分钟阅读

分享文章

手把手教你用STM32F103和W25Q64做个‘固态U盘’:SPI Flash挂载FATFS实战指南
STM32F103与W25Q64打造高性能存储方案从SPI驱动到FATFS文件系统实战在嵌入式开发中数据存储是一个永恒的话题。当项目需要可靠的非易失性存储时SPI Flash因其体积小、功耗低、成本适中等优势成为许多开发者的首选。本文将带你深入探索如何基于STM32F103和W25Q64 SPI Flash构建一个完整的存储解决方案从底层驱动编写到上层文件系统集成最终实现类似U盘的便捷使用体验。1. 硬件架构设计与SPI通信基础1.1 硬件选型与连接W25Q64是一款64Mbit(8MB)容量的SPI Flash内部组织为128个块(Block)每个块64KB进一步划分为16个4KB的扇区(Sector)。这种结构直接影响我们的擦写策略W25Q64存储结构 - 总容量64Mbit (8MB) - 块(Block)128个 × 64KB - 扇区(Sector)16个 × 4KB (每块) - 页(Page)16个 × 256B (每扇区)硬件连接采用标准SPI接口典型接线方式如下STM32引脚W25Q64引脚功能说明PB12CS片选(软件控制)PB13CLKSPI时钟PB14MISO主入从出数据线PB15MOSI主出从入数据线1.2 SPI初始化与基础通信SPI初始化需要特别注意时钟极性和相位设置这对Flash通信稳定性至关重要void SPI_Flash_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 配置SPI引脚 GPIO_InitStruct.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); // 配置CS引脚 GPIO_InitStruct.GPIO_Pin GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStruct); // SPI参数配置 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_2; SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStruct); SPI_Cmd(SPI2, ENABLE); }提示SPI时钟分频不宜设置过高W25Q64最高支持104MHz时钟但实际使用中建议先以较低频率(如18MHz)测试稳定性。2. W25Q64底层驱动开发2.1 基本操作指令实现W25Q64通过指令集实现各种操作以下是关键指令的宏定义#define W25X_WriteEnable 0x06 #define W25X_WriteDisable 0x04 #define W25X_ReadStatusReg 0x05 #define W25X_WriteStatusReg 0x01 #define W25X_ReadData 0x03 #define W25X_FastReadData 0x0B #define W25X_PageProgram 0x02 #define W25X_SectorErase 0x20 #define W25X_BlockErase 0xD8 #define W25X_ChipErase 0xC7 #define W25X_JedecDeviceID 0x9F实现基本的读写函数前需要先完成几个辅助函数// 等待Flash操作完成 void W25Q64_WaitBusy(void) { uint8_t status; do { SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_ReadStatusReg); status SPI_Flash_SendByte(Dummy_Byte); SPI_FLASH_CS_HIGH(); } while(status 0x01); // 检查BUSY位 } // 写使能 void W25Q64_WriteEnable(void) { SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_WriteEnable); SPI_FLASH_CS_HIGH(); }2.2 数据读取与编程读取操作相对简单可以连续读取跨越多个扇区void W25Q64_ReadData(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t Length) { SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_ReadData); SPI_Flash_SendByte((ReadAddr 16) 0xFF); SPI_Flash_SendByte((ReadAddr 8) 0xFF); SPI_Flash_SendByte(ReadAddr 0xFF); while(Length--) { *pBuffer SPI_Flash_SendByte(Dummy_Byte); } SPI_FLASH_CS_HIGH(); }写入操作则复杂得多必须考虑页边界和擦除要求void W25Q64_PageProgram(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t Length) { W25Q64_WriteEnable(); SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_PageProgram); SPI_Flash_SendByte((WriteAddr 16) 0xFF); SPI_Flash_SendByte((WriteAddr 8) 0xFF); SPI_Flash_SendByte(WriteAddr 0xFF); while(Length--) { SPI_Flash_SendByte(*pBuffer); } SPI_FLASH_CS_HIGH(); W25Q64_WaitBusy(); }注意Flash编程前必须确保目标区域已被擦除全为0xFF且单次写入不能跨页256字节边界。2.3 擦除操作优化擦除是Flash操作中最耗时的环节合理规划擦除策略对性能至关重要void W25Q64_SectorErase(uint32_t SectorAddr) { W25Q64_WriteEnable(); SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_SectorErase); SPI_Flash_SendByte((SectorAddr 16) 0xFF); SPI_Flash_SendByte((SectorAddr 8) 0xFF); SPI_Flash_SendByte(SectorAddr 0xFF); SPI_FLASH_CS_HIGH(); W25Q64_WaitBusy(); } void W25Q64_BlockErase(uint32_t BlockAddr) { W25Q64_WriteEnable(); SPI_FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_BlockErase); SPI_Flash_SendByte((BlockAddr 16) 0xFF); SPI_Flash_SendByte((BlockAddr 8) 0xFF); SPI_Flash_SendByte(BlockAddr 0xFF); SPI_FLASH_CS_HIGH(); W25Q64_WaitBusy(); }实际项目中建议根据数据更新频率采用不同的擦除策略频繁更新的小数据保留几个专用扇区循环使用大块数据按需擦除整个块初始化时可考虑全片擦除确保一致性3. FATFS文件系统集成3.1 磁盘IO接口实现要让FATFS识别W25Q64需要实现diskio.c中的几个关键函数DSTATUS disk_initialize(BYTE pdrv) { // 初始化SPI和Flash SPI_Flash_Init(); return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { uint32_t addr sector * FLASH_SECTOR_SIZE; W25Q64_ReadData(buff, addr, count * FLASH_SECTOR_SIZE); return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) { uint32_t addr sector * FLASH_SECTOR_SIZE; uint32_t end_addr addr count * FLASH_SECTOR_SIZE; while(addr end_addr) { uint32_t sector_start addr ~(FLASH_SECTOR_SIZE-1); uint16_t offset addr (FLASH_SECTOR_SIZE-1); uint16_t len FLASH_SECTOR_SIZE - offset; if(len (end_addr - addr)) { len end_addr - addr; } // 读取-修改-写入策略 uint8_t sector_buf[FLASH_SECTOR_SIZE]; W25Q64_ReadData(sector_buf, sector_start, FLASH_SECTOR_SIZE); memcpy(sector_buf offset, buff, len); W25Q64_SectorErase(sector_start); W25Q64_WriteData(sector_buf, sector_start, FLASH_SECTOR_SIZE); addr len; buff len; } return RES_OK; }3.2 文件系统格式化与挂载首次使用前需要对Flash进行格式化FATFS fs; FRESULT res; // 挂载文件系统 res f_mount(fs, 0:, 1); if(res FR_NO_FILESYSTEM) { // 没有文件系统进行格式化 MKFS_PARM opt; memset(opt, 0, sizeof(opt)); opt.fmt FM_FAT32; // 使用FAT32格式 opt.au_size 4096; // 分配单元大小与Flash扇区一致 res f_mkfs(0:, opt, work, sizeof(work)); if(res FR_OK) { // 格式化成功后重新挂载 res f_mount(fs, 0:, 0); } }3.3 文件操作示例实现基本的文件读写功能void WriteLogFile(const char* message) { FIL file; UINT bw; // 以追加方式打开日志文件 f_open(file, 0:/system.log, FA_WRITE | FA_OPEN_ALWAYS); f_lseek(file, f_size(file)); // 移动到文件末尾 // 写入当前时间和日志信息 char buffer[128]; sprintf(buffer, [%lu] %s\r\n, HAL_GetTick(), message); f_write(file, buffer, strlen(buffer), bw); f_close(file); } void ReadConfigFile(void) { FIL file; char buffer[256]; UINT br; if(f_open(file, 0:/config.ini, FA_READ) FR_OK) { while(f_gets(buffer, sizeof(buffer), file)) { // 处理每一行配置 printf(%s, buffer); } f_close(file); } }4. USB大容量存储设备(MSC)实现4.1 USB MSC初始化使用STM32的USB外设实现MSC设备类USBD_HandleTypeDef hUsbDeviceFS; void USB_MSC_Init(void) { // 初始化USB设备库 USBD_Init(hUsbDeviceFS, FS_Desc, DEVICE_FS); // 添加存储类 USBD_RegisterClass(hUsbDeviceFS, USBD_MSC); // 添加存储介质接口 USBD_MSC_RegisterStorage(hUsbDeviceFS, USBD_Storage_Interface_fops); // 启动USB设备 USBD_Start(hUsbDeviceFS); }4.2 存储介质接口实现需要实现以下几个关键回调函数int8_t STORAGE_Init(uint8_t lun) { // 初始化Flash和文件系统 SPI_Flash_Init(); f_mount(fs, 0:, 0); return 0; } int8_t STORAGE_GetCapacity(uint8_t lun, uint32_t *block_num, uint16_t *block_size) { *block_num FLASH_SECTOR_COUNT; *block_size FLASH_SECTOR_SIZE; return 0; } int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { uint32_t addr blk_addr * FLASH_SECTOR_SIZE; W25Q64_ReadData(buf, addr, blk_len * FLASH_SECTOR_SIZE); return 0; } int8_t STORAGE_Write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { uint32_t addr blk_addr * FLASH_SECTOR_SIZE; uint32_t end_addr addr blk_len * FLASH_SECTOR_SIZE; while(addr end_addr) { // 实现类似disk_write的擦写策略 // ... } return 0; }4.3 性能优化技巧提升U盘使用体验的几个关键点缓存策略实现读写缓存减少实际Flash操作对小数据写入采用缓冲延迟写入磨损均衡实现简单的动态块映射表对频繁更新的数据区域进行轮换使用后台处理将擦除操作放在系统空闲时进行实现写入队列避免阻塞USB传输// 简单的磨损均衡示例 typedef struct { uint32_t logical_sector; uint32_t physical_block; uint16_t erase_count; } SectorMapEntry; SectorMapEntry sector_map[MAX_SECTORS]; uint32_t GetPhysicalSector(uint32_t logical_sector) { // 查找或分配物理扇区 for(int i0; iMAX_SECTORS; i) { if(sector_map[i].logical_sector logical_sector) { return sector_map[i].physical_block; } } // 分配新的物理扇区选择擦除次数最少的 uint32_t min_erase 0xFFFFFFFF; uint32_t selected 0; for(int i0; iMAX_SECTORS; i) { if(sector_map[i].erase_count min_erase) { min_erase sector_map[i].erase_count; selected i; } } // 更新映射表 sector_map[selected].logical_sector logical_sector; sector_map[selected].physical_block AllocateNewBlock(); sector_map[selected].erase_count; return sector_map[selected].physical_block; }在实际项目中我发现最影响用户体验的往往是写入速度。通过实现一个简单的写入缓存可以将小文件写入的响应速度提升5-10倍。具体做法是在RAM中维护一个4KB的缓存块只有当缓存满或收到同步命令时才实际写入Flash。

更多文章