ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F107 USB Host读U盘:MSC/BOT协议与FatFs完整指南

STM32F107 USB Host读U盘:MSC/BOT协议与FatFs完整指南 简介STM32F107 USB 主机读取 U 盘的工程源码包面向熟悉单片机基础、希望在嵌入式方向进阶的开发者也适合需要在物联网设备、数据采集或智能控制中实现 U 盘数据交换的工程师参考。项目基于 STM32F107 内置 USB OTG 控制器覆盖 USB 总线结构、设备枚举、四种传输类型等协议要点代码体现了从硬件寄存器配置、USB 驱动层、设备管理层到用户应用程序层的完整 USB 主机软件栈并包含批量传输读写 U 盘的实现。压缩包内共 416 个文件以 C 源文件与头文件为源码主体另有 IAR/Keil 工程文件、编译生成的 axf/map/out 中间文件、少量文档与脚本资源整体约 10.49MB目录结构便于对照源码理解开发流程。已有 284 人学习下载阅读代码可掌握 USB Host 枚举流程、批量读写和异常处理思路对提升嵌入式底层驱动与固件开发能力有直接帮助。1. STM32F107 的 USB Host 读 U 盘先把四层栈摆清楚场景很常见一台设备要离线升级固件、导出运行日志最省事的交互就是插 U 盘。STM32F107 自带 OTG_FS 控制器和片内 PHYBOM 上只加一个 Type-A 座子和 5V 电源就能当主机跑是 F1 系列里少数能原生读 U 盘的片子。但读 U 盘从来不是插上就认USB 协议枚举、MSC 类、BOT 传输、FATFS这四层任何一层出问题现象都长得差不多——f_open 失败、读回乱码或者干脆没反应。这篇文章按协议栈 → 最小工程 → 磁盘对接 → 现场验证把这条路完整走一遍用的是 ST 官方 Host 库加 FatFs 的常规方案新手能照着搭熟手可以当兼容性排查清单用。2. USB Host 协议栈与 MSC 类枚举、BOT 和 CSW 怎么把 U 盘变成块设备2.1 F107 的 OTG 控制器在 Host 模式下管什么STM32F107 内置的 OTG_FS 是 Synopsys OTG 核收发器也在片内不用外挂 USB3300 这类 ULPI 芯片速度上限是 Full Speed 12Mbps。Host 模式下控制器负责往总线上发复位信号、SOF 令牌并通过内部 DMA 把端点数据搬到共享 RAM。应用代码几乎不碰寄存器操作对象是 Host 通道Host Channel每个通道对应一个端点一次读写就是往对应通道提交传输请求完成后再由中断回调通知协议栈。有一点先划清楚F107 的 host 不是 EHCI/XHCI完全不参与 High Speed 握手。高速 U 盘插进来host 不发 chirp设备主控会自动降级到 Full Speed 运行绝大多数盘没问题少数盘降级后对 INQUIRY、READ CAPACITY 响应慢甚至枚举期间反复复位这是后面兼容性清单里的重点怀疑对象。如果产品确实要高速思路不是给 F107 超频而是换成带 ULPI 接口的 F4/H7 或带 HS 控制器的国产型号。枚举阶段所有描述符交换都走端点 0 的控制传输F107 上控制端点最大包长按 64 字节处理这些细节在抓包时能看到但应用层不用管。真正要操心的是把 USBH_Process 的调用稳定住它的调用间隔直接决定枚举超时的基准。2.2 从控制传输到 BOTU 盘要过三个状态机U 盘是 MSCMass Storage Class设备99% 走 Bulk-Only TransportBOT子类。要读到扇区依次过三个状态机枚举状态机地址分配、读设备描述符、读配置描述符、SetConfiguration这是 USBH_Process 里一串 HOST_DEV_* 状态干的事。MSC 类状态机配置完成后发起 INQUIRY、TEST UNIT READY、READ CAPACITY(10)确认 LUN 就绪并拿到容量。文件系统状态机FATFS 把扇区解释成文件和目录。BOT 的每次命令都是固定三拍31 字节 CBW 命令块、数据阶段、13 字节 CSW 状态块。数据方向由 bmCBWFlags 决定CSW 的 0x53425355 签名和 bCSWStatus 才是命令成败的最终判定。看库代码时盯住 CSW就能判断是命令没发出去还是设备明确回了失败。/* 移植时可对照 usbh_msc.h 里的结构抓包时按这个布局核对字节序 */ typedef struct { uint32_t dCBWSignature; /* 固定 0x43425355小端 */ uint32_t dCBWTag; /* 每次命令递增的标签CSW 原样带回 */ uint32_t dCBWDataTransferLength; /* 数据阶段字节数READ CAPACITY 就是 8 */ uint8_t bmCBWFlags; /* 0x80设备到主机(读)0x00主机到设备(写) */ uint8_t bCBWLUN; /* 逻辑单元号U 盘固定 0 */ uint8_t bCBWCBLength; /* 有效 CDB 长度READ(10) 为 10 */ uint8_t CBWCB[16]; /* 内层是 SCSI 命令描述块 */ } MSC_CBW;dCBWTag 由库内部自增应用层不用维护排错真正要看的是 bmCBWFlags 和 CBWCB[0] 的 opcode。如果发现 host 发出去的 READ(10) 和同一块盘在 PC 上的表现不一致先怀疑字节序和包长配置而不是驱动逻辑。MSC 类实际用到的 SCSI 命令很少读盘场景就四五个CDB opcode命令数据方向作用0x00TEST UNIT READY无数据轮询 LUN 是否就绪0x12INQUIRYIN返回厂商/产品字符串0x25READ CAPACITY(10)IN返回最后 LBA 和块长决定容量0x28READ(10)IN真正读扇区f_read 的底层实现四条命令分别落在 usbh_msc.c 的 USBH_MSC_TestUnitReady、USBH_MSC_Inquiry、USBH_MSC_ReadCapacity10、USBH_MSC_Read10 里。调试 U 盘不认时第一步就是把日志打到卡在哪个命令的粒度而不是盯着 FatFs 的返回值猜。顺便说一句老掉牙的 CBI/UFI 命令集是给软驱时代留的现在的库只做 BOT碰到只能跑 CBI 的古董盘直接放弃别浪费精力。2.3 ST 官方 Host 库的目录与回调框架新工程用 STM32CubeF1 的 Middlewares/ST/STM32_USB_Host_Library老工程是 STM32_USB-Host-Device_Lib_V2.x两代 API 签名不完全一致但目录逻辑一脉相承usbh_core.cUSBH_Init / USBH_Process / USBH_DeInit整套状态机入口usbh_ctlreq.c标准设备请求与控制传输usbh_ioreq.c批量/中断传输的提交与完成回调usbh_hcd.cOTG 底层寄存器与通道调度Class/usbh_msc.cMSC 类状态机对外提供 USBH_MSC_Read / USBH_MSC_WriteApp/usbh_usr.c用户回调日志出口 USBH_UsrLog / USBH_ErrLog这个库是超级循环驱动型设计所有状态靠每次调用 USBH_Process 往前推。裸机或 RTOS 里开一个任务都行但有一条纪律USBH_Process 的调用要高频且稳定两次调用之间不要长时间关中断或跑耗时算法否则枚举超时会被误伤。常规做法是在主循环里每 1~2ms 调一次或者放到 1ms 定时任务里f_open、f_read 这些耗时的文件操作放在循环的低频段绝对不要在中断回调里做文件系统调用。提示USBH_UsrLog 和 USBH_ErrLog 的默认实现经常是空函数移植第一步就应该把两个都接到调试串口后面能省掉大量盲猜时间。3. 最小工程硬件连线、usbh_conf 参数与 USBH_Process 调度3.1 硬件上最容易翻车的三个点VBUS 供电、ID 引脚与 USB-C 的 CCF107 的 OTG_FS 引脚是固定的PA9VBUSPA10IDPA11DMPA12DP。做 Host 时VBUS 必须由板子主动供 5V不能指望 U 盘回灌OTG 核内部比较器和库的会话检测会读 PA9 电平。常规做法是用 5V 升压或 DC-DC 单独给 U 盘供电电流余量按 500mA 设计插盘瞬间的浪涌靠一颗 100~220uF 电容压住。直接从 3.3V LDO 棵升上去、或者共用系统 5V 却不做限流经常导致枚举到一半电压跌落、设备反复复位。第二个点是 ID 引脚。库初始化用强制主机模式USB_OTG_FORCE_HOST 或 hcd 配置里指定 HOST能兜住大部分问题但最稳妥的做法还是在板上把 PA10 拉低或直接接地避免 OTG 会话机制把总线挂进挂起态。DP/DM 差分对尽量短且等长各串一颗 22R 电阻Type-A 座子外壳接 GND——这些是老生常谈U 盘兼容性差时第一个要复查的就是走线。第三个点专门写给用 USB-C 座子的工程。USB-C 的 CC 引脚语义和 Type-A 完全不同作为设备UFP要放 5.1k 下拉让主机识别。很多人板子上焊了 Type-C 座、也跟着放了 5.1k插上 U 盘自然不认——因为你把主机做成了设备。Host 侧DFP的常规做法是 CC1/CC2 各挂一颗 56k 上拉到 5V或者干脆换 Type-A 母座规避整个协商流程。简单说把 5.1k 下拉去掉并不等于切换成主机模式CC 上的 Rp/Rd 方向也要跟着翻过来。3.2 usbh_conf 与 hcd 配置四个必须设对的参数Cube 模板把 Host 配置放在 usb_host.c老库在 usbh_conf.h。真正影响 U 盘读取的其实就几个值配置项含义F107 上典型值USBH_MAX_EP_NUM句柄预留的端点描述符数量2~4够用即可改大费 RAMUSBH_MAX_INTERFACES_NUM支持的接口数1U 盘只有一个 MSC 接口USBH_MAX_ALT_SETTING每个接口的备选设置数1USBH_MSC_FS_MAX_PACKET批量端点最大包长64Full Speed 上限从老库往 Cube 迁移时注意数据结构从 USB_OTG_CORE_HANDLE 换成了 USBH_HandleTypeDef但 USBH_MSC_Read(host, lun, sector, buf, count) 的语义不变。还有一类 RAM 项是底层端点 FIFO 大小CubeMX 生成后不要再手动改小否则连续读扇区时会丢数据表现是 f_read 偶发返回错误而不是稳定失败这类问题最难定位。NVIC 这边有两路中断必须开OTG_FS 全局中断和 OTG_FS 唤醒中断。优先级建议放在 1~2 级别抢 SysTick也不要被更高级的中断长时间打断——USB 中断处理里完成的只是把收到的数据搬进缓冲并更新通道状态不会做文件系统操作所以保持精简即可。3.3 主循环调度USBH_Process 轮询、用户回调与挂载时机下面给一份能跑通的最小骨架函数名按 STM32CubeF1 的 usb_host.c 模板对应填即可#include usbh_core.h #include usbh_msc.h #include ff.h static USBH_HandleTypeDef hUsbHostFS; static FATFS fs; static volatile uint8_t app_ready 0; /* 枚举加类初始化完成后置 1 */ static uint8_t fs_ok 0; int main(void) { /* 1. 时钟、GPIO、调试串口初始化printf 重定向到 usb转ttl 调试器 */ /* 2. 拉高 VBUS 使能脚等 5V 稳定再初始化 USB */ vbus_en(1); HAL_Delay(50); /* 3. Init 与 Class 注册由 CubeMX 生成的 MX_USB_HOST_Init 完成 */ MX_USB_HOST_Init(); for (;;) { /* 4. 高频轮询协议栈状态机靠它推进1~2ms 一次是安全区间 */ USBH_Process(hUsbHostFS); /* 5. 枚举完成后才有 U 盘卷挂载一次即可 */ if (app_ready !fs_ok) { if (f_mount(fs, 0:, 1) FR_OK) fs_ok 1; else app_ready 0; /* 让用户回调的重置逻辑生效等待换盘 */ } if (fs_ok) do_usb_file_task(); /* f_open / f_read / f_close放低频段 */ } }USBH_Process 的调用周期决定了枚举超时的基准主循环里任何超过几十毫秒的阻塞操作Flash 擦写、长延时、阻塞式打印都要绕开它否则设备会在地址分配那一步反复超时。app_ready 标志放在用户回调里置位USBH_USER_CLASS_ACTIVE 时置 1USBH_USER_DEVICE_DISCONNECTED 时清 0这是判断U 盘真正可用最可靠的方式。f_mount 的第三个参数 1 表示立即挂载传 0 表示只登记卷号等到第一次 f_open 才真正读扇区。这里的 mounted 检查之所以要等 app_ready 而不是看 USBH_MSC 的某个状态是因为不同库版本暴露的内部字段不一样用户回调是跨版本最稳定的信号。换盘场景还要注意拔盘后 protocol 栈会把磁盘标成 DISCONNECTEDapp_ready 清零但 FatFs 还持有旧卷缓存正确做法是先 f_mount(NULL, 0:, 0) 卸载再等 app_ready 重新置位后挂新盘。注意f_mount 挂载失败不要原地反复重试先打印返回值FR_NO_FILESYSTEM、FR_NOT_READY、FR_INT_ERR再去查对应协议层。第 4 章会给这三类的排查映射。4. 对接 FATFSdisk_read 映射、容量参数与三个隐藏配置4.1 disk_ioctl 与 disk_read把块设备翻译给 FATFSFatFs 不关心底层是 Flash、SD 还是 U 盘它只认 diskio 接口。读 U 盘只需要实现四个函数disk_write 在只读场景下可以返回写保护位。USBH_MSC_Read 的入参是 host 句柄、LUN、起始块号、块数块大小恒为 512 字节函数内部会拆成发 READ(10) → 收数据 → 等 CSW多次批量传输全部成功才返回 USBH_OK。#include ff.h #include diskio.h #include usbh_msc.h extern USBH_HandleTypeDef hUsbHostFS; static DWORD disk_sectors 0; /* 由 READ CAPACITY 得到挂载前置好 */ DSTATUS disk_status(BYTE pdrv) { return RES_OK; /* 能在 f_mount 前走到这里说明 U 盘已就绪 */ } DSTATUS disk_initialize(BYTE pdrv) { return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (USBH_MSC_Read(hUsbHostFS, 0, sector, buff, count) USBH_OK) return RES_OK; return RES_ERROR; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_SIZE: *(WORD *)buff 512UL; break; case GET_SECTOR_COUNT: *(DWORD *)buff disk_sectors; break; case GET_BLOCK_SIZE: *(DWORD *)buff 1UL; break; case CTRL_SYNC: break; default: return RES_PARERR; } return RES_OK; }disk_sectors 建议在枚举完成后从 USBH_MSC_GetLUNInfo 或等价接口读一次存全局不要在每次 ioctl 里现查省一次控制传输是一回事避免 ioctl 里出现阻塞式 USB 操作是更重要的。FatFs 的 LBA_t 在 R0.14 之后是 32 位无符号U 盘容量 2TB 以内不用改配置2TB 以上需要打开 FatFs 的大扇区选项并且确保 disk_ioctl 返回的容量字段是 64 位。对齐问题是最容易踩的坑F107 的 OTG 内部 DMA 要求缓冲区按 4 字节对齐。FatFs 内部的 win[] 和目录缓冲自带对齐声明一般没事但你自己定义的 f_read 目标数组如果嵌在结构体中间或者把一个 uint8_t 数组地址强转成其他类型就会出现读回数据错位、偶发 FR_INT_ERR 的诡异现象。习惯做法是用户缓冲统一加attribute((aligned(4)))并且不要在函数栈上开大数组F107 的 SRAM 也就 32~64KB 量级栈上 512 字节的文件缓冲很容易把任务栈挤爆。4.2 ffconf.h 的读场景配置一张表说清三个边界配置宏值为什么这样设FF_FS_READONLY1编译期裁掉写路径disk_write 可留空省几百字节 RAMFF_MIN_SS / FF_MAX_SS512 / 512只支持 512 字节扇区改 4096 能吃大扇区盘但每份内部缓冲多占约 3.5KBFF_USE_LFN1不开 LFN 时中文文件名 f_open 直接返回 FR_NO_FILEFF_VOLUMES1只挂一个 LUN多分区盘取第一个有效分区FF_MULTI_PARTITION0卷 0 自动找第一个有效分区省去手写分区表解析FF_FS_EXFAT0不开 exFAT 时遇到 exFAT 盘f_mount 返回 FR_NO_FILESYSTEMFF_USE_LFN 打开后FatFs 需要额外一块转换缓冲F107 上建议用静态数组或堆不要放任务栈。FF_MULTI_PARTITION 保持 0 时卷 0 指第一个存在的分区市面上绝大多数 U 盘是 MBR 单 FAT32 分区f_mount(0:) 直挂少数没有分区表、扇区 0 直接是 FAT 引导块的 superfloppy 盘也能挂上前提是你没开多分区支持去破坏这个回退逻辑。4.3 f_mount 失败的三类现场与对应排查现象一f_mount 返回 FR_NO_FILESYSTEM。枚举和容量都成功了但卷挂不上。优先怀疑磁盘格式量产成 exFAT 的盘、或者被工具改成裸 exFAT 的盘在 FF_FS_EXFAT0 时就是这个返回值。把盘在 PC 上重新格成 FAT32、分配单元选 4096是兼容性最高的组合。其次是量产成光驱 U 盘的盘LUN0 是 USB-CDROMINQUIRY 能过、容量也能读到但真正读扇区全是未就绪——官方库默认只支持一个 LUN遇到这种盘直接放弃换普通盘测试。现象二挂载成功但读大文件到一半返回 FR_INT_ERR。优先查两件事VBUS 供电跌落长时间连续读电流不稳以及缓冲区是否 4 字节对齐。还有一种隐蔽情况USBH_Process 在文件操作期间被 f_read 内部的长循环拖慢导致底层传输超时。把 do_usb_file_task 里的单次读块数从 1 改大到 8一次读 4KB能减少传输次数、显著缓解这个问题代价是缓冲数组变大。现象三拔盘再插盘后 f_mount 直接 FR_NOT_READY。热插拔必须走完整的断开回调先把 fs_ok 清掉调用 f_mount(NULL, 0:, 0) 卸载旧卷再等 app_ready 重新置位。不要在 app_ready 复位之前重挂新盘否则 FatFs 内部还持有旧卷的扇区缓存新盘的分区表会被缓存污染。这里还关联到一个常见认知有些 U 盘带物理只读开关它只影响写、不影响读f_mount 能正常过但用 Rufus、PE 工具做过启动盘的 U 盘分区表可能是 GPT ESPFF_MULTI_PARTITION0 时 FatFs 认第一个分区如果第一个分区是个 32MB 的 ESP能挂上却读不到你想要的普通文件段。这类盘建议直接格式化后再用于设备场景。注意FR_NO_FILESYSTEM 和 FR_INVALID_DRIVE 是两个不同层级的错误。前者说明 USB 链路是通的、问题出在文件系统层后者说明驱动号或挂载参数写错了先检查 FF_VOLUMES 和 0: 的写法。5. 现场验证串口日志、USB 抓包与一张兼容性清单5.1 给 USBH_UsrLog 加时间戳把状态机变成可观测的库的日志出口是 USBH_UsrLog / USBH_ErrLog默认实现往往是空的。第一步是重写它们挂上 1ms tick 计数输出到调试串口usb转ttl 调试器就够。日志不要只在调试宏里开生产固件也保留只读开关现场问题一半靠它定位。#include stdarg.h #include stdio.h extern volatile uint32_t tick_ms; void USBH_UsrLog(const char *fmt, ...) { char buf[128]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); printf([%6lu] %s\n, (unsigned long)tick_ms, buf); }时间戳能让枚举流程变成一条可回放的序列哪一刻检测到插入、哪一刻完成地址分配、哪一刻 INQUIRY 返回。卡在哪个状态一目了然这比反复用示波器点总线高效得多。5.2 用 PC 端 USB 抓包反查 U 盘主控行为MCU 端日志显示卡在 INQUIRY 时先别急着改驱动把同一块 U 盘插到 Linux PC 上用 usbmon 抓包看它在标准主机下是否正常、走的是什么命令序列lsusb dmesg | grep -i usb | tail -20 sudo modprobe usbmon sudo wireshark -i usbmon1 # 或 tshark过滤 usb.bmRequestType 辅助分析usbmon 抓的是 PC 主机控制器的总线包能看到 INQUIRY 返回的厂商串、块大小、LUN 数。如果 PC 下同样卡顿或返回 BA逻辑单元未就绪基本可以断定是盘本身对 Full Speed 降级不友好不要再回 F107 驱动里找原因。反过来PC 下一切正常那就是 F107 侧配置或时序问题集中看供电和配置参数。5.3 一张排查 U 盘不读的兼容性清单现象优先怀疑方向插入后无任何中断和日志VBUS 没供上、DP 上拉缺失、ID 悬空被会话检测挂起地址分配后反复复位差分走线过长、供电跌落、串联电阻偏大枚举正常但 INQUIRY 超时U 盘主控 Full Speed 降级响应慢换盘验证挂载返回 FR_NO_FILESYSTEMexFAT、量产多 LUN、启动盘残留分区表f_read 偶发 FR_INT_ERR缓冲区未 4 字节对齐、VBUS 跌落、单次读块太小按供电 → 枚举日志时间戳 → PC 抓包 → FATFS 返回值的顺序排查一块 U 盘十分钟内能收敛到协议层或电源层。先确认现象归属哪一层再决定改驱动配置还是改硬件这是 USB host 调试里最值钱的动作。我一般会把这份清单和两个日志接口直接固化在工程模板里新盘第一次跑就能快速定位问题出在电源、协议还是文件系统而不是靠换盘试错来碰运气。本文还有配套的精品资源点击获取
返回列表