1. 项目概述从“用”到“造”的跨越如果你拆开过U盘会发现里面通常只有一小块电路板和一个闪存芯片。这块小小的电路板就是我们要聊的“MSC设备”的核心。MSC全称Mass Storage Class是USB协议中定义的一个标准设备类专门用于大容量存储设备比如U盘、移动硬盘、读卡器。我们日常“使用”U盘是把它当成一个即插即用的存储工具而“开发”一个MSC设备则是要扮演U盘制造者的角色让我们的硬件比如一块STM32单片机开发板在插入电脑时能被系统识别为一个标准的、可读写的U盘。这不仅仅是软件模拟那么简单。它意味着你的设备需要完整地实现USB协议栈、MSC类协议、以及底层的存储介质如SPI Flash、SD卡驱动。当你在电脑上复制一个文件到这个“虚拟U盘”时数据流会经历这样的旅程Windows或macOS的文件系统驱动发出SCSI命令这是MSC类设备通信的“语言”通过USB总线传输到你的设备设备端的USB固件解析这些命令并将其转化为对实际物理存储芯片的读写操作。整个过程你的设备都在“伪装”成一个标准的存储设备与主机无缝交互。为什么要做这件事场景远比想象中丰富。对于嵌入式开发者这可能是为产品提供一个便捷的固件升级或日志导出接口用户无需拆机接串口插上USB线就能像操作U盘一样更新程序。对于极客玩家可以用ESP32-S3这样的芯片做一个“智能U盘”在存储文件的同时还能跑点网络服务或传感器程序。在工业领域自定义的MSC设备可以用于安全的数据采集盒只有特定的上位机软件才能解析其特殊的文件格式。从“消费者”到“创造者”理解并实现MSC设备开发是深入嵌入式系统和USB协议的一个绝佳切入点。2. 核心需求与方案选型解析2.1 明确你的设备要“扮演”什么角色在动手写代码之前必须先想清楚你的MSC设备要满足哪些核心需求这直接决定了技术方案的复杂度。第一存储介质是什么这是最根本的问题。是板载的SPI NOR/NAND Flash还是通过SDIO或SPI接口连接的SD/TF卡或者是模拟一块RAM空间作为临时存储不同的介质其块大小Block Size通常是512字节、容量、读写寿命和驱动复杂度天差地别。例如SPI Flash需要处理坏块管理和磨损均衡如果文件系统支持的话而SD卡本身内置了控制器驱动相对标准。第二需要支持什么文件系统主机电脑看到的是一个“RAW”磁盘它会在上面创建分区并格式化文件系统如FAT32、exFAT。你的设备固件并不需要实现完整的FAT32驱动那是主机操作系统的事。你只需要确保能正确响应主机对逻辑块地址LBA的读写请求。但是如果你希望设备出厂时就自带一些文件或者需要设备自己能读取存储的文件双机模式那么可能需要在固件侧集成一个轻量级的文件系统库如FatFs。第三USB性能要求如何是USB Full Speed12 Mbps就够用还是必须达到USB High Speed480 Mbps这取决于主控芯片的USB外设能力和你的应用场景。升级一个几兆的固件Full Speed足够如果要实时记录高速数据流则必须追求High Speed。性能也直接关系到缓冲区Buffer的设计大小和DMA的使用。第四是否需要复合设备Composite Device你的设备除了是U盘是否还需要同时是一个串口CDC或者HID设备例如一个数据采集器既需要MSC导出数据又需要CDC来接收实时指令。这就需要配置USB复合设备描述符对驱动开发和系统兼容性提出更高要求。基于这些需求常见的方案选型有使用内置USB和存储控制器的MCU如STM32F4/F7/H7系列、GD32、ESP32-S3。这类方案集成度高成本可控资料丰富是学习和中小批量项目的首选。MCU 专用USB桥接芯片如CH376、GL827。MCU通过SPI/UART与桥接芯片通信桥接芯片处理所有USB MSC协议。这种方案将MCU从复杂的USB协议中解放出来但增加了BOM成本和PCB空间。FPGA/SoC方案用于超高速或定制化程度极高的场景如基于USB 3.0的采集设备。开发门槛最高。对于绝大多数开发者方案1是最务实的选择。我们接下来的讨论也将基于此展开。2.2 开发环境与工具链搭建工欲善其事必先利其器。MSC设备开发是软硬件紧密结合的工作。硬件准备主控开发板选择一款带有USB Device功能并且通常有片上PHY的MCU开发板。例如STM32F407 Discovery、STM32F429 Discovery带USB HS ULPI接口、或者更流行的STM32F103C8T6“蓝色小药丸”仅USB FS。ESP32-S3开发板因其双核和内置USB OTG也成为一个热门选择。存储介质根据你的板子可能是板载的QSPI Flash或者需要自己焊接一个W25Qxx系列的SPI Flash芯片亦或是插入一张MicroSD卡。USB线缆与连接器确保是可靠的数据线。对于USB HS设备可能需要连接外部ULPI PHY芯片。软件准备集成开发环境IDESTM32系列首选STM32CubeIDE它集成了CubeMX配置工具和编译调试环境。对于ESP32可以用官方的ESP-IDF框架和VSCode插件。Keil MDK或IAR也是可选项。STM32CubeMX针对ST用户这是至关重要的图形化配置工具。它可以帮助你可视化地配置USB外设时钟、引脚、中间件Middleware等并生成初始化代码框架能避免大量底层寄存器配置的坑。USB协议分析仪这不是必须的但却是深度调试的“神器”。如Beagle USB 480、Ellisys等。当你的设备枚举失败或者数据传输异常时协议分析仪可以捕获USB总线上的原始数据包让你清晰地看到描述符、令牌、数据、握手包是定位问题的终极手段。对于预算有限的开发者可以尝试用软件方案如Wireshark配合USBPcap驱动抓取主机端的USB流量虽不完整但也有一定帮助。串口调试工具用于输出设备固件的调试日志这是你了解设备内部状态的眼睛。注意在CubeMX中配置USB时务必确保USB时钟源正确。对于STM32F1系列USB时钟必须来自PLL且分频后精确为48MHz。时钟配置错误是导致USB设备无法被识别的最常见原因之一。3. USB MSC设备核心实现详解3.1 USB描述符设备的“身份证”和“说明书”当你的设备插入主机主机发出的第一个请求就是Get_Descriptor。描述符是一系列标准化的数据结构告诉主机“我是什么”、“我能做什么”。对于MSC设备关键描述符包括设备描述符Device Descriptor定义设备的全局信息。// 示例片段 (基于USB 2.0) __ALIGN_BEGIN static uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END { 0x12, // bLength: 描述符长度18字节 USB_DESC_TYPE_DEVICE, // bDescriptorType: 设备描述符 0x0200, // bcdUSB: USB协议版本 2.00 0x00, // bDeviceClass: 设备类在接口中定义所以为0 0x00, // bDeviceSubClass 0x00, // bDeviceProtocol USB_MAX_EP0_SIZE, // bMaxPacketSize0: 端点0最大包大小64 0x0483, // idVendor: 厂商ID (STMicroelectronics示例) 0x5740, // idProduct: 产品ID 0x0200, // bcdDevice: 设备版本号 0x01, // iManufacturer: 厂商字符串索引 0x02, // iProduct: 产品字符串索引 0x03, // iSerialNumber: 序列号字符串索引 0x01 // bNumConfigurations: 配置数量 };关键点idVendor和idProduct非常重要。如果不想让系统自动安装通用驱动你可以向USB-IF申请自己的VID/PID需付费或者使用芯片厂商提供的测试用PID如ST的0x0483。在开发阶段使用测试PID即可。配置描述符、接口描述符、端点描述符它们捆绑在一起。一个配置下有一个或多个接口。对于纯MSC设备通常只有一个接口。接口描述符bInterfaceClass 0x08Mass StoragebInterfaceSubClass和bInterfaceProtocol的组合定义了使用的传输协议。最常见的是SubClass 0x06 (SCSI)Protocol 0x50 (Bulk-Only Transport, BOT)。BOT协议因其简单高效而成为绝对主流。端点描述符MSC BOT协议需要三个端点端点0控制端点用于枚举和类特定请求。端点 IN (Bulk In)用于设备向主机发送数据如读请求的数据、命令状态。端点 OUT (Bulk Out)用于主机向设备发送数据如写请求的数据、命令块。字符串描述符提供人类可读的信息如厂商名“MyTech Co.”产品名“My MSC Device”。它们是可选的但强烈建议提供方便在系统设备管理器中识别。实操心得初次开发时最容易在描述符上栽跟头。长度不对、类型不对、端点地址或包大小配置错误都会导致枚举失败。一个很好的调试方法是在设备代码中将主机请求的描述符内容通过串口打印出来与USB规范文档逐字节比对。使用CubeMX生成代码能极大减少这类错误。3.2 BOT协议与SCSI命令集设备与主机的“对话”MSC设备的核心通信建立在BOT协议和SCSI命令集之上。这不是两个独立的东西而是分层的关系BOT是“运输方式”卡车SCSI命令是“货物内容”集装箱里的东西。BOT协议流程 BOT协议将一次数据传输称为一个CBW-CSW事务分为三个阶段命令传输块CBW主机通过Bulk OUT端点发送一个31字节的CBW结构体。它包含了本次操作是读还是写、数据长度dCBWDataTransferLength、逻辑块地址dCBWLUN和dCBWCB中的LBA以及封装的SCSI命令CBWCB字段最多16字节。数据阶段可选如果CBW中指定了数据长度不为0则进行数据传送。对于读操作设备通过Bulk IN端点发送数据对于写操作主机通过Bulk OUT端点发送数据。数据必须严格按照dCBWDataTransferLength指定的长度传输。命令状态包装CSW数据阶段完成后设备必须通过Bulk IN端点发送一个13字节的CSW结构体给主机。CSW中的dCSWDataResidue字段表示未处理的数据量应为0bCSWStatus字段表示命令执行状态0成功1失败2阶段错误。核心SCSI命令解析 主机通过CBW发送的SCSI命令是驱动设备行为的真正指令。你需要实现一个SCSI命令解析器。最关键的几个命令是INQUIRY (0x12)主机询问设备的基本信息如厂商、产品名、版本。设备需要返回一个标准格式的数据。READ CAPACITY(10) (0x25)主机获取设备的容量信息总块数和块大小。这是让系统显示U盘总空间的关键。READ(10) (0x28)/WRITE(10) (0x2A)读/写命令。你需要从命令参数中解析出起始LBA和要传输的块数。REQUEST SENSE (0x03)当命令执行出错时主机用此命令获取详细的错误信息。TEST UNIT READY (0x00)主机检查设备是否就绪。通常直接返回成功即可。MODE SENSE(6) (0x1A)主机获取设备的各种模式参数。对于简单设备可以返回一个固定的、精简的响应。PREVENT ALLOW MEDIUM REMOVAL (0x1E)用于锁住介质如光盘弹出对于U盘设备通常忽略或直接返回成功。实现要点 在你的固件中你需要一个状态机来处理BOT协议的三阶段。当收到完整的CBW后解析其中的SCSI命令根据命令执行相应的操作如从Flash读取数据到发送缓冲区或将接收缓冲区的数据写入Flash最后发送CSW。必须严格保证CBW、数据、CSW的传输顺序和长度任何差错都会导致主机认为设备错误可能重置USB连接。3.3 存储介质驱动与逻辑块映射USB MSC层处理的是逻辑块地址LBA而你的存储介质如SPI Flash有自己物理地址。你需要实现一个中间层完成LBA到物理地址的转换。块大小Block Size USB MSC标准通常使用512字节作为一个逻辑块。这也是绝大多数文件系统如FAT32的扇区大小。你的存储介质可能以不同的页大小Page Size如256字节、4096字节进行操作。你需要确保对外向主机宣称的块大小是512字节。对内操作Flash时处理好地址对齐。例如如果Flash页是4096字节那么每8个LBA8*5124096对应一个物理页。读操作需要缓存整个页再提取所需部分写操作需要先读取整个页到缓存修改对应部分再擦除整个扇区Sector通常包含多个页后写入。读写函数实现 你需要为底层存储实现至少三个核心函数// 初始化存储介质 int8_t STORAGE_Init(uint8_t lun); // 获取存储容量信息 (块数量 块大小) int8_t STORAGE_GetCapacity(uint8_t lun, uint32_t *block_num, uint16_t *block_size); // 读写数据块 int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len); int8_t STORAGE_Write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);这里的lun逻辑单元号对于简单U盘设备就是0。这些函数会被上层的MSC驱动回调。缓冲区管理 USB传输是异步的。当主机发起一个读64KB文件的请求时会分解成多个BOT事务。你不能等所有数据都从Flash读出来再发送那样太慢且占用大量内存。正确的做法是使用**双缓冲Ping-Pong Buffer**机制准备两个缓冲区如各4KB。当USB正在从缓冲区A发送数据时DMA或CPU可以同时从Flash读取下一批数据到缓冲区B。两个缓冲区交替使用实现流水线操作最大化吞吐量。写操作同理。注意对于SPI Flash写操作前必须先擦除Erase而擦除单位扇区通常4KB/64KB远大于写单位。这意味着实现STORAGE_Write时必须包含复杂的擦除管理逻辑。一种简单策略是在内存中维护一个“写缓存区”攒够一个扇区的数据后再执行擦除和写入。但这会带来数据丢失的风险突然断电。更健壮的做法是实现一个简单的闪存转换层FTL但这会显著增加复杂度。4. 基于STM32CubeMX与HAL库的实战步骤我们以STM32F407 Discovery板自带USB HS PHY和板载的MicroSD卡为例展示一个典型的开发流程。4.1 硬件与软件初始化配置创建CubeMX工程选择你的MCU型号STM32F407VG。配置时钟树Clock Configuration确保系统时钟HCLK正确并特别关注USB时钟。对于F4的USB HS在ULPI模式下需要使能PLL并配置PLL48CK时钟为48MHz。这是USB外设工作的绝对前提时钟不对一切白费。配置USB外设在“Connectivity”选项卡下使能“USB_OTG_HS”模式选择“Device_Only”。在“Middleware”选项卡下选择“USB_DEVICE”。Class选择“Mass Storage Class (MSC)”。在“Mass Storage Class (MSC)”子配置中将“BOT”接口类型设置为“Storage”。在“STORAGE”标签页你可以暂时将“Max Lun”设为1。配置存储介质接口由于使用SD卡需要配置SDIO接口在“Connectivity”-“SDIO”。模式选择“4-bit Wide bus”并配置合适的时钟分频初期调试可设低一些如24MHz。配置GPIO和DMACubeMX会自动配置USB和SDIO相关的引脚。建议为USB HS的OTG_HS_VBUS引脚配置一个GPIO输入模式用于检测VBUS供电。为USB的Bulk IN/OUT端点启用DMA在USB_DEVICE配置中以减轻CPU负担提高性能。生成代码设置好工程路径和IDE点击“GENERATE CODE”。4.2 填充MSC应用回调函数CubeMX生成的代码框架中关于MSC的核心应用层代码在Src/usbd_storage_if.c和Inc/usbd_storage_if.h文件中。你需要填充这个接口文件中的几个关键函数。打开usbd_storage_if.c找到STORAGE_Init,STORAGE_GetCapacity,STORAGE_Read,STORAGE_Write等函数。它们现在都是空壳或返回错误。以SD卡为例的填充逻辑初始化 (STORAGE_Init)这里调用你的SD卡初始化函数如BSP_SD_Init()或HAL_SD_Init()。返回0表示成功。int8_t STORAGE_Init(uint8_t lun) { /* 初始化SD卡 */ if(BSP_SD_Init() ! MSD_OK) { return -1; // 初始化失败 } /* 检查SD卡是否就绪 */ if(BSP_SD_IsDetected() SD_NOT_PRESENT) { return -1; // 卡未插入 } return 0; }获取容量 (STORAGE_GetCapacity)调用SD卡获取信息函数计算总扇区数和扇区大小通常是512字节。int8_t STORAGE_GetCapacity(uint8_t lun, uint32_t *block_num, uint16_t *block_size) { HAL_SD_CardInfoTypeDef CardInfo; if(HAL_SD_GetCardInfo(hsd, CardInfo) ! HAL_OK) { return -1; } *block_num CardInfo.LogBlockNbr; // 总逻辑块数 *block_size CardInfo.LogBlockSize; // 逻辑块大小应为512 return 0; }读操作 (STORAGE_Read)将SD卡的读函数封装进来。注意blk_addr是LBA地址blk_len是要读的块数。int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { uint32_t sector_addr blk_addr; uint32_t sector_count blk_len; if(HAL_SD_ReadBlocks(hsd, (uint8_t*)buf, sector_addr, sector_count, SD_TIMEOUT) ! HAL_OK) { return -1; } /* 等待读操作完成对于DMA传输 */ if(HAL_SD_CheckReadOperation(hsd, (uint32_t)SD_DATATIMEOUT) ! HAL_OK) { return -1; } return 0; }写操作 (STORAGE_Write)与读操作类似但需要调用写函数。注意SD卡写之前不需要像SPI Flash那样先擦除。int8_t STORAGE_Write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { uint32_t sector_addr blk_addr; uint32_t sector_count blk_len; if(HAL_SD_WriteBlocks(hsd, (uint8_t*)buf, sector_addr, sector_count, SD_TIMEOUT) ! HAL_OK) { return -1; } /* 等待写操作完成 */ if(HAL_SD_CheckWriteOperation(hsd, (uint32_t)SD_DATATIMEOUT) ! HAL_OK) { return -1; } return 0; }关键点usbd_storage_if.c中的函数运行在USB中断上下文或特定的MSC任务上下文中。在这些函数内部绝对不能使用HAL_Delay()这类阻塞式延时而应该使用非阻塞的状态检查或者确保你的底层SD卡驱动是支持DMA和非阻塞操作的。长时间阻塞会导致USB通信超时主机认为设备无响应。4.3 编译、下载与测试编译工程确保没有错误。下载到开发板通过ST-Link或J-Link将程序下载到板子。连接USB用USB线将开发板的USB HS接口通常是Micro-USB或Type-C口标有USB OTG/HS连接到电脑。观察现象成功枚举电脑会发出“叮咚”的硬件插入提示音在“设备管理器”的“磁盘驱动器”下会出现一个新设备可能显示为“USB大容量存储设备”或你定义的产品名。稍等片刻在“我的电脑”里会出现一个新的可移动磁盘盘符。枚举失败电脑可能提示“无法识别的USB设备”或者在设备管理器中出现带黄色感叹号的“Unknown Device”。4.4 功能验证与性能测试格式化与读写尝试右键点击新出现的盘符选择“格式化”。选择FAT32或exFAT文件系统进行格式化。如果成功说明设备的读写基础功能正常。大文件传输测试复制一个几十兆或几百兆的大文件到设备观察传输速度。同时通过串口调试助手观察设备固件的打印信息看是否有错误发生。稳定性测试进行长时间的连续读写操作如使用磁盘测速工具CrystalDiskMark检查是否会出现设备断开、文件损坏或传输错误。异常处理测试在传输过程中突然拔插USB线看设备能否重新正确枚举。测试写入时突然断电对于有后备电池或超级电容的系统看数据一致性如何。5. 深度调试与疑难问题排查实录开发MSC设备90%的时间可能花在调试上。以下是我在实际项目中踩过的坑和总结的排查思路。5.1 设备枚举失败电脑无法识别这是最常见的问题。请按以下顺序排查硬件检查供电USB VBUS电压是否稳定5V电流是否足够某些开发板需要单独供电或设置跳线帽才能为USB PHY供电。连接USB数据线是否完好尝试更换一条已知良好的线。DP/DM线是否接反或短路上拉电阻USB FS设备需要在D全速或D-低速上接一个1.5kΩ的上拉电阻到3.3V以告知主机这是一个全速/低速设备。这个电阻通常集成在MCU内部需要通过软件配置使能在CubeMX的USB配置中找到“VBUS sensing”和“Connect pull-up”相关选项并启用。软件配置检查时钟重中之重用调试器或通过代码读取时钟寄存器确认USB核心时钟在STM32中为48MHz是否精确。哪怕有1%的偏差都可能导致枚举失败。描述符使用USB协议分析仪是最直接的方法。如果没有可以在USBD_LL_Init或USBD_LL_SetupStage等函数中设置断点当主机请求描述符时将准备发送的描述符数组内容通过串口完整打印出来与USB规范或已知正确的示例逐字节比对。特别检查端点地址、包大小、接口类/子类/协议字段。端点配置确保Bulk IN和Bulk OUT端点已正确配置并使能。端点最大包大小MPS必须匹配FS设备是64字节HS设备是512字节。利用系统日志在Windows上打开“设备管理器”右键点击“未知设备”-“属性”-“事件”选项卡可以看到设备枚举过程中的关键步骤和可能的失败原因代码。在Linux上插入设备后使用dmesg | tail或journalctl -f命令查看内核日志会打印出详细的USB枚举过程和信息非常有助于定位问题。5.2 枚举成功但无法格式化或读写设备能识别但打开时提示“需要格式化”格式化又失败或读写文件出错。STORAGE_GetCapacity返回值错误这是最可能的原因。主机依赖此函数获取磁盘大小。如果返回的块数或块大小是0或非法值系统就无法正确初始化磁盘。确保你的存储介质初始化成功并且计算容量的公式正确。对于SD卡LogBlockNbr就是总扇区数。SCSI命令响应错误主机在格式化前会发送一系列SCSI命令如INQUIRY,READ CAPACITY,MODE SENSE。你的命令处理函数必须返回符合标准的数据格式。一个常见的错误是INQUIRY命令返回的数据长度不对或者MODE SENSE命令返回了设备不支持的模式页。可以参考usbd_msc_scsi.c中的默认实现确保你的响应数据格式正确。读写函数逻辑错误STORAGE_Read和STORAGE_Write函数中的地址转换或长度处理有误。例如LBA地址直接当作字节地址传递给底层驱动或者没有处理好跨物理页/扇区的读写。在读写函数中加入调试打印输出每次操作的LBA地址和长度与主机请求进行比对。缓冲区对齐与DMA问题如果使用了DMA必须确保传递给USB DMA和存储介质DMA的缓冲区地址是对齐的通常是4字节或32字节对齐。非对齐访问在有些MCU上会导致硬件错误或数据损坏。使用__attribute__((aligned(4)))或ALIGN_32BYTES来定义缓冲区。5.3 传输速度慢或不稳定未使用DMA检查USB和存储介质如SDIO的DMA是否已启用并正确配置。轮询Polling模式会大量占用CPU且效率低下。缓冲区大小不足USB MSC驱动中的缓冲区大小STORAGE_BLK_SIZ和MSC_MEDIA_PACKET设置过小导致频繁的小数据包传输开销巨大。对于HS设备可以尝试将MSC_MEDIA_PACKET增加到16KB或32KB需匹配端点MPS。存储介质本身速度慢SPI Flash的读写速度远低于SD卡。如果使用SPI Flash确保SPI时钟开到最高并使用Quad SPIQSPI模式如果芯片支持。检查Flash芯片的等待状态dummy cycles配置是否正确。文件系统开销如果你在设备端也运行了文件系统如FatFs用于双机访问那么主机端的文件操作会经过两层文件系统速度会下降。评估是否真的需要设备端文件系统。中断优先级冲突USB中断和存储介质如SDIO的中断优先级设置不当可能导致一个中断长时间阻塞另一个影响实时性。确保USB中断具有较高的优先级。5.4 数据损坏或设备意外断开电源噪声大电流的Flash擦写操作可能引起电源波动导致USB PHY或MCU内核复位。在电源引脚增加足够的去耦电容如10uF钽电容0.1uF陶瓷电容。看门狗复位如果开启了独立看门狗IWDG或窗口看门狗WWDG而USB或存储操作耗时过长可能导致看门狗超时复位。适当延长看门狗超时时间或在长时间操作如Flash全片擦除前临时喂狗。堆栈溢出USB中断和回调函数可能会使用较多栈空间。检查并增大任务的堆栈大小。可以通过在初始化时用特定值如0xDEADBEEF填充栈空间运行一段时间后再检查被修改的深度来估算栈使用量。并发访问冲突如果你的应用有其他任务如网络、显示同时访问存储介质必须做好互斥保护如使用信号量、互斥锁。USB MSC的读写回调可能在任何时候被调用。一个实用的调试技巧构建一个“调试模式”。在代码中通过一个引脚电平或串口命令可以切换设备的工作模式。例如模式1正常MSC设备模式2将设备变成一个USB串口CDC专门用来打印内部调试信息如接收到的SCSI命令、读写地址、错误代码。这样你可以在不占用调试器的情况下实时观察设备与主机的交互极大提升调试效率。开发一个稳定可靠的USB MSC设备是一个系统工程涉及硬件、固件、协议和驱动。它要求开发者有细致的耐心和系统性的调试方法。从最简单的SPI Flash开始逐步增加复杂度每完成一个功能就充分测试是通往成功的稳妥路径。当你第一次看到自己的设备在电脑上弹出盘符并成功读写时那种成就感会告诉你这一切的折腾都是值得的。