资讯详情

资讯详情

ESP32-P4 USB读卡器实验:TinyUSB MSC与SD卡扇区读写实现

1. 项目缘起与整体设计思路1.1 为什么要在 ESP32-P4 上折腾 USB 读卡器拿到 ESP32-P4 这块芯片的时候我第一反应是它终于把 USB 2.0 OTG High-Speed 做进去了而且带独立的 USB PHY不像以前某些型号还得外挂。手头正好有一张闲置的 microSD 卡和几个读卡器模块就想着能不能把 P4 做成一个“插上电脑就能认的读卡器”——也就是让 PC 把开发板识别成一个 USB 大容量存储设备MSCMass Storage Class而背后真正读写的是板子上的 SD 卡。这个需求听起来简单但实际做下来涉及的东西不少USB 设备控制器怎么配、TinyUSB 协议栈怎么接、SD 卡驱动怎么和 USB 存储回调对接、FATFS 文件系统要不要参与、读写性能怎么调。标题里写的是“USB 读卡器Slave实验”这里的 Slave 指的是 P4 作为 USB 设备端DevicePC 作为主机端Host。搞清楚这个角色定位很关键因为后面所有的描述符配置、端点分配、类驱动选择都围绕“我是一个 U 盘”这个身份展开。适合谁来参考这篇内容我觉得有三类人一是刚上手 ESP32-P4、想找个综合性强一点的例程练手的二是做过 ESP32-S3 或其他型号 USB 功能、想迁移到 P4 的三是单纯想搞明白 USB MSC 设备端到底怎么跑起来的。不管哪类只要你能把 ESP-IDF 环境跑通、能烧录例程剩下的跟着做就行。1.2 方案选型为什么用 TinyUSB 而不是自己撸协议栈ESP-IDF 从 5.x 开始就把 TinyUSB 作为官方 USB 设备栈集成进来了ESP32-P4 的 USB OTG 外设驱动也是围绕它做的。自己从零写 USB 描述符和端点处理不是不行但工作量巨大而且调试成本极高——USB 协议本身的状态机、枚举流程、各种标准请求随便一个环节出错就是“设备无法识别”连日志都看不到。TinyUSB 的好处在于它把设备端的通用逻辑都封装好了你只需要提供几个回调设备描述符、配置描述符、字符串描述符以及 MSC 类需要的读写回调。剩下的枚举、端点 0 控制传输、BOTBulk-Only Transport协议处理TinyUSB 全包了。实测下来用 TinyUSB 从零到 PC 认盘熟练的话半天就能跑通自己写的话一周都不一定稳。另一个选型点是存储介质。ESP32-P4 支持 SDMMC 主机控制器可以直接接 SD 卡也可以走 SPI 模式。做读卡器实验肯定优先用 SDMMC 4-bit 模式因为带宽高、CPU 占用低。SPI 模式虽然接线简单但速度上限摆在那做读卡器体验会很差。所以整体架构就是PC ←USB→ P4 USB OTG ←SDMMC→ microSD 卡中间 TinyUSB 负责 USB 侧SDMMC 驱动负责卡侧MSC 回调把两边粘起来。1.3 整体数据流与角色划分先把数据流理清楚不然后面看代码容易晕。当 PC 往“U 盘”里写文件时流程是这样的PC 的文件系统驱动发出 SCSI WRITE 命令通过 USB Bulk OUT 端点传到 P4TinyUSB 的 MSC 类驱动解析出这是一个写请求带上 LBA逻辑块地址和扇区数据然后调用我们注册的tusb_msc_write_cb回调把数据写到 SD 卡的对应扇区。读的过程反过来PC 发 SCSI READ回调从 SD 卡读扇区通过 Bulk IN 端点回给 PC。这里有个关键点MSC 设备是按扇区通常 512 字节操作的不关心文件系统。也就是说P4 这边不需要挂载 FATFS不需要知道 PC 写的是 FAT32 还是 exFAT它只管把扇区数据搬来搬去。这一点很多人一开始会搞混以为要在 P4 上先格式化卡、挂文件系统其实完全不用。PC 会自己识别卡上的文件系统并挂载。当然如果卡上没有文件系统PC 会提示你格式化格式化过程也是通过扇区读写完成的P4 照样只搬数据。角色划分上P4 是“哑设备”所有文件系统逻辑都在 PC 侧。这个认知建立起来之后代码就很好理解了我们做的就是实现两个扇区读写函数剩下的交给 TinyUSB。2. 核心细节解析与实操要点2.1 硬件连接与引脚确认ESP32-P4 的 SDMMC 接口引脚不是固定的可以通过 GPIO 矩阵灵活映射但为了稳定和性能建议用默认的 IO MUX 引脚。以常见的 P4 开发板为例SDMMC 的 4-bit 模式一般用到 CLK、CMD、D0~D3 六根线再加上卡检测CD和电源使能如果有。接线的时候有几个坑要注意。第一CLK 线尽量短而且远离其他高速信号否则高速模式下容易出错。我一开始用杜邦线飞线40MHz 时钟下读写就偶尔报 CRC 错误后来换成短的排线就好了。第二CMD 和 D0~D3 最好加上拉电阻很多 SD 卡模块自带上拉但如果你用的是裸卡座记得补上 10K 上拉不然初始化阶段可能识别不到卡。第三电源要稳SD 卡在写入瞬间电流会跳变如果开发板供电不足会出现写失败甚至掉卡。我用 USB 供电时遇到过写大文件到一半卡死后来给 SD 卡单独加了 100uF 电容就稳了。引脚定义在代码里通过sdmmc_host_t和sdmmc_slot_config_t配置下面是一个典型的 4-bit 配置片段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 SDMMC_SLOT_CONFIG_DEFAULT(); slot.width 4; slot.clk GPIO_NUM_43; slot.cmd GPIO_NUM_44; slot.d0 GPIO_NUM_39; slot.d1 GPIO_NUM_40; slot.d2 GPIO_NUM_41; slot.d3 GPIO_NUM_42;这里的max_freq_khz设成SDMMC_FREQ_HIGHSPEED50MHz还是SDMMC_FREQ_DEFAULT20MHz要看你的布线和卡的质量。实测下来好的卡加短排线50MHz 没问题飞线的话老老实实 20MHz稳定第一。2.2 TinyUSB 配置与描述符编写TinyUSB 在 ESP-IDF 里的配置主要通过menuconfig完成路径是Component config → TinyUSB Stack。几个关键项TinyUSB Task Stack Size建议给到 4096 以上因为 MSC 处理扇区数据时栈消耗不小TinyUSB Task Priority给个中等偏上保证 USB 响应及时Mass Storage Class要勾上。另外CONFIG_TINYUSB_MSC_ENABLED必须开否则 MSC 回调根本不会被调用。描述符这块TinyUSB 提供了默认的 MSC 描述符模板但 VID/PID 和字符串最好改成自己的不然 PC 上显示的是通用名字。设备描述符里bDeviceClass对于 MSC 设备可以设成 0由接口描述符指定类也可以设成 8MSC 类。我一般设 0然后在接口描述符里指定bInterfaceClass 0x08。配置描述符里要包含一个 MSC 接口两个 Bulk 端点IN 和 OUT端点大小对于 High-Speed 建议 512 字节Full-Speed 用 64 字节。字符串描述符里有个细节iSerialNumber最好每个设备不一样否则 PC 会认为是同一个设备换板子后盘符可能不刷新。可以用芯片的 MAC 地址生成一个唯一序列号。这个坑我踩过两块板子烧同样的固件PC 上只认一个折腾半天才发现是序列号重复。2.3 SD 卡扇区读写回调的实现MSC 类需要两个核心回调tusb_msc_read_cb和tusb_msc_write_cb签名在tusb_msc.h里定义。读回调收到lba起始扇区和offset扇区内偏移通常为 0以及buflen要读的字节数需要把数据填到buffer里返回实际读的字节数。写回调类似从buffer取数据写到卡里。实现的时候直接用sdmmc_read_sectors和sdmmc_write_sectors就行注意这两个函数要求 buffer 是 4 字节对齐的而且长度是扇区大小的整数倍。TinyUSB 传过来的 buffer 一般是对齐的但保险起见可以加个断言检查。另外扇区大小要设成 512和 PC 侧一致这个在tud_msc_init里通过block_count和block_size告诉 TinyUSB。static int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t buflen) { esp_err_t err sdmmc_read_sectors(s_card, buffer, lba, buflen / 512); return err ESP_OK ? buflen : -1; } static int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t buflen) { esp_err_t err sdmmc_write_sectors(s_card, buffer, lba, buflen / 512); return err ESP_OK ? buflen : -1; }这里返回 -1 表示失败TinyUSB 会向 PC 报告错误PC 侧可能弹出“写入失败”提示。调试阶段可以在失败时打印lba和buflen方便定位是哪个扇区出的问题。2.4 容量上报与写保护处理tud_msc_init回调里要上报卡的容量也就是block_count。这个值从 SD 卡的 CSD 寄存器里读ESP-IDF 的sdmmc_card_t结构体里有csd.capacity字段单位是扇区数。直接赋值即可。如果容量上报错了PC 会显示错误的盘符大小甚至无法格式化。写保护方面如果卡上有物理写保护开关或者你想在软件层面禁止写入可以在tud_msc_init里设置tud_msc_set_writeprotect。不过做读卡器实验一般不需要除非你想模拟一个只读设备。我试过设成写保护PC 侧会显示“磁盘被写保护”效果挺直观。3. 实操过程与核心环节实现3.1 环境搭建与工程创建先说环境。ESP-IDF 我用的是 v5.3 版本对 ESP32-P4 的支持比较完整。安装方式我推荐用 VS Code 的 ESP-IDF 插件虽然有人吐槽插件市场里找不到但其实官方插件叫Espressif IDF搜 esp-idf 也能出来。如果你用 CLion确实在 Marketplace 里找不到官方 ESP-IDF 插件得手动配置工具链比较折腾新手还是 VS Code 省事。安装过程中最常见的坑是下载卡在 0%。这个一般是网络问题解决办法是设置国内镜像源或者手动下载离线包。我一般用idf_tools.py配合镜像具体命令网上很多这里不展开。装完之后用idf.py --version确认一下能输出版本号就说明环境 OK。创建工程可以直接复制 ESP-IDF 里的examples/peripherals/usb/device/tusb_msc例程这个例程本身就是做 MSC 设备的只不过默认用的是 RAM 盘或者 SPI Flash我们要把它改成 SD 卡。复制过来之后先编译一遍确保环境没问题再动代码。3.2 从例程到 SD 卡的改造步骤例程默认的存储介质是tusb_msc_storage组件里的 RAM 盘我们要换成 SD 卡。第一步在main里初始化 SDMMC挂载卡拿到sdmmc_card_t句柄。第二步把 MSC 的读写回调替换成调用sdmmc_read_sectors和sdmmc_write_sectors。第三步在tud_msc_init里用卡的实际容量替换原来的固定值。改造过程中有个顺序问题必须先初始化 SD 卡再初始化 TinyUSB。因为 TinyUSB 初始化时会调用tud_msc_init上报容量如果这时候卡还没初始化容量就是 0PC 会认成一个 0 字节的设备。我一开始把 TinyUSB 初始化放在前面结果 PC 上盘符出来了但打不开后来调换顺序就好了。另外SD 卡初始化失败的处理也要注意。如果卡没插好或者接触不良sdmmc_init会返回错误这时候要么重试要么就让设备以 0 容量上报PC 会提示“请插入磁盘”。实际产品里一般会加个卡检测中断插卡后重新初始化但实验阶段手动复位也行。3.3 关键代码逐段解析先看 SD 卡初始化部分。除了前面提到的引脚配置还要注意sdmmc_host_init和sdmmc_card_init的调用顺序。sdmmc_host_init初始化主机控制器sdmmc_card_init初始化卡本身。如果卡是第一次用可能还需要发送 CMD0 复位。ESP-IDF 的驱动都封装好了直接调就行。esp_err_t ret sdmmc_host_init(); if (ret ! ESP_OK) { /* 处理错误 */ } ret sdmmc_host_init_slot(SDMMC_HOST_SLOT_0, slot); if (ret ! ESP_OK) { /* 处理错误 */ } sdmmc_card_t *card malloc(sizeof(sdmmc_card_t)); ret sdmmc_card_init(host, card); if (ret ! ESP_OK) { /* 处理错误 */ }然后是 TinyUSB 初始化。在app_main里调用tusb_init()它会启动 TinyUSB 任务。MSC 的初始化和回调注册通过tud_msc_init和tud_msc_read_cb等函数完成。注意tud_msc_init是在 USB 枚举过程中被调用的所以里面用到的card句柄必须已经有效。void tud_msc_init_cb(uint8_t lun, uint8_t *vendor_id, uint8_t *product_id, uint8_t *product_revision, uint16_t *block_count, uint16_t *block_size) { *block_count card-csd.capacity; *block_size 512; memcpy(vendor_id, ESP32-P4, 8); memcpy(product_id, SD Reader, 9); memcpy(product_revision, 1.0, 3); }这里block_count是uint16_t类型但 SD 卡容量可能超过 65535 个扇区也就是 32MB所以 TinyUSB 内部其实用的是 32 位回调里这个参数是历史遗留实际容量通过tud_msc_capacity_cb上报。我一开始被这个类型误导以为只能支持 32MB后来看源码才发现是 32 位的。这个细节文档里没写清楚踩过坑才知道。3.4 编译烧录与 PC 端验证编译用idf.py build烧录用idf.py -p /dev/ttyUSB0 flash monitor。烧录完成后用 USB 线把 P4 的 USB OTG 口连到 PC。注意P4 开发板一般有两个 USB 口一个是调试串口UART一个是 OTG。要连 OTG 口否则 PC 认不到设备。我第一次连错了口死活没反应后来看原理图才发现。PC 端如果是 Windows会在“此电脑”里多出一个盘符名字就是描述符里设的 product_id。打开之后能看到 SD 卡里的文件。如果是 Linux用dmesg能看到设备枚举日志lsblk能看到新的块设备。macOS 下会在桌面挂载一个磁盘图标。验证读写性能可以用dd命令或者直接拷文件。我实测下来50MHz 时钟下读速度能到 20MB/s 左右写速度 10MB/s 左右。这个速度受限于 SD 卡本身和 USB 协议开销对于读卡器来说够用了。如果速度明显偏低先检查时钟频率再检查是不是跑在 Full-Speed 模式Full-Speed 上限 12Mbps实际也就 1MB/s 左右。4. 常见问题与排查技巧实录4.1 PC 认不到设备怎么办这是最常见的问题排查思路从硬件到软件逐层来。先看 USB 线是不是数据线有些线只能充电不能传数据换一根试试。再看是不是插的 OTG 口插错口肯定没反应。然后看设备管理器里有没有未知设备如果有但带感叹号说明枚举失败可能是描述符有问题。如果连未知设备都没有那可能是 USB PHY 没初始化检查tusb_init有没有被调用。枚举失败的话打开 TinyUSB 的日志在menuconfig里把TinyUSB Log Level调到 Debug能看到枚举过程中的标准请求。常见错误是描述符长度不对比如配置描述符的总长度和实际字节数不匹配PC 会直接拒绝。这个用 USB 分析仪抓包最直观没有分析仪的话就对着 TinyUSB 的日志一行行看。4.2 盘符出来了但打不开或提示格式化这种情况一般是容量上报错误或者扇区读写有问题。先确认block_count是不是卡的真实容量如果上报成 0 或者一个很小的值PC 会认为磁盘损坏。再确认扇区读写回调有没有正确返回如果读回调返回 -1PC 读引导扇区失败就会提示格式化。还有一个可能是卡上的文件系统 PC 不认识。比如卡是 ext4 格式Windows 下就会提示格式化。这时候不要急着格式化先确认卡里有没有重要数据。如果卡是空的格式化也无所谓格式化过程本身也能验证读写回调是否正常。4.3 写入大文件时卡死或报错这个多半是电源或者时钟问题。写入时电流跳变导致电压跌落SD 卡会复位后续读写全失败。解决办法是加电容或者换供电更足的 USB 口。时钟太快也会导致写入出错尤其是便宜的卡降到 20MHz 试试。另外如果 PC 侧开了写缓存大量数据堆积后一次性写入对 P4 的缓冲压力也大可以在 TinyUSB 配置里调大端点缓冲区。我遇到过一次写 1GB 文件到一半卡死日志显示sdmmc_write_sectors返回ESP_ERR_TIMEOUT。后来把时钟从 50MHz 降到 20MHz问题消失。所以稳定性优先的话20MHz 是更稳妥的选择。4.4 常见问题速查表现象可能原因排查方法解决措施PC 完全无反应USB 线或接口问题换线、换口、查原理图用数据线连 OTG 口设备管理器有未知设备描述符错误开 TinyUSB Debug 日志检查描述符长度和类型盘符出现但打不开容量上报错误打印 block_count用卡的真实容量赋值提示格式化扇区读失败或文件系统不识别检查读回调返回值确认卡格式或格式化写入卡死电源不稳或时钟过快测电压、降时钟加电容、降到 20MHz换板子后盘符不刷新序列号重复查 iSerialNumber用 MAC 生成唯一序列号4.5 几个容易被忽略的细节第一个是tud_msc_init的调用时机。它是在 USB 枚举过程中被调用的如果这时候 SD 卡还没初始化完容量就是错的。所以一定要先初始化卡再初始化 TinyUSB。第二个是扇区对齐sdmmc_read_sectors要求 buffer 4 字节对齐TinyUSB 传过来的 buffer 一般没问题但如果你自己做了缓冲层要注意对齐。第三个是热插拔实验阶段不建议热插拔 SD 卡因为 TinyUSB 已经上报了容量拔卡后 PC 还在读写就会出错。要做热插拔的话得在卡检测中断里重新枚举 USB比较麻烦。我在实际调试中还发现一个现象如果 PC 侧同时挂了多个 U 盘P4 这个可能会被分配到比较靠后的盘符有时候在“此电脑”里要往下翻才看得到。这不是 bug是 Windows 的盘符分配策略。可以在磁盘管理里手动改盘符或者拔掉其他 U 盘再试。5. 性能调优与扩展思路5.1 提升读写速度的几个手段前面提到时钟频率是最直接的手段50MHz 比 20MHz 快一倍多。但时钟上去了对布线和卡的要求也高。除了时钟还可以调整 TinyUSB 的端点缓冲区大小High-Speed 下 Bulk 端点最大 512 字节设满能减少传输次数。另外SDMMC 的 DMA 要开ESP-IDF 默认是开的但可以确认一下host.flags里有没有SDMMC_HOST_FLAG_DMA。还有一个容易被忽略的点是文件系统的簇大小。PC 格式化卡的时候如果簇设得小读写同样大小的文件会产生更多扇区操作。做读卡器实验的话建议格式化成 exFAT 或者大簇的 FAT32减少碎片和元数据开销。我试过用 64KB 簇的 exFAT拷贝大文件的速度比 4KB 簇的 FAT32 快不少。5.2 从读卡器扩展到复合设备MSC 只是 USB 设备类里的一种TinyUSB 还支持 CDC串口、HID键鼠、MIDI 等。P4 的 USB OTG 支持复合设备也就是一个 USB 口同时呈现多个功能。比如你可以把 P4 做成“读卡器 串口”PC 上既能看到盘符又能看到一个 COM 口。这个在调试时很有用因为 USB 口被 MSC 占了之后串口日志就得走另一个口复合设备能省一个口。配置复合设备需要在描述符里加多个接口TinyUSB 的tusb_config.h里把对应的类都打开然后在tud_descriptor_configuration_cb里返回包含多个接口的配置描述符。这个稍微复杂一点但 TinyUSB 的例程里有参考改起来不难。5.3 后续可以玩的方向如果读卡器跑通了可以试试把存储介质换成 SPI Flash 或者外部 DDR做成一个“U 盘式”的数据记录仪。P4 有丰富的外设可以一边采集传感器数据写到 Flash一边通过 USB 让 PC 读取。还可以加上按键和屏幕做一个带界面的便携读卡器。这些扩展都建立在 MSC 跑通的基础上所以先把基础打牢。我个人觉得USB MSC 这个实验最大的价值不在于读卡器本身而在于它把 USB 设备栈、存储驱动、文件系统这几个模块串起来了。搞明白之后再去看其他 USB 类或者存储方案思路会清晰很多。踩过的那些坑比如描述符长度、容量上报、时钟稳定性都是通用的经验换个项目照样用得上。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →