尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32/GD32 USB Host读取U盘实战:从原理到FatFS移植与排坑

STM32/GD32 USB Host读取U盘实战:从原理到FatFS移植与排坑 简介面向嵌入式开发者尤其是使用意法半导体STM32与兆易创新GD32系列微控制器的工程师这份USB主机U盘读取例程工程包专注于解决在USB Host模式下与U盘进行数据交互并借助Fatfs文件系统模块完成文件读写操作的实际需求。压缩包内含1054个文件以c/h源码、Keil5工程配置uvprojx/ewp、编译中间及输出文件o/d/crf/axf/hex、批处理脚本等为主其中c/h源码承载完整逻辑工程配置文件便于直接导入编译编译输出文件可用于运行分析与对照调试整体大小约123.54MB目录结构清晰方便按需提取。目前已有5785人学习/下载。例程覆盖USB Host枚举、控制传输与Bulk端点通信关键流程并给出Fatfs常用接口的调用示例f_open/f_read/f_write等同时附带基于YH-F429平台的工程文件和cc936.c等字库可在Keil5中快速编译运行适用于数据记录、文件传输、外接存储等嵌入式应用场景。1. 项目概述与整体思路1.1 为什么需要USB Host U盘读取做嵌入式开发的朋友应该都有过这种经历设备跑起来了传感器数据也采到了但数据怎么导出来用串口打印吧数据量一大就卡而且拔线插线的操作在生产现场特别不友好。用SD卡吧还得专门配读卡器现场工人操作麻烦。这时候让设备直接识别U盘把数据往U盘里一写拔下来插电脑就能看是最省事的方案。这个项目说白了就是让STM32或GD32单片机做主设备Host主动去识别并读取插在它身上的U盘里的文件。比如U盘里放一个配置文件设备上电后自动读进去更新参数或者反过来设备把采集的数据写成文件存到U盘里。这个功能在工业仪器、数据采集器、广告一体机、甚至一些智能家居设备里都特别常见。我最早是从STM32F407开始玩的后来因为芯片缺货和成本问题陆续在几个量产项目里换成了GD32F450和GD32F303。两者在USB Host这个功能上代码兼容度很高但还有一些细节差异这篇文章我把自己踩过的坑和最终跑通的例程都整理出来给大家一个可以直接抄作业的参考。1.2 方案选型USB Host FATFS的组合逻辑USB Host端的协议栈选择其实没有太多悬念。ST官方提供了USB Host Library也就是我们常说的ST的USBH中间件它把底层的OHCI/EHCI如果是F1系列或者OTG如果是F4系列驱动都封装好了对上层提供了标准API。GD32这边官方也有类似例程而且GD32F4xx系列的USB硬件和STM32F4xx高度相似所以很多ST代码稍作修改就能在GD上跑。文件系统这块自然就是FatFS了。为什么选它因为它够轻量、够稳定而且几乎所有的嵌入式文件操作需求它都能覆盖。U盘格式化的时候一般是FAT32或exFATFatFS对FAT32的支持非常成熟主要注意exFAT需要打开对应宏定义_FS_EXFAT。整个方案的技术栈就是三层应用层f_open / f_read / f_write 等FatFS API 中间件USB Host Library枚举、Mass Storage类驱动 底层STM32/GD32 USB OTG 外设驱动这种分层结构的好处是每一层都可以单独验证。比如你怀疑文件系统有问题可以先不挂U盘用RAM磁盘或者SD卡先测FatFS怀疑U盘枚举有问题可以先用调试串口打印USBH_State的值确认走到哪一步。这个排查思路我后面会详细讲特别管用。2. USB Host核心原理与硬件准备2.1 USB Host到底是什么很多人刚开始接触USB Host和USB Device会搞混。简单说Device是被动的它等着主机来查询它、给它发指令。而Host是主动的它要检测设备插入、给设备供电、发送枚举请求、分配地址、读取设备描述符整个过程非常像一个主机系统在“叫醒”一个外设。U盘本身就是一个Mass Storage Class大容量存储类简称MSC设备。它里面其实没有太多“智能”全靠Host端来驱动。Host通过Bulk传输方式向U盘发送CBWCommand Block Wrapper命令块U盘处理后返回CSWCommand Status Wrapper状态块。使用SCSI命令集包括INQUIRY、READ CAPACITY、READ(10)、WRITE(10)等来读写扇区。STM32F4和GD32F4系列内部集成了USB OTG FS/HS控制器。OTG的意思就是既可以做Host也可以做Device由ID引脚的电平决定。但我们在做U盘读取时直接把OTG_FS或OTG_HS配置为Host模式而且要说清楚F4系列跑USB Host非常吃香因为它的USB OTG内嵌了DMA数据搬运不占用CPU。而STM32F103这类不带OTG只有USB Device的芯片如果要支持U盘读取就得外挂CH376或者USB3300之类的芯片。那个方案比较绕而且芯片成本不低不如直接选F4/GD32F4省心。我见过不少新手选型时没注意这个区别画完板子才发现F103不能直接读U盘只能改板非常尴尬。2.2 硬件连接与最小系统以STM32F407VE为例它的USB OTG FS引脚是PA11DM、PA12DP如果有高速需求走OTG HS的话需要外接USB PHY芯片比如USB3300我们这里用内置的FS PHY就够了U盘读取速率在1MB/s左右绝大多数场景够用。GD32F450系列的USB引脚也是PA11/PA12和ST对应得很好。GD32F303的USB引脚布置略有不同我记得是PB14/PB15OTG_FS_DM/DP这个建议直接查数据手册确认别凭印象画板。硬件上除了DM/DP这两根信号线VBUS供电一定要注意。USB Host要给U盘提供5V电源电流至少要有500mA。很多开发板直接用稳压芯片输出的5V如果这个5V同时给别的外设供电U盘一插入就可能压降轻则枚举失败重则U盘不认。稳妥的做法是用独立的DC-DC或LDO给USB口供电加一个大电容47uF~100uF稳压。另外DM/DP线要加15kΩ下拉电阻到地让Host识别设备插入这个在STM32/ GD32内部已经集成了不需要外接但如果做的是板子layout的时候还是要注意差分走线尽量短且等长。供电电路我用的是这个搭配VBUS输入USB 5V经过一个自恢复保险丝500mAVBUS滤波并联一个100uF坦电容 0.1uF陶瓷电容信号线PA11/PA12直连U盘的DM/DP走线控制在20mm以内2.3 芯片选型对比参考这部分是给还在纠结选型的朋友一个参考。我实际用过STM32F407、GD32F450、GD32F303这三颗来做U盘读取列个表总结一下芯片型号USB硬件Host库兼容度实测U盘读取速率备注STM32F407VEOTG FS/HS内置FS PHYST官方库直接跑约900KB/s资料最多踩坑最容易搜到答案GD32F450VEOTG FS内置FS PHY用ST库小改可用GD官方例程也齐全约850KB/s引脚和F407完全兼容可直接替换GD32F303OTG FS内置FS PHY需用GD官方库库函数接口不同约700KB/s价格更低但引脚不兼容F4系列如果你做的是对成本敏感的产品GD32F303是个不错的选择。如果是学习或者快速验证想法STM32F407开发板一百来块钱资料遍地都是建议先用它跑通逻辑再移植到目标芯片。3. 实操过程与核心环节实现3.1 初始化流程从配置到检测U盘我用的是STM32CubeMX配合STM32CubeIDEGD32也可以用STM32CubeMX生成初始化代码再替换为GD的库。CubeMX的配置如下在USB_OTG_FS里选择Mode为Host_Only然后在 Middleware and Software Packs 里勾选 USB_HOSTClass 选择 Mass Storage Host Class。这会在代码里自动生成USBH_HandleTypeDef结构体、MSC类句柄、以及USBH_Init等函数的调用。初始化代码最关键的是这个流程基于ST官方库/* 初始化USB Host库 */ USBH_Init(hUsbHostFS, USBH_MSC_Class, HOST_FS); /* 启动USB Host处理内部注册回调 */ USBH_Start(hUsbHostFS); /* 主循环里周期调用处理枚举状态机 */ while (1) { USBH_Process(hUsbHostFS); }很多新手不知道USBH_Process必须放到主循环或者RTOS任务里周期调用。这个函数是USB Host的状态机每次调用推进一个状态。如果你忘了调用它U盘插上去就不会有任何反应。这一点和USB Device模式完全不同Device模式靠中断驱动就够了Host模式必须要靠周期性的Process来驱动状态机。检测U盘是否枚举成功的方法是在USBH_UserProcess回调函数里判断状态void USBH_UserProcess(USBH_HandleTypeDef *phost, uint8_t id) { if (id HOST_USER_CONNECTION) { /* 设备插入 */ } else if (id HOST_USER_DISCONNECTION) { /* 设备拔出 */ } else if (id HOST_USER_CLASS_ACTIVE) { /* MSC类已经激活枚举成功可以准备挂载文件系统 */ g_usb_ready 1; } }这里有个细节HOST_USER_CLASS_ACTIVE这个事件只在设备枚举完成、MSC驱动绑定成功之后触发。我习惯在这里置一个标志位然后在主循环里去挂载FATFS而不是直接在回调里挂载。因为回调上下文里做文件系统操作容易跟USBH_Process产生重入问题如果开了中断嵌套实测在低优先级主循环再做非常稳。3.2 FATFS挂载与文件读写封装USB Host把U盘枚举成一个逻辑驱动器后FatFS层做的事情其实和SD卡读写很相似。区别只在于底层磁盘读写函数的实现FatFS的disk_read和disk_write需要调用USB Host提供的接口。我在代码里这样写关键部分#include ff.h #include ff_gen_drv.h #include usbh_diskio.h FATFS fs; FIL file; /* 将USB Host的磁盘IO驱动注册到FatFS */ FATFS_LinkDriver(USBH_Driver, disk_path); // disk_path 通常是 0: /* 挂载文件系统1表示立即挂载不需要等f_mount返回 */ FRESULT res f_mount(fs, disk_path, 1); if (res ! FR_OK) { printf(Mount fail: %d\n, res); }挂载失败的原因一般是U盘文件系统损坏、U盘格式不是FAT32比如exFAT但FatFS没开对应宏、或者枚举根本没成功就急着挂载。我建议在挂载前先用printf把USBH_GetState(hUsbHostFS)打出来确认状态是HOST_CLASS再去执行挂载。文件写入的封装代码我一般这样写方便其它模块调用uint8_t WriteLogToUsb(const char *filename, uint8_t *buf, uint16_t len) { FRESULT res; if (g_usb_ready 0) return 1; res f_open(file, filename, FA_CREATE_ALWAYS | FA_WRITE); if (res ! FR_OK) return 2; UINT bw; res f_write(file, buf, len, bw); f_close(file); if (res ! FR_OK || bw ! len) return 3; return 0; }注意FA_CREATE_ALWAYS每次都会覆盖原文件如果你需要追加写日志应该用FA_OPEN_ALWAYS然后f_lseek到文件末尾。日志型应用一般都要这么处理。3.3 GD32上的移植注意事项如果用GD32的库移植思路有两种。一种是直接用GD官方提供的例程在GD32F4xx_Firmware_Library里就带USB Host和FatFS的demo。但GD的库函数命名和ST不太一样如果你是ST的老用户初次接触GD库会有那么一点不适应比如ST的HAL_GPIO_WritePin对应GD自己的库函数gpio_bit_set。另一种方式是保留ST的HAL代码但把底层的芯片支持包替换为GD的。GD32F4xx的寄存器布局和ST的F4很接近但不是完全一致尤其是USB OTG的寄存器和ST的HAL头文件里定义的偏移略有区别。这个方法工作量不小我试过一次最后还是老老实实基于GD官方库重新建工程反而快很多。实际用的GD方案是这样的/* GD32F450, USB Host MSC, FatFS (基于GD官方库) */ usb_otg_core_parameter_struct usb_param; usb_param.type USB_OTG_FS; usb_param.speed USB_SPEED_FULL; usb_param.ep0_maxpacket 64; usb_param.phy_itf USB_EMBEDDED_PHY; /* 初始化OTG核心 */ usb_otg_core_init(USB_OTG_FS, usb_param); /* 注册MSC类驱动 */ usbh_msc_init(usb_host_msc);GD的库已经把USB Host中间件做了很大程度的封装usbh_msc_init、usbh_msc_loop这类函数用起来很像ST的USBH_MSC_Class。我实际测试下来GD32F450跑U盘读写的稳定性还是相当不错的连续写入2GB的大文件没有掉线。3.4 支持exFAT和大容量U盘的处理现在的新U盘容量动不动就64GB、128GB出厂格式一般是exFAT。FatFS默认是不支持exFAT的你要用exFAT的话必须在ffconf.h里把_FS_EXFAT这个宏改成1。同时还需要把_USE_LFN打开建议设为2使用堆上动态分配否则长文件名文件读不出来中文文件名更是直接乱码。还有个大容量相关的坑当U盘容量大于2GB时FAT32的扇区地址是32位的。FatFS的FSIZE_t默认是32位够用。但如果你单文件超过4GB比如往U盘写一段连续几小时的视频流那FSIZE_t需要改成64位对应宏是_FS_LBA64和_FS_EXFAT得一起打开。另外在FatFS配置里_MAX_SS需要设为4096。因为高端U盘的物理扇区大小可能不是标准512B而是4096B。如果你没有改这个宏挂载可能会成功但读写到某些区域会出莫名其妙的数据错误。这个问题非常隐蔽别问我怎么知道的都是泪。4. 常见问题与排查技巧实录4.1 U盘插上去完全没反应这是最常遇到的问题。插上U盘后串口一点打印都没有USBH_Process一直在撸空。先排除硬件用万用表量VBUS是不是5VU盘插入瞬间有没有电流跳变。如果没有电流跳变基本是供电问题或者USB口接触不良。如果有电流但枚举不成功大概率是DP/DM接线问题或者USB口的15k下拉没生效。软件层面确认USBH_Process被周期调用。很多用RTOS的朋友容易把USBH_Process放在一个被阻塞的任务里导致状态机卡死。建议专门开一个10ms周期的定时器或者任务来调度它。如果以上都没问题那就是枚举超时。打印USBH_GetState看看卡在哪个状态比如一直卡在HOST_ENUM说明设备描述符读取失败极有可能是时钟配置不对。USB OTG FS必须要48MHz的时钟U盘才会响应这个很多人忽略。4.2 枚举成功但f_mount返回错误f_mount返回FR_NO_FILESYSTEM说明FatFS没有识别到U盘上的文件系统。先用USB读卡器把U盘插电脑确认格式是FAT32或exFAT。如果U盘是NTFS格式那FatFS默认是识别不了的需要在电脑上重新格式化为FAT32。另一个可能是_MAX_SS没改成4096导致大扇区U盘的BPBBIOS Parameter Block读取错误。之前碰到过一个64G的U盘怎么挂载都失败后来翻FatFS作者的文档想起来这个坑改完就通了。还有一个大家容易忽略的是FatFS配置里_MIN_SS和_MAX_SS要和diskio层的CAP_ERASE等匹配。我们用的是官方USBH_Driver一般不用改但如果你用过老的驱动要注意物扇区大小相关的宏。4.3 读写过程中U盘掉线表现为写入一半或读取一半USBH_UserProcess返回了HOST_USER_DISCONNECTIONU盘好像被拔掉了一样。这个问题90%是供电不稳引起的。U盘工作时电流会波动尤其是突然写入大数据块时电流尖峰很高劣质稳压器带不动就会拉低VBUS导致U盘内部复位重新枚举。解决办法在VBUS和GND之间并联一个470uF电解电容用带使能端的电源开关芯片尽量选低Dropout的LDO或DC-DC软件上每次读写前做一次检测如果掉线就重试挂载如果是电池供电的设备还要考虑Cable线压降USB线越长压降越大。我现在的方案是U盘接口处用独立的5V电源轨跟主控的模拟部分隔离开来问题就再也没有出现过。4.4 写入文件后电脑上看不到更新这个现象是设备提示写入成功U盘重新拔插到电脑上文件要么不存在要么是旧的内容。原因是FatFS写文件后数据还在缓存里没有同步到物理介质。解决办法是在每次写完文件后调用f_sync(file)或者直接f_close后再做一次diskio层的flush。FatFS的f_close内部会做flush但前提是_FS_READONLY为0并且f_close的返回值必须正确检查。我见过有同学写代码不看返回值f_close失败还以为文件写好了。最好是在U盘拔出前强制做一个10ms的延时等U盘干净断电用f_mount(NULL, disk_path, 0)手动卸载文件系统再把USB Host状态机复位这样U盘拔下来随时插电脑都是对的。4.5 常见问题速查表现象可能原因解决方向插入U盘无任何反应供电不稳、DM/DP接反、USBH_Process未调用测量VBUS、检查接线、主循环周期调用枚举卡在HOST_ENUMUSB时钟不是48MHz检查USB时钟配置挂载失败FR_NO_FILESYSTEMU盘格式非FAT32/exFAT或扇区宏配置不对格式化U盘设置_MAX_SS4096读写中途掉线USB供电瞬态跌落加大VBUS电容独立供电中文或长文件名乱码FatFS未开启LFN支持设置_USE_LFN2写文件后电脑看不到未sync或未关闭文件写完调用f_sync或f_close5. 更多实战心得与扩展思路5.1 USB Host的RTOS集成方法如果你用的是FreeRTOSUSBH_Process这件事要慎重安排。我建议直接单独开一个任务void usb_host_task(void *arg) { while (1) { USBH_Process(hUsbHostFS); vTaskDelay(pdMS_TO_TICKS(5)); } }为什么用5ms而不是1ms或者10ms实测USB枚举过程对时序比较敏感5ms的调度周期既能及时处理状态机又不会太频繁地占用CPU。如果周期太长某些U盘会在枚举阶段超时太短的话任务切换开销增加而且USBH_Process内部有些延迟逻辑需要足够的时间间隔才能正确判断。文件读写建议放在应用任务里不要和USBH_Process共用一个任务避免长时间占用Process导致Host状态机卡死。这在大量写入U盘时特别重要写入会阻塞如果状态机和写文件同在一个任务USB Host这边就无法及时响应U盘的事件。5.2 我想读取启动配置文件动态参数更新USB Host U盘读取最常见的量产场景之一就是做配置文件导入。比如设备里有几十个参数用串口或者按键一格格地改太费劲直接做一个“配置导出/导入”功能设备端写一个CSV或TXT格式的配置文件到U盘用户在电脑上打开编辑改参数存回U盘插回设备上电时自动读取并应用我在一个数据采集项目里做过这种功能用了CSV格式方便用户用Excel直接编辑。设备解析CSV文件时要注意CSV可能带BOM头直接用strstr去匹配第一列会失败。解决办法是读取文件头3个字节如果发现是EF BB BF就跳过再去解析。/* 跳过UTF-8 BOM */ uint8_t bom[3]; f_read(file, bom, 3, bw); if (bom[0] ! 0xEF || bom[1] ! 0xBB || bom[2] ! 0xBF) { f_lseek(file, 0); // 不是BOM回到文件开头 }这个小细节第一次做的时候坑了整整半天后来PowerPoint里才想起来CSV是可以用BOM标记编码的。如果你用的是Notepad或者某些中文Windows环境编辑CSV是会自动加上BOM的。5.3 数据掉电保护在写入日志时别拍板断电如果你在用USB Host往U盘写日志掉电断电是必须考虑的场景。FatFS写文件有缓存断电会丢数据甚至可能破坏FAT表。工业设备里我见过因为写U盘时被拨电导致整个U盘目录结构损坏的案例。我的做法是打开文件后每写一条日志立即f_sync单文件写满一定大小比如2MB就关闭重新开一个新文件掉电后重启时如果发现U盘文件系统无法挂载打印告警信息提示用户插电脑检查修复当然f_sync的代价是写入速度下降因为每次都要等底层flash写入完成。但做数据记录这个场景正确性永远比速度重要。5.4 后续扩展兼容更多USB设备USB Host能做到的事情不只是读U盘。ST的USB Host库里面还带了HID类驱动鼠标键盘、CDC类驱动串口设备、甚至MTP类驱动手机/相机传文件。如果你已经跑通了U盘的MSC类再扩展HID类是非常快的只需要在USBH_Init里面同时注册多个类驱动USBH_Init(hUsbHostFS, USBH_MSC_Class | USBH_HID_Class, HOST_FS);设备插入时USB Host库会自动匹配它对应的类驱动去绑定。这就能实现一个USB口既支持U盘又支持键鼠或者USB扫码枪对做工业控制面板、一体机的场景非常实用。GD32F4这一系列在USB Host的应用上性价比很高这几年已经越来越多的产品用它替代ST。如果你现在的产品还在用F103我觉得换到F4/GD32F4解决USB Host的功能是成本可控且回报明显的一次升级值得评估。6. 工程代码结构与测试建议6.1 推荐的工程文件结构一个清晰的工程结构能让你少踩很多坑尤其是代码量上来之后。我现在的USB Host项目推荐这样组织文件Project/ ├── Core/ // 主函数、中断、时钟配置 ├── USB_HOST/ │ ├── App/ // usb_host.c, usb_host.h (应用层回调、状态机) │ ├── Target/ // usbh_conf.c, usbh_diskio.c (平台相关) │ └── STM32_USB_Host_Library/ │ ├── Core/ // usbh_core.c, usbh_pipes.c, usbh_ctlreq.c │ └── Class/ │ └── MSC/ // usbh_msc.c, usbh_msc_bot.c, usbh_msc_scsi.c ├── FATFS/ │ ├── App/ // fatfs.c, fatfs.h (FatFS移植层) │ ├── Target/ // ff_gen_drv.c, diskio.c │ └── Source/ // ff.c, ff.h, ffconf.h, ffunicode.c ├── Drivers/ // HAL库或GD库 └── User/ // 业务逻辑这个结构是我参考ST官方例程、结合自己几个量产项目总结出来的。关键是USB_HOST/App和User分开USB Host只做“管道”业务逻辑别往中间层塞。比如你要在U盘里读配置文件业务解析写在User不要写在usbh_msc的驱动文件里。否则后面换芯片或者换协议栈的时候你还得在一堆底层代码里抠业务逻辑。6.2 测试流程怎么验证整个链路是通的每次拿到一个新的板子或者新的U盘我都按这个顺序验证排查问题效率最高先跑官方例程不管ST还是GD先把官方仓库自带的USB Host U盘demo烧进去确认硬件部分的USB口是好用的。这一步不开文件系统只验证枚举。枚举OK后再接上FatFS在官方例程基础上挂载FatFS做一次“创建文件、写几行、关闭、再打开读出来校验”的简单自测。换不同U盘测试FAT32的小容量、exFAT的大容量、USB 2.0/3.0的都试一遍。至少测试5个以上的U盘才能放宽心。连续压力测试持续写入1GB以上数据看看会不会中途掉线以及写入后插电脑文件能不能正确读出。断电测试边写文件边断电上电后检查U盘文件系统是否还能挂载。这个测试不要太频繁对U盘寿命有影响但至少做几十次。这个过程很费时间但嵌入式这行就是这样前期的验证越充分后面量产和售后越省心。我见过太多开发者在实验室里试了两三个U盘没问题就急着量产结果现场用户的U盘一个比一个“奇葩”各种兼容问题都冒出来。USB这东西就是著名的“谁插上都能用谁插上都可能出问题”的玄学外设多做兼容性测试永远是对的。6.3 写在最后的个人体会USB Host读U盘这个功能技术上不算特别难但它牵扯到的知识点非常杂USB协议栈的枚举状态机、Mass Storage类的BOT传输过程、SCSI命令集、FatFS的磁盘IO对接再加上不同芯片、不同U盘的兼容性坑。一个人如果没有实际调过很难对这些坑有直观的感知。我自己的经验是第一次做不用求快把CubeMX生成代码的每个函数都点进去看一下理解它调用的层次关系比直接跑通一个demo学到的东西多得多。遇到问题也别急着换方案先用打印大法把当前状态机、FatFS的返回值都打出来定位到具体是哪一层出了问题再针对性去搜资料。如果你正在做类似的项目希望这篇文章能帮你少走点我当年走过的弯路。有问题欢迎在评论里交流我看到都会回复。本文还有配套的精品资源点击获取
返回列表