简介:这份资源面向嵌入式开发初学者与中级工程师,聚焦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 的扇区参数是 DWORD,count 是 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,同时启动扇区 N+1 的预读;写的时候,先写缓冲区 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 的底层配置,都强制走一遍这个循环测试,确认稳定了才敢往产品上搬。希望这份拆解能帮到你,少走一点我当年走过的弯路。
本文还有配套的精品资源,点击获取