- 物联网
- 嵌入式
- 驱动开发
- 硬件开发
【免费下载链接】esp-iot-solution
Espressif IoT Library. IoT Device Drivers, Documentations and Solutions.
本文围绕 esp-iot-solution 中的usb_uf2_ota示例,系统讲解如何借助 Microsoft 提出的 UF2 文件格式与 USB MSC(Mass Storage Class)能力,把支持 USB-OTG 的 ESP 芯片变成一个"虚拟 U 盘",用户只需把.uf2格式固件拖拽进磁盘即可完成 OTA 升级。读完本文,你将掌握示例的构建烧录流程、分区表设计、esp_tinyuf2组件 API 用法、idf.py uf2-ota命令以及uf2conv.py转换工具,并了解该方案在 NVS 读写、USB 日志控制台与 Bootloader 层面的扩展玩法。
UF2 虚拟磁盘拖拽升级示意
UF2 与 MSC:为什么拖拽就能升级
UF2(USB Flashing Format)是 Microsoft 为 PXT 项目开发的一种文件格式,特别适合通过 MSC(Mass Storage Class,即 U 盘)对微控制器进行烧录。其核心思路是:把固件镜像切分成带地址信息的 512 字节块(block),每个块自身携带"该写到哪个 Flash 地址"的元数据,因此宿主端(PC)只需像拷贝普通文件一样把.uf2文件复制到 U 盘,设备端就能依据块内地址信息把固件逐块写入指定分区。
esp-iot-solution 的 esp_tinyuf2 组件正是 TinyUF2 针对带 USB-OTG 的 ESP 芯片(ESP32-S2 / ESP32-S3 / ESP32-P4)的增强实现,在"拖拽刷写"之上还叠加了三项能力:
- 通过虚拟 USB 驱动器进行 OTA 升级;
- 将 NVS 键值对导出为虚拟 U 盘中的
ini文件; - 修改
ini文件内容后写回 NVS,实现免串口配置。
而 usb_uf2_ota 示例就是这套机制最直接的落地演示:设备上电后枚举为一个名为ESP32S2-UF2/ESP32S3-UF2的 U 盘,拖入升级固件后自动完成分区写入并重启。
示例工程结构一览
先看示例的完整文件布局,便于后续逐项展开:
examples/usb/device/usb_uf2_ota/ ├── main/ │ ├── CMakeLists.txt # 注册主源文件 │ ├── Kconfig.projbuild # 开发板选择菜单 │ ├── idf_component.yml # 声明依赖 esp_tinyuf2 组件 │ └── usb_uf2_ota_main.c # 示例主程序 ├── CMakeLists.txt # 顶层工程文件 ├── README.md # 官方使用说明 ├── partitions_factory_two_ota_4m.csv ├── partitions_factory_two_ota_8m.csv ├── partitions_two_ota_4m.csv ├── partitions_two_ota_8m.csv ├── pytest_usb_uf2_ota.py # 自动化测试 ├── sdkconfig.ci.esp32s3_usb_otg └── sdkconfig.defaults其中顶层 CMakeLists.txt 是标准的 IDF 工程骨架(project(usb_uf2_ota)),主程序的组件清单 idf_component.yml 通过override_path将esp_tinyuf2指向仓库内的本地组件:
dependencies: idf: ">=4.4" esp_tinyuf2: version: "*" override_path: "../../../../../components/usb/esp_tinyuf2"这意味着本示例要求 IDF 版本不低于 4.4,且直接复用仓库内 components/usb/esp_tinyuf2 组件的源码,便于调试与学习。
构建与烧录
在终端中执行(PORT为设备串口,例如/dev/ttyUSB0):
idf.py -p PORT flash monitor该命令会依次完成编译、烧录并打开串口监视器(退出监视器按Ctrl-])。第一次烧录必须走传统串口方式,因为此时设备尚未具备 UF2 能力;烧录完成后,后续固件升级即可完全脱离串口工具,改用"拖拽"方式。
示例通过 Kconfig.projbuild 提供开发板选择菜单,支持三种目标板,且随IDF_TARGET自动切换默认项:
ESP32 S3 USB OTG(IDF_TARGET_ESP32S3,默认);ESP32 S3 GENERIC(IDF_TARGET_ESP32S3);ESP32 S2 GENERIC(IDF_TARGET_ESP32S2)。
CI 配置 sdkconfig.ci.esp32s3_usb_otg 中即包含CONFIG_ESP32_S3_USB_OTG=y。当选中该板卡时,主程序会在启动阶段将 GPIO18 拉低(usb_uf2_ota_main.c),用于配合 ESP32-S3-USB-OTG 开发板的硬件接线。
分区表:OTA 方案的基础
与任何 OTA 方案一样,UF2 OTA 也要求至少存在两个 OTA 应用分区。示例为不同 Flash 容量准备了四张分区表,其中 partitions_two_ota_4m.csv(4MB Flash)如下:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, , 0x4000, otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, ota_0, app, ota_0, , 1500K, ota_1, app, ota_1, , 1500K,nvs:存储 NVS 键值数据;otadata:OTA 数据分区,记录当前/下一个启动的应用槽位;phy_init:射频校准数据;ota_0/ota_1:两个对等的应用槽位,供升级时交替写入。
对应的 8MB 版本 partitions_two_ota_8m.csv 将每个应用槽位扩大到 3500K;而 partitions_factory_two_ota_4m.csv 额外保留了 1MB 的factory出厂分区,构成factory + ota_0 + ota_1布局。
默认配置 sdkconfig.defaults 做了如下设置:
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_two_ota_4m.csv" CONFIG_PARTITION_TABLE_MD5=y CONFIG_ENABLE_UF2_USB_CONSOLE=y即默认按 4MB Flash、自定义分区表partitions_two_ota_4m.csv构建,并开启 UF2 USB 控制台(日志同时输出到 USB 串口)。如果你使用 8MB Flash 的模组,将sdkconfig.defaults中的分区表文件名替换为partitions_two_ota_8m.csv即可。
主程序解析:安装、等待、重启
整个示例逻辑非常精简,全部集中在 usb_uf2_ota_main.c 中,核心调用链如下:
/* 升级完成回调:通知 main 任务 */ static void uf2_update_complete_cb() { TaskHandle_t main_task_hdl = xTaskGetHandle("main"); xTaskNotifyGive(main_task_hdl); } void app_main(void) { /* 1. 初始化目标板 IO(ESP32-S3-USB-OTG 拉低 GPIO18) */ /* 2. 安装 UF2 OTA */ tinyuf2_ota_config_t config = DEFAULT_TINYUF2_OTA_CONFIG(); config.complete_cb = uf2_update_complete_cb; config.if_restart = false; /* 关闭自动重启,改为手动控制 */ esp_tinyuf2_install(&config, NULL); /* 3. 阻塞等待 UF2 升级完成事件 */ ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ESP_LOGI(TAG, "Firmware update complete"); /* 4. 5 秒倒计时后卸载 UF2 并重启到新固件 */ for (int i = 5; i >= 0; i--) { ESP_LOGI(TAG, "Restarting in %d seconds...", i); vTaskDelay(1000 / portTICK_PERIOD_MS); } esp_tinyuf2_uninstall(); ESP_LOGI(TAG, "Restarting now"); esp_restart(); }这里的关键点是配置结构体tinyuf2_ota_config_t与默认宏DEFAULT_TINYUF2_OTA_CONFIG(),它们定义在组件头文件 components/usb/esp_tinyuf2/esp_tinyuf2.h 中:
#define DEFAULT_TINYUF2_OTA_CONFIG() \ { \ .subtype = ESP_PARTITION_SUBTYPE_ANY, \ .label = NULL, \ .if_restart = true \ }各字段语义如下:
| 字段 | 说明 |
|---|---|
subtype | 目标分区子类型;设为ESP_PARTITION_SUBTYPE_ANY时默认写入esp_ota_get_next_update_partition()返回的下一个 OTA 槽位 |
label | 分区标签;如需写入指定名称的分区(如factory)可在此指定 |
if_restart | UF2 刷写完成后是否自动重启到新分区 |
complete_cb | 升级完成后的用户回调(update_complete_cb_t类型) |
示例将if_restart设为false,改为在回调中通过 FreeRTOS 任务通知唤醒main任务,再自行倒计时、esp_tinyuf2_uninstall()恢复 USB 设备状态并esp_restart()重启——展示了"接管重启时机"的典型用法;如果你不关心时序细节,保留默认的if_restart = true即可让组件全自动完成升级重启。
esp_tinyuf2_install()的第二个参数是 NVS 配置tinyuf2_nvs_config_t,传NULL表示不启用 NVS 的 ini 导出功能。完整 API 还包括:
esp_tinyuf2_uninstall():卸载 tinyuf2,将 USB 恢复为默认状态(受限于 tinyusb 不支持 teardown,内存不会释放);esp_tinyuf2_current_state():查询当前状态(TINYUF2_STATE_NOT_INSTALLED/TINYUF2_STATE_INSTALLED/TINYUF2_STATE_MOUNTED);esp_restart_from_tinyuf2():重启系统并将复位原因置为UF2_RESET_REASON_VALUE(默认0xF2),供 Bootloader 判断是否进入 UF2 模式。
menuconfig 关键配置项
执行idf.py menuconfig进入Component config → TinyUF2 Config可调优以下参数(定义于 components/usb/esp_tinyuf2/Kconfig):
| 配置项 | 含义 | 默认值 | 取值范围 |
|---|---|---|---|
USB Virtual Disk size(MB) | 虚拟 U 盘在文件管理器中显示的大小 | 8 MB | 4–32 |
Max APP size(MB) | 允许的最大应用大小 | 4 MB | 2–8 |
Flash cache size(KB) | 缓存待写入 bin 片段的 Flash 缓存大小 | 32 KB | 4–64 |
USB Device VID | USB 厂商 ID | 0x303A(Espressif) | — |
USB Device PID | USB 产品 ID | 0x8000(Espressif 测试 PID) | — |
USB Disk Name | 虚拟 U 盘卷标 | ESP32S2-UF2/ESP32S3-UF2/ESP32P4-UF2 | — |
USB Device Manufacture | 制造商字符串 | Espressif | — |
Product Name | 产品名 | ESP TinyUF2 | — |
Product ID | 序列号 | 12345678 | — |
Product URL | 会生成index文件放入 U 盘,点击可打开网页 | https://products.espressif.com/ | — |
UF2 NVS ini file size | 保存 NVS 键值对的 ini 文件大小 | 512 | 256–2048 |
Enable USB Console For log | 日志输出到 USB 串口(默认输出 UART) | 关闭 | — |
OTA Factory Only | 限制 OTA 只写factory分区 | 关闭 | — |
OTA Reset Reason Value | UF2 OTA 复位的复位原因值 | 0xF2 | — |
其中"虚拟磁盘大小"与"最大 APP 大小"决定了拖拽升级时设备端预留的写入/缓存空间,需要与你的分区表实际容量匹配;Kconfig 中ENABLE_UF2_FLASHING会自动在 ESP32-S2 / ESP32-S3 / ESP32-P4 上默认开启并联动选中 TinyUSB,从配置层面保证了组件只在支持 USB-OTG 的芯片上启用。
拖拽升级:完整的实战流程
按官方 README 的步骤,一次完整的"拖拽升级"分为四步:
- 首次烧录:用
idf.py -p PORT flash monitor烧入第一个支持 TinyUF2 的固件; - 插入 USB:把
ESP32S3(或 ESP32-S2 等)设备通过 USB 接入 PC,文件管理器中会出现新磁盘,卷标为ESP32S2-UF2/ESP32S3-UF2(对应 Kconfig 中的UF2_VOLUME_LABEL); - 生成升级固件:运行
idf.py uf2-ota,将后续版本的固件生成/转换为 UF2 格式(详见下一节); - 拖拽升级:把
.uf2格式固件直接拖入磁盘,升级自动进行。
从组件文档(docs/zh_CN/usb/usb_device/esp_tinyuf2.rst)可以确认,idf.py uf2-ota是组件通过工程钩子注入的新命令,编译完成后会在project目录下生成${PROJECT_NAME}.uf2文件:
idf.py uf2-ota重要提示:UF2 OTA 要能连续使用,更新后的应用中必须同样启用 tinyuf2——否则升级完成后新固件不再提供虚拟 U 盘,也就无法进行下一轮拖拽升级。这是整个方案能否"链式迭代"的关键约束。
将现有 bin 转换为 UF2 格式
如果手头已有现成的应用 bin(例如 CI 产出的固件),无需重新编译工程,可直接使用组件自带的转换脚本 components/usb/esp_tinyuf2/utils/uf2conv.py(family id 定义在 utils/uf2families.json):
# 按芯片家族名指定(推荐) uf2conv.py your_firmware.bin -c -b 0x00 -f ESP32S3 # 或直接指定 family id 魔数 uf2conv.py your_firmware.bin -c -b 0x00 -f 0xc47e5767-c:转换(convert)为 UF2 格式;-b:指定基地址,tinyuf2 将把它作为写入 OTA 分区的偏移量;-f:family id,ESP32S2/ESP32S3均有对应魔数。
升级成功的日志表现
拖拽升级完成后,串口/USB 控制台会输出与官方 README 一致的信息(节选):
I (8456) esp_image: segment 0: paddr=00010020 vaddr=3f000020 size=0857ch ( 34172) map I (8496) esp_image: segment 4: paddr=00038270 vaddr=40027cd4 size=05be4h ( 23524) I (8556) uf2_example: Firmware update complete I (8556) uf2_example: Restarting in 5 seconds... I (9556) uf2_example: Restarting in 4 seconds... I (10556) uf2_example: Restarting in 3 seconds... I (13556) uf2_example: Restarting now可以看到:esp_image逐段校验并映射新固件镜像,随后uf2_example打印"Firmware update complete",倒计时 5 秒后esp_restart()重启进入新固件——这正是主程序中回调 +ulTaskNotifyTake+ 倒计时逻辑的外在表现。
自动化测试:验证虚拟磁盘真的出现
示例自带 pytest 用例 pytest_usb_uf2_ota.py,覆盖 ESP32-S2 与 ESP32-S3 两个目标,测试流程值得借鉴:
@pytest.mark.target('esp32s2') @pytest.mark.target('esp32s3') @pytest.mark.env('usb-otg_camera') def test_usb_uf2_ota(dut: Dut)-> None: usb_name = 'Espressif ESP TinyUF2' dut.expect(r'TUF2: Enable USB console, log will be output to USB', timeout=5) time.sleep(3) assert check_usb_device(usb_name), f"USB disk with name '{usb_name}' not found on the test machine"它先等待设备打印"Enable USB console"日志,确认 UF2 功能已装载;随后通过宿主机lsusb检查是否枚举出名为Espressif ESP TinyUF2的 USB 设备(若系统缺lsusb,用例会自动安装usbutils后重试)。这也从侧面印证了虚拟 U 盘在系统层面的真实存在。
进阶玩法:NVS 导出、USB 控制台与 Bootloader UF2
esp_tinyuf2不止于 OTA,围绕虚拟磁盘还能扩展出多类实用能力:
1. NVS 键值导出与回写安装时传入tinyuf2_nvs_config_t(默认宏DEFAULT_TINYUF2_NVS_CONFIG()指向nvs分区的tuf2命名空间),即可把 NVS 键值导出为磁盘中的ini文件,修改保存后写回 NVS;nvs_modified_cb回调会在 NVS 被修改后通知应用。结合menuconfig的UF2 ini file hide NVS value与 APIesp_tinyuf2_add_key_hidden("password"),还能将敏感键值以****遮蔽后导出(最多隐藏键数由UF2_INI_NVS_HIDDEN_MAX_NUM控制,默认 32)。
2. USB 日志控制台开启Enable USB Console For log(即示例 sdkconfig 中的CONFIG_ENABLE_UF2_USB_CONSOLE=y)后,日志改由 USB 串口输出,为无 UART 引出的量产/调试场景提供了便捷通道——pytest 正是依赖这条日志来判断 UF2 装载成功。
3. Bootloader 级 UF2 下载模式组件文档还描述了"将带 UF2 功能的特殊 APP bin 隐藏进 Bootloader"的方案(需开启CONFIG_ENABLE_BOOTLOADER_UF2与CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED),可实现三种进入 UF2 下载模式的途径:当factory/test/ota分区无固件时自动进入、手动拉低BOOT_UF2引脚进入、或在用户 APP 中调用esp_restart_from_tinyuf2()主动进入。需要注意bootloader_uf2.bin必须烧录在CONFIG_MMU_PAGE_SIZE(ESP32-S2/S3/P4 默认 64KB)对齐的地址上,并同步扩大CONFIG_PARTITION_TABLE_OFFSET以容纳新增的 bin。
总结
usb_uf2_ota示例展示了 UF2 + MSC 这一"零上位机、免串口"的 OTA 形态在 ESP 芯片上的完整落地:一次串口烧录建立能力,之后所有升级都简化为"生成 .uf2 文件 + 拖入磁盘"。其工程结构清晰、配置项完备,且依托 esp_tinyuf2 组件还能进一步解锁 NVS 免串口配置、USB 日志控制台与 Bootloader 级恢复模式,非常适合量产维护、现场升级和用户体验优先的消费类产品场景。建议动手实践时从 4MB 分区的默认配置起步,逐步尝试 NVS ini 导出与uf2conv.py的 bin 转换流程,完整打通"构建 → 转换 → 拖拽 → 重启"的全链路。
- 物联网
- 嵌入式
- 驱动开发
- 硬件开发
【免费下载链接】esp-iot-solution
Espressif IoT Library. IoT Device Drivers, Documentations and Solutions.
相关推荐
ESP MSC OTA:基于 USB Mass Storage 的 U 盘 OTA 升级方案(esp-iot-solution 组件实战指南)
ESP MSC OTA:基于 USB Mass Storage 的 U 盘 OTA 升级方案(esp iot solution 组件实战指南) 导读 esp_m
物联网嵌入式驱动开发硬件开发esp_tinyuf2 组件实战:基于 USB 虚拟磁盘实现 UF2 OTA 升级与 NVS 配置管理(ESP IoT Solution)
esp_tinyuf2 组件实战:基于 USB 虚拟磁盘实现 UF2 OTA 升级与 NVS 配置管理(ESP IoT Solution) 本文系统讲解 ESP
物联网嵌入式驱动开发硬件开发ESP32 USB Host MSC OTA 实战指南:基于 esp-iot-solution 实现 U 盘固件升级
ESP32 USB Host MSC OTA 实战指南:基于 esp iot solution 实现 U 盘固件升级 导读 本文以 examples/usb/h
物联网嵌入式驱动开发硬件开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考