十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32H7实现SD卡虚拟U盘:SDMMC+FATFS+USBMSC+FreeRTOS整合指南

STM32H7实现SD卡虚拟U盘:SDMMC+FATFS+USBMSC+FreeRTOS整合指南 简介面向嵌入式开发者的STM32H7虚拟U盘完整工程资源集成SDMMC、FATFS、USBMSC与FreeRTOS四个核心组件实现通过USB接口将SD卡作为大容量存储设备挂载到PC端适用于便携数据交互、固件升级、量产烧录等场景。工程基于HAL/CubeMX搭建覆盖底层初始化、外设驱动、文件系统挂载、USB MSC协议处理及多任务并发代码层次分明适合已掌握基础的中级嵌入式开发者进阶。压缩包共203个文件以C源文件68个和头文件119个为主另有IOC工程配置、链接脚本、启动文件等便于重新生成工程和交叉验证整体仅1.39MB。已有1249人学习作者对实际调试中的地址对齐、DMA缓冲、FATFS并发访问等问题做了梳理附带的目录结构便于快速定位存储层、协议层与任务层可迁移至其他STM32系列有助于掌握SDMMC高速读写、FATFS文件管理、USB MSC通信与FreeRTOS多任务划分等关键技能。 最近在做一个数据记录仪的项目采集的数据量大了以后怎么把数据导出来成了最头疼的问题。串口太慢网线又受限于现场环境后来干脆一步到位直接把设备本身变成一个U盘——主机插上USB线就能读写内部的SD卡。这个方案在STM32H7上实现起来并不复杂核心就是SDMMCFATFSUSBMSCFreeRTOS这四个组件的整合。这篇博文我会把整个工程的搭建过程、关键代码、以及我实际调试中踩过的坑完整梳理一遍。如果你用的是STM32H743或者H750想做一个带文件系统、能通过USB枚举成U盘的设备同时还不想让裸机轮询把CPU拖垮那么这篇文章很适合你。看完你应该能对着CubeMX把整个工程搭起来并理解每一层在干什么而不是只会复制粘贴。1. 整体架构四个组件各司其职数据流是怎么串起来的先捋一下数据流。SD卡物理挂在SDMMC1外设上FATFS是一个文件系统层它不关心底层是SD卡还是NAND Flash只负责把文件的写入转换成对底层磁盘的扇区读写。USB MSC设备层则把整个存储介质抽象成“一块硬盘”呈现给主机。FreeRTOS在这里扮演的是调度器角色它让USB的轮询、文件系统的写入、以及应用逻辑可以并行跑不至于互相阻塞。从主机的角度看U盘就是一个块设备主机发SCSI命令如READ CAPACITY、READ 10、WRITE 10USBMSC的回调函数收到这些命令后通过FATFS已经挂载好的卷把数据从SD卡读出来或写进去。换句话说FATFS不直接对接USBUSB MSC也不直接操作SD卡两者在“磁盘访问”这个接口上是重叠的但在功能上完全解耦。这套方案的优点是主机侧不需要装任何驱动Windows、Linux、macOS都认U盘设备侧也省去了自己写USB Mass Storage类协议的底层细节ST的USB中间件已经把类协议处理好了。我们需要做的核心工作就是把FATFS的disk_read和disk_write接到SDMMC驱动上再把MSC的底层读写回调接到FATFS上。需要注意的是当U盘被主机挂载时主机可能会同时读写FATFS管理的文件系统而设备自身也可能在写日志文件。这种并发访问如果不加保护轻则文件损坏重则整个SD卡的分区表被搞坏。后面我会专门讲如何用FreeRTOS的互斥量把这两条访问路径串行化。2. 工程搭建第一步CubeMX里的四件套配置一个都不能省2.1 SDMMC的时钟和DMA配置跑稳定比跑快更重要在CubeMX里我用的MCU是STM32H743VIT6SDMMC1挂的是SD卡。SDMMC1的时钟源是CK最大可以到120MHz但我实际选择的是50MHz左右也就是SD的高速模式SDR25附近。原因很简单稳定性优先。对于数据记录仪这种场景持续写入的可靠性远比峰值速度重要。DMA一定要开。SDMMC的数据传输如果走CPU搬运会占用大量的内核带宽尤其是配合FreeRTOS后中断响应延迟会明显变差。我配置的是DMA1请求方向是存储器到外设/外设到存储器数据宽度都设成字32位因为SDMMC的数据FIFO是32位宽的。Cache方面H7有D-CacheSD卡DMA缓冲区建议放在非Cache段或者在MPU里把这块内存配成非Cache、可缓冲的。这块有个典型的坑DMA缓冲区地址必须是4字节对齐。CubeMX生成的HAL_SD_ReadBlocks_DMA如果传入非对齐地址直接HardFault。我在调试时遇到过好几次后面专门加了对齐检查和__ALIGN_BEGIN宏。2.2 FATFS模块的配置支持长文件名是底线FATFS组件在CubeMX里可以直接勾选。有几个配置项需要留意FF_USE_LFN必须开建议设成2动态内存否则SD卡里的中文文件名、长文件名全是乱码。FF_VOLUMES我设成1只挂载一个卷。如果有多个存储介质这个值要相应增加。FF_USE_MKFS如果你希望在设备端直接格式化SD卡而不是依赖电脑格式化这个选项要打开。我用它做了一个“长按按键格式化SD卡”的功能对现场维护人员来说非常方便。FF_USE_STRFUNC建议打开配合f_printf输出日志调试时会省很多事。FF_FS_RPATH如果需要在设备上浏览目录结构设为1或2我设为2支持递归路径。还有一点H7的FATFS堆栈在FreeRTOS下要格外留意。f_open和f_read这类函数内部会使用栈空间如果你的任务栈只给128字大概率跑着跑着就栈溢出了。建议给FATFS相关任务至少512字调试阶段给1024字等稳定后再往下压。2.3 USB MSC的配置HS还是FS要提前想清楚STM32H7自带USB OTG HS和FS。HS外设如果使用内嵌PHY可以跑全速12Mbps或高速480Mbps模式。我手里这块板子HS PHY的外置部分没接好所以最终用的是USB OTG FS即外设工作在全速模式。全速模式下MSC的实测读写速度大约是1MB/s左右对于日志导出这个场景够用了。如果是产品设计我建议直接把HS外部PHY做上去读速度能到十几MB/s用户体验完全不在一个级别。但这篇文章还是以FS为例因为FS不需要额外PHY硬件布线简单调试也省事。CubeMX生成USB中间件后在Middleware and Software Packs里勾选USB_DEVICEClass for USBD应选择Mass Storage Class。同时注意USB MSC的配置里有一个“Logical Unit Number”参数默认是1个逻辑单元也就是一个U盘。这里不用动。还有一个很重要的参数是“Maximum Logical Block Number”和“Block Size”这两个值不是CubeMX帮你算好的它默认是512字节块大小如果你的SD卡扇区是512字节那就正好。2.4 FreeRTOS与堆大小设定FreeRTOS在CubeMX里勾选后默认会生成一个默认任务。对于这个工程我建了三个任务USB MSC处理任务其实是中间件内部的中断回调模式不需要单独任务、FATFS操作任务负责把数据写入SD卡、应用逻辑任务模拟传感器数据产生。任务数量不多但堆大小要给够。我最终配置的FreeRTOS heap大小是64KB因为FATFS的LFN动态分配和USB MSC的缓冲区都要从堆里出初始用32KB时曾经出现pvPortMalloc失败导致文件打开不了的问题。3. FATFS与SDMMC的桥接层diskio.c里最容易被忽略的几个细节CubeMX会自动生成FATFS的底层接口文件也就是diskio.c但默认生成的代码只适配了内部FlashSD卡的读写函数需要自己补。这一步是整个工程的核心也是最容易出错的地方。我的实现思路是通过一个全局结构体保存当前激活的磁盘状态在disk_initialize里调用HAL_SD_Init在disk_read和disk_write里调用HAL_SD_ReadBlocks_DMA或HAL_SD_WriteBlocks_DMA然后等待DMA完成信号量。// diskio.c 关键实现片段 DSTATUS disk_initialize(BYTE pdrv) { if (HAL_SD_Init(hsd1) ! HAL_OK) return STA_NOINIT; if (HAL_SD_ConfigWideBusOperation(hsd1, SDMMC_BUS_WIDE_4B) ! HAL_OK) return STA_NOINIT; return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { // 关键buff必须4字节对齐否则DMA直接异常 if ((uint32_t)buff % 4 ! 0) return RES_PARERR; if (HAL_SD_ReadBlocks_DMA(hsd1, buff, sector, count) ! HAL_OK) return RES_ERROR; // 等待DMA传输完成信号带超时 if (xSemaphoreTake(sd_read_sem, pdMS_TO_TICKS(2000)) ! pdTRUE) return RES_TIMEOUT; return RES_OK; }有几个细节值得展开。第一HAL_SD_ReadBlocks_DMA的第三个参数是扇区地址注意是LBA地址单位是扇区不是字节。FATFS传入的sector就是这个值直接透传即可。第二DMA完成中断里需要释放信号量。CubeMX生成的HAL_SD_RxCpltCallback是弱函数需要自己重写。如果同时用了SDMMC的中断模式还要在HAL_SD_ErrorCallback里也释放信号量防止卡死。第三SD卡的初始化时序与FATFS的f_mount是强相关的。我的做法是在任务启动阶段先f_mount一次失败了就提示用户。注意f_mount返回FR_NO_FILESYSTEM不代表SD卡坏了可能是卡里还没有文件系统。这时候可以调用f_mkfs手动格式化。// 获取当前时间的钩子函数FATFS在写文件时会调用它 DWORD get_fattime(void) { return ((DWORD)(2025 - 1980) 25) | ((DWORD)1 21) | ((DWORD)1 16); }get_fattime这个钩子如果不实现FATFS编译会报错因为它是强引用。如果只是实验验证随便返回一个固定时间就行正式产品要做好时间戳管理文件创建时间才准确。4. USB MSC底层回调里在做什么把存储介质“借”给主机USB MSC中间件的底层回调主要在usbd_storage_if.c里默认模板实现了六个回调GetCapacity、IsReady、IsWriteProtected、Read、Write、GetMaxLun。CubeMX会自动生成这些函数的空壳但不会帮你实现真正的读写逻辑。关键在于这些回调函数运行在USB中断上下文或USB处理线程中不能在里面做阻塞操作太久否则USB会枚举失败或传输超时从而被主机判定为无法识别的USB设备。我的实现方式是把USBD_MSC_Read和USBD_MSC_Write里的实际操作通过消息队列或信号量转交给一个专门的应用层任务处理USB回调只负责提交请求然后等待完成。这样既保证USB的实时性又避免长时间占用中断上下文。// usbd_storage_if.c 中的读写回调简化实现 int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { // 从FATFS挂载的卷读取指定扇区 FRESULT res f_read(file_object, buf, blk_len * 512, br); return (res FR_OK) ? 0 : -1; }但是这里有一个大坑直接调用f_read前提是这个U盘对应的“文件”已经提前打开了。虚拟U盘有两种实现思路思路A把整个SD卡分区暴露给主机主机直接读写FATFS的原始扇区。这种方式最简单USB MSC的读写就是SD卡的扇区读写完全不用关心文件系统。问题在于设备自身如果同时用FATFS写同一个卷就必须处理缓存一致性问题。H7上FATFS默认没有写缓存但会有FAT表缓存主机直接改扇区后设备侧缓存的FAT表就过期了导致文件损坏。思路B把设备端的一个文件暴露成“虚拟U盘”主机的读写被重定向到这个固定文件内。这种方式隔离性强但兼容性差Windows会把文件系统分区和U盘分区混在一起容易出现“需要格式化”的提示。我的选择是思路A配合设备侧不主动写盘仅USB连接时暂停记录并在USB连接期间让FATFS重新挂载卷确保文件系统的FAT表始终和主机同步。这样虽然牺牲了“边记录边导出”的能力但换来了最稳定的兼容性。如果你的产品确实需要同时访问就需要引入文件系统级别的同步锁复杂度会成倍上升这个后面再单独说。5. FreeRTOS任务划分与优先级设计不要让U盘拖垮数据记录5.1 任务划分这个工程的任务并不复杂但优先级设计很关键。我的三个任务和优先级如下USB_MSC任务优先级3通常USB中间件是在USB中断里轮询的但PCDProgrammable Clock Device的中断回调很频繁为了不让其他低优先级任务饿死我设了一个专门的任务处理USB外设的SOF和事件。实际运行中这个任务占用CPU很低但每次唤醒的延迟要求很高优先级最高。应用逻辑任务优先级2负责产生数据、拼接日志、周期性写入SD卡。FATFS任务已在上面提到和应用的SD卡写合并为一个任务优先级1执行f_open、f_write、f_close。这种优先级安排的逻辑是USB枚举和主机访问的实时性最重要优先级最高应用数据产生不能丢次高文件系统写入是相对低频的大块操作放在最低但必须保证不会被饿死所以我用了互斥量加超时机制。5.2 互斥量FreeRTOS里处理“同一资源多人访问”的正确工具SD卡这个外设天然是独占资源USB MSC任务会访问它应用任务也会访问它。如果两个任务同时调用HAL_SD_ReadBlocks_DMADMA请求会冲突SDMMC状态机就会卡死。正确的做法是用互斥量Mutex而不是二值信号量。互斥量有优先级继承机制可以很好地解决优先级反转。我给SD卡访问定义了一个全局互斥量// FreeRTOS互斥量创建 sd_mutex xSemaphoreCreateMutex();在所有访问SD卡的地方包括disk_read、disk_write以及USB STORAGE_Read、STORAGE_Write里都要先获取这个互斥量。但这里有一个隐含的坑如果USB和FATFS都持有互斥量而其中一个正在访问时调用了vTaskDelay任务会发生上下文切换。如果抢占不当就是死锁。所以我的习惯是尽量让互斥量持有时间最短只在HAL_SD函数的调用期间持有不要在等待DMA完成期间也持有。不过这个也不绝对需要具体场景仔细推演。5.3 栈溢出检测FreeRTOS提供了两种栈溢出检测机制在CubeMX里可以直接勾选。我在调试阶段用的是方法2栈指针异常检测这个方法在每次任务切换时检查当前任务的栈指针是否越界能比较早地发现问题。但要注意方法2会大幅增加上下文切换开销产品阶段建议关掉或者改成方法1仅任务创建时检查。另外我还在每个任务的无限循环里加了一个“水位线监测”每隔几秒调用uxTaskGetStackHighWaterMark把剩余栈空间最小的值记录到日志里。这样即使上线后也能源源不断地看到每个任务的栈余量不用等崩溃才去查。堆栈溢出真正跑出来的案例是我最初把FATFS调用和USB回调放在同一个任务里栈给了256字结果f_open之后就栈溢出。后面我把USB回调里的重定向逻辑改成通过消息队列转交FATFS单独一个任务栈设为1024字问题彻底消失。6. 实测性能与三个必踩的坑6.1 性能实测数据我手头这块板子SD卡是SanDisk 32GB Class10USB全速模式实测主机复制文件到U盘的写入速度约900KB/s从U盘复制到主机约1.1MB/s。这个速度基本是USB FS的理论上限了12Mbps除以10位编码有效数据约1.2MB/s。如果使用HS模式速度能提升到10MB/s左右瓶颈就从USB变成SD卡的持续写速。CPU占用方面持续写入时FreeRTOS空闲任务占比大约55%说明CPU没有被拖垮。DMA传输期间CPU完全可以去做其他事情这也是用DMA比轮询好一大截的原因。6.2 坑一DMA缓冲区对齐不小心就HardFaultH7是Cortex-M7内核带D-Cache。DMA读取的内存必须和Cache行对齐否则DMA搬的数据和Cache里的数据不一致。最典型的场景是HAL_SD_WriteBlocks_DMA传入的buff是局部变量位于任务栈上地址可能是2字节对齐但非4字节对齐DMA访问就直接bus fault。我的解决方式定义全局DMA缓冲区并显式对齐__ALIGN_BEGIN static uint8_t sd_dma_buffer[512] __ALIGN_END;所有涉及SD卡扇区读写的临时数据都拷到这个缓冲区里再传给HAL层。同时在MPU配置里把这段缓冲区设为非Cache、可缓冲的防止D-Cache把数据留在Cache里导致不一致。6.3 坑二热插拔时FATFS挂载状态失效USB U盘可以热插拔但SD卡通常是在设备内部焊死或卡槽固定不存在热插拔问题。然而USB线缆的热插拔是常态。主机在枚举后会缓存U盘的分区信息如果设备端重启了SDMMC主机会认为U盘异常拔出随后又识别为新设备。看起来就是“U盘频繁掉线”。这个问题从两个方面解决一是USB线缆的VBUS检测和地线处理PCB布局时VBUS走线要短粗D/D-差分对要用受控阻抗尽量减少信号完整性隐患二是软件层面当USB检测到主机重新枚举时不要立即重挂FATFS而是等主机发出第一个SCSI命令再初始化SDMMC这样枚举时序更稳。6.4 坑三f_mount之后必须f_unmount否则文件打不开这是一个特别迷的坑。FATFS的f_mount负责注册工作区但如果你反复调用f_mount而从不f_unmount工作区会被重复分配文件系统对象损坏表现为f_open返回FR_NOT_ENABLED或FR_INT_ERR。正确做法是在每次USB断开连接后调用f_mount(NULL, , 1)来卸载卷下一次USB连接时再重新f_mount。同时设备自身写日志的路径和USB挂载路径严格隔离避免双方同时操作同一个卷。7. 排错方法论当虚拟U盘不被识别时从哪里开始查调试这个系统时我形成了自己的排查顺序按这个顺序查90%的问题能快速定位第一步先确认USB枚举是否成功。插上USB线后在PC的设备管理器里看是否有未知设备或“大容量存储设备”。如果设备管理器连设备都没枚举出来问题在USB硬件或中间件配置先别碰FATFS。常见原因是D/D-接反、USB时钟频率不对、外设电源没配置。第二步枚举成功但提示“需要格式化”这说明SCSI命令能通但文件系统层不认可SD卡内容。这时候从FATFS角度检查SD卡是否有合法分区表f_mount是否返回FR_OK在设备端用串口打印f_mount的返回值能快速判断问题出在FATFS还是USB。第三步能识别U盘但文件复制失败大概率是扇区读写函数的问题。在STORAGE_Read和STORAGE_Write里加日志看主机发的LBA地址和长度是否符合预期。如果地址跨度很大说明FATFS的“底层扇区大小”配置和USB MSC的“逻辑块大小”不一致。标准U盘是512字节逻辑块你的FATFS配置也要用512字节扇区。第四步复制小文件成功、大文件失败这是典型的缓冲区一致性问题。检查DMA缓冲区是否在Cache管理范围内以及FATFS工作区的分配情况。大文件读写会跨越多个扇区DMA连续传输更容易踩到Cache行边界。这套排查链路我在调试中反复用过。遇到问题时不要急着改代码先确认当前卡在哪一层再动手。分层排查往往比全局臆测高效得多。关于后续扩展我目前正在做两个方向一是在USB连接期间仍允许设备写日志通过文件系统级别的同步锁解决并发写问题二是增加U盘容量选择功能让用户在设备端决定是把SD卡整个暴露给主机还是只暴露指定文件夹。等这两个功能稳定了我再专门写一篇分享。本文还有配套的精品资源点击获取
返回列表