STM32F107 USB Host读写U盘实战:FATFS移植与避坑指南

发布时间:2026/10/16 6:57:16
STM32F107 USB Host读写U盘实战:FATFS移植与避坑指南
简介这份资源面向嵌入式开发初学者与中级工程师聚焦STM32F107微控制器在USB主机模式下的U盘读写实现解决嵌入式设备与移动存储设备进行数据交换的典型问题。压缩包共298个文件约8.18MB以C源码与头文件为主体包含USB主机协议栈、FatFS文件系统、STM32外设驱动及Keil工程配置另附少量编译产物与说明文档便于直接打开工程对照学习。目前已有354人学习下载说明该方案在同类项目中具备一定参考价值。资源完整呈现了从USB主机接口初始化、U盘枚举挂载到文件打开、读写、目录操作及错误处理的实现链路读者可据此掌握ST官方库与FatFS的配合方式理解嵌入式文件系统的移植要点并借鉴其工程组织与排错思路快速搭建自己的USB主机读写U盘应用。1. 从一份 USB HOST 压缩包说起STM32F107 读写 U 盘到底难在哪如果你手上有 STM32F107 的板子想让它读写 U 盘大概率已经翻过一圈资料了。官方库有 USB Host 例程但例程跑通和真正能稳定读写文件之间隔着 FATFS 移植、时钟配置、中断优先级、U 盘兼容性这几道坎。这份 USB HOST.zip 就是围绕 STM32F107 的 USB 主机功能整理的一套工程资源核心目标是让芯片作为 Host 去枚举 U 盘、挂载文件系统、完成文件读写。它适合两类人一是刚拿到 F107 板子、想快速验证 USB Host 通路的嵌入式新手二是已经在用标准库或 HAL 库、但被 U 盘识别不稳定、读写偶发失败折腾过的熟手。下面我按实际拆包和复现的顺序把这份资源里真正要动的东西讲清楚。2. STM32F107 的 USB Host 硬件通路为什么它和 F103 不是一回事2.1 OTG 控制器与 PHY 的差异决定了配置路径STM32F107 属于互联型产品线它的 USB 外设是一个 OTG 控制器支持 Host、Device 和 OTG 三种模式。和 F103 上那个只能做 Device 的 USB 外设不同F107 的 OTG 控制器内置了 PHY但 Host 模式下需要外部提供 5V VBUS 供电并且要通过 GPIO 控制一个电源开关或者直接拉高 VBUS。很多新手第一次接线只连了 D、D- 和 GND忘了 VBUS 这一路结果 U 盘插上去一点反应都没有枚举阶段就卡死了。这份资源里的工程默认使用 OTG_FS 模式也就是全速 12Mbps。对于 U 盘读写来说全速带宽足够FATFS 层的小文件读写不会成为瓶颈。真正需要注意的是时钟F107 的 USB 时钟必须精确配置到 48MHz通常从 PLL 分频得到。如果时钟有偏差枚举可能时好时坏表现为同一个 U 盘插拔几次才能识别一次这种玄学问题十有八九出在时钟树上。2.2 工程目录里真正要关注的几个文件解压后不要急着打开工程编译先看这几个位置。USB Host 库的底层驱动通常在STM32_USB_OTG_Driver或类似目录下里面usbh_core.c负责枚举状态机usbh_msc.c是 U 盘大容量存储类驱动usbh_msc_scsi.c处理 SCSI 命令。FATFS 相关的在FATFS目录diskio.c是衔接层把 USB MSC 的读写接口映射成 FATFS 能调用的disk_read和disk_write。我一般会先确认usbh_conf.h里的USBH_MAX_NUM_SUPPORTED_LUN和USBH_MAX_DATA_BUFFER这两个宏。前者是逻辑单元数普通 U 盘填 1 就行后者决定单次传输缓冲区大小太小会导致大文件读写频繁分包效率下降太大又吃 RAM。F107 的 RAM 是 64KB给 USB Host 栈留 4KB 到 8KB 缓冲区是比较稳妥的范围。2.3 中断优先级与 VBUS 控制的实操配置USB Host 的中断优先级不能配得太低否则枚举过程中断被其他任务打断状态机会乱。常见做法是把 OTG_FS_IRQn 的抢占优先级设为 1 或 2不要和 SysTick 冲突。VBUS 控制如果板子上有专门的电源开关芯片需要在USBH_LL_DriveVBUS里根据原理图拉对应 GPIO如果是直接供电也要确保在初始化阶段就把 VBUS 使能位打开。/* usbh_conf.h 中关键宏的典型配置 */ #define USBH_MAX_NUM_ENDPOINTS 2 #define USBH_MAX_NUM_INTERFACES 2 #define USBH_MAX_NUM_SUPPORTED_LUN 1 #define USBH_MAX_DATA_BUFFER 512 #define USBH_CFG_MAX_NUM 1 #define USBH_INT_MAX_NUM 1 /* 中断优先级配置在 main 初始化阶段调用 */ HAL_NVIC_SetPriority(OTG_FS_IRQn, 2, 0); HAL_NVIC_EnableIRQ(OTG_FS_IRQn);上面这段配置里USBH_MAX_DATA_BUFFER设为 512 字节刚好是一个扇区大小和 FATFS 的默认扇区尺寸对齐能减少一次读写里的拷贝次数。中断优先级设为 2低于 SysTick 但高于普通外设保证枚举时序不被拖慢。这些值不是绝对的如果你的工程里还有其他高频中断需要根据实际负载微调。3. 从枚举到挂载U 盘识别流程与 FATFS 移植的四个关键点3.1 USB Host 枚举状态机的实际走向上电后USB Host 库会进入HOST_IDLE状态检测到 D 拉高后开始复位、分配地址、读取设备描述符、配置描述符然后根据接口类代码匹配驱动。U 盘对应的类代码是 0x08匹配到 MSC 类后usbh_msc.c会发起 SCSI 的 INQUIRY 和 READ CAPACITY 命令拿到扇区数量和扇区大小。这一步如果失败串口通常会打印USBH_MSC_DEV_NOT_SUPPORTED或者卡在HOST_ENUMERATION状态。我习惯在USBH_UserProcess回调里加串口打印把USBH_ApplicationState的变化打出来。这样一旦枚举失败能快速判断是卡在复位阶段还是描述符读取阶段。如果是复位阶段就失败多半是硬件接线或 VBUS 问题如果是描述符读取失败可能是 U 盘本身兼容性差换一个品牌试试往往就过了。3.2 FATFS 的 diskio.c 衔接层怎么写FATFS 不直接认识 USB MSC中间要靠diskio.c里的四个函数桥接disk_initialize、disk_status、disk_read、disk_write。这份资源里的实现思路是在disk_initialize里等待 USB Host 状态变为HOST_CLASS且 MSC 就绪然后返回 0 表示成功。disk_read和disk_write则调用USBH_MSC_Read和USBH_MSC_Write传入扇区号、缓冲区指针和扇区数量。/* diskio.c 中 disk_read 的典型实现 */ DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (USBH_MSC_UnitIsReady(hUsbHostFS, 0) ! USBH_OK) return RES_NOTRDY; /* USBH_MSC_Read 的扇区参数是 DWORDcount 是 UINT */ if (USBH_MSC_Read(hUsbHostFS, 0, sector, buff, count) ! USBH_OK) return RES_ERROR; return RES_OK; }这里有个容易翻车的点USBH_MSC_Read的返回值判断。有些版本的库在部分成功时也返回USBH_OK但实际读出的数据不完整。稳妥做法是在调用后检查USBH_MSC_GetLastError或者在 FATFS 层开启_FS_NOFSINFO并配合校验。另外disk_write里要注意U 盘写操作比读慢很多如果超时时间设得太短大文件写入会报FR_DISK_ERR。3.3 扇区大小与簇大小的匹配问题FATFS 默认扇区大小是 512 字节绝大多数 U 盘也是 512 字节扇区但少数大容量 U 盘可能报告 4096 字节扇区。如果disk_initialize里拿到的扇区大小和ffconf.h里_MAX_SS定义不一致挂载会直接失败。常见做法是把_MAX_SS设为 4096让 FATFS 支持最大 4KB 扇区代价是每个 FATFS 对象多占一点 RAM。簇大小则影响读写效率。FATFS 在格式化或挂载时会读取 BPB 里的簇大小如果簇太小大文件会产生大量碎片读写速度下降簇太大小文件浪费空间。对于 F107 这种 RAM 有限的平台我一般建议 U 盘格式化成 FAT32簇大小 4KB 或 8KB兼顾效率和空间。3.4 挂载测试与文件读写的最小验证代码挂载成功后先别急着写复杂逻辑用最小代码验证f_open、f_write、f_read、f_close这条链路。下面这段代码在main里 USB Host 就绪后调用作用是创建一个测试文件、写入字符串、再读回来比对。FATFS fs; FIL fil; UINT bw, br; char wbuf[] STM32F107 USB HOST TEST; char rbuf[32] {0}; /* 挂载文件系统1 表示立即挂载 */ if (f_mount(fs, 0:, 1) ! FR_OK) { printf(mount fail\r\n); return; } /* 创建并写入 */ if (f_open(fil, 0:test.txt, FA_CREATE_ALWAYS | FA_WRITE) FR_OK) { f_write(fil, wbuf, sizeof(wbuf), bw); f_close(fil); printf(write %d bytes\r\n, bw); } /* 重新打开读取 */ if (f_open(fil, 0:test.txt, FA_READ) FR_OK) { f_read(fil, rbuf, sizeof(rbuf), br); f_close(fil); printf(read: %s\r\n, rbuf); }这段代码里f_mount的第三个参数为 1 表示立即挂载如果返回FR_OK说明 FATFS 已经能通过 USB MSC 访问 U 盘。写入后必须f_close否则 FAT 表不会更新拔盘后文件可能丢失。读取时缓冲区要足够大并且注意字符串结尾实际项目中建议用memcmp比对而不是直接打印。4. 避坑与排查U 盘识别不稳定、读写失败的五个血泪经验4.1 现象U 盘插上后串口只打印一次枚举开始之后无反应原因通常是 VBUS 没有真正输出 5V或者 D 上拉电阻缺失。F107 的 OTG_FS 在 Host 模式下需要外部提供 VBUS如果板子上没有电源开关芯片必须确保 VBUS 引脚有 5V。解决方法是先用万用表量 U 盘座子的 VBUS 脚确认有 5V如果没有检查原理图上 VBUS 是直接接 5V 还是经过 MOS 管控制后者需要在代码里拉高对应 GPIO。4.2 现象同一个 U 盘有时候能识别有时候识别不了这种玄学问题多半出在 USB 时钟精度上。F107 的 USB 时钟要求 48MHz误差超过 0.25% 就可能导致枚举失败。检查SystemInit里的 PLL 配置确认 USB 预分频系数正确。另外如果板子上用的是外部晶振晶振负载电容不匹配也会导致时钟偏差。我遇到过一批板子晶振负载电容焊错换了电容之后 U 盘识别率从五成提升到几乎百分百。4.3 现象挂载成功但写文件时返回 FR_DISK_ERR先看disk_write里的超时设置。U 盘的写操作尤其是廉价 U 盘单次写扇区可能耗时几十毫秒。如果 USB Host 库里的传输超时设得太短写操作会被中断。常见做法是把USBH_MSC_Write里的超时计数加大或者在 FATFS 的ffconf.h里把_FS_TIMEOUT调大。另外写之前确认 U 盘没有写保护开关有些 U 盘侧面有个物理开关拨到只读位置也会导致写失败。4.4 现象读大文件时数据错乱小文件正常这通常是USBH_MAX_DATA_BUFFER太小导致一次读操作被拆成多次传输而中间没有正确处理分包边界。把USBH_MAX_DATA_BUFFER调到 512 的整数倍比如 1024 或 2048同时确保disk_read里传入的count和缓冲区大小匹配。如果 RAM 紧张至少保证缓冲区是扇区大小的整数倍避免跨扇区时数据错位。4.5 现象热插拔后第二次识别失败USB Host 库在 U 盘拔出后需要重新初始化状态机。如果代码里没有处理USBH_DEV_DISCONNECTED事件状态机会停在错误状态第二次插入无法重新枚举。解决方法是在USBH_UserProcess回调里检测到断开事件后调用USBH_ReInit或者手动把hUsbHostFS.gState重置为HOST_IDLE。另外FATFS 在 U 盘拔出前要先f_mount(NULL, 0:, 0)卸载否则缓存里的 FAT 数据会残留下次挂载可能读到旧数据。5. 进阶技巧用 DMA 和双缓冲把 U 盘读写速度再提一档5.1 为什么中断传输模式下 CPU 占用率高默认的 USB Host 库使用中断方式搬运数据每次传输完成触发中断CPU 在中断里拷贝数据。对于小文件读写这种方式够用但连续读写大文件时CPU 频繁进出中断主循环几乎被拖死。F107 的 OTG 控制器支持 DMA可以把数据搬运从 CPU 手里接过来。配置 DMA 的关键是让 OTG_FS 的 RX FIFO 和 TX FIFO 与 DMA 通道关联然后在USBH_LL_Read和USBH_LL_Write里改用 DMA 传输。5.2 双缓冲在 FATFS 层的落地方式FATFS 本身不直接支持双缓冲但可以在disk_read和disk_write外面包一层。思路是开两个扇区大小的缓冲区一个用于当前读写另一个用于预取或回写。读的时候当 FATFS 请求扇区 N同时启动扇区 N1 的预读写的时候先写缓冲区 A写满后切换到缓冲区 B同时把 A 的内容刷到 U 盘。这样能把 U 盘的等待时间和 CPU 处理时间重叠起来。/* 双缓冲读的简化示意 */ static BYTE buf_a[512], buf_b[512]; static BYTE *cur buf_a, *next buf_b; DRESULT disk_read_prefetch(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { /* 先返回当前缓冲区数据 */ memcpy(buff, cur, 512); /* 启动下一扇区预读不阻塞 */ USBH_MSC_Read_Async(hUsbHostFS, 0, sector 1, next, 1); /* 交换缓冲区指针 */ BYTE *tmp cur; cur next; next tmp; return RES_OK; }这段示意代码省略了异步完成判断和错误处理实际用的时候要配合 DMA 传输完成中断在中断里切换缓冲区状态。双缓冲的收益在连续大文件读写时最明显我实测过把缓冲区从单 512 字节改成双 512 字节后写一个 10MB 文件的耗时大概能降两到三成。但代价是 RAM 占用翻倍F107 的 64KB RAM 要精打细算如果工程里还有网络协议栈或者图形库就得权衡。5.3 验证读写稳定性的一个笨办法但有效调完 DMA 和双缓冲后别只看一次读写成功就收工。我一般会写一个循环测试创建 100 个 1KB 的小文件再创建 5 个 1MB 的大文件然后全部读出来做 CRC 校验反复跑 50 轮。这个过程中如果出现任何一次FR_DISK_ERR或者 CRC 不匹配就说明还有隐藏问题。这个笨办法帮我抓到过好几次偶发的 DMA 传输未完成就切换缓冲区的问题。从那以后我每次改完 USB Host 的底层配置都强制走一遍这个循环测试确认稳定了才敢往产品上搬。希望这份拆解能帮到你少走一点我当年走过的弯路。本文还有配套的精品资源点击获取