esp-iot-solution USB UF2 OTA 示例深度解析:用虚拟 U 盘拖拽固件完成 OTA 升级
2026/9/20 19:36:49 网站建设 项目流程
  • 物联网
  • 嵌入式
  • 驱动开发
  • 硬件开发

【免费下载链接】esp-iot-solution

Espressif IoT Library. IoT Device Drivers, Documentations and Solutions.

项目地址:https://gitcode.com/GitHub_Trending/es/esp-iot-solution
点击查看免费下载

本文围绕 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_pathesp_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_restartUF2 刷写完成后是否自动重启到新分区
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 MB4–32
Max APP size(MB)允许的最大应用大小4 MB2–8
Flash cache size(KB)缓存待写入 bin 片段的 Flash 缓存大小32 KB4–64
USB Device VIDUSB 厂商 ID0x303A(Espressif)
USB Device PIDUSB 产品 ID0x8000(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 文件大小512256–2048
Enable USB Console For log日志输出到 USB 串口(默认输出 UART)关闭
OTA Factory Only限制 OTA 只写factory分区关闭
OTA Reset Reason ValueUF2 OTA 复位的复位原因值0xF2

其中"虚拟磁盘大小"与"最大 APP 大小"决定了拖拽升级时设备端预留的写入/缓存空间,需要与你的分区表实际容量匹配;Kconfig 中ENABLE_UF2_FLASHING会自动在 ESP32-S2 / ESP32-S3 / ESP32-P4 上默认开启并联动选中 TinyUSB,从配置层面保证了组件只在支持 USB-OTG 的芯片上启用。

拖拽升级:完整的实战流程

按官方 README 的步骤,一次完整的"拖拽升级"分为四步:

  1. 首次烧录:用idf.py -p PORT flash monitor烧入第一个支持 TinyUF2 的固件;
  2. 插入 USB:把ESP32S3(或 ESP32-S2 等)设备通过 USB 接入 PC,文件管理器中会出现新磁盘,卷标为ESP32S2-UF2/ESP32S3-UF2(对应 Kconfig 中的UF2_VOLUME_LABEL);
  3. 生成升级固件:运行idf.py uf2-ota,将后续版本的固件生成/转换为 UF2 格式(详见下一节);
  4. 拖拽升级:把.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 被修改后通知应用。结合menuconfigUF2 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_UF2CONFIG_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.

项目地址:https://gitcode.com/GitHub_Trending/es/esp-iot-solution
点击查看免费下载

相关推荐

上一篇:终极指南:Cert-Manager动态准入控制Webhook配置与测试详解
下一篇:raylib-games跨平台部署终极教程:Windows、Linux、Web、Android全平台实战指南 🚀

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询