需求描述
现有一块主控为ESP32S3-WROOM-1U-MCN8的板做数据采集,需要将采集后的数据放在ESP32的Flash里,通过蓝牙切换U盘(MSC)模式后进行采集数据的读取。
可从以下三个问题进行考虑,确定具体实现:1.如何分区;2.如何写入数据进对应分区;3.如何确保使用MSC模式时,数据能够被电脑正确读取。
如何分区
参考[2]Using a Custom Partition Scheme,将以下文件partitions.csv放入.ino文件同一目录下,Arduino IDE中 工具 - Partition Scheme:选择“Custom”。
partitions.csv
- # Name, Type, SubType, Offset, Size, Flags
- nvs, data, nvs, 0x9000, 0x5000,
- otadata, data, ota, 0xe000, 0x2000,
- app0, app, ota_0, 0x10000, 0x140000,
- app1, app, ota_1, 0x150000,0x140000,
- ffat, data, fat, 0x290000,0x560000,
- coredump, data, coredump,0x7F0000,0x10000,
复制代码 对于问题2和3:标准FAT16 这种格式可以被电脑读取到,而FATFS实现的FAT16不行,但在ESP32端用FATFS有库很方便。于是我们使用虚拟FAT16方案,集二者之优点。
USB MSC(U 盘模式)— 虚拟 FAT16 方案
使用 ESP32 Core 3.x 内置 USBMSC 类(与 USB CDC Serial 共享 TinyUSB 栈)。
核心思路: FFat 分区同时被两个"文件系统"使用——ESP32 端用 FFat(FATFS)正常读写 CSV,电脑端看到的是在 RAM 中动态构建的标准 FAT16 磁盘镜像。MSC 回调读取数据时,FAT 头部(boot/FAT/root dir)从预构建缓冲区返回,文件数据通过 FFat API 实时读取。
USB MSC 调试全过程记录
起始状态
初始方案:进入 MSC 模式时卸载 FFat,将整个 ffat 分区的原始扇区直接暴露给电脑。电脑看到的是 FATFS 原始数据,无法直接用 Windows 打开 CSV。
问题:电脑端无法正常打开文件,因为 FFat (FATFS) 的内部布局与标准 FAT16 存在差异(LBA 对齐、元数据位置不同等)。
第一版:动态生成 FAT16(失败)
方案:在 MSC read 回调中,根据请求的 LBA 扇区号动态构建 FAT16 头部(boot sector / FAT table / root directory),数据区通过 FFat 的 VFS 接口读取文件内容。
实现:
- 定义 FAT16 常量:SECTORS_PER_CLUSTER = 1(每簇 512 字节)
- gen_header(lba, buf) 函数根据 LBA 号在 g_sec_buf(512 字节栈缓冲区)中生成对应扇区
- FAT 表条目通过 fat_entry(cluster) 函数计算
- 文件列表通过 scan_files() 扫描 FFat 获取
问题 1:CSV 文件报错"不是有效文件"
- 电脑能看到磁盘和文件名,但打开 CSV 时报错
- 排查发现 FAT16 簇分配错误
根因分析:
- fat_entry() 中 cl == 2 返回 0xFFFF(EOF 标记),将 cluster 2 标记为结束簇
- scan_files() 中 next_cl = 3,跳过了 cluster 2
- 但 FAT16 规范中 cluster 0 和 1 是保留的,cluster 2 是第一个可用的数据簇
- 数据区扇区 0 映射到 cluster 2:cluster = data_sec / SECTORS_PER_CLUSTER + 2
- 如果文件从 cluster 3 开始,cluster 2 区域返回全零,FAT 中也是 EOF,Windows 无法正确追踪文件链
修复:
- fat_entry() 删除 if (cl == 2) return 0xFFFF;
- scan_files() 中 next_cl = 3 改为 next_cl = 2
第二版:预构建缓冲区(U盘为空)
方案改进:将动态生成改为预构建——进入 MSC 模式时一次性用 malloc 分配 RAM,构建完整的 FAT16 头部(boot + FAT1 + FAT2 + root dir),之后 read 回调只做 memcpy。
同步优化:
- SECTORS_PER_CLUSTER 从 1 改为 4(2048 字节/簇),更符合 FAT16 常见配置
- 增加蓝牙命令 FMT:格式化 FFat + 写入测试 CSV
问题 2:FMT 后 U 盘为空
- 串口日志:[Format] 测试文件: /test_msc.csv (339 bytes) — 文件创建成功
- 串口日志:[MSC] 扫描到 0 个文件 — scan_files() 找不到文件
排查过程:
- 确认 formatAndTest() 中 FFat.open("/test_msc.csv", "w") 创建成功,f.size() 返回 339 字节
- setMSCMode(true) 中 scan_files() 打开根目录成功(没有报错),但 openNextFile() 返回空
- 发现中间主循环的 appendRecord() 打印 [Flash] 无法打开: /ppg_000.csv,说明 FFat 状态异常
根因分析:
- formatAndTest() 调用 FFat.end() 卸载文件系统时,_fs_mounted 仍为 true
- 主循环中的 appendRecord() 检查 _fs_mounted == true 后尝试 FFat.open(),但文件系统正在卸载/已卸载
- 这种并发访问导致 FFat (FATFS) 内部状态不一致
- FFat.begin(true) 重新格式化挂载后,写入的文件可能只存在于缓存中,未正确刷入 flash 目录结构
- 之后 scan_files() 遍历目录时看不到文件
修复:
- formatAndTest() 中 FFat.end() 前先设 _fs_mounted = false,阻止主循环并发访问
- _fs_mounted = false 放在 FFat.end() 之前,这样 PPGTask 中的 appendRecord()、getUsedKB() 等都会在检查 _fs_mounted 时直接返回,不会在文件系统卸载过程中并发访问 FFat。
- setMSCMode(true) 在扫描文件前执行 FFat.end() + FFat.begin(false) 强制重新挂载,确保文件系统状态干净
最终方案(成功)
架构:- setMSCMode(true):
- 1. FFat.end() → delay(200) → FFat.begin(false) // 重新挂载确保状态干净
- 2. fat_compute() → scan_files() // 扫描文件列表
- 3. build_fat16_headers() → malloc + 一次性构建 // 预构建FAT16头部到RAM
- 4. msc_usb.mediaPresent(true) // 激活磁盘
- ppg_msc_read_cb(lba, offset, buffer, bufsize):
- if (lba < g_data_start):
- memcpy(buffer, g_header_buf + lba*512 + offset, bufsize) // RAM直接拷贝
- else:
- 查找文件 → FFat.open → seek → read → 返回数据 // FFat实时读取
复制代码 遇到的所有问题及修复清单
#问题根因修复1电脑显示 RAW 格式直接暴露 FATFS 原始扇区改为在 MSC 回调中构建标准 FAT162CSV 打开报错Cluster 2 被错误标记为 EOF,簇分配从 3 开始簇分配从 2 开始,删除 cl==2 的 EOF 标记3动态生成潜在不一致每次回调重新生成,存在状态竞争改为进入 MSC 时一次性预构建到 RAM4SECTORS_PER_CLUSTER=1 兼容性Windows 对 512 字节/簇的 FAT16 处理不佳改为 4 扇区/簇 (2048 字节)5FMT 后 U 盘为空FFat.end() 时 _fs_mounted 未清零,并发访问导致文件系统状态损坏end() 前设 _fs_mounted=false;MSC 进入前强制重新挂载6Windows 缓存旧数据同一卷标序列号被缓存每次进入 MSC 生成随机 serial关键经验
- FAT16 簇号从 2 开始:Cluster 0/1 保留给介质描述符,cluster 2 是数据区第一个簇,data_sec / SECTORS_PER_CLUSTER + 2 是正确的映射公式。
- ESP32 FFat 并发安全:FFat.end() 前必须阻止其他代码路径访问文件系统(设标志位),否则 FATFS 内部状态会被破坏。
- 预构建 vs 动态生成:对确定性数据(FAT头部),预构建比动态生成更可靠——避免回调中的复杂逻辑和潜在的状态竞争。
- MSC offset 参数:TinyUSB 的 read10_cb 中 offset 是 LBA 块内的字节偏移(当 CFG_TUD_MSC_EP_BUFSIZE < 512 时出现),不是文件偏移。
- Windows 磁盘缓存:修改磁盘内容后如果 Windows 不识别,可能是因为卷标序列号未变化导致缓存命中。每次进入 MSC 时生成新的随机 serial 可解决
Reference
[1]https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-reference/kconfig-reference.html#compiler-options
[2]https://docs.espressif.com/projects/arduino-esp32/en/latest/tutorials/partition_table.html
[3]https://github.com/aoscarius/esp32usbmsc
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |