免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于TinyUSB的STM32 U盘实现:从RAM Disk到SPI Flash完整教程

基于TinyUSB的STM32 U盘实现:从RAM Disk到SPI Flash完整教程 做USB开发之前我一直觉得这是嵌入式里最劝退的方向之一。协议规范几百页描述符里错一个字节可能就枚举失败调试工具又贵又难上手。直到后来用了TinyUSB我才发现实现一个标准USB设备并没有想象中那么可怕。拿最近同事问得最多的例子来聊用TinyUSB在STM32上做一个U盘。核心代码量很少环境顺的话五分钟跑通真不是吹牛踩坑点也高度集中完全有套路可循。这篇文章我基于最常用的STM32F103C8T6开发板来演示用的是内部RAM模拟U盘再进阶到外挂W25Q128 Flash实现掉电保存。无论你是做毕业设计、做数据采集存储还是想把手头的板子变成移动存储工具这套流程都可以直接照着搬。顺便会把我在实际调试中遇到的不识别、无盘符、读写报错这几个经典问题一起拆掉。1. 先说清楚为什么是TinyUSB为什么要拿STM32做U盘1.1 USB驱动开发让人崩溃的地方很多人一提到USB开发就头大这种头大不是没道理的。USB协议栈从物理层、传输层到协议层、类层层层叠叠真要自己完整实现一遍光是把《USB 2.0 Specification》啃明白就得花掉不少精力。哪怕只做最简单的设备也要操心设备描述符、配置描述符、字符串描述符怎么填端点怎么分配控制传输的状态机怎么走枚举过程中主机发来标准请求怎么回复。一套折腾下来很多人还没看到U盘盘符就先放弃了。更痛苦的还在后头设备的枚举只是第一步真正跑起来之后批量传输的Buffer怎么管理、传输错误怎么重试、Class-specific请求怎么处理这些全是经验活。就算你用STM32官方USB库它那一套回调加状态机的设计历史包袱也重配置繁琐遇到问题排查起来并不轻松。所以如果你只是想快速实现一个能用的USB设备从零写协议栈或者硬啃厂商中间件性价比都很低。1.2 TinyUSB到底解决了什么问题TinyUSB是Adafruit发起维护的开源USB协议栈现在很多主流芯片的开发环境里都在用比如RP2040的Pico SDK、ESP32-S系列底层都集成了TinyUSB。它最大的特点是不绑定某个特定芯片厂家的SDK通过一套统一的移植层适配不同硬件同时对CDC、HID、MSC、MIDI、UAC这些常见设备类做了完整支持。你不需要理解USB规范里的每个字节只需要按它约定的回调函数把数据搬进来、搬出去就行。对比一下传统的开发方式就更直观。用STM32官方USB中间件配置描述符、处理各种请求回调都要自己来而且每个系列芯片的库风格还有差异。TinyUSB则是一套代码通吃你换芯片平台应用层代码基本不用动。它还自带了不少示例工程最合适拿来当起点改。授权也很友好MIT协议商用项目用着没有顾虑。就我实际使用感受来说TinyUSB最厉害的地方是把协议栈里最复杂的枚举、标准请求、Class协议解析都封装掉了留给你的接口非常清爽。1.3 选择MSC类USB存储设备的理由在TinyUSB支持的各种设备类里MSCMass Storage Class大容量存储类是最适合练手也最实用的方向之一。它的本质是把设备模拟成一个块设备操作系统把它当成普通U盘来访问不需要额外安装驱动插入电脑就能看到盘符。开发流程相对简单因为它只需要处理SCSI命令里的那么几个关键操作查询设备、读取容量、读块、写块。这个场景的实际价值很大。嵌入式产品需要把采集到的数据导出到电脑或者需要从电脑拷贝配置文件、升级固件USB U盘方式都是很自然的交互手段。很多基于STM32的毕业设计项目比如数据记录仪、离线编程器、智能设备升级工具核心存储模块用这套方案就够了。后面我会把RAM盘和Flash盘两种做法都走一遍你按自己的需求选。2. 硬件和开发环境别在这些小事上翻车2.1 一套能跑的硬件清单硬件准备并不复杂。开发板用最常见的STM32F103C8T6最小系统板就行就是那种网上二三十块钱一块的“蓝板”或者“黑板”很多人手里都有想验证代码完全够用。如果你用江科大入门教程那块STM32F103C8T6学习板也可以直接拿来跑PA11、PA12两个引脚自带USB座省了飞线。我做这件事用到的硬件清单是这样硬件规格/型号用途STM32开发板STM32F103C8T6最小系统板主控跑USB设备调试器ST-Link V2烧录和在线调试USB线数据线不是只充电的线连接板子到电脑供电和数据SPI Flash进阶用W25Q128模块做掉电保存的U盘介质杜邦线公对母若干接Flash模块时用这里有个硬件上的关键点必须单独说STM32F103C8T6的USB D引脚PA12在外部需要接一个1.5kΩ上拉电阻到3.3V。很多开发板上已经集成了这个电阻但如果是自己做的最小系统板千万别漏掉漏了电脑永远识别不到设备。D-PA11和DPA12不能接反接反了也会枚举失败。2.2 Keil5环境芯片包没装一切都白搭开发环境我以Keil MDK5为例因为这是大多数人电脑上已经装了的工具。一个很常见的问题是Keil MDK装好了新建工程时却找不到STM32F103系列芯片原因就是没装对应的芯片包。Keil5和过去的Keil4不一样芯片支持是以后缀为.pack的芯片包形式提供的需要单独安装。具体做法是打开Keil MDK在菜单栏选Pack Installer然后在Device列表里找到STMicroelectronics - STM32F1 Series点击Install。如果Pack Installer加载慢也可以直接从Keil官网下载STM32F1系列的DFP安装包双击安装。装完之后新建工程你会发现芯片型号列表里已经能选STM32F103C8T6了。顺便提醒一句STM32F103C8T6在Keil里显示为STM32F103C8两个名字指的是同一个东西。另外一个很多人会忽略的点Keil5并不能直接兼容老的C51工程如果平时还要写51单片机需要单独装C51的编译器并切换到对应的Toolchain。这和本文主题无关但如果你在一台电脑上同时遇到“为什么选不了ST芯片”和“为什么选不了51芯片”多半就是芯片包或者编译器安装不完整。环境问题虽然简单但排查起来最浪费时间。2.3 拿到TinyUSB源码库目录怎么看TinyUSB源码托管在GitHub上仓库名就叫tinyusb直接Download ZIP下载或者git clone都行。它的目录结构很清晰核心就两个地方src是协议栈本体examples是官方示例。我们做MSC设备直接看examples/device/msc这个目录就够了。它里面已经包含了main.c、tusb_config.h、usb_descriptors.c还有一个用于演示存储介质的msc_disk.c。第一次打开TinyUSB源码的人可能会被hw目录吓到里面各种开发板的板级文件看起来非常庞大。但实际上我们用到的东西很少大多数情况下只需要拷贝src目录再加一个针对自己芯片的port实现。对STM32F1来说TinyUSB在src/portable/st/stm32_fsdev里已经把USB硬件驱动写好了你不需要碰底层只需要保证芯片时钟、GPIO配置正确然后在主循环里调用协议栈的处理函数就行。3. MSC设备栈的工作原理四回调吃透U盘3.1 TinyUSB设备栈的运行机制先把TinyUSB设备栈的运行模型搞清楚后面写代码才不会懵。整个模型可以理解成“一个总机加一堆分机”。总机就是协议栈核心它负责处理USB总线上所有底层事务枚举、标准请求、端点传输、错误处理。分机就是你写的应用层回调函数。主机发来一个请求协议栈解析之后按类型去调用对应的回调你的代码只需要在那个回调里把数据准备好或者收下来。代码层面只有两个函数要关注。第一是tusb_init()上电时执行一次完成硬件初始化和协议栈状态机初始化。第二是tud_task()这个函数要放在主循环里反复调用它相当于协议栈的心脏所有USB中断事件都在它这里被处理。很多人第一次移植失败就是因为main函数里只调了tusb_init()忘了在while(1)里调tud_task()结果设备插上电脑完全没反应。这套设计对应用层非常友好。比如主机要读取U盘某个扇区协议栈解析完READ(10)命令后会调用你实现的tud_msc_read10_cb()回调你只需要把对应扇区的数据拷贝到协议栈指定的缓冲区里返回true剩下的传输控制全部由TinyUSB完成。3.2 CBW、CSW和SCSI命令和主机打交道的基础MSC设备在USB总线上和主机的通信模式是这样的主机通过批量端点OUT发送一个31字节的CBWCommand Block Wrapper命令块包装里面包含了SCSI命令块设备执行完命令后通过批量端点IN回复一个13字节的CSWCommand Status Wrapper状态包装告诉主机命令执行成功还是失败。这个过程你也可以在生活中找到类比。CBW就像你去食堂窗口递菜单菜单上写明了想吃什么菜、要几份、送到哪张桌子。CSW就是窗口师傅做完菜之后喊一嗓子“做好了来取”。SCSI命令则是菜单上的具体菜名。MSC类设备里标准SCSI命令很多但实际经常用到的就那么几个TEST UNIT READY设备是否就绪、INQUIRY设备厂商和产品信息、READ CAPACITY(10)返回容量、READ(10)读扇区、WRITE(10)写扇区。主机操作系统探测U盘时会依次发这些命令你的代码逐条响应系统就能识别出“这是一个可用的磁盘”。好消息是TinyUSB已经把CBW、CSW的封装和解析都做完了你根本不需要手动构造这些结构体。你要做的只是实现几个语义明确的回调查询就绪、返回容量、读数据、写数据。协议栈负责把回调返回的数据打包成CSW发送到主机。3.3 存储介质怎么选RAM盘先上手Flash再进阶TinyUSB的MSC层并不关心你的数据存在哪里它只认“逻辑块地址LBA偏移长度”这一套存取模型。所以存储介质可以是内部RAM、外部SPI Flash、SD卡甚至自定义的加密存储设备接口都是一样的。这就给开发带来了很大灵活性。我建议你第一版先用RAM Disk验证就是把一块全局数组当作磁盘。这么做的原因是RAM访问速度快读写逻辑简单不涉及擦除和写对齐问题适合先把USB枚举、主机识别这些流程跑通。等确认U盘功能正常了再把RAM替换成Flash存储介质变了但USB侧的代码几乎不用改。RAM Disk的缺点是掉电丢数据只能作为调试手段。想做出真实的U盘效果必须用非易失存储。STM32内部的Flash也可以用来存储但一方面容量有限另一方面需要处理Flash磨损均衡和擦写保护反而比外挂一颗SPI Flash更麻烦。所以我后面进阶部分用最常见也最便宜的W25Q128来做8MB容量SPI接口一颗芯片就能解决。4. 5分钟跑通一个RAM Disk U盘代码逐段拆4.1 工程构建与文件组织现在进入实操环节。我建议的工程组织方式是这样的新建一个Keil工程把TinyUSB的src目录整个加进来然后从examples/device/msc里拷贝tusb_config.h、usb_descriptors.c、msc_disk.c、main.c这几个文件到自己的应用目录。这样改起来最快因为官方示例已经把很多细节处理好了我们只需要做裁剪。如果你不想拷贝官方示例也可以在freertos和device两个目录之外独立建一个应用层文件。更直接的方式是直接用官方示例目录里的board文件夹下的stm32f103各文件那里面有现成的时钟和GPIO初始化。不过官方示例用的是GCC Makefile方式在Keil里需要自己建立工程手动添加源文件。操作上不算复杂关键是别漏文件。src目录下所有.c文件都可以加进去编译时没被调用的部分会被链接器丢弃不影响最终体积。main.c的基本逻辑很朴素先初始化系统时钟、GPIO和USB硬件然后调用tusb_init()进入一个死循环循环里调用tud_task()。有些示例里还会调用一些板级LED闪烁函数那只是为了看板子活着删掉不影响功能。4.2 tusb_config.h你想要的关键配置tusb_config.h里集中了TinyUSB的编译期配置项。对MSC设备来说下面几项是关键。#ifndef TUSB_CONFIG_H_ #define TUSB_CONFIG_H_ // 指定TinyUSB运行在设备模式全速设备 #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED) // 设备支持的Class启用MSC #define CFG_TUD_CDC 0 #define CFG_TUD_MSC 1 #define CFG_TUD_HID 0 #define CFG_TUD_MIDI 0 #define CFG_TUD_VENDOR 0 // MSC读写的内部缓冲区大小必须一个逻辑块大小 #define CFG_TUD_MSC_BUFSIZE 512 #endifCFG_TUSB_RHPORT0_MODE里的OPT_MODE_DEVICE表示设备模式OPT_MODE_FULL_SPEED表示全速设备。STM32F1的USB外设是USB 2.0 Full Speed设备只能选全速模式不能选高速。CFG_TUD_MSC设为1启用MSC类值为0的类会被裁掉不编译对应的协议栈代码节省Flash空间。CFG_TUD_MSC_BUFSIZE默认512就够了因为后面存储介质的逻辑块大小我就按512字节来设计SCSI读写最小单元就是512字节。还有一个容易忽略的配置设备端点缓冲区在哪里分配。TinyUSB默认用了自定义内存段宏如果你用的是标准Keil工程直接在tusb_config.h里加上#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4)))就行保证缓冲区的4字节对齐USB DMA传输时不会出奇怪问题。4.3 描述符文件VID/PID和端点分配usb_descriptors.c负责生成设备描述符、配置描述符和字符串描述符。对MSC设备来说最值得关注的是配置描述符里的接口描述符和端点描述符。MSC类接口必须包含两个批量端点一个IN端点用于向主机发送数据一个OUT端点用于接收主机数据。官方示例的配置描述符会定义接口类为0x08MSC类子类0x06SCSI命令集协议0x50Bulk-Only Transport即BBB传输协议。端点分配方面F103的USB设备有固定的端点资源PA11/PA12对应USB D-/D端点0用于控制传输普通端点地址从0x01到0x0F。我习惯用0x81作为批量IN端点0x01作为批量OUT端点最大包长设置为64字节。设备描述符里的VID和PID建议改成自己项目的唯一值。默认的VID是0xCafe这是TinyUSB的测试VIDWindows下虽然能用但如果同时插多个TinyUSB设备可能会冲突。量产产品需要向USB-IF申请VID个人学习或者毕业设计直接用0xCafe也没问题。字符串描述符里可以写“TinyUSB STM32 Disk”这类产品名Windows的资源管理器会显示这个名字。4.4 核心回调MSC设备灵魂四件套这一步是整个工程最核心的部分。官方示例msc_disk.c里的回调函数我抽出来逐段解读。下面是一份基于RAM Disk的完整实现逻辑非常简单但功能完整。#include tusb.h #define BLOCK_SIZE 512 #define BLOCK_COUNT 32 // 16KB RAM Disk适合F103的小内存 static uint8_t disk[BLOCK_SIZE * BLOCK_COUNT]; // 设备信息厂商、产品、版本 void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) { (void) lun; memcpy(vendor_id, TinyUSB, 8); memcpy(product_id, STM32 Disk, 16); memcpy(product_rev, 1.0, 4); } // 设备是否就绪 bool tud_msc_test_unit_ready_cb(uint8_t lun) { (void) lun; return true; } // 返回容量逻辑块数量和每块字节数 void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint32_t* block_size) { (void) lun; *block_count BLOCK_COUNT; *block_size BLOCK_SIZE; } // 读操作把RAM中的数据拷贝给主机 bool tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) { (void) lun; memcpy(buffer, disk lba * BLOCK_SIZE offset, bufsize); return true; } // 写操作把主机发来的数据写入RAM bool tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) { (void) lun; memcpy(disk lba * BLOCK_SIZE offset, buffer, bufsize); return true; } // 其他SCSI命令这里统一返回0表示不额外处理 int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize) { (void) lun; (void) scsi_cmd; (void) buffer; (void) bufsize; return 0; } // 启动停止介质比如弹出U盘 bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) { (void) lun; (void) power_condition; (void) start; (void) load_eject; return true; }这段代码里有几个地方需要解释。tud_msc_capacity_cb里返回的block_count和block_size决定了Windows看到的是一个多大的磁盘。block_size固定512字节这是大多数存储设备的逻辑块大小block_count是总块数两个相乘就是磁盘容量。我把RAM盘设为32块总容量16KB这个容量小到Windows资源管理器可能都不会显示可用的空间但足够用来验证枚举和基本读写。如果你用的是F407这种大内存芯片可以把BLOCK_COUNT调到128甚至更多容量大一些Windows更能接受。tud_msc_read10_cb和tud_msc_write10_cb是实际读写函数。因为RAM可以按任意地址任意字节访问所以逻辑就是一个memcpy。注意这里传入的lba是逻辑块地址offset是块内偏移bufsize是本次操作要搬运的字节数。主机一次READ(10)可能请求1个扇区也可能请求多个扇区所以地址计算要写成lba * BLOCK_SIZE offset而不是简单地lba * BLOCK_SIZE。tud_msc_scsi_cb是用来兜底的。TinyUSB已经把INQUIRY、READ CAPACITY、READ、WRITE这些常用命令封装好了但SCSI命令集很大总有一些命令没有对应的专用回调比如MODE SENSE(6)、START STOP UNIT它们会走到这个回调里。返回0表示不处理协议栈会返回错误状态给主机如果你确信某个命令需要返回特定数据也可以在这个回调里填充buffer并返回正数。对绝大多数应用来说返回0就够了。4.5 编译烧录与电脑侧验证代码写完之后在Keil里配置一下烧录器。Options for Target - Debug页面选择ST-Link Debugger然后进入Settings在Flash Download里勾选Reset and Run烧录完成之后板子自动复位运行。连接方式用SWD四线SWDIO、SWCLK、GND、3.3V。烧录完成把USB线连接到开发板的USB口另一端插电脑。这时设备管理器里应该出现“USB大容量存储设备”磁盘管理里也能看到一块16KB左右的磁盘。由于容量太小资源管理器可能不显示盘符这是正常的不代表设备没工作。想验证写入是否成功有两种办法一种是在磁盘管理里手动分配一个盘符另一种是直接把BLOCK_COUNT调大比如在F407上调到1024做成512KB的RAM盘这时候Windows会弹窗提示“需要格式化”。如果弹窗了格式化时选FAT32或exFAT都行。格式化的过程会执行一系列SCSI命令包括MODE SENSE、WRITE、READ CAPACITY等你的回调函数都会被真实调用到。格式化完成之后往U盘里拖一个文件再读出来对比文件内容没变化说明RAM Disk版U盘功能完全跑通了。这个过程就是整个方案的核心验证。5. 从Demo到产品把W25Q128变成真正的U盘5.1 为什么要换掉RAM DiskRAM Disk跑通之后功能上已经实现了U盘但它有一个致命问题断电之后数据全部消失。真实的U盘产品必须使用非易失存储介质。换用外部SPI Flash之后U盘的数据在掉电后依然能保留这就让这个方案具备了实际产品的雏形。比如做个数据记录仪把采集到的数据写到U盘里拔下来插电脑上就能看这才是MSC设备最有价值的应用方式。W25Q128是我用得最多的SPI NOR Flash芯片容量8MB实际是128Mbit毫秒级擦写接口简单市面上模块也很便宜。它虽然不是专门的U盘存储芯片但作为嵌入式设备的导出存储已经完全够用。如果你的应用需要更大容量也可以换成W25Q25632MB或者SD卡方案接口逻辑类似。5.2 W25Q128驱动SPI读写的几个关键点W25Q128的数据手册很薄核心操作就几类命令读ID0x90、写使能0x06、读数据0x03、扇区擦除0x20一次擦4KB、页编程0x02一次最多写256字节。SPI接口在STM32F103上我用SPI1引脚分配是SCKPA5、MISOPA6、MOSIPA7、CSPA4工作模式SPI Mode 0时钟分频8分频。初始化代码的要点是先拉低CS发送0x90命令读厂商ID和设备ID确认Flash连接正常然后把Flash设置为掉电模式解除虽然W25Q128上电默认就处于活动状态但养成习惯总没错。这一步如果返回值不对后面所有读写都是空中楼阁。读操作最简单发送0x03命令后跟24位地址然后连续读出任意字节。写操作就要小心了W25Q128有两个硬性约束写入之前必须先擦除擦除粒度是4KB扇区页编程单次最多写256字节而且不能跨页边界。这意味着如果主机发来的写请求超过256字节或者要写入到某个扇区的尾部且跨过页边界驱动必须拆分处理。5.3 回调改写从内存搬运到Flash搬运把RAM Disk的回调改成Flash版本本质上是把memcpy替换成Flash读写函数但有一点必须处理W25Q128只支持按扇区擦除不支持按512字节逻辑块擦除。如果主机每次写512字节而Flash扇区是4KB直接写之前必须先擦除整个4KB扇区这会引来两个问题一是经常做整扇区擦除会严重影响写入速度二是如果主机写入的数据量小于4KB不能简单把整个扇区擦掉否则没被覆盖的数据就丢了。我采用的是“先读后写再回写”的经典方案在tud_msc_write10_cb里根据lba计算出对应的Flash地址如果本次写请求跨越了Flash扇区边界就直接把涉及的4KB扇区读到RAM缓冲区在缓冲区里修改对应字节然后把整个4KB擦除并写回。如果本次写请求只落在同一个Flash扇区内也按同样方式处理。这样做逻辑清晰代码量小缺点是有额外读写开销但对我们这种低频率的数据导入导出场景完全够用。bool tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) { (void) lun; uint32_t flash_addr lba * BLOCK_SIZE offset; // 计算涉及哪些Flash扇区每个扇区4KB // 读扇区内容修改擦除写回 ... return true; }实现细节我不展开写了核心思路是设计一个w25qxx_write_sector辅助函数传入Flash地址和数据长度内部自动处理擦除、读回、页编程拆分这些事务。写完之后再改动几个地方tud_msc_capacity_cb里的block_count要改成Flash实际容量除512比如W25Q128是8MBblock_count 8 * 1024 * 1024 / 512。tud_msc_read10_cb里直接调用W25Q128读函数。5.4 实测数据格式化慢、写入快、要防掉电flash版U盘第一次插入电脑Windows同样会提示格式化格式化为FAT32。格式化过程中Windows会进行大量写入操作你会看到进度条走得比较慢这是正常的因为每次写之前都要做扇区擦除。格式化完成后向盘里拷贝文件速度取决于SPI频率和擦除策略实测在18MHz SPI时钟下连续写入大文件的平均速度大约在100~200KB/s这个量级读取速度会快一些。值得提醒的是SPI Flash的等待状态和电源稳定性对写入可靠性影响很大。如果在写入过程中拔掉USB线可能造成文件系统损坏或者Flash某个扇区处于半擦除状态。产品化时建议增加电容储能掉电检测或者把文件系统设计成只读挂载需要写入时才切换。还有一点W25Q128擦写寿命大约10万次如果频繁擦写同一扇区需要做磨损均衡否则Flash会提前老化。6. 联调翻车现场不识别、没盘符、读写失败的排查手册6.1 插上没任何反应先检查时钟和上拉电脑插上USB线之后完全没动静设备管理器里也没有任何新设备出现这是最常见的故障。我排查这个问题的顺序固定是三步先换一根数据线排除线材问题再量D引脚的电平最后检查时钟配置。很多人第一步就被劣质线材坑了USB线只供电不同数据你换了三块板子都一样最后发现是线的问题非常耽误时间。D的电平检查很有意义设备上电后如果USB逻辑正常D引脚会被1.5kΩ电阻拉高到3.3V左右主机检测到这个电平跳变才认为有设备插入。如果量出来D一直是低电平说明上拉电阻没接或者USB外设时钟没正常工作。STM32F103的USB外设必须由48MHz时钟驱动这个时钟通常来自PLL输出分频。很多人的板子外部晶振不是8MHz导致PLL倍频后无法得到48MHzUSB模块就没法工作。检查一下系统时钟树确认USB时钟源正确配置为48MHz这是STM32 USB开发里绕不过去的一关。6.2 设备管理器有设备我的电脑没盘符设备管理器里能看到“USB大容量存储设备”说明枚举成功但“我的电脑”里找不到盘符。这个问题的排查优先级是先看磁盘管理再查容量回调最后用工具看枚举细节。磁盘管理里如果能看到一块未格式化、未分配盘符的磁盘手动右键分配盘符或格式化就行。如果磁盘管理里磁盘容量显示为0那基本可以确定是tud_msc_capacity_cb返回的容量不对。Windows会向设备发送READ CAPACITY命令获取容量如果返回的block_count为0系统就会认为介质无效不分配盘符。另外检查一下tud_msc_test_unit_ready_cb是否一直返回false这个回调如果返回false主机也会认为介质未就绪同样不会显示盘符。还有一个不太容易想到的原因VID/PID和电脑里已有的某个设备冲突导致Windows的驱动缓存识别错误。这种情况换一个VID/PID重新烧录就能解决。6.3 复制文件报错或盘符消失排查SCSI和Flash能识别盘符但一复制文件就报“参数错误”或者“无法完成操作”甚至整个盘符消失这类问题更多出现在Flash版本里。首先检查你的回调函数有没有做长时间阻塞操作。TinyUSB的回调是在USB中断上下文里被调用的不是你可以随意阻塞的地方。如果tud_msc_write10_cb里做了Flash擦除这种毫秒级操作时钟中断和USB传输时序就会被破坏最终导致主机认为设备异常。解决办法有两个方向一是把Flash操作放到应用层任务里用信号量告知USB回调层“数据已接收、马上写”但这会改变TinyUSB的处理模型复杂度高二是优化Flash写入流程把数据先在RAM里攒够一个4KB扇区再一次性擦写期间短暂阻塞是可以接受的只要阻塞时间在一个合理范围内。我个人的经验是先做容量适配和日志调试确认SCSI命令流程没问题再考虑并发优化。还有一类问题是tud_msc_scsi_cb返回了错误状态比如Windows在格式化时会发MODE SENSE(6)查询介质写保护状态如果这个命令返回异常Windows可能认为U盘只读或直接中止格式化。调试这类问题最简单的方法是把CFG_TUSB_DEBUG调到2TinyUSB会通过串口输出大量协议栈日志配合一个USB转串口模块很快就能定位到是哪条SCSI命令被拒绝。6.4 常用工具清单与我的调试顺序这里列一下我常用的工具基本是免费的遇到问题按这个顺序走能省掉大量无效尝试。工具用途使用场景STM32CubeProgrammer烧录、擦除、查看芯片信息芯片被锁死或Flash异常时救砖ChipGenius查看USB设备枚举详情确认VID/PID、端点、接口信息DiskGenius格式化、分区、坏块检测Windows自带磁盘管理不好用时Validrive验证U盘真实容量和读写速度确认Flash版本U盘的实际性能Wireshark USBPcapUSB总线抓包分析枚举和SCSI交互的终极调试手段串口助手查看TinyUSB调试日志打开CFG_TUSB_DEBUG后输出协议栈信息最后分享一个我一直在用的调试顺序代码能烧进去先不急着插USB用串口确认TinyUSB初始化成功然后插USB看设备管理器有没有“未知设备”有就说明枚举失败优先查描述符和硬件没有未知设备但也没有U盘查容量回调和磁盘管理有U盘但读写失败开TinyUSB日志逐条分析SCSI命令。按这个顺序走绝大多数问题半小时内能定位。我个人在实际操作中的体会是TinyUSB做MSC设备最大的价值不是省下了写协议栈的时间而是把“USB驱动开发”这个听起来很高的大门变成了“填几个回调函数”这种很具体的小事。只要先把RAM Disk版跑通再往Flash上迁移整个过程的成功率会高很多。如果你准备拿这个方案做东西建议从一开始就把Flash容量和文件系统规划好哪怕第一版只跑RAM Disk也要为后面的产品化留好接口。这个内容后续也可以进一步扩展配合一个简单的Bootloader把U盘当成固件升级入口电脑端拷入bin文件就能完成OTA整套方案会变得非常完整。
返回列表