简介:STM32/GD32 USB Host U盘读取例程是一份面向嵌入式开发者的完整参考工程,主要解决单片机通过USB Host模式识别并操作U盘、结合Fatfs文件系统实现文件读写的问题。资源适用于使用STM32F407/GD32F407等带OTG接口的芯片进行数据记录、文件传输等项目的工程师,也适合正在学习HAL库与Fatfs移植的进阶用户。压缩包共1054个文件,大小约123.54MB,以o、h、c源文件及Keil工程配置为主,同时包含编译生成的axf、hex、map等输出文件,以及少量脚本和说明文档,整体目录结构完整,可配合Keil5直接打开、编译与二次开发。资源浏览学习人数已达5785,具有较高参考热度。内容涵盖USB Host驱动框架、设备枚举流程、Bulk传输机制、Fatfs文件系统配置与常见异常处理思路,代码中留有中文注释和演示工程,便于快速理解FAT32读写流程并迁移到实际项目中,是一份拿来即用的USB存储扩展范例。 很多人在论坛里问:GD32能不能直接跑STM32的USB Host例程?U盘读取这东西,原厂给的USB库文件动辄几千行,一换芯片型号就要重新适配,想改个端点都得翻半天文档。这篇文章我直接把STM32/GD32的USB Host读取U盘这套东西掰开揉碎讲清楚,核心思路是脱离HAL库和USB Library的封装,用寄存器操作把USB Host枚举、BOT传输、FatFS挂载这条链路完整跑通。文章里会给出关键代码框架和排查经验,适合已经有基础、想彻底搞懂USB Host工作原理,或者正在做U盘读写功能但被原厂库折腾得不行的开发者参考。
1. 为什么我放弃了原厂USB库,改成寄存器操作
先说我踩过的坑。最初做U盘读取用的STM32官方USB Host库,功能确实全,但问题在耦合度太高:整个协议栈绑死HAL层,换到GD32F407之后,HAL库的底层实现有差异,库文件里的延时、DMA配置逻辑都要重新调。更麻烦的是,官方库把U盘的枚举、BOT协议、SCSI命令全部封装成状态机,出问题的时候根本不知道卡在哪一步,只能一层层打日志,非常痛苦。
后来我翻了一遍STM32F4参考手册的USB OTG章节,发现USB Host的核心操作其实就那么几件事:检测设备插入、复位端口、发送标准请求拿描述符、分配地址、切配置,然后是BOT协议批量传输。这些东西用寄存器操作完全可以搞定,代码量反而少一半,而且逻辑透明,每一步都知道在干什么。
寄存器操作的另一个好处是跨平台性。GD32F407和STM32F407的USB OTG控制器是同一款IP核,寄存器地址和位定义几乎一致,我把STM32上调通的代码直接烧到GD32板子上,枚举和读写U盘一次通过。这个经验让我后来敢把整套USB Host逻辑做成一个独立的模块,不依赖任何厂商的HAL库,换MCU只需要改底层寄存器地址映射,成本很低。
2. 硬件电路和初始化配置的细节
2.1 引脚连接与电路设计要点
先看硬件。USB OTG FS全速接口在STM32F407和GD32F407上固定使用PA11作为DM、PA12作为DP,除此之外还有几个关键信号:VBUS、ID、电源地。
做U盘读取这种Host应用,ID引脚必须接地,这样控制器才会工作在Host模式。VBUS方面,F4系列的USB OTG控制器内部有VBUS检测逻辑,但驱动能力很弱,不能直接给U盘供电。我试过用板载3.3V转5V的小电流LDO给VBUS供电,结果插上U盘就枚举失败——U盘启动瞬间电流能到200mA以上,小LDO电压跌落太厉害。后来改成单独的5V DC-DC供电,问题才解决。
再说DP/DM的接法。很多人忽略的一点是:Host模式下控制器内部已经集成了15kΩ下拉电阻,用来检测设备插入;而U盘这一侧必须自己上拉D+到3.3V来表明自己是全速设备。如果自己做的U盘接口板少了这个1.5kΩ上拉电阻,主机永远检测不到设备连接,我调试的时候就因为这个浪费了半天时间。
2.2 CubeMX的基础配置
用CubeMX配置STM32F407VET6,选择USB_OTG_FS,Mode选Host Only。这里有个坑:有些CubeMX版本里Host Only的配置默认把VBUS sensing关掉了,如果板上没有做VBUS分压检测电路,就保持关掉,否则会一直报过流错误。
时钟这块,USB OTG FS要求48MHz的时钟输入。我的板子用的25MHz外部晶振,配置成PLLCLK倍频到168MHz,然后通过USB OTG FS的时钟分频器(USBPLL)分频得到48MHz。用标准库或者HAL库初始化的时候,直接调用USBH_Init就把时钟配好了,但寄存器操作需要自己检查RCC_CFGR的USBPRE位是否正确设置,这个位配错的话,USB控制器完全跑不起来,而且没有任何报错提示。
2.3 GD32平台的兼容性确认
GD32F407和STM32F407的USB OTG控制器几乎完全一致。我对照了两家的数据手册和参考手册,寄存器偏移地址、位定义都相同。唯一要注意的是GD32的主频和Flash等待周期配置差异,但这不影响USB模块。实际测试中,我把原先为STM32F407写的USB Host寄存器代码,只改了芯片头文件和系统时钟初始化部分,编译下载到GD32F407开发板,插上U盘直接识别成功。
不过GD32F303的情况就不同了。F303系列对标的是STM32F103,USB是全速设备控制器,不是OTG控制器,寄存器结构差异很大,代码不能直接搬。所以如果你用的是GD32F303,建议要么换F407,要么老老实实用GD32官方USB库。
3. 枚举流程的完整拆解:从插上U盘到识别设备
3.1 设备检测与端口复位
USB Host枚举的第一步是检测到设备插入。OTG控制器的Host端口状态寄存器(HPRT)会反映当前端口连接状态,当U盘插入时,D+被上拉,控制器检测到电平变化,HPRT的PCDET位(端口连接检测)会被置位。
检测到连接后不能立刻发命令,需要先等待一段时间让U盘内部的电源稳定,一般延时100ms以上。然后要对端口做复位操作:往HPRT的PRST位写1,保持至少10ms,再清零。这个复位动作会让U盘重新进入默认状态,地址恢复为0,为后续的标准请求做准备。
3.2 控制传输的实现原理
USB的枚举过程全靠控制传输完成,控制传输分成三个阶段:Setup阶段、Data阶段(可选)、Status阶段。在寄存器层面,需要操作OTG控制器的Host通道寄存器组。
Setup阶段的核心是一个8字节的Setup包,数据结构定义如下:
typedef struct { uint8_t bmRequestType; uint8_t bRequest; uint16_t wValue; uint16_t wIndex; uint16_t wLength; } __attribute__((packed)) SetupPacket;发送Setup包的流程是:配置Host通道的CHCTL寄存器,设置传输方向为OUT,设置EP类型为控制端点,然后往CH0的FIFO里写入8字节Setup数据,最后使能通道。等传输完成中断后,检查CHINT的TRCP位确认无误。
数据阶段和状态阶段也依赖中断标志位来推进,我在代码里把所有中断处理集中在一个函数里,用状态机变量记录当前枚举进度,每个中断到来时根据当前状态决定下一步动作。
3.3 标准请求与描述符解析顺序
枚举过程发起的标准请求顺序是固定的,必须严格按照以下步骤:
- GET_DESCRIPTOR获取设备描述符前8字节,这一步只需要拿到端点0的最大包长(bMaxPacketSize0),通常为64字节,用于后续控制传输的分包。
- SET_ADDRESS分配地址,给U盘设置一个唯一地址,一般都是1。
- 再次GET_DESCRIPTOR获取完整设备描述符(18字节),解析厂商ID、产品ID、设备类别等信息。
- GET_DESCRIPTOR获取配置描述符,这里要注意配置描述符后面还跟着接口描述符和端点描述符,通常一次请求取整个配置描述符集合(一般是32字节或更多)。
- SET_CONFIGURATION选择配置,把配置值设为1,U盘开始工作。
整个流程走完后,枚举就完成了。如果中途任何一步返回错误,U盘就不会进入就绪状态,需要检查硬件连接或重新复位。
3.4 枚举失败时的定位方法
枚举失败是最常见的问题,我的排查思路是:先用逻辑分析仪抓D+/D-信号,看主机有没有正确发出SOF包,再看U盘有没有返回ACK。如果完全没有响应,大概率是设备侧的D+上拉电阻问题,或者USB数据线接触不良。
如果主机发了SETUP包但U盘不回ACK,先查地址分配对不对,再查设备是否被意外复位。我遇到过一次很奇怪的现象:U盘在枚举过程中突然断开,重试几次都一样,后来发现是供电不稳导致U盘内部电压跌落触发掉电重启,换供电方案后问题消失。所以说排查枚举问题要结合信号观察和电源测试,不能只盯着软件。
4. BOT传输协议与SCSI命令:让U盘真正响应读写
4.1 Bulk-Only Transport协议结构
枚举完成后,U盘作为Mass Storage类设备,通过BOT协议进行数据传输。BOT协议的核心是CBW(Command Block Wrapper)和CSW(Command Status Wrapper)两个结构体。
CBW是主机发给U盘的命令包,长度31字节,包含签名、标签、数据传输长度、标志位、LUN逻辑单元号和SCSI命令块。CSW是U盘返回的状态包,长度13字节,包含签名、标签、状态值。代码如下:
typedef struct { uint32_t dCBWSignature; // 固定为0x43425355 uint32_t dCBWTag; // 命令标签 uint32_t dCBWTransferLength; // 传输字节数 uint8_t bmCBWFlags; // 0x00表示主机读数据,0x80表示主机写数据 uint8_t bCBWLUN; // 逻辑单元号,一般为0 uint8_t bCBWCBLength; // SCSI命令长度 uint8_t CBWCB[16]; // SCSI命令块 } __attribute__((packed)) CBW_t;U盘本身不解析SCSI命令,它只是按照BOT协议把CBW里的命令块透传给内部固件处理。传输方向由bmCBWFlags决定,必须和CBW里SCSI命令的读写方向一致,否则U盘直接返回错误状态。
4.2 关键SCSI命令与超时处理
BOT协议承载的是SCSI命令,常用的几条:
- INQUIRY:获取U盘基本信息(厂商、产品名、版本),命令代码0x12。
- TEST UNIT READY:查询U盘是否就绪,命令代码0x00。上电后U盘可能需要几百毫秒才能就绪,需要循环发送这条命令,直到返回成功。
- READ CAPACITY:获取U盘总扇区数和扇区大小,命令代码0x25。
- READ(10):读取指定扇区数据,命令代码0x28。
- WRITE(10):写入指定扇区数据,命令代码0x2A。
每次BOT事务必须成对出现,先发CBW,然后按需传输数据,最后必须接收CSW,三个环节缺一不可。CSW的dCBWSignature必须等于0x53425355,dCBWTag必须和CBW中的标签一致,否则说明传输异常。
超时处理也很重要。我在代码里写了一个简单的超时计数器,每次轮询通道中断状态时递增,如果超过500ms还没有收到响应,就主动复位批量端点,清除错误标志,返回失败。有了这个机制,即使U盘偶尔卡住,系统也不会死等,可以直接重试。
5. 对接FatFS,实现文件读写
5.1 FatFS底层接口对接思路
U盘在逻辑上是一个块设备,按扇区读写,而FatFS文件系统库正好建立在块设备抽象之上。要做的核心工作就是实现disk_read、disk_write、disk_status、disk_initialize这几个函数,把FatFS的扇区读写请求转换成BOT协议的SCSI READ(10)/WRITE(10)命令。
在disk_initialize里做BOT协议复位和查询就绪状态。disk_read和disk_write函数接收参数包括扇区起始编号、扇区数量和缓冲区指针,内部循环读取每个扇区,通过批量端点发送数据。
5.2 数据缓冲对齐的注意事项
USB全速批量传输的最大包长是64字节,而U盘的扇区大小是512字节,意味着每个扇区要分8次批量传输完成。FatFS默认的缓冲区不一定是4字节对齐的,但USB控制器的FIFO要求32位对齐访问,否则会触发总线错误。
我在代码里专门设置了一个512字节的扇区缓冲区,地址用__attribute__((aligned(4)))强制对齐,所有和USB FIFO之间的数据搬运都通过这个缓冲区中转:
static uint8_t usb_sector_buf[512] __attribute__((aligned(4))); DRESULT disk_read(BYTE *buff, LBA_t sector, UINT count) { for (UINT i = 0; i < count; i++) { // 发送READ(10)命令读取一个扇区 MSC_SCSI_Read10(sector + i, usb_sector_buf, 512); // 将缓冲区内容拷贝到FatFS传入的buffer memcpy(buff + i * 512, usb_sector_buf, 512); } return RES_OK; }5.3 挂载和读写测试实录
底层对接完成后,上层就是FatFS的标准用法。先把U盘格式化成FAT32格式(后面我会专门说格式化的问题),然后调用f_mount挂载,接着就可以用f_open、f_read等API读写文件了。
测试时我写了一个简单的验证流程:挂载成功后,在根目录创建test.txt,写入一段字符串,关闭文件,再重新打开读取,把读到的内容通过串口打印出来。代码如下:
FATFS fs; FIL file; UINT bw, br; char write_buf[] = "USB Host U盘读取测试\r\n"; f_mount(&fs, "", 1); // 写文件 f_open(&file, "test.txt", FA_CREATE_ALWAYS | FA_WRITE); f_write(&file, write_buf, strlen(write_buf), &bw); f_close(&file); // 读文件 char read_buf[64] = {0}; f_open(&file, "test.txt", FA_READ); f_read(&file, read_buf, sizeof(read_buf), &br); f_close(&file); printf("读取内容: %s\r\n", read_buf);实测下来,普通U盘读取速度在900KB/s左右,写入速度稍慢,约700KB/s,这是全速USB的理论上限(1MB/s)附近的合理水平。读写过程中FatFS返回FR_OK,没有出现CRC错误或超时。
6. 实战中踩过的坑和几条重要经验
6.1 枚举偶尔失败,重启后又正常
这个现象我排查了很久,最后发现是电源问题。U盘初始插入时冲击电流很大,如果5V电源走线太长或者电容不够,电压会瞬间跌落,导致U盘内部逻辑复位。处理办法是在U盘座旁边加一个100μF的电解电容和0.1μF的陶瓷电容,并且把5V供电线加粗。改完硬件之后,枚举失败的现象再没出现过。
6.2 FatFS挂载返回FR_NO_FILESYSTEM
遇到这个错误时第一反应是底层读写有问题,后来发现是U盘本身格式不对。现在市面上有些U盘出厂是exFAT格式,而老版本的FatFS不支持exFAT,需要启用_USE_LFN和_EXFAT宏定义。如果不想用exFAT,直接把U盘重新格式化成FAT32就行,注意分配单元大小保持默认的4K,逻辑扇区大小选512字节,不要选4096字节,否则FatFS挂载也会出问题。
6.3 大量读写时偶尔出现CSW错误
连续读写几百个扇区之后,偶尔会出现CSW状态错误或者标签不匹配。我加了一个简单的重试机制:事务失败后先发Mass Storage Reset命令复位BOT状态,然后重新发起原命令,重试3次,如果在重试过程中恢复正常就直接继续,不需要重新枚举。这个机制在实际测试中效果很明显,错误出现后基本都能自动恢复,不会导致整个读写流程中断。
6.4 GD32和STM32之间的代码迁移经验
总结一下我的迁移经验:F407级别的GD32和STM32在USB OTG模块上是通用的,代码可以直接复用;如果项目用的是F303或F103级别的芯片,USB控制器架构不同,建议直接评估是否换用F4系列,或者接受官方库的兼容层。
另外,如果你买的是GD32的核心板,注意有些板子板载的USB座没有把ID引脚引出,没法直接当Host用。选型的时候多看一眼原理图,确认ID引脚可以被拉低,否则就得自己飞线处理。
7. 后续优化方向:从能用走向稳定
目前这套寄存器版的USB Host读U盘方案我已经稳定跑了一段时间,后续优化我打算从几个方向继续:一是增加对USB Hub的支持,现在直接插U盘没问题,但过Hub之后枚举流程会复杂很多,主要是地址分配和TT事务翻译的问题;二是完善异常恢复机制,目前重试3次的策略对于偶尔的干扰足够,但如果U盘在写入过程中被直接拔出,文件系统很容易损坏,需要增加掉电保护机制;三是尝试把这套Host逻辑移植到其他芯片上,比如支持USB Host的国产单片机和部分Cortex-M内核系列,打通更多平台。
如果你也打算自己动手写一套USB Host,我的建议是:先别急着啃完整协议,先把枚举流程跑通,看到设备描述符再说。枚举通了,后面的BOT和FatFS都是水到渠成的事。调试的时候备一个USB分析仪或逻辑分析仪,效率会翻倍,别全靠猜。
本文还有配套的精品资源,点击获取