1. 项目缘起与核心需求拆解
1.1 为什么要在ESP32-P4上折腾USB读卡器
第一次拿到ESP32-P4这块芯片的时候,我盯着它的USB OTG接口看了很久。之前用ESP32-S3做USB相关项目,要么是CDC虚拟串口,要么是HID设备,总感觉没把USB的潜力完全榨干。P4不一样,它原生带了一个高速USB OTG控制器,支持USB 2.0 High-Speed,理论带宽480Mbps,这个规格放在MCU圈子里相当能打。
所谓USB读卡器(Slave)实验,本质上就是让ESP32-P4扮演一个USB大容量存储设备(MSC,Mass Storage Class),把板子上挂载的SD卡或者SPI Flash模拟成一个U盘。插到电脑上,系统识别出来就是一个可移动磁盘,能直接拖文件进去,也能把里面的东西拷出来。这个功能听起来简单,但背后涉及的东西不少:USB协议栈的枚举过程、MSC类的BOT传输协议、底层存储介质的读写适配、文件系统的挂载,还有TinyUSB这个协议栈在ESP-IDF里的集成方式。
我之所以选这个方向来写,是因为在实际项目中它的应用场景非常明确。比如工业数据采集设备,现场没有网络,工人需要把设备里的日志导出来,最直接的办法就是插根USB线到电脑上,设备变成一个U盘,日志文件直接拖走。再比如一些带SD卡的音乐播放器或者录音笔,用户想换歌、导录音,也是同样的逻辑。比起写一个上位机软件通过串口慢慢传,USB MSC的体验好太多了,即插即用,不需要装驱动,Windows、macOS、Linux通吃。
这个实验适合谁看?我觉得有三类人。第一类是刚接触ESP32-P4和ESP-IDF的开发者,想找一个综合性强但又不太偏门的项目练手,USB MSC刚好覆盖了外设驱动、协议栈、文件系统几个层面。第二类是做产品开发的工程师,手头项目需要文件导出功能,想评估P4能不能胜任。第三类是对USB协议本身感兴趣的人,想通过一个实际案例理解MSC类是怎么工作的。不管你是哪一类,只要跟着把实验跑通,再回头看看代码,收获都不会小。
1.2 核心关键词背后的技术脉络
把标题里的几个关键词拆开看,其实能理出一条完整的技术链路。ESP32-P4是硬件平台,它提供了USB OTG控制器和SDIO/SPI接口;USB OTG是物理层和协议层的基础,决定了设备能不能作为Slave被主机识别;TinyUSB是跑在ESP-IDF上面的USB协议栈,负责处理枚举、描述符、端点传输这些脏活累活;ESP-IDF是开发框架,提供编译工具链、驱动库和FreeRTOS运行环境;USB读卡器是最终呈现的功能形态,用户看到的是一个U盘,但背后是MSC协议在支撑。
这条链路里,TinyUSB的角色特别关键。早期ESP32系列用的是乐鑫自己写的USB协议栈,后来逐步迁移到TinyUSB上,因为TinyUSB是开源社区维护的,设备类支持更全,MSC、CDC、HID、MIDI都有现成实现,移植起来省事。在ESP-IDF里,TinyUSB被封装成组件,通过idf.py menuconfig可以配置启用哪些类,底层跟ESP32-P4的USB HAL对接。你要做的,主要是实现MSC类的回调函数,告诉协议栈“读哪个扇区、写哪个扇区、容量多大”,剩下的枚举、端点管理、SCSI命令解析,TinyUSB都帮你处理了。
这里有个容易混淆的点:USB MSC设备跟文件系统是两码事。电脑通过USB看到的是一块块扇区,它自己会去解析FAT文件系统。所以ESP32-P4这边只需要把SD卡或者Flash的原始扇区暴露出去就行,不需要在设备端挂载FATFS。当然,如果你设备端也要同时读写这些文件,那就得注意两边不能同时操作,否则文件系统会损坏。这个坑后面会详细说。
2. 整体方案设计与选型考量
2.1 硬件平台与存储介质的选择
ESP32-P4开发板通常板载了一颗SD卡槽,通过SDIO或者SPI跟主控连接。做USB读卡器实验,最自然的方案就是把SD卡作为存储介质,因为SD卡本身就是块设备,扇区读写接口很标准,TinyUSB的MSC回调直接对接SDMMC驱动就行。另一种方案是用SPI Flash的某个分区来模拟,但Flash有擦除块大小的限制,扇区读写需要做映射,实现起来麻烦一些,而且容量通常不大,实用性有限。
我手头这块P4开发板用的是SDIO 4-bit模式,理论上读写速度能到几十MB/s,但实际受限于卡本身的速度等级和USB FS/HS模式。这里要特别注意:ESP32-P4的USB OTG支持High-Speed,但很多开发板为了省成本,USB接口上没加高速PHY,只能跑Full-Speed,也就是12Mbps。这个差距很大,FS模式下实际文件传输速度大概在1MB/s左右,拷个大文件会等到怀疑人生。所以选板子的时候一定要确认USB口是不是支持HS,或者至少心里有数。
存储介质选SD卡还有个好处:容量灵活。你插个32GB的卡,电脑上就显示32GB的U盘。但要注意SD卡的分区表和文件系统格式,电脑识别U盘依赖MBR分区表和FAT32/exFAT文件系统。如果卡是空的或者格式不对,电脑会提示“需要格式化”。所以实验前最好用读卡器在电脑上把SD卡格式化成FAT32,再插回开发板。
2.2 TinyUSB在ESP-IDF中的集成方式
ESP-IDF从v5.0开始把TinyUSB作为官方组件集成进来,但默认可能没启用。你需要通过idf.py menuconfig进入Component config -> TinyUSB Stack,勾选启用,然后选择Mass Storage Class。这里有个细节:TinyUSB的MSC类支持两种模式,一种是MSC with RAM disk,一种是MSC with SD card。前者用内存模拟一个盘,适合测试;后者对接真实存储,才是我们要的。
配置的时候还要注意USB模式的选择。ESP32-P4的USB OTG可以配置成Host、Device或者OTG模式。做读卡器,设备要作为Slave被电脑识别,所以选Device模式。在menuconfig里对应TinyUSB Stack -> USB Peripheral,然后选Device (Peripheral)。如果你选错了,编译出来的固件插到电脑上没反应,排查半天才发现模式不对,这种低级错误我见过不止一次。
另外,TinyUSB的任务优先级和堆栈大小也需要关注。默认配置下,TinyUSB跑在一个独立的任务里,优先级是5,堆栈4096字节。如果你在MSC回调里做了耗时操作,比如等待SD卡写入完成,可能会阻塞USB任务,导致主机端超时。我的做法是把SD卡读写放在回调里同步执行,但确保SDMMC驱动的超时时间设置合理,不要动辄等几秒。如果实在慢,可以考虑用双缓冲或者DMA,但那是进阶优化了。
2.3 文件系统层面的取舍
前面提到,设备端不需要挂载FATFS就能让电脑识别U盘。但实际项目中,设备本身往往也要读写这些文件。比如数据采集设备,它自己要把传感器数据写成文件存到SD卡,同时又要通过USB把文件导出去。这时候就有两种架构:
第一种是“独占式”,设备端挂载FATFS,USB MSC回调直接读写FATFS文件。但这样有个问题:电脑通过USB访问的是扇区,它不知道设备端FATFS的缓存状态,两边同时写会冲突。所以通常的做法是,当USB连接建立后,设备端卸载FATFS,把SD卡控制权完全交给USB;USB断开后,再重新挂载。这个切换逻辑需要处理好,否则电脑上看到的数据可能不是最新的。
第二种是“透传式”,设备端不挂载FATFS,所有文件操作都通过USB由电脑完成。设备端只负责采集原始数据,存成二进制格式,电脑端再用工具解析。这种方式简单,但用户体验差一些,因为设备脱离电脑就不能独立工作。
我这次实验选的是第一种架构的简化版:设备端启动时挂载SD卡,做一些初始化检查,然后卸载,把控制权交给USB。这样电脑插上就能看到盘,拔掉后设备重新挂载。虽然切换有点繁琐,但逻辑清晰,不容易出问题。如果你要做产品,建议在USB连接状态变化时加个状态机,用GPIO或者软件标志位来协调。
3. 核心细节解析与实操要点
3.1 USB描述符的配置与枚举过程
USB设备被电脑识别,第一步是枚举。枚举过程中,主机读取设备描述符、配置描述符、接口描述符、端点描述符,还有字符串描述符。TinyUSB帮你生成了大部分描述符,但有些字段需要你根据实际情况修改,比如VID、PID、厂商字符串、产品字符串。
VID和PID特别重要。如果你随便填一个,可能会跟系统里已有设备冲突,导致识别异常。建议用乐鑫的VID(0x303A)加上自己定义的PID,或者申请一个正规的VID。实验阶段用默认的也行,但产品里一定要改。字符串描述符支持Unicode,你可以把产品名写成“ESP32-P4 USB Disk”,这样电脑上显示的就是这个名字,而不是一堆乱码。
MSC类的接口描述符里有个bInterfaceSubClass字段,通常填0x06表示SCSI透明命令集,bInterfaceProtocol填0x50表示BOT(Bulk-Only Transport)协议。这两个值不能乱改,否则主机不认。端点方面,MSC需要一对Bulk端点,一个IN一个OUT,最大包长在High-Speed下是512字节,Full-Speed下是64字节。TinyUSB会根据你配置的USB速度自动调整,但你要确保端点地址不冲突。
枚举过程中还有个细节:主机可能会发送GET_MAX_LUN请求,询问设备支持几个逻辑单元(LUN)。读卡器通常只支持一个LUN,返回0就行。TinyUSB的MSC类默认处理了这个请求,你不需要额外写代码。但如果你的设备要模拟多个盘符,比如同时暴露SD卡和Flash,那就需要返回1,并在回调里根据LUN号区分操作对象。这个实验只做一个盘,所以不用操心。
3.2 MSC回调函数的实现要点
TinyUSB的MSC类要求你实现几个回调函数,最核心的是tud_msc_read10_cb和tud_msc_write10_cb,分别处理主机的读和写请求。这两个函数的参数包括LUN号、扇区地址、扇区数量、数据缓冲区指针。你要做的就是把数据从存储介质读到缓冲区,或者从缓冲区写到存储介质。
这里有个关键点:扇区大小。标准MSC设备扇区是512字节,但SD卡的物理扇区可能是512字节,也可能是4096字节。SDMMC驱动通常按512字节块读写,所以直接对接没问题。但如果你用SPI Flash模拟,Flash的擦除块是4KB,写之前要先擦除,而且擦除操作很慢,会导致USB传输超时。所以Flash方案只适合只读或者很少写的场景。
读写回调里还要注意数据对齐。SDMMC驱动通常要求缓冲区地址4字节对齐,DMA传输甚至要求更大对齐。TinyUSB传进来的缓冲区地址不一定满足这个要求,所以你可能需要用一个中间缓冲区做拷贝。这会增加内存开销和CPU占用,但能保证稳定性。我的做法是定义一个512字节的静态缓冲区,加上__attribute__((aligned(4))),读写时先拷贝到这个缓冲区,再调用SDMMC接口。
另外,回调函数的返回值很重要。返回true表示操作成功,返回false表示失败,主机会收到错误。如果SD卡读写失败,比如卡被拔了,你要返回false,并且最好在日志里打印错误信息,方便排查。但不要在里面做太复杂的错误恢复,因为USB任务上下文不允许阻塞太久。
3.3 SD卡驱动的初始化与性能调优
SD卡驱动这块,ESP-IDF提供了sdmmc组件,支持SDIO和SPI两种模式。SDIO模式速度快,但占用引脚多;SPI模式引脚少,但速度慢。P4开发板一般用SDIO 4-bit,初始化流程包括:配置SDMMC主机、设置时钟频率、发送CMD0复位、CMD8检查电压、ACMD41初始化、CMD2获取CID、CMD3获取RCA、CMD9获取CSD、CMD7选中卡、ACMD6设置总线宽度、CMD6切换高速模式。
这一串命令听起来复杂,但sdmmc_host_init和sdmmc_card_init两个函数基本都帮你做了。你需要关注的是时钟频率。初始化阶段用400kHz,识别完成后可以提高到20MHz甚至40MHz。但频率越高,信号完整性要求越高,PCB走线不好的话容易出错。如果发现读写不稳定,先把频率降到10MHz试试。
性能调优方面,有几个参数可以调。一是sdmmc_host_set_bus_width,设成4位比1位快很多。二是sdmmc_host_set_card_clk,在卡支持的前提下尽量提高。三是DMA缓冲区大小,ESP-IDF默认是4096字节,可以适当增大。四是CONFIG_SDMMC_ISR_IRAM_SAFE,如果中断服务程序在IRAM里跑,响应更快,但会占用IRAM空间。
实测下来,SDIO 4-bit @ 40MHz,读速度能到20MB/s左右,写速度10MB/s左右。但USB Full-Speed只有12Mbps,也就是1.5MB/s,所以瓶颈在USB这边。如果你板子支持High-Speed,那SD卡的速度就能发挥出来,读速度能到30MB/s以上。所以再次强调,选板子要看USB速度。
4. 实操过程与核心环节实现
4.1 开发环境搭建与工程创建
先确保ESP-IDF环境装好。我用的是v5.2版本,配合VS Code的ESP-IDF插件。安装过程网上教程很多,这里不赘述。需要注意的是,ESP32-P4的支持在v5.1之后才比较完善,所以尽量用新版本。如果你在VS Code的Marketplace里找不到ESP-IDF插件,那可能是搜索关键词不对,应该搜“Espressif IDF”,而不是“ESP-IDF”。安装完成后,用idf.py --version确认版本。
创建工程可以用idf.py create-project usb_msc_demo,然后进入目录。接下来配置目标芯片:idf.py set-target esp32p4。这一步会下载对应的工具链和依赖,如果卡在0%不动,多半是网络问题,可以配置代理或者用离线包。国内用户建议用乐鑫的镜像源,在idf.py menuconfig里可以设置。
工程创建后,打开menuconfig,按顺序配置:Component config -> TinyUSB Stack -> Enable TinyUSB Stack,然后Mass Storage Class选上。接着USB Peripheral选Device。再往下,MSC Class -> MSC with SD card选上。最后在SDMMC配置里,根据你的板子设置引脚和总线宽度。这些配置会生成sdkconfig文件,编译时自动生效。
4.2 关键代码实现与注释
主程序的结构大概是:初始化NVS、初始化SD卡、初始化TinyUSB、启动任务循环。下面是我实际用的代码片段,基于ESP-IDF的示例修改而来。
#include "esp_log.h" #include "esp_vfs_fat.h" #include "sdmmc_cmd.h" #include "driver/sdmmc_host.h" #include "tinyusb.h" #include "tusb_msc_storage.h" static const char *TAG = "USB_MSC"; // SD卡引脚定义,根据实际板子修改 #define PIN_NUM_CLK 43 #define PIN_NUM_CMD 44 #define PIN_NUM_D0 39 void app_main(void) { // 1. 初始化SD卡 sdmmc_host_t host = SDMMC_HOST_DEFAULT(); host.flags = SDMMC_HOST_FLAG_4BIT; host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); slot_config.width = 4; slot_config.clk = PIN_NUM_CLK; slot_config.cmd = PIN_NUM_CMD; slot_config.d0 = PIN_NUM_D0; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5, .allocation_unit_size = 16 * 1024 }; sdmmc_card_t *card; esp_err_t ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { ESP_LOGE(TAG, "SD卡挂载失败: %s", esp_err_to_name(ret)); return; } ESP_LOGI(TAG, "SD卡容量: %llu MB", ((uint64_t)card->csd.capacity) * card->csd.sector_size / (1024 * 1024)); // 2. 卸载FATFS,把控制权交给USB esp_vfs_fat_sdcard_unmount("/sdcard", card); // 3. 初始化TinyUSB MSC存储 tinyusb_msc_sdmmc_config_t msc_config = { .card = card, .callback_mount_changed = NULL, .callback_premount_changed = NULL, .mount_config = mount_config }; ret = tinyusb_msc_storage_init_sdmmc(&msc_config); if (ret != ESP_OK) { ESP_LOGE(TAG, "MSC存储初始化失败: %s", esp_err_to_name(ret)); return; } // 4. 配置TinyUSB tinyusb_config_t tusb_cfg = { .device_descriptor = NULL, .string_descriptor = NULL, .external_phy = false, .configuration_descriptor = NULL }; ret = tinyusb_driver_install(&tusb_cfg); if (ret != ESP_OK) { ESP_LOGE(TAG, "TinyUSB驱动安装失败: %s", esp_err_to_name(ret)); return; } ESP_LOGI(TAG, "USB读卡器已启动,插上USB线试试"); }这段代码的关键在于第2步:挂载SD卡只是为了获取卡的信息和初始化驱动,然后立刻卸载FATFS。因为TinyUSB的MSC存储组件需要直接操作SDMMC驱动,如果FATFS还挂着,两边会冲突。卸载后,card指针仍然有效,可以传给MSC组件。
tinyusb_msc_storage_init_sdmmc这个函数是ESP-IDF封装好的,内部会注册MSC回调,处理扇区读写。你不需要自己写tud_msc_read10_cb,除非有特殊需求。这大大简化了开发,但也意味着灵活性降低。如果你想在读写时加日志或者做数据过滤,就得自己实现回调。
4.3 编译、烧录与验证
编译命令是idf.py build,烧录是idf.py -p /dev/ttyUSB0 flash monitor,Windows下端口可能是COMx。烧录完成后,用USB线把开发板的USB OTG口连到电脑。注意,有些板子有两个USB口,一个是调试串口,一个是OTG口,别插错了。插对之后,电脑会提示发现新硬件,稍等几秒,文件管理器里就会出现一个可移动磁盘。
如果电脑没反应,先看串口日志。正常的话会打印“USB读卡器已启动”。然后检查设备管理器,看有没有未知设备或者带感叹号的设备。如果有,可能是描述符配置有问题,或者USB模式没选对。如果设备管理器里能看到“USB大容量存储设备”,但文件管理器里没有盘符,那可能是SD卡的分区表有问题,需要在电脑上重新格式化。
验证读写速度,可以拷一个几百MB的文件进去,看进度条的速度。Full-Speed下大概1MB/s,High-Speed下能到20MB/s以上。如果速度远低于预期,检查SD卡是不是Class 4的老卡,或者USB线是不是只支持充电不支持数据传输。我遇到过一根劣质USB线,只能充电,数据传不了,换了根线就好了。
5. 常见问题与排查技巧实录
5.1 电脑识别不到设备怎么办
这是最常见的问题,排查思路要系统化。第一步,确认USB线插的是OTG口,不是调试口。第二步,看串口日志有没有打印TinyUSB初始化成功的消息。第三步,在电脑的设备管理器里找“通用串行总线控制器”,看有没有新增设备。如果完全没有,可能是硬件问题,比如USB DP/DM接反了,或者PHY没供电。
如果设备管理器里出现了“未知USB设备(设备描述符请求失败)”,那通常是描述符有问题。检查tinyusb_config_t里的device_descriptor是不是NULL,如果是NULL,TinyUSB会用默认描述符,但默认的VID/PID可能跟系统里某个设备冲突。建议自定义一个描述符,把VID设成0x303A,PID随便设一个不常用的值。
还有一种情况是电脑识别到了,但提示“需要格式化”。这多半是SD卡的分区表或文件系统不对。用读卡器把SD卡插到电脑上,用磁盘管理工具看是不是MBR分区,文件系统是不是FAT32。如果是exFAT,Windows能认,但有些Linux发行版需要额外驱动。最稳妥的是FAT32,兼容性最好。
5.2 读写过程中断或报错
传输大文件时中途断开,首先怀疑供电。ESP32-P4加上SD卡读写,峰值电流可能超过500mA,如果USB口供电不足,电压跌落会导致复位。解决办法是用带外部供电的USB Hub,或者换一个供电能力强的USB口。其次怀疑SD卡接触不良,尤其是用TF卡加卡套的,卡套质量差会导致随机断连。直接换一张好卡试试。
如果串口日志里出现sdmmc_read_sectors failed或者msc_read10_cb failed,那说明SD卡读写出错。先降低SDMMC时钟频率,从40MHz降到20MHz甚至10MHz。如果降频后稳定,说明是信号完整性问题,检查PCB走线或者换短一点的排线。另外,确保SD卡的供电电压稳定,有些卡对电压波动很敏感。
写入过程中报错,还要考虑文件系统缓存。电脑端写文件时,操作系统会缓存数据,过一会儿才真正写入。如果这时候拔掉USB线,缓存数据丢失,文件系统可能损坏。所以拔线前一定要在电脑上“安全弹出”。设备端也可以在检测到USB断开时,重新挂载FATFS并做一次fsync,但TinyUSB的断开检测需要配置回调。
5.3 性能不达预期的优化方向
如果速度只有几百KB/s,先确认USB模式。在串口日志里找TinyUSB: USB speed: Full-Speed还是High-Speed。如果是Full-Speed,那1MB/s就是上限,没法再高了。要提速只能换支持High-Speed的板子。如果是High-Speed但速度还是慢,检查SD卡的速度等级,Class 10以上的卡才能跑到20MB/s。
软件层面,可以调整TinyUSB的任务优先级和堆栈。在menuconfig里把TinyUSB Task Priority从5调到10,堆栈从4096调到8192。这样USB任务能更快响应主机请求。另外,SDMMC的DMA缓冲区可以增大到8KB或16KB,减少中断次数。但要注意内存占用,P4的RAM虽然不小,但也要省着用。
还有一个容易被忽略的点:电脑端的USB控制器。有些老电脑的USB 2.0控制器性能差,或者接了太多设备导致带宽不足。换个USB口,或者直接插在主板后置的USB口上,速度可能会有明显提升。我实测过,同样的板子和卡,插在机箱前置口只有5MB/s,插后置口能到25MB/s。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 电脑完全没反应 | USB口插错、线缆故障、PHY未初始化 | 检查串口日志、换线、换口 | 插OTG口、换数据线、确认menuconfig模式 |
| 设备管理器显示未知设备 | 描述符错误、VID/PID冲突 | 查看设备管理器错误代码 | 自定义描述符、更换PID |
| 提示需要格式化 | 分区表或文件系统不对 | 用磁盘管理查看分区 | 重新格式化为FAT32/MBR |
| 传输中途断开 | 供电不足、SD卡接触不良 | 监测电压、换卡 | 外接供电、换好卡 |
| 读写报错 | 时钟太高、信号完整性差 | 降低SDMMC频率 | 降到20MHz或10MHz |
| 速度只有1MB/s | USB Full-Speed模式 | 查看串口日志 | 换支持High-Speed的板子 |
| 速度波动大 | 电脑USB控制器瓶颈 | 换USB口 | 插后置USB口 |
注意:拔USB线前务必在电脑上安全弹出,否则SD卡文件系统可能损坏,下次挂载会失败。
5.5 独家避坑经验
第一个坑:SD卡热插拔。实验阶段经常要换卡,但SD卡不支持热插拔,带电插拔可能导致卡损坏或者驱动异常。换卡前先断电,或者至少先卸载FATFS。我烧坏过一张卡,就是因为带电拔插,后来再也不敢了。
第二个坑:FATFS和USB同时挂载。前面说过,两边同时操作会冲突。但有时候你忘了卸载FATFS,编译烧录后电脑也能识别,但写入的数据会丢失或者文件系统损坏。所以代码里一定要确保esp_vfs_fat_sdcard_unmount在tinyusb_msc_storage_init_sdmmc之前调用。
第三个坑:字符串描述符长度。TinyUSB对字符串描述符的长度有限制,太长会导致枚举失败。产品名字符串不要超过32个字符,厂商字符串不要超过16个。而且要用UTF-8编码,不要用GBK,否则电脑上显示乱码。
第四个坑:menuconfig配置丢失。有时候改了sdkconfig,但编译时没生效,是因为idf.py build不会自动重新加载配置。改完menuconfig后,最好idf.py fullclean再build,确保配置生效。或者直接编辑sdkconfig.defaults文件,这样每次编译都会应用。
6. 功能扩展与进阶玩法
6.1 多盘符与多LUN支持
基础实验只暴露一个SD卡,但TinyUSB的MSC类支持多个LUN。你可以把SPI Flash的某个分区也模拟成一个盘,这样电脑上会显示两个可移动磁盘。实现方法是修改tud_msc_get_maxlun_cb返回1,然后在读写回调里根据lun参数区分操作对象。LUN 0对应SD卡,LUN 1对应Flash。Flash的读写需要自己做扇区映射,因为Flash擦除块是4KB,而MSC扇区是512字节,写之前要先读整个擦除块到缓冲区,修改后再擦除写入。
这个功能适合需要同时导出日志和配置文件的场景。但要注意,多LUN会增加枚举复杂度和内存开销,而且电脑端可能会把两个盘符搞混。建议只在确实需要的时候用。
6.2 只读模式与写保护
有些场景下,你希望设备只能被电脑读取,不能写入,防止误删数据。TinyUSB的MSC类支持写保护,在tud_msc_write10_cb里直接返回false就行。但这样电脑端会报写错误,体验不好。更好的做法是在SCSI命令层面返回“写保护”状态,让电脑知道设备是只读的。这需要修改tud_msc_scsi_cb,处理SCSI_CMD_WRITE10时返回TUSB_MSC_SCSI_WRITE_PROTECTED。
只读模式适合固件升级或者数据备份场景。设备端把固件文件放在SD卡里,电脑只能读不能改,避免误操作。
6.3 与WiFi/蓝牙共存
ESP32-P4支持WiFi和蓝牙,但USB OTG和无线射频同时工作时,可能会有干扰。尤其是USB High-Speed的480MHz时钟,可能会影响2.4GHz WiFi的接收灵敏度。如果项目里既要USB读卡器又要WiFi,建议在USB传输时降低WiFi吞吐量,或者把WiFi信道固定在1、6、11这几个不重叠的信道。另外,USB的电源滤波要做好,否则射频噪声会串到USB线上,导致传输错误。
我试过同时开WiFi和USB MSC,发现WiFi ping延迟从5ms涨到50ms,但USB传输没受影响。如果对WiFi实时性要求高,建议分时复用,USB传输时暂停WiFi扫描。
6.4 低功耗优化
如果设备是电池供电,USB读卡器功能不能一直开着。可以在检测到USB插入时唤醒,拔出后进入睡眠。ESP32-P4支持USB唤醒,但需要配置PMU和USB PHY的低功耗模式。具体做法是在tinyusb_config_t里启用self_powered和low_power选项,然后在USB断开回调里调用esp_light_sleep_start。但要注意,睡眠期间SD卡也要断电,否则漏电流会很大。
低功耗优化是个细致活,需要测量各模块的电流,逐个关闭。USB PHY在空闲时电流大概几mA,SD卡空闲时也有几mA,加起来对电池续航影响不小。如果产品对功耗敏感,建议只在需要导出数据时才启用USB功能,平时保持关闭。
7. 个人实操体会与建议
这个实验我从头到尾跑了三遍,第一遍用官方示例,第二遍自己改代码,第三遍优化性能和稳定性。踩过的坑不少,但收获也大。最大的体会是:USB MSC看起来简单,但细节很多,尤其是存储介质和协议栈的配合,稍不注意就出问题。
如果你刚开始做,建议先用官方示例跑通,确认硬件没问题,再逐步修改。不要一上来就自己写回调,那样出了问题很难定位。另外,多看看TinyUSB的源码和ESP-IDF的示例,很多问题在里面都有答案。串口日志一定要开详细级别,esp_log_level_set("*", ESP_LOG_VERBOSE),这样能看到USB枚举的每一步,排查起来方便很多。
最后分享一个小技巧:如果电脑识别不稳定,可以在USB DP/DM线上各串一个22欧姆的电阻,能改善信号完整性。这个电阻很多开发板上已经集成了,但如果你自己画板子,别忘了加。还有,USB座子一定要选质量好的,劣质座子接触不良,会导致各种玄学问题。