STM32H7虚拟U盘方案:SDMMC+FATFS+USBMSC+FreeRTOS实现
2026/9/9 22:28:33 网站建设 项目流程

简介:这是面向嵌入式开发者的STM32H7虚拟U盘完整方案,整合SDMMC接口读写SD卡、FATFS文件系统、USBMSC设备协议与FreeRTOS实时调度。包内共203个文件,以119个h头文件与68个c源文件为主体,辅以ioc、mxproject等CubeMX工程配置,压缩包约1.39MB,目录按驱动、中间件与任务模块划分,便于查阅。项目展示了FreeRTOS任务优先级设计、SD卡驱动与FATFS对接、USBMSC描述符及SCSI命令回调处理的完整逻辑,可用于U盘量产、数据采集存储设备或USB与文件系统的教学实验。已有1249人学习浏览。对于需要移植或二次开发的中高级开发者,可直接借鉴其中的任务划分与线程安全设计,缩短存储类与USB类项目的开发周期。 最开始做这块,起因是一台便携式数据采集设备需要给用户导出数据,客户不想每次拆壳抠SD卡。后来我把STM32H7的SDMMC、FATFS、USBMSC、FreeRTOS串成一套,插上USB线电脑就能直接识别成一个U盘,拖拽复制文件,体验和普通U盘没区别。这篇就把这套方案从架构到踩坑完整捋一遍,给正在做“MCU变U盘”这类需求的朋友一个可直接落地的参考。

1. 项目整体设计与思路拆解

1.1 为什么选STM32H7 + SDMMC而不是SPI读卡

很多老项目用SPI方式读SD卡,接口简单,但速度上不去。我最早用STM32F4的SPI读卡,实测也就两三MB/s,客户拷一个几十MB的日志文件,等待体验很糟糕。换了STM32H7的SDMMC接口后,4bit模式配合DMA,顺序读速度能到40MB/s以上,瓶颈基本在SD卡自身。

这里有个关键认知:SDMMC不是SPI的简单替代,它底层有独立的FIFO和DMA引擎,数据搬运不占CPU,这对FreeRTOS下的多任务系统很重要——读写SD卡的过程中,CPU还能去跑采集、通信、显示任务,系统才不会因为拷贝文件而“卡死”。

STM32H7上的SDMMC至少两个外设接口,每个都支持1/4/8bit模式。实际虚拟U盘场景用到4bit就够,8bit模式一般给eMMC或高速存储设计。寄存器层面它把CMD/DATA通路分开,时钟可以独立分频,这样就能实现“初始化低速、稳定后高速”的标准SD卡时序。

1.2 系统分层与数据流

这套方案从软件角度看是三层:

第一层是块设备层,对应SDMMC驱动,它把SD卡抽象成若干个512字节扇区。第二层是文件系统层,FATFS在这套扇区上建立FAT表、目录项、文件分配逻辑。第三层是USB设备协议栈,通过MSC类把“扇区读写接口”直接暴露给PC主机。

USB MSC协议的特点是不管文件系统,PC主机把设备当成一个“块设备”,发来的SCSI命令本质就是READ CAPACITY、READ10、WRITE10这类扇区级操作。这意味着:PC读到的文件条目,其实是PC自己通过FATFS对扇区解析出来的,MCU里跑FATFS只是为了让本地程序也能读写同一个文件目录。

数据流通路就两条:

  • 本地读写:App任务 -> FATFS API -> disk_read/disk_write -> SDMMC -> SD卡
  • USB读写:PC主机 -> SCSI命令 -> USB MSC回调 -> disk_read/disk_write -> SDMMC -> SD卡

两条路径最终汇聚在同一个“disk_read/disk_write”函数上,这个设计让代码量小、逻辑清晰,但也引出一个很多人第一次做会忽略的问题:并发。

1.3 最容易忽略的并发问题

既然FATFS和USB MSC共用底层disk层,当PC正通过USB访问SD卡的时候,MCU本地任务也在往SD卡写文件,两侧都在操作FAT表,数据一致性瞬间崩掉。最典型的现象是:文件拷到一半,电脑提示数据损坏,或者MCU本地打开文件失败。

我最终采用的控制逻辑是:USB主机枚举完成后,置一个usb_connected标志,应用层检测到该标志后暂停本地文件写入,只保留一些非SD卡任务继续跑。USB断开后,延迟几百毫秒再恢复本地文件操作。这个做法虽然牺牲了一部分“同时读写”的并发能力,但换来了稳定,对大多数产品足够用。

如果产品确实需要USB和本地同时访问另一块介质,那就不能用单盘映射方案,得用双分区或多盘策略,代码复杂度会上升一档。这点在项目初期就要想清楚,不然做到一半推翻重建非常痛苦。

2. 核心细节解析与实操要点

2.1 SDMMC初始化时序的几个关键点

SD卡初始化不是上电就能直接高速跑,它有一套完整的命令时序。代码里一般通过HAL_SD_InitHAL_SD_ConfigWideBusOperation配合完成。流程上几个容易出问题的点:

一是初始频率。SD卡规范要求初始化阶段时钟必须低于400kHz,H7的SDMMC时钟需要先分频到位,稳定识别之后再切换到更高的时钟。CubeMX生成的代码里一般会帮你做,但如果你手动移植或者拷贝老代码,很容易漏掉。

二是宽总线切换。SD卡默认1bit模式,要切到4bit需要发送ACMD6。CubeMX生成代码会调HAL_SD_ConfigWideBusOperation(SD_MMC_BUS_WIDE_4B),但要注意这必须在SD卡处于transfer state之后调用,顺序不能反。

三是DMA缓冲对齐。H7有L1 Cache,CPU和外设之间会对缓存数据有copy-back/write-through策略,而DMA直接读写内存不经过CPU缓存。如果你把DMA buffer定义在一个普通全局数组上,且没做cache维护,就会出现“写SD卡数据错乱、读SD卡数据陈旧”这种诡异问题。

我的处理方式是在项目初期就直接把SDMMC DMA buffer所在的4KB内存区,通过MPU配置成non-cacheable。这样虽然损失了一点cache带来的性能,但彻底避免了一致性坑。等整个系统稳定跑起来了,再考虑用SCB_CleanDCache/SCB_InvalidateDCache做细粒度优化。

2.2 FATFS移植时diskio实现要点

FATFS本身不关心底层介质是什么,它只定义了一套diskio接口,需要你实现:

  • disk_status:返回卡状态,通常返回RES_OK即可
  • disk_initialize:初始化SD卡,调用HAL_SD_Init
  • disk_read:读扇区,支持多扇区连续读
  • disk_write:写扇区,注意SDMMC写前需要判断卡状态,等待不忙
  • disk_ioctl:处理CTRL_SYNCGET_SECTOR_SIZEGET_BLOCK_SIZE等命令

disk_ioctl很多人偷懒不实现完整,结果f_mount时FATFS不知道扇区大小,或者f_mkfs无法获取擦除块大小,就会报错或性能极差。我建议至少把CTRL_SYNCGET_SECTOR_SIZE做掉,CTRL_SYNC里调用HAL_SD_GetCardState并等待卡空闲才能返回,否则FATFS可能在数据落盘前就认为写完了。

还有一个细节:FATFS默认配置FF_USE_MKFS,如果你要支持格式化SD卡,必须在ffconf.h里打开,并且disk_ioctl要实现GET_BLOCK_SIZE。格式化时block size如果返回不对,会出现“格式化到一半失败”的情况。

2.3 FATFS的多任务互斥

FATFS源码本身不是线程安全的,多个任务同时调用f_open/f_read/f_write/f_close会互相踩内存。ffconf.h里有FF_FS_REENTRANT选项,开启后需要自己实现ff_mutex_create等系统接口,比较麻烦。

我更推荐的做法是在应用层包一层文件操作API,统一加FreeRTOS互斥量。比如写日志时,所有任务通过my_fopen/my_fwrite/my_fclose访问文件系统,内部用一个静态互斥锁保护FATFS调用。这样既保证线程安全,又不侵入FATFS源码,后续升级FATFS版本也方便。

互斥量粒度不要太细。不要每个f_read都单独加锁,否则一个任务读文件期间另一个任务想关闭文件,锁状态混乱。我习惯把“打开-读写-关闭”这段完整操作看作一个临界区,虽然会阻塞其他任务,但在嵌入式文件访问频率不高的情况下,可接受。

3. 实操过程与核心环节实现

3.1 STM32CubeMX里的关键配置

我以STM32H743为例,CubeMX配置主要做这几件事:

时钟树:H7的系统时钟从外部25MHz晶振PLL上来,跑480MHz。SDMMC1的时钟需要单独确认,一般PLL1Q给48MHz左右即可。USB OTG FS外设需要48MHz时钟,所以要检查时钟树里USB的48MHz来源是否存在,否则USB枚举不了。

SDMMC1:模式选SD 4-bit Wide bus,开启DMA。DMA选择SDMMC1_RXSDMMC1_TX两个通道。注意DMA中断和SDMMC中断都要打开,HAL库的SD驱动程序依赖这两个中断完成状态通知。

USB_OTG_FS:模式选Device_Only,在Middleware里选USB_DEVICE,Class选Mass Storage Class。这里有个容易踩的坑:STM32H7的USB_OTG_FS是片上全速USB,不需要外接PHY,但如果你用的是USB_OTG_HS,必须选Internal PHY或External PHY。很多人把FS和HS搞混,导致USB完全没有响应。我建议第一个版本先用FS,等跑通了再看吞吐率够不够。

FreeRTOS:使能后建议用Heap_4,堆大小先给32KB。USB Device库和FATFS都会消耗内存,太小会导致pvPortMalloc失败,现象很隐蔽。

MPU配置:CubeMX里不一定默认配置,需要在代码里写。我直接在SystemInit之后调用自定义的MPU_Config,把SDMMC DMA buffer和USB DMA buffer所在的内存放成Device/Non-Cacheable,一劳永逸。

3.2 FreeRTOS任务划分与栈设置

虚拟U盘这种应用,任务不用太多,我一般分三个:

USB设备任务:负责USBD_StartUSBD_Connect等初始化,之后由中断驱动,任务本身可以挂起或只做状态检测。栈给512 words就够了,但如果开了浮点,建议再加64 words,H7是Cortex-M7,浮点上下文切换会额外占用栈空间。

应用主任务:运行温度传感器采集、日志写入等业务逻辑。栈大小取决于业务复杂度,我试过把文件写入、数据计算都塞进这个任务,至少要1024 words。

看门狗/状态指示任务:周期翻转LED指示系统运行状态,栈用128 words。

栈溢出检测一定要开:configCHECK_FOR_STACK_OVERFLOW设为2,并实现vApplicationStackOverflowHook,里面有打印的话调试期能救命。我第一次跑USB枚举直接hardfault,就是USB任务栈只设了128 words导致的,改成512后就正常了。

3.3 diskio与USB MSC整合

核心是让USB MSC底层操作复用diskio的读写函数。以STM32 HAL的MSC中间件为例,MassStorage_Callbacks.c里的STORAGE_ReadSTORAGE_Write回调,我会直接转发到diskio:

int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { DRESULT res; res = disk_read(0, buf, blk_addr, blk_len); if (res == RES_OK) return 0; return -1; } int8_t STORAGE_Write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { DRESULT res; if (disk_ioctl(0, CTRL_SYNC, 0) != RES_OK) return -1; res = disk_write(0, buf, blk_addr, blk_len); if (res == RES_OK) return 0; return -1; }

同时STORAGE_GetCapacity返回SD卡扇区数:

int8_t STORAGE_GetCapacity(uint8_t lun, uint32_t *block_num, uint16_t *block_size) { HAL_SD_CardInfoTypeDef info; HAL_SD_GetCardInfo(&hsd1, &info); *block_num = info.LogBlockNbr; *block_size = info.LogBlockSize; return 0; }

有一点必须提醒:USB MSC写入过程中,PC会在后台发很多SCSI命令,比如TEST UNIT READYREAD CAPACITYINQUIRY,这些回调会被USB中断高频调用。不要在回调里调用HAL_Delay或任何可能阻塞的FreeRTOS API,否则USB协议时序会被拖垮,PC端表现为“设备无法响应”。

本地App任务想要安全访问SD卡,加一个互斥量即可:

SemaphoreHandle_t sdcard_mutex; void App_Thread(void *arg) { for (;;) { if (usb_connected == 0) { if (xSemaphoreTake(sdcard_mutex, 0) == pdTRUE) { write_log_to_sd(); xSemaphoreGive(sdcard_mutex); } } vTaskDelay(pdMS_TO_TICKS(100)); } }

这样USB在线时本地不写SD卡,断开后恢复写,彻底避免FAT表两边抢。

4. 常见问题与排查技巧实录

4.1 USB枚举失败,电脑识别不到U盘

发生概率最高的一个问题。排查顺序建议按下面这个表格走:

现象可能原因检查方法
完全没有USB中断时钟没配好确认USB_OTG_FS有48MHz时钟,用调试器看RCC相关寄存器的值,或用逻辑分析仪测USB DP/DM引脚波形
设备描述符请求失败描述符buffer未对齐检查USBD_MSC_CfgDesc等描述符数组是否4字节对齐;H7若开启了USB DMA,可能要求32字节对齐
枚举成功但盘符容量为0容量获取回调没实现STORAGE_GetCapacity里打断点,确认调用到了,且返回的block大小是512
拔插一次后用不了D+/DM上拉电阻问题检查USB端口是否按要求接了1.5k上拉电阻到3.3V,或者内部上拉是否使能

我用BusHound抓USB枚举包的方法很好用,能直观看到主机在哪一步停顿了。曾经有一版代码枚举一直失败,抓包发现设备在GET_MAX_LUN命令上不回复,原因是STORAGE_GetMaxLun没实现完整,返回错误码。还有一次是USB中断优先级数值设得比FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY还高,导致USB中断里调用FreeRTOS API直接断言卡死。

4.2 电脑提示“此驱动器有问题请修复”或数据错乱

这种问题多数和Cache一致性以及扇区对齐有关。H7的DCache开启后,如果没有处理,disk_read读到的数据可能是CPU Cache里旧的数据,disk_write写入的数据也可能在Cache里还没落地就被DMA传走了错的地址。

检查点:

  • SDMMC DMA buffer是否用__attribute__((aligned(32)))声明
  • 是否在DMA操作前SCB_CleanDCache、操作后SCB_InvalidateDCache
  • 是否用MPU把DMA buffer配置为non-cacheable

还有Windows对U盘有写入策略优化,会批量下发WRITE10命令。如果STORAGE_Write里每次都等卡完全空闲,速度会很慢,甚至Windows觉得超时。我后面改为:只在CTRL_SYNC命令里才等待卡空闲,WRITE10本身先发起DMA传数据,由DMA完成中断置位“写完成”标志,这样批量写吞吐明显提升。

4.3 FreeRTOS堆栈溢出和hardfault

FreeRTOS下面两个问题出现过很多次:

一是USB任务栈过小。一开始我图省事,把USB任务栈设成128 words,结果设备枚举时任务栈溢出,系统直接hardfault。后来加了栈溢出检测钩子,定位到问题。USB设备库底层有状态机回调,会在中断上下文和任务之间切换,栈消耗比想象的更大,给512 words起步比较稳妥。

二是中断优先级配置不合规。FreeRTOS要求使用中断优先级分组4,且系统API只能在优先级不高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断中调用。USB中断里如果调用了xQueueSendFromISR这类API,优先级数值必须合法,否则触发断言。我用的是把USB中断优先级配成5(分组4下是5~15范围内),并把任务优先级普通化,这样中断和任务之间的通信都安全。

4.4 USB拔插后SD卡文件损坏

这个问题很难在现场复现,一般是用户直接拔USB线,而电脑刷新缓存不及时,导致FAT表内容与SD卡实际扇区不一致。USB MSC协议本身有一个缓解机制:主机在拔出设备前识别到容量变化或断开事件后,会做最后同步。但用户直接拔线,主机端不一定会发SYNCHRONIZE_CACHE。

我的方案是:SD卡文件系统本身在USB读写期间只依赖SCSI命令的SYNC机制,同时让MCU在检测到USB disconnect后强制执行一次disk_ioctl(CTRL_SYNC)刷新底层卡状态,等卡闲了再允许恢复本地写入。另外在产品文档里明确提示“安全弹出后再拔线”。

5. 个人经验补充

这套虚拟U盘方案完整跑通之后,我最大的感受是:硬件平台越强,越容易被Cache和DMA这些“底层细节”翻车。STM32H7性能足够,但Cortex-M7加入L1 Cache后,外设DMA和CPU共享内存的边界问题必须从一开始就设计清楚。

调试期我习惯串口打日志,但不要在USB中断回调里加日志输出,一是UART本身慢,二是共享这层之前容易引发优先级反转。真要调试USB枚举流程,用BusHound这类USB抓包工具,比盲改代码高效得多。

另外一个小技巧:SD卡在塞进系统之前,先在电脑上用专业工具格式化成FAT32、扇区大小512B。很多读卡异常问题其实是SD卡出厂格式五花八门造成的,预先格式化能把变量减到最少。对需要批量出货的产品,这一步可以考虑在产线上自动执行一次。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询