需求描述
现有一块主控为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 回调中构建标准 FAT16 |
| 2 | CSV 打开报错 | Cluster 2 被错误标记为 EOF,簇分配从 3 开始 | 簇分配从 2 开始,删除 cl==2 的 EOF 标记 |
| 3 | 动态生成潜在不一致 | 每次回调重新生成,存在状态竞争 | 改为进入 MSC 时一次性预构建到 RAM |
| 4 | SECTORS_PER_CLUSTER=1 兼容性 | Windows 对 512 字节/簇的 FAT16 处理不佳 | 改为 4 扇区/簇 (2048 字节) |
| 5 | FMT 后 U 盘为空 | FFat.end() 时 _fs_mounted 未清零,并发访问导致文件系统状态损坏 | end() 前设 _fs_mounted=false;MSC 进入前强制重新挂载 |
| 6 | Windows 缓存旧数据 | 同一卷标序列号被缓存 | 每次进入 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\]