☰
STM32F107 USB HOST读写U盘:从枚举到FatFs的完整工程实战与避坑指南
2026/10/20 8:34:08 网站建设 项目流程

简介:面向使用STM32F107微控制器进行嵌入式开发的工程师与学生,这份压缩包提供了在USB主机模式下读写U盘的完整工程实现,可有效解决外围存储设备接入的数据管理问题。资源内容围绕ST官方库与FAT文件系统展开,包含USB主机初始化、U盘枚举挂载、文件打开关闭、读写操作及目录管理等关键环节,覆盖从底层驱动到应用层调用的完整链路。包体共298个文件,以c源码和h头文件为主,同时包含编译过程生成的object、依赖文件,以及uvprojx等工程配置和hex固件,整体大小约8.18MB,便于直接导入工程或按需查看中间产物。压缩包目录结构清晰,便于定位USB驱动、文件系统移植以及应用示例代码;通过研读源码,可以掌握STM32F107在主机模式下的工作流程,以及如何利用FAT文件系统在U盘上可靠读写数据。目前已有354人学习,这份资源对于正在搭建USB存储应用的原型开发者,能够提供可复用的代码框架和避坑参考。

1. STM32F107 USB HOST:把U盘读写从玄学变成可复现的工程

STM32F107 的 USB HOST 读写U盘,看手册感觉就三步:枚举、挂载、读写,实际动手调起来才会发现每一步都有看不见的坑,枚举超时、挂载失败、写一半卡死是最常见的三个翻车点。这个 USB HOST.zip 是一份做嵌入式多年整理出来的工程包,里面带好了配置工程、FatFs 移植层和一个能直接烧录的按键读写 Demo。它解决的是 STM32F107 的 OTG_FS 全速外设在 HOST 模式下读写字库、固件升级、数据落盘这类场景问题,适合正在做 U 盘固件升级、产线批量拷贝、采集数据导出的小团队,也适合刚接触 USB Host 栈、想拿一份完整工程对照学习的新手。如果你已经花了好几个晚上还在跟状态机较劲,这份资源值得直接打开复现一遍。

2. 从 OTG 到 HOST:为什么选 STM32F107,模式切换与三个硬件前提

2.1 OTG_FS 的三种角色:HOST 模式与 OTG 模式的取舍

STM32F107 内置的 USB OTG_FS 外设支持全速 12Mbps 通信,物理层 PHY 在芯片内部,这跟很多需要外挂 USB PHY 的芯片方案相比,省掉了不少外围器件。它本身被设计成可以承担三种角色:Device、Host 和 OTG。OTG 允许同一个 USB 口在主从之间动态切换,但对绝大多数只插 U 盘的应用来说,这种灵活反而引入了额外的复杂性。

我一般会直接选 Host Only,而不是让系统跑 OTG。原因有两个:第一,OTG 模式下的 HNP 会话协商协议本身就是一套复杂状态机,调试时要多看一个变量;第二,OTG 需要硬件上把 ID 引脚拉出来做检测,很多自制板子根本没接这个脚。把角色固定为 HOST,状态机路径清晰,行为也可预测,这在生产环境里比什么都重要。

HOST 模式下,控制器负责发送 SOF 帧、管理总线调度、发起控制/批量/中断传输。U 盘属于大容量存储设备,对应的类驱动是 MSC,走 Bulk-Only Transport 协议,底层通过 SCSI 命令跟 U 盘通信。协议栈部分由 USB Host 库实现,你真正要操心的是上层 FatFs 文件系统怎么和 MSC 驱动对接。所以资源包里的代码它的分层非常清楚:底层 USBH_MSC_Class、中间 diskio 映射、上层 FatFs API,每一层是独立的.c文件。

2.2 时钟、VBUS 与 DP/DM:枚举前的三个硬件前提

先讲最容易被忽略的 DP/DM 电阻问题。STM32F107 内置 PHY 的情况下,HOST 侧需要的 15kΩ 下拉电阻已经集成在芯片内部,不需要也绝不能再在 D+/D- 上外挂下拉。我碰到过一个板子,硬件工程师照抄某些 Device 模式参考设计,在 D+ 和 D- 上各加了一个 15kΩ 下拉到地,结果当然是枚举永远失败。全速设备侧的 1.5kΩ 上拉电阻由 U 盘自己提供,主机侧不用管,D+/D- 到 USB 座之间尽量短,走差分线,别跨分割,这是基本的信号完整性要求。

第二个前提是 VBUS 5V 供电。U 盘是总线供电设备,插上瞬间冲击电流不小,如果板上的 5V 是某个小电流 LDO 转出来的,电压会被直接拉垮,U 盘主控反复复位,状态机一直在枚举和断开之间横跳。正确做法是独立的 5V 电源芯片,VBUS 输出脚和 U 盘座之间串自恢复保险,边上放一个 22μF 电解电容稳压。VBUS 使能引脚要用 GPIO 控制,不要由 USB 控制器内部自动打开,这样软件上能控制上下电顺序。

第三个前提是 48MHz USB 时钟。全速 USB 对时钟精度要求约 ±0.25%,48MHz 偏到 47.9MHz 都会导致枚举不稳、批量传输 CRC 报错。配置工具里的时钟树一定要看到 48MHz 这个数字。用示波器量 PA8 的 MCO 输出更直接,能实时看到时钟是否偏移。这里多说一句,很多移植出来的工程喜欢直接用内部 HSI,能跑但不稳定,尤其是温度变化之后,我建议还是用外部晶振走 PLL。

VBUS 感知脚也值得留意。如果配置里开了 VBUS sensing,硬件上必须用电阻分压把 5V 降到 3.3V 以下接到感知引脚,否则控制器认为 VBUS 无效,枚举根本不会开始。硬件没接分压电阻的时候,有两个选择:把感知关掉,或者补一个分压电路。我建议补电路,因为还能顺便拿来监测 U 盘是否掉电。

2.3 配置生成的几个关键点:外设、中间件与中断

用配置工具搭工程时,外设一项选择 USB_OTG_FS,工作模式选 Host_Only,中间件层开启 USB_HOST 并把 Class 选成 Mass Storage Host Class,再开启 FATFS,磁盘驱动挂到 USB 上。这里注意生成代码的顺序:先有 USB Host 底层,再有 diskio 映射层,最后是 FatFs 应用层,三层是串联关系,任何一层断掉都读不了 U 盘。

中断配置有两个:USBH_OTG_FS_IRQHandler 和 USBH_OTG_FS_WKUP_IRQHandler,都要打开。优先级设置上我一般比 SysTick 低半级,避免中断里长时间卡在 USB 处理流程导致系统调度异常。另外生成代码后需要手工补的只有两处:VBUS_EN 引脚的 GPIO 输出初始化,以及 USBH_UserProcess 回调里的用户事件处理函数,后面避坑章节会专门讲这个回调里必须做什么。

整个 HOST 栈的运行机制是:初始化时注册 MSC 类驱动,然后主循环里持续调用 USBH_Process 驱动状态机前进。这个函数就是整个 USB HOST 的心跳,少了它,所有中断和回调都不会正常工作。

3. 读写 U 盘实现:从枚举到文件落盘的完整链路

3.1 USBH_Init 与 USBH_Process:主循环里的状态机泵

初始化代码在 main 函数中是这样组织的,先把 VBUS 使能,再做 USB Host 初始化:

/* 打开 U 盘 5V 供电 */ HAL_GPIO_WritePin(VBUS_EN_GPIO_Port, VBUS_EN_Pin, GPIO_PIN_SET); /* USB Host 初始化:注册 MSC 类驱动并启动 HOST */ MX_USB_HOST_Init();

MX_USB_HOST_Init 内部实际完成两件事:调用 USBH_Init 把 MSC 类驱动挂到 Host 栈上,再调用 USBH_Start 启动状态机。这里第二个参数 0 表示主机通道号,STM32F107 只有一个 OTG_FS 外设,填 0 即可。VBUS 先于 USB 初始化上电,是保证首次枚举稳定的一个细节,我见过反过来的顺序,第一次总是枚举失败,后面每次都成功,多半就是上电时序问题。

主循环里必须无条件高频调用 USBH_Process:

while (1) { USBH_Process(&hUsbHostFS); if (hUsbHostFS.gState == HOST_APPLICATION_READY) { /* 枚举和类请求完成,可以挂载文件系统 */ MountUSB(); break; /* 挂载后跳出循环进入业务逻辑 */ } }

逻辑说明:USBH_Process 是整个 Host 栈的推进函数,负责设备插入检测、总线复位、枚举、MSC 类请求这些状态迁移。放在主循环里高频轮询是标准做法,不能把它塞进某个低频任务里,否则中断里收到的数据没有及时被协议栈消化,U 盘会工作不稳定。gState 是当前状态的只读反馈,HOST_APPLICATION_READY 表示设备已经准备好,应用层这时候才能往下走。

3.2 FATFS 挂载:卷号、立即挂载标志与超时重试

确认枚举成功后再挂载文件系统,建议带超时和重试机制:

FATFS USBDiskFS; FRESULT res; uint8_t retry = 0; /* 等待枚举完成,最多重试 5 次,每次 100ms */ while (hUsbHostFS.gState != HOST_APPLICATION_READY && retry < 5) { USBH_Process(&hUsbHostFS); HAL_Delay(100); retry++; } if (hUsbHostFS.gState != HOST_APPLICATION_READY) { return -1; /* 超时未就绪 */ } /* 立即挂载卷,卷名是 USB: */ res = f_mount(&USBDiskFS, "USB:", 1); if (res != FR_OK) { return -2; /* 挂载失败 */ }

参数说明:f_mount 的第三个参数是立即挂载标志。填 1 表示 f_mount 内部立即读取 U 盘引导扇区并注册工作区,失败会当场返回错误码;填 0 则只是登记文件系统对象,真正的挂载要等第一次 f_open 才发生。我习惯填 1,把问题暴露在前,而不是让错误延迟到读写时候才出现。卷名字符串 "USB:" 必须与 diskio.c 里实现的磁盘驱动编号对应,生成代码里默认映射到 USBH 的驱动。

另一个工程细节是 FATFS 默认打开长文件名支持后,会额外消耗 RAM,对 107 来说完全够用。如果板子 RAM 紧张,可以改配置只支持短文件名,代价是 U 盘里那些中文文件名和超长文件名都打不开。

3.3 文件读写封装:返回值、实际字节数与断点续传

FatFs 的读写函数要检查的不只是返回值,还要核对实际传输字节数:

FRESULT read_usb_file(const char *path, uint8_t *buf, uint32_t max_len, uint32_t *actual_len) { FIL f; FRESULT res; UINT rd = 0; res = f_open(&f, path, FA_READ); if (res != FR_OK) return res; /* 文件不存在或权限错误 */ res = f_read(&f, buf, max_len, &rd); f_close(&f); if (res != FR_OK) return res; /* 读错误,可能是介质异常 */ *actual_len = rd; /* 实际读到的字节数 */ return FR_OK; }

代码说明:f_open 的打开模式用 FA_READ,如果文件不存在会返回 FR_NO_FILE。f_read 的第三个参数是请求读取的长度,第四个参数是实际读到的字节数,rd 必须被检查,因为底层 SCSI 读可能返回比请求少的字节。写文件同理,f_write 的第四个参数是实际写入字节数,空间不足时返回值可能是 FR_DENIED 或 FR_INT_ERR,只看返回值会漏掉部分写入的问题。

对于要频繁写配置数据的场景,我会在写完一帧后调用 f_sync 强制将缓存写入 U 盘,防止突然掉电丢数据。代价是每次 f_sync 都有一定耗时,但换来的是掉电后文件系统不损坏。

3.4 最小可运行 Demo:一个按键触发读写

工程里附带的是一个按键触发读写的完整流程,核心代码可以浓缩成这样:

uint8_t test_buf[512]; void UserKey_Handler(void) { FIL f; UINT bw; FRESULT res; /* 写文件 */ memset(test_buf, 0x5A, sizeof(test_buf)); res = f_open(&f, "USB:test.bin", FA_CREATE_ALWAYS | FA_WRITE); if (res == FR_OK) { f_write(&f, test_buf, sizeof(test_buf), &bw); f_close(&f); } }

这个 Demo 的价值在于它把前面讲的等待机制、挂载逻辑、读写封装全部串成了一条可断点跟踪的链路。按下按键,完成一次 512 字节写入,然后重新挂载把内容读回来做比对。大多数情况下,只要你把配置里 VBUS 感知关掉或者硬件做对,这个 Demo 第一次就能跑通。跑不通的时候,优先去查的是中断优先级,而不是怀疑协议栈。

4. USB HOST 避坑指南:枚举失败、读写卡死与文件损坏的四个排查记录

4.1 枚举失败:状态机反复重启,永远到不了 APPLICATION_READY

现象:U 盘插入后,状态从 HOST_IDLE 走到 HOST_ENUMERATION,然后复位重新开始,来回循环,gState 始终进不了 HOST_APPLICATION_READY。

原因:最常见的是 VBUS 供电不足。U 盘主控在枚举阶段需要从 VBUS 取电完成内部初始化,如果 5V 在插上瞬间跌落超过 10%,U 盘主控会反复复位,主机侧看到的就是设备不停地断开重连。还有一个原因就是之前提到的 DP/DM 上被人为加了外部下拉电阻,破坏了内置 PHY 的阻抗匹配。

解决:先去掉 D+/D- 上所有外部电阻,再用示波器量 VBUS 波形。如果插上 U 盘时 5V 跌落明显,把供电换成独立电源,并在 VBUS 输出加 100μF 电解电容。做到这两点,枚举成功率基本能回到 100%。我第二版板卡上加了 VBUS 监控,枚举失败率从原来 30% 降到接近 0。

4.2 读写卡死:程序死在 USB 中断里,单步无法跳出

现象:U 盘可以挂载,f_open 也正常,但执行 f_read 或 f_write 时程序卡死,调试器断在 USB_OTG_FS_IRQHandler 中断里,怎么单步都不走。

原因:这是中断优先级配置错误导致的死锁。USB 中断优先级如果高于系统滴答定时器,而中断处理里有任何等待操作,就会和主循环里的 USBH_Process 形成互相等待。另外,传输缓冲区没有按 4 字节对齐也常见,内置 PHY 的 DMA 访问非对齐地址时触发总线错误。

解决:把 USB 中断优先级调到低于 SysTick,传输缓冲区强制对齐:

/* 4字节对齐的传输缓冲区,防止DMA访问出错 */ __attribute__((aligned(4))) uint8_t usb_tx_buf[512]; __attribute__((aligned(4))) uint8_t usb_rx_buf[512];

代码说明:attribute((aligned(4))) 是 GCC 和 MDK 的 AC5 都支持的写法,告诉编译器把这个数组放到 4 字节对齐的地址上。USB 控制器做 DMA 传输时要求缓冲区首地址对齐,否则会进入异常。这个问题在加了 L1 缓存的高端芯片上更严重,F107 虽然没有缓存但也可能因为总线矩阵访问对齐问题卡死。

4.3 挂载成功但 f_open 返回 FR_NO_FILESYSTEM

现象:枚举状态正常,f_mount 返回 FR_OK,但 f_open 创建或打开文件时报 FR_NO_FILESYSTEM,U 盘在电脑上看是正常可用的。

原因:大概率是 U 盘格式化成了 exFAT,而工程里集成的 FatFs 是旧版本,只支持 FAT12/16/32。还有一种情况是 U 盘存在多个分区,MSC 驱动默认访问 LUN 0,而 LUN 0 恰好是个没有文件系统的隐藏分区。

解决:先用电脑确认 U 盘分区格式。见到 exFAT,要么换成支持 exFAT 功能的 FatFs 版本,要么直接重新格式化成 FAT32,分配单元大小选 32KB。对于多分区 U 盘,在上层读 SCSI READ_CAPACITY 命令返回的 LUN 数量,并在 FatFs 挂载前确认当前 LUN 是否有可识别的引导扇区。我的习惯是产线用的 U 盘全部格式化为 FAT32,不折腾 exFAT,省掉一个变量。

4.4 热插拔:第一次读写正常,拔下再插入就挂载失败

现象:U 盘第一次插入读写都正常,拔掉后重新插入,枚举状态正常,但 f_mount 返回 FR_NOT_READY 或 FR_NO_FILESYSTEM,必须重启才能恢复。

原因:拔出 U 盘后,FatFs 的工作区里还缓存着旧盘的 FAT 表数据,卷对象没有被释放。USB Host 栈检测到断开时只做了底层清理,应用层的文件系统状态没人管,第二次插入新盘后,FatFs 拿着旧缓存去访问新盘,自然失败。

解决:在 USBH_UserProcess 回调里捕获断连事件,主动卸载文件系统:

void USBH_UserProcess(USBH_HandleTypeDef *phost, uint8_t id) { if (id == HOST_USER_DISCONNECTION) { /* 拔盘时主动卸载卷,释放 FatFs 工作区 */ f_mount(NULL, "USB:", 1); } }

代码说明:f_mount 第一个参数传 NULL 表示卸载卷,把之前缓存的工作区释放掉。这样第二次插入时,重新走 f_mount 挂载的就是干净状态。注意这个回调运行在中断上下文,FatFs 操作本身耗时,我一般只在这里置一个卸载标志,真正执行 f_mount 放在主循环里,避免中断里做重活导致切换卡顿。

5. 进阶:让 USB HOST 读写稳定度再上一个台阶的三个工程习惯

5.1 把 USB 生命周期收敛进一个独立状态机

不要在主循环里散落地判断什么时候该挂载、什么时候该卸载,我在量产工程里都是单独写一个 usb_app.c,把 USB 生命周期收敛成四个状态:USB_STATE_IDLE、USB_STATE_MOUNT、USB_STATE_READY、USB_STATE_EJECT。主循环只做状态机推进,每个状态对应一个函数,进入 READY 才开放文件操作接口,检测到拔出立刻回到 IDLE。这样业务代码不直接碰 FatFs 函数,出了问题也容易定位。资源包里是基于这个思路组织的,你可以拿着工程直接在这个框架上加业务。

5.2 写入过程强制防掉电

U 盘掉电损坏文件系统是产线里最头疼的故障。我后来养成的习惯是所有写操作遵循三条规则:数据先拼满一个扇区整数倍再写;写关键配置后立即 f_sync;写整个文件结束后主动 f_close。不要依赖 f_close 自动落盘,U 盘内部的写缓存不一定马上刷到闪存。对 107 这种跑裸机的场景,没有操作系统帮你做缓存管理,这些习惯是必须的,否则十次量产里总有一两次把 U 盘写坏。

5.3 一个比串口下载省心得多的 U 盘固件升级技巧

资源包里附带的固件升级 Demo 思路很实用:把编译好的 bin 文件放到 U 盘特定目录,上电后读取到 RAM,校验 CRC,校验通过跳转到 APP 执行。全速 USB 实际读速度能到 1MB/s 左右,128KB 固件几秒就完成,比串口下载有优势得多。做法是 bootloader 里加一段 FatFs 挂载逻辑,只读 U 盘里 update.bin 这一个文件,读完校验后清掉标志位。我做了这个功能后,产线不再需要连调试器,效率提升明显。

从那以后我每次做 USB HOST 工程,都会强制自己按这个顺序走一遍:先量 VBUS 波形,再确认 48MHz 时钟,然后才写应用层代码。这套检查流程帮我避开了绝大多数莫名其妙的问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询