ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32H7虚拟U盘方案:SDMMC+FATFS+USBMSC+FreeRTOS实现

STM32H7虚拟U盘方案:SDMMC+FATFS+USBMSC+FreeRTOS实现 简介这是面向嵌入式开发者的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_Init和HAL_SD_ConfigWideBusOperation配合完成。流程上几个容易出问题的点一是初始频率。SD卡规范要求初始化阶段时钟必须低于400kHzH7的SDMMC时钟需要先分频到位稳定识别之后再切换到更高的时钟。CubeMX生成的代码里一般会帮你做但如果你手动移植或者拷贝老代码很容易漏掉。二是宽总线切换。SD卡默认1bit模式要切到4bit需要发送ACMD6。CubeMX生成代码会调HAL_SD_ConfigWideBusOperation(SD_MMC_BUS_WIDE_4B)但要注意这必须在SD卡处于transfer state之后调用顺序不能反。三是DMA缓冲对齐。H7有L1 CacheCPU和外设之间会对缓存数据有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_Initdisk_read读扇区支持多扇区连续读disk_write写扇区注意SDMMC写前需要判断卡状态等待不忙disk_ioctl处理CTRL_SYNC、GET_SECTOR_SIZE、GET_BLOCK_SIZE等命令disk_ioctl很多人偷懒不实现完整结果f_mount时FATFS不知道扇区大小或者f_mkfs无法获取擦除块大小就会报错或性能极差。我建议至少把CTRL_SYNC和GET_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_RX和SDMMC1_TX两个通道。注意DMA中断和SDMMC中断都要打开HAL库的SD驱动程序依赖这两个中断完成状态通知。USB_OTG_FS模式选Device_Only在Middleware里选USB_DEVICEClass选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_Start、USBD_Connect等初始化之后由中断驱动任务本身可以挂起或只做状态检测。栈给512 words就够了但如果开了浮点建议再加64 wordsH7是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_Read和STORAGE_Write回调我会直接转发到diskioint8_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 READY、READ CAPACITY、INQUIRY这些回调会被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堆栈溢出和hardfaultFreeRTOS下面两个问题出现过很多次一是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卡出厂格式五花八门造成的预先格式化能把变量减到最少。对需要批量出货的产品这一步可以考虑在产线上自动执行一次。本文还有配套的精品资源点击获取
返回列表