简介:基于STM32F107的USB主机读写U盘完整工程包,面向嵌入式开发者和相关课程设计/毕业设计人员,帮助解决在Cortex-M3平台通过USB主机模式挂载U盘并读写文件的实际问题。工程以ST官方固件库与FatFS文件系统为两条主线,覆盖HAL/LL库初始化、USB主机枚举、Mass Storage类设备识别、U盘挂载、文件打开/关闭/读写、目录创建删除与列举、错误处理等完整流程,适合系统学习USB协议栈和FAT文件系统移植。压缩包约8.18MB,共298个文件,以h头文件与c源码为主体,另有o目标文件、hex/axf烧录镜像、uvprojx工程配置、map映射文件及lst列表,可从中查看驱动配置、编译链接产物和工程构建细节。已有354人浏览学习。通过研读USB主机配置和FatFS适配代码,能够快速掌握STM32F107的USB主机开发思路,并复用到数据采集、固件升级等需要U盘交互的嵌入式项目中。1. 嵌入式调试痛点:STM32F107 当 USB HOST 直接读写 U 盘现场调试嵌入式设备时,最怕的不是代码写错,而是“程序跑起来了,但数据拿不出来”。前几年做某野外采集设备时,设备挂在机柜里,没有网线、没有串口调试台,上位机软件和数据文件都拷不进设备,每次更新都得把整机拆回来。后来我把方案改成让 STM32F107 自带的 USB OTG 外设跑 HOST 模式,设备插上 U 盘就能读写文件——现场拷配置、导出日志、升级程序,一个 U 盘全搞定。这份“USB HOST.zip”对应的就是一套完整的 STM32F107 USB HOST 读写 U 盘工程,核心是 USB 枚举、Mass Storage 类协议和 FatFs 文件系统三层代码。文章会从硬件电路、时钟配置讲到底层 CBW/CSW 传输、FatFs 磁盘接口,再到二次开发怎么做 U 盘升级。看完你不仅能跑通它,还能把它改成自己项目里的文件读写模块。2. 硬件与工程骨架:F107 的 OTG 外设要先想清楚用哪个口2.1 引脚分配与最小硬件电路STM32F107 和普通 F103 最大的区别,就是它多了一组 USB OTG 外设,而且这组外设内置了 PHY(物理层收发器),也就是不需要外挂 USB3300 之类的收发芯片,直接拿引脚连 USB 座子就能干活。F107 其实有两套 USB:OTG_FS 和 OTG_HS。FS 内置 PHY,HS 不带 PHY,必须外接 ULPI 接口的高速收发芯片。绝大多数场景用 OTG_FS 就够了,USB 全速 12Mbps 对文件读写完全够用,做 HOST 读 U 盘绰绰有余。硬件上有几个引脚必须接对。OTG_FS 的 D+ 和 D- 分别复用 PA11 和 PA12,VBUS 检测脚是 PA9,ID 脚 PA10 在 HOST 模式下要下拉到地。这里特别提醒:PA9 不只是检测 VBUS,标准做法是拿一个 GPIO 控制外部 MOSFET 开关,给 U 盘座子供 5V 电源。如果直接把 VBUS 和 5V 电源短接,USB 枚举经常翻车——设备插入瞬间的浪涌电流会把供电拉垮。下表是这套工程最常见的引脚分配。引脚功能说明PA11USB_DMOTG_FS 数据负线,直接连 USB 座PA12USB_DPOTG_FS 数据正线,直接连 USB 座PA9VBUS检测外部 VBUS 电平,同时可作供电控制PA10IDHOST 模式下必须下拉到地PC8过流检测(可选)外部 MOSFET 过流信号反馈U 盘插入的识别逻辑其实是靠 VBUS 的电平变化:主控作为 HOST,先给 VBUS 上电,然后检测 D+ 上的上拉信号完成设备连接检测。PA9 这个脚如果只做电平检测,不做电源开关控制,很多廉价 U 盘会枚举失败,后面避坑章节里再细讲。2.2 时钟配置:48MHz 是 HOST 模式的生命线USB 外设的物理层要求 48MHz 时钟,而且精度必须控制在 ±0.25% 以内。F107 做 HOST 时,如果时钟偏了,枚举会时好时坏——设备描述符偶尔能读到,偶尔超时,这种玄学问题排查起来非常恶心。F107 的 OTG_FS 内核时钟来自 PLL 的 48MHz 输出,外部晶振必须是 25MHz,不能用 F103 常见的 8MHz,这是刚开局就要确认的硬件前提。void SystemClock_Config_48MHz(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 使能25MHz外部晶振 */ while (RCC_WaitForHSEStartUp() != SUCCESS); /* 等待晶振稳定 */ RCC_PLLConfig(RCC_PLLSource_HSE, 25, 1, 2); /* 25MHz /1 * 6 = 75MHz */ RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) ; RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB = 75MHz */ RCC_PCLK1Config(RCC_HCLK_Div2); /* APB1 = 37.5MHz */ RCC_PCLK2Config(RCC_HCLK_Div1); /* APB2 = 75MHz */ RCC_OTGFSCLKConfig(RCC_OTGFSCLKSource_PLLVCO_Div3); /* 150MHz / 3 = 50MHz */ }这段代码有个很关键的地方:OTG_FS 的时钟源选的是 PLLVCO 分频,而不是直接选 48MHz 输出。F107 的 PLL 输出 75MHz,VCO 实际是 150MHz,再分频得到 50MHz。官方手册上 OTG_FS 容忍 ±0.25% 偏差,50MHz 完全在范围内。5V 供电和时钟解决后,USB 枚举才能稳定。我一般会用逻辑分析仪抓 D+ 的波形,确认设备上拉信号出现,再继续调协议。时钟配不对,后面所有的枚举状态机都是在撞运气。3. 核心协议实现:USB 枚举与 Mass Storage 类的完整握手3.1 从复位到配置完成:HOST 状态机逐段拆解USB HOST 和从机最大的区别,是所有的事情都要主机主动发起。U 盘插上之后,主机要依次完成:总线复位、设置设备地址、读取设备描述符、读取配置描述符、设置配置,这五步做完才算“认识”了设备。很多人觉得 USB 协议复杂,其实这部分代码就是一个有限状态机,每个状态对应一个 USB 控制传输。控制传输的结构是固定的:SETUP 阶段发请求,DATA 阶段传数据,STATUS 阶段收握手。F107 的标准外设库提供了 HOST 模式的底层寄存器操作,但状态机要自己维护。下面是这套工程里的枚举核心代码:static USBH_Status USBH_EnumProcess(USB_OTG_CoreTypeDef *dev) { switch (state) { case ENUM_IDLE: USBH_ResetPort(dev); /* 拉低 D+ 至少 10ms */ state = ENUM_SET_ADDR; break; case ENUM_SET_ADDR: USBH_SetAddress(dev, 0x01); /* 分配地址 1 */ state = ENUM_GET_DEV_DESC; break; case ENUM_GET_DEV_DESC: USBH_Get_DevDesc(dev, 0x01, &pdev_desc); /* 读 18 字节设备描述符 */ if (pdev_desc.bMaxPacketSize0 > 0x40) state = ENUM_GET_CFG_DESC_RETRY; else state = ENUM_GET_CFG_DESC; break; case ENUM_GET_CFG_DESC: USBH_Get_CfgDesc(dev, 0x01, cfg_buf, 0xFF); /* 读完整配置描述符 */ state = ENUM_SET_CONFIG; break; case ENUM_SET_CONFIG: USBH_SetConfig(dev, 0x01, 0); /* 配置值为 1 */ state = ENUM_DONE; break; } return state; }这段逻辑里有两个容易踩的细节。第一,SetAddress 之后必须等设备完成内部处理,不能立刻发下一个请求,我一般加 2~5ms 延时,不然部分 U 盘会直接不响应。第二,配置描述符第一次读取时永远只读前 9 字节,拿到 bMaxPacketSize0 后可能要重读一次,这就是ENUM_GET_CFG_DESC_RETRY状态存在的原因。枚举期间所有传输都是控制传输,走端点 0,数据长度不能超过设备描述符里声明的最大包长(通常是 64 字节)。枚举完成后,设备已经处于 Configured 状态,但此时还不能读写文件。U 盘是 Mass Storage 类设备,还需要发 SCSI 命令确认它的逻辑单元号(LUN)和容量。这部分的代码往往被初学者忽略,结果 FatFs 挂载时报 FR_DISK_ERR,实际上是枚举完没有做 Mass Storage 初始化。3.2 CBW/CSW 传输封装:读扇区与写扇区的底层实现Mass Storage 类的 Bulk-Only Transport(BOT)协议很直白:主机发一个 CBW(Command Block Wrapper,31 字节的命令块),设备执行后返回一个 CSW(Command Status Wrapper,13 字节的状态块)。CBW 里的命令就是 SCSI 命令,比如 READ(10)、WRITE(10)、TEST UNIT READY。typedef struct { uint32_t dCBWSignature; /* 固定 0x43425355 */ uint32_t dCBWTag; /* 命令标签,每发一条命令递增 */ uint32_t dCBWTransferLength; /* 数据传输字节数 */ uint8_t bmCBWFlags; /* 0x80: 设备到主机(读), 0x00: 主机到设备(写) */ uint8_t bCBWLUN; /* 逻辑单元号,通常为 0 */ uint8_t bCBWCBLength; /* SCSI 命令长度,READ(10) 为 10 */ uint8_t CBWCB[16]; /* SCSI 命令本身 */ } CBW_TypeDef; typedef struct { uint32_t dCSWSignature; /* 固定 0x53425355 */ uint32_t dCSWTag; /* 必须与 CBW 的 tag 一致 */ uint32_t dCSWDataResidue; /* 剩余未传输字节数 */ uint8_t bCSWStatus; /* 0: 成功, 1: 失败, 2: phase error */ } CSW_TypeDef;BOT 协议里最核心的一点是批量传输的方向必须和 CBW 里的 bmCBWFlags 一致,如果 U 盘发现方向反了,会直接返回 phase error。READ(10) 命令格式上,第一个字节是操作码 0x28,第 2~5 字节是起始逻辑块地址(LBA,4 字节大端),第 7~8 字节是传输扇区数。写扇区用 WRITE(10),操作码 0x2A。要注意 SCSI 命令里所有多字节字段都是大端序,这和 STM32 的小端处理器正好相反,需要手动移位拼字节。static uint8_t BOT_Xfer(USB_OTG_CoreTypeDef *dev, uint8_t lun, uint8_t *buf, uint32_t lba, uint16_t sector_cnt, uint8_t dir) { CBW_TypeDef cbw; CSW_TypeDef csw; uint32_t len = sector_cnt * 512; memset(&cbw, 0, sizeof(cbw)); cbw.dCBWSignature = 0x43425355; cbw.dCBWTag = tag++; cbw.dCBWTransferLength = len; cbw.bmCBWFlags = (dir == READ) ? 0x80 : 0x00; cbw.bCBWLUN = lun; cbw.bCBWCBLength = 10; cbw.CBWCB[0] = (dir == READ) ? 0x28 : 0x2A; /* READ(10) / WRITE(10) */ cbw.CBWCB[2] = (uint8_t)(lba >> 24); /* LBA 大端序 */ cbw.CBWCB[3] = (uint8_t)(lba >> 16); cbw.CBWCB[4] = (uint8_t)(lba >> 8); cbw.CBWCB[5] = (uint8_t)(lba); cbw.CBWCB[7] = (uint8_t)(sector_cnt >> 8); cbw.CBWCB[8] = (uint8_t)(sector_cnt); USBH_SendCBW(dev, &cbw); /* 端点 OUT 发 CBW */ if (dir == READ) USBH_BulkReceive(dev, buf, len); /* 端点 IN 收数据 */ else USBH_BulkSend(dev, buf, len); /* 端点 OUT 发数据 */ USBH_ReceiveCSW(dev, &csw); /* 端点 IN 收状态 */ return (csw.bCSWStatus == 0) ? 0 : 1; }这个函数的参数要解释一下:lba是逻辑块地址,U 盘暴露给主机的是一堆连续的 512 字节扇区,文件系统的读写最终都会落到 LBA 上。dir决定方向,读操作先发 CBW 再收数据和 CSW,写操作反过来。实际使用中我发现一个稳妥做法是每次传输前先发 TEST UNIT READY,等 U 盘回到 ready 状态再发真正的 READ/WRITE 命令,避免刚插上或者刚写完时 U 盘还在内部处理,直接发命令被拒。4. FatFs 文件系统接入:让 U 盘变成“裸存储”4.1 磁盘 IO 层适配:disk_read 与 disk_write 的真实写法底层的扇区读写跑通了,文件系统还无法直接用 STM32 的库函数。FatFs 是一套独立于硬件的文件系统层,它通过一组磁盘接口函数(diskio.c 里的函数)来访问物理存储。我们做的事就是把上文的 BOT_Xfer 封装进 disk_read、disk_write、disk_ioctl 这三个函数里。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (BOT_Xfer(&hUSBHost, 0, buff, (uint32_t)sector, (uint16_t)count, READ) == 0) return RES_OK; return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (BOT_Xfer(&hUSBHost, 0, (uint8_t *)buff, (uint32_t)sector, (uint16_t)count, WRITE) == 0) return RES_OK; return RES_ERROR; }disk_read 的参数映射很直接:FatFs 的 sector 参数就是 LBA 地址,count 就是连续扇区数量,直接传给 BOT_Xfer。但 disk_ioctl 里有个坑必须处理:GET_SECTOR_SIZE。绝大多数 U 盘是每扇区 512 字节,但也有部分新式 U 盘用 4096 字节内部块,FatFs 默认编译配置按 512 处理,遇到 4K 扇区的盘就会挂载失败或者文件内容错乱。DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_SIZE: *(WORD *)buff = 512; /* 固定汇报 512 字节扇区 */ return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; /* 擦除块大小,U 盘没有擦除概念 */ return RES_OK; case CTRL_SYNC: return RES_OK; /* 一般无需额外处理 */ } return RES_PARERR; }这里有个取舍:如果盘实际是 4K 物理扇区,但 FatFs 按 512 逻辑扇区访问,BOT 层会把两次 512 字节访问合并成一次 4K 访问吗?不一定。更稳的做法是在 U 盘枚举后,通过 READ CAPACITY 命令获取真实扇区大小,再动态告诉 FatFs。不过这要求 FatFs 开启FF_MIN_SS和FF_MAX_SS编译选项,默认工程为了简单直接用 512,兼容市面上绝大多数盘。还有CTRL_SYNC这个命令,对应 FatFs 的 f_sync 和卸载时的 flush。U 盘这种块设备其实没有写缓存队列,返回 RES_OK 即可,但如果后续做掉电保护设计,需要在 f_sync 后发 SCSI 的 SYNCHRONIZE CACHE 命令,把 U 盘内部的写缓存真正落盘。4.2 文件操作实测:从打开到落盘的一条龙代码磁盘接口层接好后,上层写文件就幸福了。下面这段是从 U 盘读日志文件并打印到串口的简单例子,覆盖了 FatFs 最常用的挂载、打开、读取三个动作。FATFS fs; FIL file; UINT bytes_read = 0; char buffer[128]; /* 挂载 U 盘文件系统 */ f_mount(&fs, "", 1); /* 打开根目录下的 log.txt */ if (f_open(&file, "log.txt", FA_READ) == FR_OK) { while (f_read(&file, buffer, sizeof(buffer), &bytes_read) == FR_OK && bytes_read > 0) { /* 把读到的数据送串口 */ for (UINT i = 0; i < bytes_read; i++) USART_SendByte(buffer[i]); } f_close(&file); } /* 写文件示例 */ if (f_open(&file, "result.csv", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { f_printf(&file, "timestamp,value\n"); f_printf(&file, "1000,3.14\n"); f_close(&file); /* 关闭时会 flush 文件系统缓存 */ } f_mount(NULL, "", 0); /* 卸载文件系统 */几个使用要点:f_mount的挂载是懒加载,第一次调用只是注册文件系统对象,真正读目录时才会发 SCSI 命令;所以返回值 FR_OK 不代表 U 盘一定就绪。打开文件失败时,建议先用FR_GET_ERROR或直接打印返回值区分FR_NOT_READY(盘没就绪)和FR_NO_FILE(文件不存在)。写文件后必须 f_close,否则最后一块数据可能还停在 FatFs 的扇区缓存里,直接拔 U 盘就会丢数据或损坏 FAT 表。想在程序运行中随时落盘,可以用 f_sync 替代 f_close,效果一样,但不关闭文件句柄。如果碰到FR_DISK_ERR,基本可以断定是底层 BOT_Xfer 传输出问题,拿逻辑分析仪抓 CBW 和 CSW 的 tag 是否一致。常见错误是把 CSW 的 tag 当成了 CBW 的 tag,U 盘会视为状态错误拒绝下一次命令。5. 踩坑实录:F107 做 U 盘宿主最常见的四个坑5.1 枚举成功但 Read Capacity 超时现象:U 盘插上后枚举流程全部通过,设备描述符和配置描述符都能读到,但发 READ CAPACITY 命令(操作码 0x25)后一直收不到 CSW,直到超时。原因:这是我在两个不同项目上都遇到过的坑。问题出在批量端点的最大包长度上。很多 U 盘报告 bMaxPacketSize 是 64 字节,但实际批量传输时允许主机发的 IN 包小于 64 字节,而 F107 的 OTG_FS 外设在 HOST 模式下,批量端点 RX FIFO 默认配置只有 128 字节。READ CAPACITY 返回的数据是 8 字节,远小于 FIFO 深度,但数据会在 FIFO 里滞留,如果软件没有及时读走,后续 CSW 传不进来,表现为命令超时。解决:把端点 RX FIFO 大小调大,或者每次读数据前先读 OVRCUR 状态寄存器清空溢出标志。最省事的做法是初始化 USB 时直接给批量端点分配 512 字节 FIFO,保证任何情况下 CSW 都有地方放。5.2 读小文件正常,写大文件中途卡死现象:读写几十字节的小配置没问题,但写一个几百 KB 的日志时,写到一半主机卡死,U 盘指示灯不闪,复位后才恢复。原因:这是 FAT 表更新频率导致的。FatFs 写文件时,每写一个簇都可能更新 FAT 表,而 FAT 表所在扇区和数据扇区往往不连续,需要主机反复在“读 FAT 扇区、改 FAT 扇区、写 FAT 扇区”之间切换。某些 U 盘的主控对连续写命令之间的间隔非常敏感,如果 MCU 处理 USB 中断不及时,两次批量写之间间隔超过 U 盘内部超时(通常是 1 秒),U 盘会主动终止传输。F107 的中断优先级里,USB 中断必须设为最高优先级,否则被其他中断打断超过几百微秒就可能翻车。解决:把 USB OTG 全局中断优先级提到最高,并关闭无关中断对 USB 服务函数的干扰。同时可以在f_write循环里每写 64KB 就调一次 f_sync,把 FAT 更新拆散,减少单次命令卡顿的长度。5.3 插上 U 盘瞬间单片机直接复位现象:U 盘插入瞬间,整板复位,或者 USB 枚举失败,必需重新上电才能恢复。原因:U 盘插入瞬间,USB 座子的 VBUS 和 D+/D- 引脚会有一个很大的寄生电容充电过程,电流尖峰可能达到几百毫安。如果 VBUS 直接由板上的 3.3V LDO 供电,压降会把 MCU 核心电压拉低到复位阈值以下。另一个因素是 5V 电源轨上的电解电容容量不够,无法吸收浪涌。解决:用 MOSFET 开关控制 VBUS,软件检测 PA9 变为高电平后再延迟 20ms 打开 MOSFET,让 U 盘缓慢上电。同时在 D+/D- 上串 22Ω 电阻,可以显著降低信号线上的寄生电容效应。这个方法我在后来几个项目里都直接内化为标准电路了,低成本且极其有效。5.4 一个读卡器插进去,文件系统挂载失败现象:用多合一读卡器插 SD 卡,枚举成功但 f_mount 返回 FR_DISK_ERR,换直插式 U 盘却一切正常。原因:读卡器是多 LUN 设备,内部有多个逻辑单元,每个 LUN 对应一个卡槽。主机发的 CBW 里 bCBWLUN 字段如果始终填 0,读卡器会直接返回失败。而直插 U 盘只有 LUN 0,所以从来不会出问题。F107 的 HOST 库初始化 Mass Storage 类时调用了 Get Max LUN 命令(操作码 0xFE),但返回的 LUN 数值没有实际用于后续 SCSI 命令。解决:在枚举阶段把 GET MAX LUN 返回的数据存下来,后续所有 CBW 构造时把 bCBWLUN 字段从写死的 0 改为该值。多 LUN 设备还需要逐个 LUN 发 INQUIRY 命令判断哪个槽位插了卡,一般第一个非空 LUN 就是可用的。6. 二次开发:从“读目录”升级到“U 盘引导升级”6.1 场景设计与文件分区约定把 U 盘读写跑通之后,最值钱的改造方向就是 U 盘引导升级。现场维护人员不需要电脑和串口,把升级文件拷进 U 盘插到设备上,设备上电时检测到升级文件就自动写入内部 Flash。这个功能对 F107 来说完全可行,因为它的 Flash 支持 32 位半字编程,可以在应用运行的同时擦写另一块扇区(如果芯片是双 Bank)。我的做法是自定义一个简单的升级包格式:文件头 16 字节,前 4 字节是魔数 0x555AAAA5,接下来是固件版本号、固件长度、校验和。这样 Bootloader 读到文件后先校验魔数和长度,再逐个扇区读取并写入内部 Flash。文件放在 U 盘根目录,命名为firmware.bin,判断方式是文件已存在且魔数合法。整个逻辑独立于 FatFs 的文件读接口,但底层还是走 disk_read 和批量传输。6.2 关键代码:按扇区抓升级包并写入 Flash下面是 Bootloader 里升级流程的核心片段,重点是 Flash 擦写和文件读取的配合:uint8_t fw_buffer[1024]; FIL fw_file; uint32_t flash_addr = APP_START_ADDR; /* 0x08008000 应用起始地址 */ uint32_t fw_size, fw_crc, read_crc = 0; if (f_open(&fw_file, "firmware.bin", FA_READ) != FR_OK) return NO_UPGRADE; f_read(&fw_file, fw_buffer, 16, &br); /* 读文件头 */ if (*(uint32_t *)&fw_buffer[0] != 0x555AAAA5) { f_close(&fw_file); return BAD_FORMAT; } fw_size = *(uint32_t *)&fw_buffer[4]; fw_crc = *(uint32_t *)&fw_buffer[8]; FLASH_Unlock(); while (fw_size > 0 && flash_addr < FLASH_END) { f_read(&fw_file, fw_buffer, 1024, &br); /* 按 1KB 读取 */ read_crc = crc32(fw_buffer, br, read_crc); /* 边读边算校验 */ if ((flash_addr & 0x7FF) == 0) /* 每 2KB 擦一次 */ FLASH_ErasePage(flash_addr); for (int i = 0; i < br; i += 2) /* 32 位半字编程 */ FLASH_ProgramHalfWord(flash_addr + i, *(uint16_t *)&fw_buffer[i]); flash_addr += br; fw_size -= br; } FLASH_Lock(); f_close(&fw_file); if (read_crc != fw_crc) /* 校验失败则回滚到旧应用 */ NVIC_SystemReset();这个流程里有两个细节值得说。第一,擦除扇区的条件是地址对齐到 2KB,这是 F107 的页大小,必须先擦后写。第二,升级过程断电会变砖,我一般会先把新固件完整写进备份区,全部校验通过后再把启动标志翻转,这次升级只写一个备份区已经足够演示。6.3 验证方法与移植建议验证升级功能时,建议先做三件事:把 U 盘格式化成 FAT32 簇大小为 4KB,拷贝一个 64KB 以上的 bin 文件,然后串口打印 Flash 写入后的 CRC 值,和 PC 端算出的 CRC 对比。Bootloader 里加上串口日志可以在升级失败时迅速定位是文件读取失败还是 Flash 编程失败。把这个功能移植到别的项目时,只要换掉f_open里的文件名和APP_START_ADDR的地址值即可,底层的 USB HOST 驱动、Mass Storage 协议、FatFs 磁盘接口都可以原样复用。从那以后,我每次做带 USB 的新板子都会强制把枚举抓包、扇区读写测试、掉电恢复测试这三步走一遍,这三步能拦住绝大部分 USB HOST 的鬼问题。希望这篇拆解对你有用,拿代码跑一遍,比看十篇文章都实在。本文还有配套的精品资源,点击获取
简介:基于STM32F107的USB主机读写U盘完整工程包,面向嵌入式开发者和相关课程设计/毕业设计人员,帮助解决在Cortex-M3平台通过USB主机模式挂载U盘并读写文件的实际问题。工程以ST官方固件库与FatFS文件系统为两条主线,覆盖HAL/LL库初始化、USB主机枚举、Mass Storage类设备识别、U盘挂载、文件打开/关闭/读写、目录创建删除与列举、错误处理等完整流程,适合系统学习USB协议栈和FAT文件系统移植。压缩包约8.18MB,共298个文件,以h头文件与c源码为主体,另有o目标文件、hex/axf烧录镜像、uvprojx工程配置、map映射文件及lst列表,可从中查看驱动配置、编译链接产物和工程构建细节。已有354人浏览学习。通过研读USB主机配置和FatFS适配代码,能够快速掌握STM32F107的USB主机开发思路,并复用到数据采集、固件升级等需要U盘交互的嵌入式项目中。1. 嵌入式调试痛点:STM32F107 当 USB HOST 直接读写 U 盘现场调试嵌入式设备时,最怕的不是代码写错,而是“程序跑起来了,但数据拿不出来”。前几年做某野外采集设备时,设备挂在机柜里,没有网线、没有串口调试台,上位机软件和数据文件都拷不进设备,每次更新都得把整机拆回来。后来我把方案改成让 STM32F107 自带的 USB OTG 外设跑 HOST 模式,设备插上 U 盘就能读写文件——现场拷配置、导出日志、升级程序,一个 U 盘全搞定。这份“USB HOST.zip”对应的就是一套完整的 STM32F107 USB HOST 读写 U 盘工程,核心是 USB 枚举、Mass Storage 类协议和 FatFs 文件系统三层代码。文章会从硬件电路、时钟配置讲到底层 CBW/CSW 传输、FatFs 磁盘接口,再到二次开发怎么做 U 盘升级。看完你不仅能跑通它,还能把它改成自己项目里的文件读写模块。2. 硬件与工程骨架:F107 的 OTG 外设要先想清楚用哪个口2.1 引脚分配与最小硬件电路STM32F107 和普通 F103 最大的区别,就是它多了一组 USB OTG 外设,而且这组外设内置了 PHY(物理层收发器),也就是不需要外挂 USB3300 之类的收发芯片,直接拿引脚连 USB 座子就能干活。F107 其实有两套 USB:OTG_FS 和 OTG_HS。FS 内置 PHY,HS 不带 PHY,必须外接 ULPI 接口的高速收发芯片。绝大多数场景用 OTG_FS 就够了,USB 全速 12Mbps 对文件读写完全够用,做 HOST 读 U 盘绰绰有余。硬件上有几个引脚必须接对。OTG_FS 的 D+ 和 D- 分别复用 PA11 和 PA12,VBUS 检测脚是 PA9,ID 脚 PA10 在 HOST 模式下要下拉到地。这里特别提醒:PA9 不只是检测 VBUS,标准做法是拿一个 GPIO 控制外部 MOSFET 开关,给 U 盘座子供 5V 电源。如果直接把 VBUS 和 5V 电源短接,USB 枚举经常翻车——设备插入瞬间的浪涌电流会把供电拉垮。下表是这套工程最常见的引脚分配。引脚功能说明PA11USB_DMOTG_FS 数据负线,直接连 USB 座PA12USB_DPOTG_FS 数据正线,直接连 USB 座PA9VBUS检测外部 VBUS 电平,同时可作供电控制PA10IDHOST 模式下必须下拉到地PC8过流检测(可选)外部 MOSFET 过流信号反馈U 盘插入的识别逻辑其实是靠 VBUS 的电平变化:主控作为 HOST,先给 VBUS 上电,然后检测 D+ 上的上拉信号完成设备连接检测。PA9 这个脚如果只做电平检测,不做电源开关控制,很多廉价 U 盘会枚举失败,后面避坑章节里再细讲。2.2 时钟配置:48MHz 是 HOST 模式的生命线USB 外设的物理层要求 48MHz 时钟,而且精度必须控制在 ±0.25% 以内。F107 做 HOST 时,如果时钟偏了,枚举会时好时坏——设备描述符偶尔能读到,偶尔超时,这种玄学问题排查起来非常恶心。F107 的 OTG_FS 内核时钟来自 PLL 的 48MHz 输出,外部晶振必须是 25MHz,不能用 F103 常见的 8MHz,这是刚开局就要确认的硬件前提。void SystemClock_Config_48MHz(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 使能25MHz外部晶振 */ while (RCC_WaitForHSEStartUp() != SUCCESS); /* 等待晶振稳定 */ RCC_PLLConfig(RCC_PLLSource_HSE, 25, 1, 2); /* 25MHz /1 * 6 = 75MHz */ RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) ; RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB = 75MHz */ RCC_PCLK1Config(RCC_HCLK_Div2); /* APB1 = 37.5MHz */ RCC_PCLK2Config(RCC_HCLK_Div1); /* APB2 = 75MHz */ RCC_OTGFSCLKConfig(RCC_OTGFSCLKSource_PLLVCO_Div3); /* 150MHz / 3 = 50MHz */ }这段代码有个很关键的地方:OTG_FS 的时钟源选的是 PLLVCO 分频,而不是直接选 48MHz 输出。F107 的 PLL 输出 75MHz,VCO 实际是 150MHz,再分频得到 50MHz。官方手册上 OTG_FS 容忍 ±0.25% 偏差,50MHz 完全在范围内。5V 供电和时钟解决后,USB 枚举才能稳定。我一般会用逻辑分析仪抓 D+ 的波形,确认设备上拉信号出现,再继续调协议。时钟配不对,后面所有的枚举状态机都是在撞运气。3. 核心协议实现:USB 枚举与 Mass Storage 类的完整握手3.1 从复位到配置完成:HOST 状态机逐段拆解USB HOST 和从机最大的区别,是所有的事情都要主机主动发起。U 盘插上之后,主机要依次完成:总线复位、设置设备地址、读取设备描述符、读取配置描述符、设置配置,这五步做完才算“认识”了设备。很多人觉得 USB 协议复杂,其实这部分代码就是一个有限状态机,每个状态对应一个 USB 控制传输。控制传输的结构是固定的:SETUP 阶段发请求,DATA 阶段传数据,STATUS 阶段收握手。F107 的标准外设库提供了 HOST 模式的底层寄存器操作,但状态机要自己维护。下面是这套工程里的枚举核心代码:static USBH_Status USBH_EnumProcess(USB_OTG_CoreTypeDef *dev) { switch (state) { case ENUM_IDLE: USBH_ResetPort(dev); /* 拉低 D+ 至少 10ms */ state = ENUM_SET_ADDR; break; case ENUM_SET_ADDR: USBH_SetAddress(dev, 0x01); /* 分配地址 1 */ state = ENUM_GET_DEV_DESC; break; case ENUM_GET_DEV_DESC: USBH_Get_DevDesc(dev, 0x01, &pdev_desc); /* 读 18 字节设备描述符 */ if (pdev_desc.bMaxPacketSize0 > 0x40) state = ENUM_GET_CFG_DESC_RETRY; else state = ENUM_GET_CFG_DESC; break; case ENUM_GET_CFG_DESC: USBH_Get_CfgDesc(dev, 0x01, cfg_buf, 0xFF); /* 读完整配置描述符 */ state = ENUM_SET_CONFIG; break; case ENUM_SET_CONFIG: USBH_SetConfig(dev, 0x01, 0); /* 配置值为 1 */ state = ENUM_DONE; break; } return state; }这段逻辑里有两个容易踩的细节。第一,SetAddress 之后必须等设备完成内部处理,不能立刻发下一个请求,我一般加 2~5ms 延时,不然部分 U 盘会直接不响应。第二,配置描述符第一次读取时永远只读前 9 字节,拿到 bMaxPacketSize0 后可能要重读一次,这就是ENUM_GET_CFG_DESC_RETRY状态存在的原因。枚举期间所有传输都是控制传输,走端点 0,数据长度不能超过设备描述符里声明的最大包长(通常是 64 字节)。枚举完成后,设备已经处于 Configured 状态,但此时还不能读写文件。U 盘是 Mass Storage 类设备,还需要发 SCSI 命令确认它的逻辑单元号(LUN)和容量。这部分的代码往往被初学者忽略,结果 FatFs 挂载时报 FR_DISK_ERR,实际上是枚举完没有做 Mass Storage 初始化。3.2 CBW/CSW 传输封装:读扇区与写扇区的底层实现Mass Storage 类的 Bulk-Only Transport(BOT)协议很直白:主机发一个 CBW(Command Block Wrapper,31 字节的命令块),设备执行后返回一个 CSW(Command Status Wrapper,13 字节的状态块)。CBW 里的命令就是 SCSI 命令,比如 READ(10)、WRITE(10)、TEST UNIT READY。typedef struct { uint32_t dCBWSignature; /* 固定 0x43425355 */ uint32_t dCBWTag; /* 命令标签,每发一条命令递增 */ uint32_t dCBWTransferLength; /* 数据传输字节数 */ uint8_t bmCBWFlags; /* 0x80: 设备到主机(读), 0x00: 主机到设备(写) */ uint8_t bCBWLUN; /* 逻辑单元号,通常为 0 */ uint8_t bCBWCBLength; /* SCSI 命令长度,READ(10) 为 10 */ uint8_t CBWCB[16]; /* SCSI 命令本身 */ } CBW_TypeDef; typedef struct { uint32_t dCSWSignature; /* 固定 0x53425355 */ uint32_t dCSWTag; /* 必须与 CBW 的 tag 一致 */ uint32_t dCSWDataResidue; /* 剩余未传输字节数 */ uint8_t bCSWStatus; /* 0: 成功, 1: 失败, 2: phase error */ } CSW_TypeDef;BOT 协议里最核心的一点是批量传输的方向必须和 CBW 里的 bmCBWFlags 一致,如果 U 盘发现方向反了,会直接返回 phase error。READ(10) 命令格式上,第一个字节是操作码 0x28,第 2~5 字节是起始逻辑块地址(LBA,4 字节大端),第 7~8 字节是传输扇区数。写扇区用 WRITE(10),操作码 0x2A。要注意 SCSI 命令里所有多字节字段都是大端序,这和 STM32 的小端处理器正好相反,需要手动移位拼字节。static uint8_t BOT_Xfer(USB_OTG_CoreTypeDef *dev, uint8_t lun, uint8_t *buf, uint32_t lba, uint16_t sector_cnt, uint8_t dir) { CBW_TypeDef cbw; CSW_TypeDef csw; uint32_t len = sector_cnt * 512; memset(&cbw, 0, sizeof(cbw)); cbw.dCBWSignature = 0x43425355; cbw.dCBWTag = tag++; cbw.dCBWTransferLength = len; cbw.bmCBWFlags = (dir == READ) ? 0x80 : 0x00; cbw.bCBWLUN = lun; cbw.bCBWCBLength = 10; cbw.CBWCB[0] = (dir == READ) ? 0x28 : 0x2A; /* READ(10) / WRITE(10) */ cbw.CBWCB[2] = (uint8_t)(lba >> 24); /* LBA 大端序 */ cbw.CBWCB[3] = (uint8_t)(lba >> 16); cbw.CBWCB[4] = (uint8_t)(lba >> 8); cbw.CBWCB[5] = (uint8_t)(lba); cbw.CBWCB[7] = (uint8_t)(sector_cnt >> 8); cbw.CBWCB[8] = (uint8_t)(sector_cnt); USBH_SendCBW(dev, &cbw); /* 端点 OUT 发 CBW */ if (dir == READ) USBH_BulkReceive(dev, buf, len); /* 端点 IN 收数据 */ else USBH_BulkSend(dev, buf, len); /* 端点 OUT 发数据 */ USBH_ReceiveCSW(dev, &csw); /* 端点 IN 收状态 */ return (csw.bCSWStatus == 0) ? 0 : 1; }这个函数的参数要解释一下:lba是逻辑块地址,U 盘暴露给主机的是一堆连续的 512 字节扇区,文件系统的读写最终都会落到 LBA 上。dir决定方向,读操作先发 CBW 再收数据和 CSW,写操作反过来。实际使用中我发现一个稳妥做法是每次传输前先发 TEST UNIT READY,等 U 盘回到 ready 状态再发真正的 READ/WRITE 命令,避免刚插上或者刚写完时 U 盘还在内部处理,直接发命令被拒。4. FatFs 文件系统接入:让 U 盘变成“裸存储”4.1 磁盘 IO 层适配:disk_read 与 disk_write 的真实写法底层的扇区读写跑通了,文件系统还无法直接用 STM32 的库函数。FatFs 是一套独立于硬件的文件系统层,它通过一组磁盘接口函数(diskio.c 里的函数)来访问物理存储。我们做的事就是把上文的 BOT_Xfer 封装进 disk_read、disk_write、disk_ioctl 这三个函数里。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (BOT_Xfer(&hUSBHost, 0, buff, (uint32_t)sector, (uint16_t)count, READ) == 0) return RES_OK; return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (BOT_Xfer(&hUSBHost, 0, (uint8_t *)buff, (uint32_t)sector, (uint16_t)count, WRITE) == 0) return RES_OK; return RES_ERROR; }disk_read 的参数映射很直接:FatFs 的 sector 参数就是 LBA 地址,count 就是连续扇区数量,直接传给 BOT_Xfer。但 disk_ioctl 里有个坑必须处理:GET_SECTOR_SIZE。绝大多数 U 盘是每扇区 512 字节,但也有部分新式 U 盘用 4096 字节内部块,FatFs 默认编译配置按 512 处理,遇到 4K 扇区的盘就会挂载失败或者文件内容错乱。DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_SIZE: *(WORD *)buff = 512; /* 固定汇报 512 字节扇区 */ return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; /* 擦除块大小,U 盘没有擦除概念 */ return RES_OK; case CTRL_SYNC: return RES_OK; /* 一般无需额外处理 */ } return RES_PARERR; }这里有个取舍:如果盘实际是 4K 物理扇区,但 FatFs 按 512 逻辑扇区访问,BOT 层会把两次 512 字节访问合并成一次 4K 访问吗?不一定。更稳的做法是在 U 盘枚举后,通过 READ CAPACITY 命令获取真实扇区大小,再动态告诉 FatFs。不过这要求 FatFs 开启FF_MIN_SS和FF_MAX_SS编译选项,默认工程为了简单直接用 512,兼容市面上绝大多数盘。还有CTRL_SYNC这个命令,对应 FatFs 的 f_sync 和卸载时的 flush。U 盘这种块设备其实没有写缓存队列,返回 RES_OK 即可,但如果后续做掉电保护设计,需要在 f_sync 后发 SCSI 的 SYNCHRONIZE CACHE 命令,把 U 盘内部的写缓存真正落盘。4.2 文件操作实测:从打开到落盘的一条龙代码磁盘接口层接好后,上层写文件就幸福了。下面这段是从 U 盘读日志文件并打印到串口的简单例子,覆盖了 FatFs 最常用的挂载、打开、读取三个动作。FATFS fs; FIL file; UINT bytes_read = 0; char buffer[128]; /* 挂载 U 盘文件系统 */ f_mount(&fs, "", 1); /* 打开根目录下的 log.txt */ if (f_open(&file, "log.txt", FA_READ) == FR_OK) { while (f_read(&file, buffer, sizeof(buffer), &bytes_read) == FR_OK && bytes_read > 0) { /* 把读到的数据送串口 */ for (UINT i = 0; i < bytes_read; i++) USART_SendByte(buffer[i]); } f_close(&file); } /* 写文件示例 */ if (f_open(&file, "result.csv", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { f_printf(&file, "timestamp,value\n"); f_printf(&file, "1000,3.14\n"); f_close(&file); /* 关闭时会 flush 文件系统缓存 */ } f_mount(NULL, "", 0); /* 卸载文件系统 */几个使用要点:f_mount的挂载是懒加载,第一次调用只是注册文件系统对象,真正读目录时才会发 SCSI 命令;所以返回值 FR_OK 不代表 U 盘一定就绪。打开文件失败时,建议先用FR_GET_ERROR或直接打印返回值区分FR_NOT_READY(盘没就绪)和FR_NO_FILE(文件不存在)。写文件后必须 f_close,否则最后一块数据可能还停在 FatFs 的扇区缓存里,直接拔 U 盘就会丢数据或损坏 FAT 表。想在程序运行中随时落盘,可以用 f_sync 替代 f_close,效果一样,但不关闭文件句柄。如果碰到FR_DISK_ERR,基本可以断定是底层 BOT_Xfer 传输出问题,拿逻辑分析仪抓 CBW 和 CSW 的 tag 是否一致。常见错误是把 CSW 的 tag 当成了 CBW 的 tag,U 盘会视为状态错误拒绝下一次命令。5. 踩坑实录:F107 做 U 盘宿主最常见的四个坑5.1 枚举成功但 Read Capacity 超时现象:U 盘插上后枚举流程全部通过,设备描述符和配置描述符都能读到,但发 READ CAPACITY 命令(操作码 0x25)后一直收不到 CSW,直到超时。原因:这是我在两个不同项目上都遇到过的坑。问题出在批量端点的最大包长度上。很多 U 盘报告 bMaxPacketSize 是 64 字节,但实际批量传输时允许主机发的 IN 包小于 64 字节,而 F107 的 OTG_FS 外设在 HOST 模式下,批量端点 RX FIFO 默认配置只有 128 字节。READ CAPACITY 返回的数据是 8 字节,远小于 FIFO 深度,但数据会在 FIFO 里滞留,如果软件没有及时读走,后续 CSW 传不进来,表现为命令超时。解决:把端点 RX FIFO 大小调大,或者每次读数据前先读 OVRCUR 状态寄存器清空溢出标志。最省事的做法是初始化 USB 时直接给批量端点分配 512 字节 FIFO,保证任何情况下 CSW 都有地方放。5.2 读小文件正常,写大文件中途卡死现象:读写几十字节的小配置没问题,但写一个几百 KB 的日志时,写到一半主机卡死,U 盘指示灯不闪,复位后才恢复。原因:这是 FAT 表更新频率导致的。FatFs 写文件时,每写一个簇都可能更新 FAT 表,而 FAT 表所在扇区和数据扇区往往不连续,需要主机反复在“读 FAT 扇区、改 FAT 扇区、写 FAT 扇区”之间切换。某些 U 盘的主控对连续写命令之间的间隔非常敏感,如果 MCU 处理 USB 中断不及时,两次批量写之间间隔超过 U 盘内部超时(通常是 1 秒),U 盘会主动终止传输。F107 的中断优先级里,USB 中断必须设为最高优先级,否则被其他中断打断超过几百微秒就可能翻车。解决:把 USB OTG 全局中断优先级提到最高,并关闭无关中断对 USB 服务函数的干扰。同时可以在f_write循环里每写 64KB 就调一次 f_sync,把 FAT 更新拆散,减少单次命令卡顿的长度。5.3 插上 U 盘瞬间单片机直接复位现象:U 盘插入瞬间,整板复位,或者 USB 枚举失败,必需重新上电才能恢复。原因:U 盘插入瞬间,USB 座子的 VBUS 和 D+/D- 引脚会有一个很大的寄生电容充电过程,电流尖峰可能达到几百毫安。如果 VBUS 直接由板上的 3.3V LDO 供电,压降会把 MCU 核心电压拉低到复位阈值以下。另一个因素是 5V 电源轨上的电解电容容量不够,无法吸收浪涌。解决:用 MOSFET 开关控制 VBUS,软件检测 PA9 变为高电平后再延迟 20ms 打开 MOSFET,让 U 盘缓慢上电。同时在 D+/D- 上串 22Ω 电阻,可以显著降低信号线上的寄生电容效应。这个方法我在后来几个项目里都直接内化为标准电路了,低成本且极其有效。5.4 一个读卡器插进去,文件系统挂载失败现象:用多合一读卡器插 SD 卡,枚举成功但 f_mount 返回 FR_DISK_ERR,换直插式 U 盘却一切正常。原因:读卡器是多 LUN 设备,内部有多个逻辑单元,每个 LUN 对应一个卡槽。主机发的 CBW 里 bCBWLUN 字段如果始终填 0,读卡器会直接返回失败。而直插 U 盘只有 LUN 0,所以从来不会出问题。F107 的 HOST 库初始化 Mass Storage 类时调用了 Get Max LUN 命令(操作码 0xFE),但返回的 LUN 数值没有实际用于后续 SCSI 命令。解决:在枚举阶段把 GET MAX LUN 返回的数据存下来,后续所有 CBW 构造时把 bCBWLUN 字段从写死的 0 改为该值。多 LUN 设备还需要逐个 LUN 发 INQUIRY 命令判断哪个槽位插了卡,一般第一个非空 LUN 就是可用的。6. 二次开发:从“读目录”升级到“U 盘引导升级”6.1 场景设计与文件分区约定把 U 盘读写跑通之后,最值钱的改造方向就是 U 盘引导升级。现场维护人员不需要电脑和串口,把升级文件拷进 U 盘插到设备上,设备上电时检测到升级文件就自动写入内部 Flash。这个功能对 F107 来说完全可行,因为它的 Flash 支持 32 位半字编程,可以在应用运行的同时擦写另一块扇区(如果芯片是双 Bank)。我的做法是自定义一个简单的升级包格式:文件头 16 字节,前 4 字节是魔数 0x555AAAA5,接下来是固件版本号、固件长度、校验和。这样 Bootloader 读到文件后先校验魔数和长度,再逐个扇区读取并写入内部 Flash。文件放在 U 盘根目录,命名为firmware.bin,判断方式是文件已存在且魔数合法。整个逻辑独立于 FatFs 的文件读接口,但底层还是走 disk_read 和批量传输。6.2 关键代码:按扇区抓升级包并写入 Flash下面是 Bootloader 里升级流程的核心片段,重点是 Flash 擦写和文件读取的配合:uint8_t fw_buffer[1024]; FIL fw_file; uint32_t flash_addr = APP_START_ADDR; /* 0x08008000 应用起始地址 */ uint32_t fw_size, fw_crc, read_crc = 0; if (f_open(&fw_file, "firmware.bin", FA_READ) != FR_OK) return NO_UPGRADE; f_read(&fw_file, fw_buffer, 16, &br); /* 读文件头 */ if (*(uint32_t *)&fw_buffer[0] != 0x555AAAA5) { f_close(&fw_file); return BAD_FORMAT; } fw_size = *(uint32_t *)&fw_buffer[4]; fw_crc = *(uint32_t *)&fw_buffer[8]; FLASH_Unlock(); while (fw_size > 0 && flash_addr < FLASH_END) { f_read(&fw_file, fw_buffer, 1024, &br); /* 按 1KB 读取 */ read_crc = crc32(fw_buffer, br, read_crc); /* 边读边算校验 */ if ((flash_addr & 0x7FF) == 0) /* 每 2KB 擦一次 */ FLASH_ErasePage(flash_addr); for (int i = 0; i < br; i += 2) /* 32 位半字编程 */ FLASH_ProgramHalfWord(flash_addr + i, *(uint16_t *)&fw_buffer[i]); flash_addr += br; fw_size -= br; } FLASH_Lock(); f_close(&fw_file); if (read_crc != fw_crc) /* 校验失败则回滚到旧应用 */ NVIC_SystemReset();这个流程里有两个细节值得说。第一,擦除扇区的条件是地址对齐到 2KB,这是 F107 的页大小,必须先擦后写。第二,升级过程断电会变砖,我一般会先把新固件完整写进备份区,全部校验通过后再把启动标志翻转,这次升级只写一个备份区已经足够演示。6.3 验证方法与移植建议验证升级功能时,建议先做三件事:把 U 盘格式化成 FAT32 簇大小为 4KB,拷贝一个 64KB 以上的 bin 文件,然后串口打印 Flash 写入后的 CRC 值,和 PC 端算出的 CRC 对比。Bootloader 里加上串口日志可以在升级失败时迅速定位是文件读取失败还是 Flash 编程失败。把这个功能移植到别的项目时,只要换掉f_open里的文件名和APP_START_ADDR的地址值即可,底层的 USB HOST 驱动、Mass Storage 协议、FatFs 磁盘接口都可以原样复用。从那以后,我每次做带 USB 的新板子都会强制把枚举抓包、扇区读写测试、掉电恢复测试这三步走一遍,这三步能拦住绝大部分 USB HOST 的鬼问题。希望这篇拆解对你有用,拿代码跑一遍,比看十篇文章都实在。本文还有配套的精品资源,点击获取