ARTICLE DETAIL

资讯详情

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

STM32 USB Host读取U盘完整指南:从MSC协议到FatFs文件系统

STM32 USB Host读取U盘完整指南:从MSC协议到FatFs文件系统 1. 项目背景与整体思路1.1 这个培训到底要解决什么问题2018年5月我参加了一场STM32 USB OTG的专题培训目标很明确在一个STM32平台上跑通USB Host功能插入U盘后能正确枚举设备并且能通过文件系统读取U盘里的文件。听起来简单但中间牵扯到的知识点非常多USB协议栈、MSC类协议、SCSI命令、FatFs文件系统任何一环出问题都会导致整个工程跑不起来。当时很多做嵌入式的同行都在玩串口、I2C、SPI这些常规外设对于USB Host这个方向接触得少。因为STM32的USB Host模式比Device模式复杂得多Device模式只需要响应主机发来的请求就行而Host模式要主动发起枚举、分配地址、解析描述符、切换配置还得处理各种U盘的兼容性问题。一整套流程走下来对USB协议的理解要求完全不一样。这个项目的实际应用场景也很广泛比如设备固件升级、数据采集后导出到U盘、设备配置参数的导入导出这些都需要STM32能直接读写U盘。如果把整套逻辑跑通了后面做一个带U盘功能的产品就只是改改应用层的事。1.2 整体架构三层模型这个工程从架构上看分三层底层是STM32的USB OTG硬件控制器中间是USB Host协议栈负责枚举和管理USB设备上层是文件系统层负责把U盘里的块数据翻译成文件和目录。我在培训中实际搭建的架构是这样的物理层STM32F4系列芯片自带的OTG_FS外设支持Host模式DMA传输。协议层ST官方HAL库中的USB Host库启用MSC类Mass Storage Class负责处理枚举、BOT传输、SCSI命令。文件层FatFs文件系统组件挂载在MSC类之上通过磁盘IO接口读写U盘扇区。这个三层架构非常清晰每一层的职责单一调试的时候也能分层排查。比如枚举失败了问题大概率出在底层或协议层枚举正常但读文件报错那就大概率出在文件层或SCSI命令交互上。用三层架构还有个好处可以独立替换某一层。比如不想用FatFs可以换LittleFS或者只读FAT不想用HAL库的USB Host库也可以自己写一个精简的枚举流程。方案灵活也便于后续迁移到其他平台。1.3 硬件准备主控、U盘和供电的坑2018年那会儿市面上最常用的方案是STM32F407系列板载USB OTG FS接口主频168MHz跑USB Host绰绰有余。如果使用更高端的STM32F429或者F746还支持HS模式但这次培训用FS就足够了毕竟U盘的读取速率瓶颈不在USB上而在U盘自身的写入读取速度上。这里要特别提醒一下供电问题。U盘在工作时需要的电流远大于普通GPIO能提供的电流尤其是一些老U盘启动瞬间的电流可能达到几百毫安。如果直接用一个引脚去控制U盘的供电大概率会出现枚举失败或者U盘反复掉线的情况。培训中我们专门强调了这个点正确做法是使用外部LDO或DC-DC模块从5V电源单独拉一路给U盘供电然后用MOS管或电源管理芯片做开关控制。还要注意OTG ID引脚和VBUS引脚的接法。STM32在Host模式下VBUS必须由外部电源提供不能指望芯片内部供电。VBUS感知引脚如果没接对USB Host库会认为没有设备插入导致整个Host流程无法启动。这些硬件上的细节在原理图设计阶段就要考虑清楚。2. STM32 USB OTG核心机制与协议拆解2.1 OTG到底是怎么一回事本质上STM32的OTG外设支持三种模式Host主机模式、Device设备模式、OTG协商模式。在Host模式下STM32是整个USB总线的主控者负责发起一切通信在Device模式下STM32是外设等待主机来查询。这个项目里我们要做的是Host模式也就是说STM32要主动去“认出”插进来的U盘给它分配地址读取它的描述符然后发起MSC存储类通信。很多初学者会混淆OTG和Host的概念。OTG是USB规范中的一种补充协议主要解决“两个设备都是双角色设备时谁当主机谁当从机”的问题。但在实际嵌入式开发中我们大多数时候都是把OTG外设固定配置为Host模式不去做角色协商。即使板子上只有一个USB口也完全可以通过软件配置让它只跑Host功能。培训中用到的F407开发板USB OTG FS接口默认支持Host和Device两种角色。在Host模式下芯片的ID引脚接到地或者配置为Host模式时忽略ID外设就固定以主机身份工作。这里有一个常见误区很多人以为插上U盘、代码跑起来USB通信就自动建立了。实际上USB是轮询式总线Host必须每隔一段时间发送SOF包设备才能维持正常工作。如果Host没有正确启动SOF发送U盘就算插上也不会被识别。而SOF的发送依赖于USB时钟和PHY的正确配置这也是为什么很多人第一步就卡在枚举失败上。2.2 STM32 USB控制器的硬件细节STM32的USB OTG控制器分两类FS全速和HS高速。F407内置的是OTG_FS只支持FS模式速率12Mbps。虽然看起来比HS的480Mbps慢很多但读取U盘绰绰有余因为U盘的机械结构和Flash读写速度本身也是几十MB/s级别FS的1.5MB/s理论带宽对一般应用足够。OTG_FS外设还需要一个外部PHY吗不需要。F407的OTG_FS内置了PHY可以直接通过PA11DM、PA12DP连接到USB座子。只要硬件电路正确直接用这两根线就行。但如果是OTG_HS外设那就不一样了HS模式下必须外接ULPI接口的高速PHY芯片比如USB3300或者USB3320数据线从ULPI接口过一遍不能直接接USB座子。我见过很多朋友拿F407的HS接口接U盘结果发现根本识别不了。原因就是HS外设缺少外部PHY而芯片内部的HS PHY实际没有实现完整功能。F407的HS如果要跑高速模式必须外接ULPI PHY。但有个小技巧是HS外设可以配置为“HS core FS PHY”模式也就是使用HS的外设核心但选择FS模式这时可以复用内置的FS PHY但速率仍然是FS而且能正常使用。这个对我们做U盘读取来说完全够用。2.3 MSC类协议U盘怎么和主机对话USB协议本身非常抽象它定义了四种传输类型控制传输、批量传输、中断传输、等时传输。U盘属于MSC类设备主要使用控制传输枚举阶段和批量传输数据传输阶段。枚举阶段Host通过控制传输向地址0发送标准请求获取设备描述符、配置描述符、接口描述符、端点描述符。这个过程最关键的点在于设备在枚举期间还没有正式地址所有通信都走地址0。Host先把地址分配给设备Set Address请求然后再用新地址继续沟通。枚举完成后MSC类设备会暴露两个批量端点一个Bulk Out主机到设备一个Bulk In设备到主机。后续所有的SCSI命令都是通过BOTBulk-Only Transport协议封装在批量端点上传输。BOT协议每笔通信分为三个阶段命令阶段CBW、数据阶段可选、状态阶段CSW。CBW是31字节的命令块包含命令签名、命令标签、数据传输方向和长度、以及SCSI命令块。设备执行完命令后会返回13字节的CSW状态块告诉主机命令执行成功还是失败。我们不需要自己手动去组CBW和CSW。ST的USB Host库MSC类已经封装好了这些逻辑应用层只需要调用USBH_MSC_Read和USBH_MSC_Write就能读写扇区。但理解底层机制很重要因为很多诡异的兼容性问题最终都需要看协议交互才能定位。SCSI命令中常用的有INQUIRY查询设备信息、READ CAPACITY读容量、READ10读扇区、WRITE10写扇区、TEST UNIT READY设备就绪检查。FatFs在调用disk_read时底层就是通过SCSI READ10命令实现的。2.4 FatFs文件系统和U盘的分区表U盘在出厂时通常会被格式化成一个或多个分区分区里才是FAT32/exFAT文件系统。FatFs是一个专为嵌入式设计的文件系统库它不关心底层是什么存储介质只要提供六个底层磁盘IO函数就能在这个“磁盘”上实现文件和目录操作。但有一个容易被忽略的点FatFs处理的是“裸磁盘”它并不认识MBR分区表。如果U盘有分区表而我们把disk_read直接指向U盘的第一个物理扇区FatFs会把这个扇区当作引导扇区来解析最终报错不识别。标准做法是在disk_initialize阶段扫描MBR分区表找到活动分区的起始LBA然后通过disk_ioctl的CTRL_SYNC和GET_SECTOR_SIZE等命令把逻辑盘映射到那个分区上去。我自己在项目中用得最多的方案是U盘只用一个分区格式化为FAT32然后在FatFs的disk_initialize里读取MBR扇区解析出第一个分区的起始偏移之后的读写都加上这个偏移。这样就绕开了多分区的复杂度而且兼容性最好。另外需要注意exFAT的问题。官方FatFs是支持exFAT的但需要启用_USE_EXFAT宏并且某些老版本的FatFs库对exFAT支持不完善。如果用户插入一个exFAT格式的U盘挂载会失败报FR_NO_FILESYSTEM。这个在做产品时一定要提前想清楚最好在产品文档里明确支持的U盘格式或者在软件里做提示。3. 工程配置与代码实现3.1 CubeMX配置从零开始建立工程这个项目我推荐用STM32CubeMX生成HAL库工程可以省去很多外设初始化代码的编写时间。关键配置如下时钟树外部晶振25MHzPLL倍频到168MHzUSB时钟必须为48MHz。F407的USB外设时钟来自PLLQ配置时要确保PLLQ输出的频率是48MHz。USB OTG FS配置为Host模式启用内部PHY。在CubeMX中选择USB_OTG_FS外设Mode选Host_Only然后使能MSC类。USB Host库参数在Middleware项中勾选USB_HOST开启MSC类。这一步会生成整个USB Host协议栈包括枚举、类驱动管理、状态机等。FatFs在Middleware中勾选FATFS并选择USB Disk作为物理介质。CubeMX会自动把diskio底层函数和USB MSC驱动关联起来。这里有个细节CubeMX生成的USB Host代码中默认使用轮询方式处理USB事件即main函数里要反复调用MX_USB_HOST_Process()。如果使用OS比如FreeRTOS可以把USB Host处理放到一个独立任务里但要注意优先级和响应时间不能让USB任务被其他任务饿死。对于2018年的培训来说当时CubeMX生成的代码还是1.4.X分支的HAL库和现在的版本有一些差异但整体流程没有本质变化。如果你现在用的是新版CubeMX6.X配HAL 1.11不要慌思维框架是一样的只是API命名和部分返回值有调整。3.2 USB Host库驱动层状态机与回调USB Host库的核心是一个状态机它负责管理从设备插入到枚举完成再到类连接成功的整个流程。这个状态机在USBH_Process函数中不断推进应用代码只需要在主循环里持续调用它。关键代码如下// 主循环中反复执行USB Host处理确保状态机持续运行 while (1) { MX_USB_HOST_Process(); // 处理应用层逻辑比如FATFS操作 }Host库的状态机流程大致是HOST_IDLE等待连接、HOST_DEV_ATTACHED检测到设备插入、HOST_ENUM枚举、HOST_CLASS_REQUEST配置类、HOST_CLASS类工作状态。每个状态都有对应的处理函数比如枚举状态会读取设备描述符、配置描述符、设置地址等。当MSC类设备完成枚举并配置后库会回调USBH_MSC_AppState或者User_MSC_App这类应用层函数通知上层“设备已就绪”。此时应用层就可以开始挂载文件系统了。回调函数里通常要处理三类事件设备连接、设备就绪、设备断开。设备断开时必须做资源清理包括卸载文件系统、关闭文件句柄、重置状态标志否则下一次插入U盘时会残留旧状态导致挂载失败。培训中我犯过一个低级错误在回调里直接做了文件操作但回调是在USB中断上下文执行的有些操作不允许在此处执行导致系统死机。正确做法是在回调里只设置一个标志位把真正的文件操作放到主循环中处理。3.3 FatFs适配层六个底层函数必须写对FatFs通过底层磁盘IO函数和USB MSC驱动交互CubeMX生成的代码已经帮我们搭好了框架但还是要理解每个函数在干什么否则出了问题很难定位。这六个函数是disk_initialize初始化磁盘。对于U盘来说通常不需要做额外工作但可以在这里调用USBH_MSC_Read读取MBR扇区确认分区存在。disk_status检查磁盘状态。如果USB状态正常返回0OK。disk_read读扇区。底层调用USBH_MSC_Read参数有扇区号、缓冲区指针、扇区数。disk_write写扇区。底层调用USBH_MSC_Write。disk_ioctl控制命令包括获取扇区数、扇区大小、擦除块大小等。get_fattime获取当前时间用于文件时间戳。这里可以直接返回一个固定值或者从RTC读时间。disk_read和disk_write最重要参数中有一个SECTOR_SIZE的概念不同的U盘扇区大小可能不一样常见512字节也有4096字节的。FatFs的_MAX_SS宏要设置为支持的最大扇区大小否则遇到4K扇区的U盘会读取失败。我习惯在disk_initialize里先做一次扇区读取测试确认MSC层通信正常再解析MBR分区。这样可以把“USB枚举问题”和“文件系统问题”分开不至于糊成一团。3.4 应用层逻辑怎么读取U盘里的文件应用层是整个工程的“门面”用户通常不关心底层只关心能不能读到文件。我习惯把应用逻辑设计成一个简单的状态机等待U盘插入、挂载文件系统、遍历文件并读取、安全卸载。核心代码如下// 主循环中的应用层逻辑 if (USBH_IsDeviceConnected(hUsbHostHS)) { if (!fs_ready) { if (f_mount(fs, , 1) FR_OK) { fs_ready 1; // 打开文件并读取 FIL fil; if (f_open(fil, 0:/test.txt, FA_READ) FR_OK) { UINT bytes_read; uint8_t buf[128]; f_read(fil, buf, sizeof(buf), bytes_read); f_close(fil); } } else { // 挂载失败提示用户检查U盘格式 } } } else { if (fs_ready) { f_mount(NULL, , 1); // 卸载文件系统 fs_ready 0; } }这里要注意路径的写法。FatFs的盘符逻辑是第一个磁盘挂载为“0:”第二个为“1:”依此类推。我们这里只有一个U盘所以路径是“0:/test.txt”。如果开发板上有SD卡和U盘共存两个磁盘的盘符分配要理清楚避免冲突。文件读取完之后最好做f_mount(NULL, , 1)卸载文件系统。这一步很多人忽略但如果不卸载U盘拔出时可能还有缓存的写入数据没有落盘导致文件损坏。4. 实操过程与核心环节实现4.1 从CubeMX生成工程到首次编译整个过程的第一步是生成工程。我在CubeMX中选择了STM32F407VET6时钟配置为168MHzUSB_OTG_FS配置为Host模式Middleware勾选USB_HOSTMSC类和FATFS。生成后打开Keil工程直接编译。这里经常会遇到的第一个坑是USB Host库和FatFs库中有重复的符号定义比如某些宏或者变量重名。解决办法是检查编译器的Include路径确保库的版本一致不要混用不同版本的HAL库和中间件。还有一个常见问题是堆栈溢出。USB Host库和FatFs都是比较吃内存的组件如果栈的大小设置得太小程序会在运行时莫名其妙地跑飞。我在培训中建议把Stack Size设置为0x20008KB以上Heap Size设置为0x10004KB以上。这个配置在Keil的Startup文件中修改或者在CubeMX的Project Manager中设置。编译通过后先不要急着插U盘把工程烧录到开发板上用串口打印一些调试信息确认USB Host库的状态机在正常轮询。如果串口能看到设备插入检测事件说明底层基本没问题。4.2 串口调试日志USB状态监控调试USB Host工程时串口日志是命脉。我在工程里加了一个简单的日志模块通过USART1打印USB Host的状态变化和FATFS操作的返回码。这样每一步都能看到系统的行为而不是黑盒运行。关键的日志点包括USBH_Process主状态机的状态切换HOST_IDLE → HOST_ENUM → HOST_CLASSMSC类连接回调触发disk_initialize的返回值挂载FatFs的返回码文件打开、读取的返回码串口输出效果类似USB Host Started Device Attached Enumeration Success MSC Class Connected Mount OK: FR_OK Open 0:/test.txt: FR_OK Data: Hello from STM32 USB Host!有了这些日志当U盘插入但读不到文件时就能明确问题出在哪个阶段没走枚举流程枚举失败MSC没连上还是FatFs挂不上逐层排查效率高很多。4.3 实机测试不同U盘的兼容性验证培训现场我们准备了几个不同品牌、不同容量的U盘做测试结果发现兼容性问题非常明显。一个老旧的2GB U盘能正常读写另一个32GB的新款U盘却在枚举阶段就失败。排查后发现问题出在新U盘在SCSI INQUIRY命令的响应上可能返回了比较长的厂商信息导致缓冲区溢出。遇到这种情况建议通过USB分析仪抓包查看USB总线上的实际数据交互能很快定位是协议交互的问题还是命令超时问题。对于普通开发者使用一个简单的USB分析仪或虚拟USB分析软件比如Wireshark配合USBPcap在PC端抓包也能看到总线数据流但嵌入式端裸跑时更常用的是在代码里加打印把每个SCSI命令的响应状态打出来。培训中我们测试的U盘格式全部为FAT32没有一个用exFAT或NTFS。之所以不用NTFS是因为FatFs根本不支持NTFS本来就不在考虑范围内。而exFAT虽然在官方版本中支持但需要单独开启配置宏且2018年那会儿ST的USB Host库和FatFs版本对exFAT的支持还不稳定稳妥起见我们统一用FAT32。如果是现在做产品我依然建议默认要求用户使用FAT32格式的U盘或者由软件同时支持FAT32和exFAT并在检测到不支持的文件系统时给出明确提示。4.4 代码优化与细节打磨超时和重试机制实测中还会遇到一个很魔性的问题U盘插入后偶尔会出现第一次挂载失败拔插一次就好了。这个问题的根源在于有些U盘在插入后的几毫秒内还没有完成内部初始化如果此时主机发送SCSI命令U盘可能直接无响应或者返回错误。解决问题的办法是加超时和重试机制。在MSC类设备连接成功后不立即挂载文件系统而是先循环发送TEST UNIT READY命令等待U盘返回Ready状态最多等2-3秒。如果超时就报错。// 等待U盘就绪 int timeout 100; // 100 * 20ms 2s while (timeout--) { if (USBH_MSC_TestUnitReady(hUsbHostHS) USBH_OK) break; HAL_Delay(20); }这种重试机制非常有必要U盘的启动时间差异很大有些快有些慢特别是那些内部带有机械结构的U盘虽然现在很少见了需要的时间更长。另外每次读写操作也建议加超时判断。ST的USBH_MSC_Read和USBH_MSC_Write都有超时参数默认可能比较长如果U盘卡死了会导致整个系统长时间停顿。合理设置超时时间比如500ms超时后恢复系统运行不要陷入死循环。5. 常见问题与排查技巧实录5.1 U盘插上后毫无反应状态机不跳转这是最基础也最常见的问题。第一步检查硬件接线PA11DM、PA12DP是否和USB座子的D-、D对应正确如果两根数据线接反了设备完全无法被识别。其次检查VBUS是否正常供电PA9VBUS感知是否配置正确。软件层面确认USB_OTG_FS的时钟使能是否正确。在F407中USB OTG FS外设依赖AHB1总线时钟和48MHz的PLLQ时钟。如果PLLQ配置错误USB PHY无法正常工作状态机永远停在HOST_IDLE。我当时排查过一个现象U盘插入后状态机偶尔能走通偶尔卡死。用示波器测量后发现有时候D线的上拉信号不稳定。这是因为U盘供电电压跌落导致USB差分电平异常。把U盘供电从单片机电源改为外部独立的5V电源后问题彻底消失。5.2 枚举成功但MSC类无法连接枚举成功说明USB底层通信已经建立设备描述符也读回来了。但MSC类连接失败通常是设备端点信息不匹配。有些U盘的Bulk端点大小不是标准的512字节而是64字节如果Host库实现里对端点大小做了硬编码就会导致配置失败。检查ST的USBH_MSC类驱动找到端点描述符解析部分确保从描述符中读取实际端点大小而不是使用固定值。这个在CubeMX生成的库中默认是支持的但如果你从旧版本移植代码就要留意。另一个原因是多配置设备。部分U盘有多个配置描述符默认使用第一个配置但如果第一个配置不包含MSC接口而Host库没有正确选择后续配置MSC类自然无法初始化。这种情况比较少见但遇到时可以通过USB分析仪确认。5.3 MSC连接成功但FatFs挂载失败挂载失败分两种情况FR_NO_FILESYSTEM或FR_NOT_ENABLED。FR_NO_FILESYSTEM表示FatFs在磁盘上找不到合法的FAT文件系统。常见原因是MBR分区表解析出错。我之前遇到一个情况disk_initialize里读取扇区时没有对MBR扇区做偏移处理直接把整个U盘当作FAT卷导致FatFs在引导扇区找不到合法标记。FR_NOT_ENABLED多半是配置问题检查_MIN_SS和_MAX_SS宏是否适配了U盘的扇区大小以及_USE_MKFS如果需要格式化是否开启。还有一个小细节FatFs的f_mount的第二个参数是挂载点路径第三个参数是立即挂载标志。如果传1FatFs会立即尝试挂载如果传0只注册工作区后续首次访问时才挂载。这个标志不要搞错否则会出现明明挂载了但访问还是失败的错觉。5.4 读写文件时随机出现超时或数据错误这类问题的元凶往往是DMA和Cache一致性问题。STM32F4没有D-Cache但DMA缓冲区的对齐和内存分配仍然需要注意。USB Host库的DMA缓冲区要求4字节对齐如果使用非对齐的内存地址DMA传输会失败或者数据损坏。解决办法是在定义缓冲区时使用__ALIGN_BEGIN修饰符或者使用uint32_t数组来保证4字节对齐。例如__ALIGN_BEGIN static uint8_t buf[512] __ALIGN_END;另外如果开了中断优先级嵌套USB Host中断优先级过低可能会被其他高频中断打断导致USB事务超时。把USB OTG全局中断优先级调到较高等级能有效减少这种数据错误。5.5 兼容性差异为什么有的U盘就是不行培训中特别强调U盘兼容性问题是最难啃的硬骨头。即使USB协议是标准化的不同厂商的实现细节差异极大。有的U盘在RESET后需要额外的等待时间有的U盘对SCSI命令的长度要求严格有的U盘支持Auto-Parity但响应异常。应对思路是“软硬结合”软件上加入重试机制和超时机制尽量允许部分U盘以“慢速模式”工作。硬件上保证U盘供电稳定数据线短而粗必要时在D和D-上串联22Ω电阻以抑制信号反射。测试上准备尽可能多的不同品牌U盘来做兼容性测试建立自己的U盘兼容性清单为后续产品发布提供依据。在实际项目中不可能做到100%兼容所有U盘但通过良好的协议实现和充分的测试能达到95%以上的兼容率对大多数应用场景已经足够。6. 从培训到实战的几点体会6.1 动手做一遍比看十遍文档有用这个培训最大的收获不是代码本身而是完整地理解了USB Host的工作链路。以前看协议文档总觉得抽象什么控制传输、批量传输、描述符翻来覆去记不住。但真的跑通一个U盘读取工程后这些概念就变成了看得见摸得着的东西我能知道什么时候切片在哪里、CSW在哪里、数据在哪里。所以如果你也想入门STM32 USB Host不要先啃《USB 2.0规范》先照着CubeMX生成一个工程烧进去用串口打印日志一步步看状态机怎么跳。代码会告诉你一切问题也会逼你去查文档。6.2 调试工具不要舍不得买做一个USB Host项目建议至少准备一个USB分析仪。虽然可以用逻辑分析仪和示波器替代但USB分析仪能直接解析协议层节省大量时间。我用的是一台百来块钱的USB分析仪对于调试MSC设备足够用了。如果没有分析仪也可以通过软件在PC端装一个虚拟USB分析工具把STM32的USB Host功能通过电脑的USB口扩展出来在PC上抓取USB总线数据。这种方法虽然不是实时分析底层PHY信号但对协议调试帮助很大。6.3 扩展方向这个工程还能怎么玩跑通U盘读取只是第一步后面可以扩展的方向很多。比如增加U盘写入功能实现数据日志导出。把U盘作为固件升级的载体在Bootloader中读取U盘里的固件文件进行升级。结合外部传感器把采集到的数据直接写入U盘做成便携式数据记录仪。增加对USB Hub的支持让一个STM32同时管理多个USB设备。把FatFs换成LittleFS或只读的ROMFS适配不同的应用场景。这些扩展方向本质上都没有脱离这个基础工程的核心USB Host协议栈加文件系统。底层框架跑通了上层逻辑只是堆功能而已。2018年那次培训结束后我把这套工程扩展成了一个支持FAT32和exFAT的U盘固件升级方案在后来的多个产品中反复复用替我省了不少时间。希望这篇经验分享也能帮你少走一些弯路。
返回列表