拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 USB OTG读取U盘文件系统:硬件配置与FatFS移植全攻略

做嵌入式开发的朋友应该都有过这种经历设备跑起来了想把里面的数据导出来要么串口慢慢拉要么SD卡拔来拔去既慢又容易出问题。2018年5月我做了一个基于STM32的USB OTG工程目标非常明确——让STM32通过USB OTG接口直接读取U盘里的文件设备需要更新配置或者导出日志时插上U盘就能搞定整个过程不需要上位机也不需要额外的读卡器。这篇博客就来完整拆解这个“STM32 USB OTG读取U盘文件系统”工程的搭建过程包括硬件选型、CubeMX配置、FatFS移植、代码实现和踩坑实录适合正在做USB Host、想要让MCU直接读写U盘的朋友参考。1. 项目背景与整体思路1.1 为什么要在STM32上直接读U盘先说清楚这个项目的实际需求。当时手头的设备是一个数据采集终端每天会产生一批日志文件和参数记录。原来的方案是把数据存到板载Flash里需要导出时通过串口发给电脑量大时差不多要等十几分钟用户体验很差。后来想换成SD卡方案但SD卡需要额外的卡座用户手里不一定有而且拔卡插卡容易损坏卡座。U盘方案就顺理成章了。U盘是通用存储设备基本人手一个插上USB口就能识别不需要额外硬件设备外壳开个USB座的口就行。而且U盘的读写速度相比串口有数量级的提升一个10MB的日志文件串口115200bps大概要拉十几分钟U盘方式几秒钟就完成了。当时对比过几个方案我简单列了个表格方案优点缺点SD卡 FatFS体积小、速度快需要专用卡座用户操作不便串口传输实现简单速度慢需要PC端配合网络传输无需复杂线缆硬件成本高配置麻烦U盘 USB OTG通用性强、热插拔、速度快USB协议栈复杂兼容性问题多最终选了U盘方案核心原因就一个通用性。用户不需要学习任何操作手边有U盘就能用这对现场维护来说太重要了。1.2 整体架构与工作流程整个工程分成四个层次从下到上依次是硬件层、USB Host库、FatFS文件系统、应用层。硬件层就是STM32芯片内置的USB OTG控制器加上外部U盘的物理连接。中间层是STM32CubeMX生成的USB Host库它负责跟U盘通信处理USB协议栈里和MSCMass Storage Class相关的部分。再往上FatFS负责将U盘的裸扇区转换成我们熟悉的文件操作接口f_open、f_read、f_write。最上面就是自己的应用逻辑比如读取配置文件、写日志等。工作流程大概是这样的STM32上电后初始化USB Host控制器然后开始检测U盘是否插入。当检测到U盘时USB Host库会自动完成枚举过程识别设备类型。识别为MSC大容量存储设备后U盘就变成了一个可用的块设备。此时应用层调用FatFS的挂载函数把FAT文件系统挂载到U盘上之后就可以像直接用PC操作U盘一样读写文件了。这个分层架构的优点是职责清晰每一层只关注自己的事。USB Host库不需要理解什么是文件FatFS只需要把扇区抽象成文件和目录应用层不需要关心U盘的底层协议大大降低了开发难度。我不用从零实现USB协议栈也不用手动解析FAT表这几百行代码量省得相当值。2. 硬件接口与协议基础2.1 STM32 OTG硬件特性与接线当时用是STM32F407VET6这颗芯片内置了USB OTG_FS控制器引脚是PA11OTG_FS_DM和PA12OTG_FS_DP支持USB 2.0全速12Mbps速率自带FS PHY不需要外部PHY芯片硬件设计非常省事。这里说的FS是Full Speed也就是12Mbps不是480Mbps的USB 2.0高速对于读取U盘文件这种场景12Mbps的实际传输速率在1MB/s左右虽然不算快但完全够用。接线方面USB座的D接PA12D-接PA11VBUS接5VGND接GND看起来很简单但有一个坑U盘工作电流可能达到几百毫安开发板自带的USB座如果直接从板载3.3V LDO取电大概率带不动U盘。我当时的做法是用一个电源控制开关比如STM32F4开发板上常见的NCP380或类似芯片由GPIO控制U盘VBUS的通断供电直接从5V电源轨拉。这里分享一个经验U盘枚举失败的第一大原因就是供电不足。如果U盘插上去完全没反应先量一下VBUS电压是不是5V再量一下D/D-的对地电阻全速设备D上应该有1.5kΩ上拉到3.3V没有上拉就说明PHY没有正常工作。2.2 USB枚举与MSC协议简述虽然USB Host库把协议细节都封装好了但理解基本原理对排查问题非常有帮助。USB设备接入后主机端要完成一系列枚举动作检测设备插入U盘上电后内部会在D线上拉一个1.5kΩ电阻全速设备主机检测到D电平变化就知道有设备插入了。设备复位主机将D/D-拉到低电平至少10ms让设备复位进入默认地址0状态。设置地址主机发送SET_ADDRESS请求给设备分配一个唯一地址。读取描述符主机读取设备描述符、配置描述符、接口描述符、端点描述符等获取设备的基本信息。配置设备主机发送SET_CONFIGURATION请求让设备进入配置状态。开始数据传输配置完成后设备和主机之间通过批量端点Bulk Endpoint进行数据交换。MSCMass Storage Class协议是基于Bulk-Only TransportBOT的简单理解就是主机发送命令块CBW设备执行后返回状态块CSW。每个CBW是31字节包含命令标签和具体的SCSI命令比如READ CAPACITY、READ 10、WRITE 10等。比如读取一个扇区主机发送一个READ CAPACITY命令获取U盘总容量然后发送READ 10命令指定起始扇区和长度数据通过Bulk端点返回最后设备返回一个CSW告诉主机命令执行成功或失败。这套协议栈的逻辑其实很像快递流程CBW就是快递单上面写清楚了要做什么取件还是送件、从哪里取起始扇区、取多少扇区数CSW就是回执单告诉快递公司这个单子执行成功了。理解了这个流程之后排查“U盘能识别但读不出数据”这种问题就方便多了至少能判断是卡在命令阶段还是数据阶段。3. CubeMX工程配置实战3.1 时钟树与引脚配置第一步是配置系统时钟。USB控制器必须工作48MHz这个频率可以由PLLQ或HSI48提供。我用的是外部8MHz晶振系统主频跑168MHzAPB1是42MHzAPB2是84MHzPLLQ设置为48MHz给USB使用。在CubeMX的Clock Configuration界面里把“48MHz CLOCK”的来源选为PLLQRC并确认输出是48MHz即可。时钟配置错了会出现一个很经典的现象U盘偶尔能识别但速度异常慢或者经常报告数据错误。这是因为USB的位同步依赖于准确的48MHz时钟偏差超过一定范围就无法正常通信。引脚配置相对简单CubeMX会自动分配PA11/PA12给USB功能。需要注意的是如果你的板子VBUS电源开关由GPIO控制需要在GPIO_Init中手动配置这个引脚为推挽输出并默认输出高电平使能供电。我在刚开始调试时忘了这个设置导致U盘一直没有供电枚举卡在“设备插入检测”阶段白白排查了很久。3.2 中间件组件配置CubeMX配置USB Host主要有两个地方第一个是USB_OTG_FS在“Connectivity”菜单下将Mode改为“Host Only”速度选择Full Speed。这里不要去选“Device Only”或“Host/Device dual role”我们这个项目只需要Host功能。第二个是USB_HOST中间件在“Middleware”菜单下找到USB_HOST在Class for Host IP选项里选择“Mass Storage Host Class”。这里要注意如果选了多个Class比如同时选HID和MSC代码会多一点但可能引入不必要的复杂度。如果只是读取U盘只选MSC就够了。配置完成后CubeMX还会自动生成一个usbh_diskio.c文件这个文件是FatFS和USB Host库之间的桥梁后面会详细讲解。3.3 中断优先级与生成工程中断配置是一个很容易被忽视的环节。USB_OTG_FS的全局中断OTG_FS_IRQHandler必须使能否则USB Host库的底层驱动无法工作。中断优先级方面建议把USB的中断优先级设为较高抢占优先级1子优先级0因为USB协议是有时间限制的比如控制传输的超时是5秒但如果系统正在进行长时间的关键运算中断响应不及时可能导致设备控制传输超时。生成工程时选择Keil MDK-ARM V5编译器选AC5或AC6都行不过AC6对代码优化更激进有时候会暴露一些未定义行为的隐患如果遇到奇怪的问题可以先换回AC5试试。生成后的工程结构大致如下usb_host.c/hUSB Host初始化入口usbh_conf.c/h底层配置包含HAL_HCD_IRQHandler等回调usbh_msc.cMSC类驱动usbh_diskio.cFatFS与USB Host桥接层app_usb_host.c/h用户回调接口4. FatFS文件系统移植4.1 为什么选FatFSU盘相当于是个裸块设备没有文件系统的话读写数据要自己计算扇区还要手动维护文件分配表太痛苦了。FAT文件系统的实现方案里FatFS可以说是嵌入式领域应用最广的开源方案。它开源、轻量、支持FAT12/16/32和exFAT用起来很简单在淘宝几块钱的开发板上都有现成的移植资料多遇到问题好查。你可能会问STM32官方不是说有ST提供的FAT文件系统组件吗确实有CubeMX里也集成了FatFS但它本质上就是FatFS本身只是帮你生成了底层接口。所以直接用CubeMX自带的FatFS就行不用额外下载。4.2 FatFS关键配置参数CubeMX生成FatFS时会有一个配置文件ffconf.h里面对应很多宏。这里列出几个关键参数的我的设置宏值说明FF_USE_LFN1长文件名支持1表示静态缓冲区2表示动态堆栈FF_VOLUMES1只有U盘一个卷FF_MIN_SS512最小扇区大小FF_MAX_SS512最大扇区大小很多U盘物理扇区就是512字节FF_USE_MKFS0不需要格式化功能省点Flash空间FF_FS_RPATH1支持相对路径方便切换目录FF_CODE_PAGE936中文编码如果文件都是英文名可以设成437省空间重点说一下FF_USE_LFN。FAT文件系统默认只支持8.3短文件名8个字符主文件名3个字符扩展名超过这个长度或者包含中文就会被截断。如果U盘里的文件是中文名或者长文件名的必须打开这个宏。但打开后有个代价需要一块缓冲区存储长文件名的路径FF_USE_LFN为1时是静态数组会占内存比较可观STM32F407有192KB RAM无所谓但对于小内存的MCU就要慎重了。4.3 diskio底层接口实现CubeMX生成FatFS时会自动生成diskio.c和usbh_diskio.c其中disk_read、disk_write、disk_ioctl这几个函数在usbh_diskio.c里已经实现了对USBH_MSC_Read/Write的调用。看一下核心代码DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (USBH_MSC_Read(hUsbHostFS, 0, sector, buff, count) USBH_OK) { return RES_OK; } return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (USBH_MSC_Write(hUsbHostFS, 0, sector, (uint8_t *)buff, count) USBH_OK) { return RES_OK; } return RES_ERROR; }这里的pdrv是卷号单卷系统就是0sector是扇区号LBAcount是扇区数。USBH_MSC_Read的第二个参数是LUN逻辑单元号大多数U盘都是单LUN填0第三个参数是块地址第四个参数是数据缓冲区第五个参数是块数。这里要注意USBH_MSC_Read/Write的块大小是512字节而FatFS默认的扇区大小也是512字节两者刚好对应。如果未来遇到4K扇区的U盘这里就会出现错位需要做扇区地址换算不过目前市面上绝大多数U盘逻辑扇区都是512字节不用太担心。还有一个很重要的点USBH_MSC_Read的缓冲区要求4字节对齐。如果你在函数里面定义一个大数组作为缓冲区编译器可能默认4字节对齐但保险起见可以在数组定义处加上__attribute__((aligned(4)))。这个坑我踩过现象是读取数据偶尔出现错位或者CRC错误排查了好久才发现是对齐问题。5. 核心代码实现与调试5.1 初始化流程与主循环CubeMX生成的主函数结构是这样的int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_HOST_Init(); MX_FATFS_Init(); // 用户代码 while (1) { USBH_Process(hUsbHostFS); // 你的应用代码 } }MX_USB_HOST_Init()内部会对USB Host库进行初始化注册MSC类并启动USB Host。主循环里的USBH_Process是整个USB协议栈的轮询引擎负责处理状态机、枚举、数据传输等工作必须被频繁调用。如果让它执行间隔太长比如你在主循环里做了耗时的Flash写入、打印大量调试信息等会导致USB枚举超时或者数据传输超时。我当时的做法是把USBH_Process放在主循环的最前面然后所有应用逻辑都放后面确保每次循环都会调用它。如果使用了实时操作系统RTOS则建议专门开一个线程优先级设置为普通或者略低于关键实时任务循环调用USBH_Process即可。5.2 U盘挂载与文件读取示例USB Host库识别到U盘并完成枚举后状态会变成HOST_CLASS此时才能安全地调用FatFS接口。我在主循环里加了一个状态检测当检测到U盘就绪时执行文件读取操作uint8_t previousState 0; while (1) { USBH_Process(hUsbHostFS); // 检查USB主机状态 if (USBH_GetState(hUsbHostFS) HOST_CLASS) { if (!previousState) { printf(U盘已连接开始读取文件\r\n); UDisk_ReadConfigFile(); previousState 1; } } else { previousState 0; } }UDisk_ReadConfigFile函数负责挂载文件系统和读取文件void UDisk_ReadConfigFile(void) { FRESULT res; FATFS fs; FIL file; UINT bytesRead; char buf[128]; // 挂载U盘 res f_mount(fs, , 1); if (res ! FR_OK) { printf(挂载失败: %d\r\n, res); return; } // 打开根目录下的config.txt文件 res f_open(file, config.txt, FA_READ); if (res ! FR_OK) { printf(打开文件失败: %d\r\n, res); f_mount(NULL, , 0); // 卸载 return; } // 读取文件内容 res f_read(file, buf, sizeof(buf) - 1, bytesRead); if (res FR_OK) { buf[bytesRead] \0; printf(文件内容: %s\r\n, buf); } f_close(file); f_mount(NULL, , 0); // 操作完毕后卸载 }这里有几个细节值得注意f_mount的第二个参数传空字符串表示挂载到根路径第三个参数1表示强制立即挂载。如果传0系统会延迟到第一次文件操作时才挂载虽然也能用但错误处理会麻烦一些。f_mount(NULL, , 0)是卸载文件系统。这个操作在拔掉U盘前调用可以防止数据丢失。但要注意如果卸载后USB Host库仍然认为U盘连接着再次挂载时需要重新枚举。如果用户在不通知系统的情况下直接拔掉U盘重新插上后还需要重新枚举。我在实际项目中是这样处理的检测到USBH_GetState从HOST_CLASS变为其他状态时执行一次f_mount(NULL, , 0)清理现场等下次U盘就绪时再重新挂载。5.3 完整示例目录遍历与多个文件读取只读一个配置文件太简单实际项目中往往需要遍历U盘里的所有文件。FatFS提供了f_opendir、f_readdir、f_closedir等接口下面是一个读取根目录下所有文件的示例void UDisk_ListFiles(void) { DIR dir; FILINFO fno; FRESULT res; res f_opendir(dir, ); if (res ! FR_OK) { printf(打开目录失败: %d\r\n, res); return; } while (1) { res f_readdir(dir, fno); if (res ! FR_OK || fno.fname[0] 0) { break; // 结束或出错 } if (fno.fattrib AM_DIR) { printf([目录] %s\r\n, fno.fname); } else { printf([文件] %s, 大小: %lu字节\r\n, fno.fname, fno.fsize); } } f_closedir(dir); }如果需要在子目录里操作用f_chdir切换当前目录或者直接用带路径的文件名打开比如dir1/data.txt。FatFS的路径分隔符是/注意不是反斜杠。6. 常见问题与排查技巧实录6.1 枚举失败U盘插上没反应现象是USBH_GetState一直卡在HOST_ENUMERATION或HOST_DEV_WAIT_FOR_RESET等状态或者根本没检测到设备。排查顺序用万用表量VBUS有没有5V。没有5V就看电源开关电路和GPIO控制。量D/D-对地电压。U盘插入后D应该被拉高到大约3V如果一直低电平可能是U盘没供电或者线缆问题。这里有个经验USB线缆质量差会导致D/D-上电平不稳定建议用短一点、屏蔽好的线。换一个U盘测试。U盘兼容性是老生常谈有些U盘的枚举时序比较“特殊”遇到兼容性问题可以先换个品牌USB 2.0的U盘确认到底是硬件问题还是协议问题。检查中断是否挂了。在HAL_HCD_IRQHandler里加一个调试计数器看插入时有没有触发中断。如果一直不触发检查CubeMX是否使能了OTG_FS_IRQn。6.2 文件系统挂载失败U盘枚举成功但f_mount返回FR_NO_FILESYSTEM错误码13说明U盘里没有FAT文件系统或者FatFS不认识。最常见原因是U盘格式化成exFAT或NTFS而FatFS配置里没开exFAT支持。STM32CubeMX生成的FatFS默认支持FAT12/16/32不支持exFAT需要到ffconf.h里把FF_USE_EXFAT宏改为1。不过exFAT支持会明显增加代码体积而且很多老U盘默认就是FAT32所以如果不是必须用超过4GB的单文件我建议直接方案更简单用PC把U盘格式化成FAT32。有些U盘出厂时分区比较奇怪只有一个LUN但分区表类型不是FAT也会导致挂载失败。先用PC确认U盘文件系统类型再用f_mount基本都能解决。6.3 读取数据错误或超时如果文件能打开但读取的数据偶尔不对或者读取超时优先排查这几个点缓冲区对齐USBH_MSC_Read要求4字节对齐数组加上aligned(4)属性。主循环堵塞检查主循环中是否有耗时过长的操作比如HAL_Delay、Flash写操作可以在USBH_Process调用前加一个计数器打印执行间隔。多任务竞争如果用了RTOS多个任务同时访问U盘驱动导致数据被破坏必须加互斥锁保护。FatFS缓冲区大小FATFS每次读写一个扇区如果应用层给f_read的缓冲区小于扇区大小FatFS内部会做拼接性能会下降但不会出错。如果数据量大建议用512倍数的大缓冲区。6.4 调试工具与技巧USB协议调试比较麻烦串口打印是基础手段。我在USBH_Process调用前加了一个状态打印每次状态变化就输出一行这样能清楚看到枚举进度static USBH_StatusTypeDef printState USBH_IDLE; if (USBH_GetState(hUsbHostFS) ! printState) { printState USBH_GetState(hUsbHostFS); printf(USB状态: %d\r\n, printState); }如果卡在某个状态就可以把问题定位到具体的协议阶段。比如一直卡在HOST_ENUMERATION可能是控制传输超时卡在HOST_CLASS_REQUEST可能是MSC类驱动初始化失败。结合USB协议栈的代码能精准定位问题。有条件的话买一个便宜的USB协议分析仪或者用逻辑分析仪抓D/D-数据能看到主机给设备发了什么命令、设备怎么回复的对排查USB兼容性问题帮助极大。不过普通逻辑分析仪带宽可能不够抓全速USB信号至少需要100MHz采样率建议买专用的USB分析仪以免白花钱。6.5 供电与插拔问题U盘热插拔是个大问题如果用户正在读写文件时拔掉U盘可能导致文件系统损坏。我做的方案是在硬件上加一个电源管理芯片比如TPS2065或NCP380在软件上每次读写前检查USBH_GetState当状态不是HOST_CLASS时禁止任何文件操作。另外在检测到U盘拔出后立即调用f_mount(NULL, , 0)卸载文件系统将缓冲区中的数据刷新到U盘。这个卸载操作不能省否则可能会导致最后一个文件的数据没有写入U盘。供电方面U盘启动瞬间的电流可能达到500mA如果用USB Hub或者劣质线缆供电电压跌落会导致U盘内部逻辑复位表现为“插入后能枚举但读写时设备消失”。我后来改成了独立5V电源供电加上一个大电容220uF以上问题就消失了。7. 实操总结与后续扩展这个工程做下来我的体会是STM32 USB Host协议栈和文件系统移植本身并不难CubeMX生成代码已经完成了90%的体力活真正的难点在于硬件供电设计、U盘兼容性处理、以及对协议底层原理的理解。我见过很多人卡在供电和枚举阶段其实大部分原因不是代码写错而是硬件设计不够健壮。如果你打算把这套方案用到实际产品里建议在出厂前就找几种市面上常见的U盘做兼容性测试至少覆盖USB 2.0和USB 3.0的U盘因为USB 3.0的U盘虽然速率高但在全速USB Host下表现往往不如USB 2.0的U盘稳定。另外文件名和文件内容编码建议统一用英文和UTF-8避免中文编码在不同设备上出现乱码问题。后来我在这个基础上还扩展了几个功能一个是通过U盘升级固件把hex文件放到U盘里设备启动时检测到就自动烧录另一个是数据记录设备运行时的传感器数据直接以CSV格式写入U盘用户拿到电脑上就能用Excel打开分析。这两个功能在项目交付时都得到了很好的反馈也证明了“MCU U盘”这个组合在工业现场的实用价值确实很高。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门