简介:这套STM32/GD32 USB Host U盘读取例程,面向嵌入式开发者和电子爱好者,重点解决单片机通过USB OTG接口识别U盘,并配合Fatfs文件系统完成文件读写的问题,适用于HAL库开发的F4系列平台。资源共1054个文件,以C源码、头文件、HAL底层库为主,并包含Keil工程配置、链接脚本、编译与调试辅助文件,压缩包约123.54MB,可直接在Keil5中打开使用。已有5810人学习下载,适合产品原型开发、课程设计或技术学习。例程完整覆盖USB主机枚举流程、Bulk端点传输配置、Fatfs挂载、文件创建/读/写/关闭等操作,附有CC936等多语言编码表,能有效规避U盘兼容性坑点,尤其适合批量数据采集场景。从工程结构到调用逻辑都做了清晰安排,可帮助开发者快速将读写能力移植到自己的STM32/GD32项目中,也可扩展至数据记录、固件升级等实际应用场景。
1. USB Host U盘读取:一个看起来简单、做起来翻车的串行工程
拔出USB线插上U盘,在电脑上不过是点开一个文件夹的动作,放到STM32/GD32上却是一条从USB主机栈、Mass Storage类、SCSI命令一路通到FATFS文件系统的完整链路。USB Host U盘读取例程,表面上是个“读文件”的Demo,实际上让你同时面对三套状态机:USB枚举状态机、BOT传输状态机和文件系统挂载状态机。更难的是,这三者必须按严格时序协同,早一步或晚一步都会出稀奇古怪的故障:USB枚举成功却挂载不上、读一个文件后系统卡死、拔掉U盘直接HardFault。这篇笔记从硬件时钟和电源讲起,再到CubeMX配置、代码移植、FATFS参数调整,最后落到热插拔和掉电保护的踩坑记录。适合正在调试USB Host的嵌入式开发,也适合想把这段例程移植到自家量产板卡上的硬件工程师。
2. 先搞懂三个底层模型:USB Host、Mass Storage和FATFS的职责边界
2.1 USB Host与Device:总线控制权是第一个“翻车”点
平时调试ST-Link、USB转串口,芯片大多工作在Device模式,被动接受主机的请求。USB Host则完全不同:芯片要主动发出复位信号、分配地址、发送请求获取描述符,然后管理总线上的供电和调度。STM32/GD32内部有一个USB OTG控制器,既能做Device也能做Host,部分型号还支持OTG模式下的ID线切换。
做U盘读取时,第一个要确认的就是控制器配置成Host模式,并且USB_OTG_FS或USB_OTG_HS对应的GPIO引脚被正确初始化。常见做法是使用内置的OTG外设,U盘接在FS接口上,速率12Mbps,足够应付大部分读写下发。Source:在CubeMX里如果选错了Device模式,接入U盘后完全没反应,枚举报文发不出去,这是第一个“翻车”点。
2.2 Bulk-Only传输:U盘读写的标准动作
U盘属于USB Mass Storage类,最常用的是BOT(Bulk-Only Transport)协议。简单理解:主机通过控制端点拿到设备信息后,会配置一个Bulk IN端点和Bulk OUT端点,U盘的所有读写操作都通过这两个端点跑。BOT协议把每次命令封装成三部分:
- CBW(Command Block Wrapper):主机发给U盘的命令块,里面包含SCSI命令(比如READ(10)、WRITE(10))和传输长度。
- 数据传输:根据CBW的方向在Bulk端点上传数据。
- CSW(Command Status Wrapper):U盘返回的执行状态。
例程里那串USBH_MSC_Read、USBH_MSC_Write之类的函数,最终就是把FATFS下发的扇区读写作换成这些Bulk传输。如果U盘本身没问题,枚举也成功,但读写就是失败,通常不是协议栈的问题,而是缓冲区指针没有按4字节对齐,或者一次传输长度不是扇区大小的整数倍。很多开发者在这里踩坑:FATFS的下层disk_read一次读取多个扇区,底层MSC驱动对单次传输长度的限制又不一样,两者没对齐就会出现偶发失败。
2.3 FATFS挂在哪一层:块读写与文件读写的鸿沟
FATFS是一个纯软件层的文件系统模块,它不关心数据怎么从U盘里读出来,只负责把文件目录解析成扇区号。真正的扇区读写由底层接口完成,在CubeMX生成的工程里对应disk_read、disk_write、disk_ioctl这几个函数,它们内部再调用USB Host的MSC驱动。
这两层之间的关键参数是扇区大小。绝大多数U盘是512字节/扇区,但也有4K扇区的移动硬盘或大容量U盘。如果disk_ioctl返回的GET_SECTOR_SIZE不对,FATFS计算文件簇地址时就会全盘错乱。例程里一般默认512,遇到4K扇区设备必须先读设备描述符里的逻辑块长度,动态适配。
2.4 STM32和GD32的USB IP差异:例程不能直接硬搬
STM32的USB Host库基于HAL层,函数名以USBH_开头;GD32虽然也集成了USB OTG控制器,但寄存器布局和库接口有差异。直接拿STM32的HAL例程往GD32工程里塞,编译都会报错。GD32原厂提供的标准外设库里面也有USB Host的MSC例程,函数名通常以usb_、udisk_开头,底层回调机制也不同。
移植时最省力的办法是保持应用层代码不变,只替换驱动层接口。把f_mount、f_open这些FatFs调用原样保留,把disk_read底下的USBH_MSC_Read换成GD32库的udisk_read或usb_host_msc_read。注意两边的回调函数签名可能不一致,尤其是状态回调里传递的参数含义:STM32用USBH_UserProcess传入状态ID,GD32有的库直接用一个回调结构体指针,改起来需要对准事件类型。如果只打算在GD32上跑,直接跑原厂的Host例程会更快,不要硬磕HAL移植。
3. 硬件与最小系统:U盘不被识别多半是电和时钟的问题
3.1 引脚和时钟:USB需要48MHz不是玄学
USB FS模式的物理层需要精确的48MHz时钟,这个时钟可以由外部晶振经PLL分频产生,也可以由内部PLL直接输出。STM32/GD32的很多例程里会看到专门输出48MHz给USB外设的配置,如果时钟频率偏了,枚举过程会随机失败,表现为偶尔识别偶尔不识别。说它是“玄学”,其实是时钟精度不够导致的边沿采样错误。
建议在设计板卡时使用8MHz或12MHz晶振,通过PLL产生48MHz。不要省掉DP/DM线上的串联电阻和滤波电容。常见做法是DP、DM各串一个22Ω电阻,再接1.5kΩ下拉或上拉,具体要看芯片内部是否有上拉电阻。有些型号内部有上拉电阻,外部再接上拉反而导致信号幅值不对。我一般先查数据手册里USB_FS部分的推荐电路,不直接照搬开发板。
引脚选择也要提前规划:STM32的OTG_FS通常占用PA11/PA12,有些GD32型号的USB脚位不同。如果板子上这两个引脚被别的功能占用,就要切到HS接口,但HS外部通常需要ULPI PHY芯片,成本和复杂度都会上来。普通U盘读取用FS接口完全够用。
3.2 VBUS供电与限流开关:U盘启动瞬间的电流尖峰
U盘虽然小,但启动瞬间电流可能达到100mA到300mA,有些机械移动硬盘甚至更高。MCU的GPIO绝对供不起这个电流,必须从外部5V电源给VBUS供电。很多开发板的Host口直接用USB线的5V电源,在调试时没问题,量产产品上却容易在插拔瞬间产生电压跌落,导致MCU复位。
建议在VBUS路径上加一个限流开关,常见做法是使用支持过流检测的电源开关芯片,把过流标志接到MCU中断引脚。MCU检测到过流后主动断开VBUS并提示用户。这个细节在例程里没有体现,但做产品必须补上。插入U盘瞬间,如果MCU电源和VBUS共用一组LDO,电压跌落会导致USB控制器状态异常。我用示波器量过插U盘瞬间的波形,跌落幅度能到300mV,所以VBUS输入电容至少要100μF容值。
3.3 枚举过程的探测手段:逻辑分析仪看端点包
U盘枚举不上时,先别急着改代码,用逻辑分析仪看DP/DM上的信号。FS模式下的包波形可以通过普通逻辑分析仪抓取,如果看到SOF包正常发出,但U盘没有ACK响应,问题多半在设备端供电或上拉电阻;如果连SOF都没有,说明USB控制器没工作起来,查时钟。
如果想看协议层内容,USB协议分析仪能直接解析枚举请求和描述符。没有分析仪的时候,我一般先在例程里加打印:把USBH_Status的中断回调每个case都打出来,看到卡在哪一步。常见卡点包括CONNECT后不进入ADDRESS、拿到配置描述符后select interface失败,这些都可以在回调里定位。例程里不要只打印最终结果,要把状态迁移日志留下来。
4. 移植并跑通最小例程:从CubeMX生成到文件写入
4.1 CubeMX工程配置:勾选USB_HOST和FATFS的完整路径
先在CubeMX的Pinout页左侧勾选USB_OTG_FS,Mode选择Host Only。如果不是纯Host,也可以选OTG模式,但要注意ID引脚的电平。接着在中间件里勾选USB_HOST,Class选Mass Storage。FATFS中间件也要同时勾选,并确保它绑定的底层驱动是USB,而不是SD卡或Flash。
时钟树部分务必确认USB时钟来源是48MHz,CubeMX会自动分配。如果芯片主频跑在72MHz但PLL配置不合理,CubeMX会提示USB时钟超标或不足,此时不能点“OK”继续,必须先调整PLL参数。生成工程后,打开GPIO设置,CHECK一下USB的DP/DM两个引脚被正确配置为USB模式,不要留成普通GPIO。
生成代码后,先编译一次,确保没有配置遗漏。CubeMX生成的工程默认会带一个usbh_diskio.c文件,FATFS底层disk_read已经在里面实现了对USB Host MSC的调用。这一层是打通的关键,不要随意删除或替换。修改这个文件前,最好先跑一遍原始例程确认“USB枚举 + 文件系统”通路没问题。
4.2 代码结构:USBH_Process与f_mount的调用时序
生成后的main函数会周期调用USBH_Process,这是USB Host的中断轮询函数,必须保证它高频调用。很多移植失败的原因是把USBH_Process放进了比U盘读写优先级更低的任务里,导致枚举超时。我一般把它放到主循环开头,不阻塞、不延时。
文件系统挂载不能在USB枚举完成前调用。CubeMX生成的例程里,USBH_UserProcess是处理枚举事件的回调,挂载动作建议放在HOST_USER_CONNECTED之后,借助一个标志位去主循环里执行。下面是一段标准处理逻辑:
volatile uint8_t app_usb_state = APP_USB_DISCONNECTED; void USBH_UserProcess(USBH_HandleTypeDef *phost, uint8_t id) { switch (id) { case HOST_USER_SELECT_CONFIGURATION: USBH_SelectInterface(phost, 0); /* 选择第一个MSC接口 */ break; case HOST_USER_CONNECTED: app_usb_state = APP_USB_CONNECTED; /* 通知主循环挂载 */ break; case HOST_USER_DISCONNECTED: app_usb_state = APP_USB_DISCONNECTED; break; default: break; } }这里把状态机拆成“USB层事件”和“应用层动作”。回调里只更新标志位,不直接调用f_mount,避免在中断上下文做文件系统操作。主循环里检测到APP_USB_CONNECTED后,延时几百毫秒等U盘内部稳定,再调用f_mount。参数0表示立即挂载,如果返回FR_NOT_READY,不要无限重试,先看一下USB层状态。
调用f_mount挂载时,卷号要和diskio.c对应。CubeMX生成的FATFS通常注册了一个卷,卷号是“0:”,写成空字符串也表示默认卷。挂载成功后,应用层就能用标准文件API读写U盘。整个过程最忌讳的是在USB状态还在枚举中就调用f_mount,百分之百返回错误。等连接稳定后再挂载,是血泪经验。
4.3 文件读写例程:f_open/f_read/f_write的参数与超时
挂载完成后,读写文件其实就回到了传统FATFS套路。下面是一个读取U盘第一个目录下config.txt并打印内容的函数:
void read_usb_file(void) { FIL file; UINT br = 0; uint8_t buf[128]; FRESULT res; res = f_open(&file, "config.txt", FA_READ); if (res != FR_OK) { printf("f_open fail: %d\r\n", res); return; } res = f_read(&file, buf, sizeof(buf), &br); if (res == FR_OK && br > 0) { printf("read %u bytes\r\n", br); } f_close(&file); }f_open的第一个参数是FIL结构体指针,第二个参数是相对根目录的文件路径。f_read的第三个参数是读取字节数,建议和buf大小一致。返回值FR_OK表示成功,FR_DISK_ERR表示底层U盘读写失败,FR_NOT_READY表示文件系统没有挂载好。如果意外返回FR_INVALID_NAME,检查文件路径是否包含长文件名或非法字符。
写入U盘时更要注意文件系统缓存。写入后必须f_close或f_sync,否则数据可能只停留在FATFS内部缓冲区,拔掉U盘就会丢失。f_write第四个参数返回实际写入字节数,如果小于请求长度,通常是因为磁盘满了或底层错误。
4.4 移植到GD32:库函数改名与延时适配
把STM32的例程移植到GD32时,不要指望CubeMX。GD32有自己的固件库,USB Host的初始化通常是一组独立函数。常见做法是保留FATFS层和应用层的f_open/f_read,把disk_read和disk_write的实现替换成GD32的U盘驱动接口。
GD32的USB Host库在枚举完成后的回调函数名、参数类型和STM32不同。我遇到过的GD32库会通过一个结构体把事件类型传给应用,例如usb_host_callback结构体里的connect、disconnect、read、write回调。需要自己把应用事件映射到这些回调中。另外,GD32的延时函数叫nop或delay_1ms,不能用HAL_Delay。移植时把例程里所有HAL_Delay替换掉。
GD32部分型号的USB控制器没有独立的48MHz时钟源,必须由主时钟分频产生。如果主频跑在108MHz或120MHz,分频关系要仔细看时钟树。这类问题在GD32上比STM32更容易出现,很多时候枚举失败不是代码逻辑,而是时钟配置没同步。参考原厂提供的Host例程内的时钟配置函数,不要套用STM32的SystemClock_Config。
5. 避坑:U盘读取的5个经典翻车现场
5.1 插入U盘后USBH_Process永远停在HOST_USER_CONNECT
现象:插入U盘后,串口打印一直输出CONNECT事件,之后状态不再变化,枚举没有进展。 原因:USB控制器重复检测到连接,但无法完成设备地址分配。多数是USB时钟不精确或DP/DM走线过长导致信号质量差。 解决:先用逻辑分析仪确认SOF包是否正常。如果SOF正常,检查DP/DM上的上拉电阻位置是否匹配芯片要求。如果时钟频率用频率计测量偏差超过0.05%,重新配置PLL。
5.2 f_mount返回FR_NOT_READY但USB已经连接
现象:回调里已经收到HOST_USER_CONNECTED,f_mount却返回FR_NOT_READY。 原因:连接事件只是说明枚举完成,Mass Storage设备还需要时间准备扇区和文件系统。有些U盘在上电后几百毫秒内对INQUIRY命令都无响应。 解决:在连接事件和f_mount之间加至少500ms延时,或者先调用USBH_MSC_GetMaxLUN确认设备就绪。不要用固定死延时,而是循环查询MSC类驱动状态,直到准备就绪再挂载。
5.3 读取大文件到一半卡死:DMA与Cache一致性
现象:读写小文件正常,读写大文件(比如几十MB)时程序卡死或数据错乱。 原因:MCU的DMA和Cache不一致。USB的DMA搬运数据到内存缓冲区,但CPU读取数据时从Cache读到了旧数据。常见于带D-Cache的高性能STM32/GD32型号。 解决:将接收缓冲区定义到32字节对齐的地址,并在每次读取前执行SCB_InvalidateDCache_by_Addr。写入方向则要先CleanCache,再启动DMA。如果只在U盘例程里使用,最简单的方法是在启动DMA前直接关闭对应区域的Cache或全片Invalidate。
5.4 拔掉U盘后系统死机:没有释放FATFS和MSC
现象:读写正常,但运行中直接拔掉U盘,系统进入HardFault。 原因:FATFS还持有挂载状态,U盘的底层驱动指针已被释放,再次调用f_read/f_write时访问了非法地址。 解决:在HOST_USER_DISCONNECTED回调里先调用f_mount(NULL, "0:", 0),卸载文件系统,同时把应用层状态置为空闲。如果拔掉时正好在读写文件,还需要增加一个“正在读写”标志,读写完再允许卸载。热插拔是U盘读取最容易被忽略的“坑”,不处理的话不仅死机,还可能损坏U盘分区。
5.5 格式化成exFAT后读不到:FATFS的exFAT开关
现象:U盘在Windows下格式化成exFAT,插到例程里f_mount返回FR_NO_FILESYSTEM。 原因:默认FATFS配置没有开启exFAT支持,编译时_USE_EXFAT为0。 解决:在ffconf.h中把_USE_EXFAT设为1,同时打开_USE_LFN和_MULTI_PARTITION支持。注意exFAT相关代码会占用更多Flash,小容量MCU要确认空间。另外,如果只是读文件不是写文件,可以把_FS_READONLY设为1,能省下写相关代码占用的RAM。
6. 把U盘读写做成可靠的数据通道:状态机封装与验证方法
6.1 用回调状态机管理生命周期:别在主循环里“死等”
U盘读取例程跑通后,下一步要把它封装进产品代码。不要在主循环里用while反复调用f_mount直到成功,这样会把整个系统卡死。我一般会建立一张小的应用状态表:USB_DISCONNECTED、USB_CONNECT_PENDING、USB_MOUNTED、USB_FILE_BUSY。每个状态只做一步操作,状态切换由USB回调驱动或定时器驱动。这样U盘读写只是系统里的一个任务,不阻塞其他逻辑。
6.2 性能优化:缓冲区大小与多扇区读写的权衡
FATFS读大文件时,默认每次读一扇区再解析一层FAT表,效率不高。例程里通过f_read的buffer大小间接控制块读粒度。我一般会定义一个2KB或4KB的缓冲区,让FATFS一次读多个扇区,减少USB Bulk传输次数。代价是RAM占用增大。如果需要极致性能,可以在disk_read里使用MSC多块读命令,而不是一次一块。但要注意有些U盘的BOT协议实现不规范,多块读容易超时,我通常先按512字节验证全流程,再优化性能。
6.3 掉电与完整性验证:文件校验和测试方法
U盘作为数据通道,最怕写到一半断电。FATFS处理这类掉电还不够鲁棒,建议在文件数据段末尾写入CRC或CRC32校验值。读取端校验不通过时重传或提示用户重新插拔。我习惯在量产固件里加一个测试模式:往U盘连续写100个1KB文件,然后全部读回来比对内容。这种测试能暴露DMA、缓存和文件系统参数几乎全部问题。跑完两轮后,再手动热插拔测试100次,才能确认例程真正“稳定”。
把U盘Host例程从Demo变成产品功能,不是把代码复制进工程就能完成。每个坑都值得单独写一段注释到工程里,方便后来的人维护。我也会把这些踩坑点更新到自己的移植笔记里,下次换型号时能少翻几次车。希望帮到你。
本文还有配套的精品资源,点击获取