ESP32 OTA升级进阶:手把手教你修改源码,实现固件下载进度条显示(基于IDF 4.4/5.3)
ESP32 OTA升级进阶:手把手教你修改源码,实现固件下载进度条显示(基于IDF 4.4/5.3)
在物联网设备开发中,OTA(Over-The-Air)升级功能已经成为ESP32项目的标配能力。但对于追求极致用户体验的开发者来说,标准的OTA流程往往显得过于"黑盒"——用户点击升级后,设备就像进入了一个不可知的状态,直到升级成功或失败才会给出反馈。这种体验在升级大型固件时尤其令人焦虑。
想象一下这样的场景:你的智能家居设备正在执行一个10MB的固件升级,用户盯着毫无反应的设备指示灯,完全无法判断是升级正在进行中,还是网络已经断开。这种不确定性会显著降低产品的专业感和可信度。本文将带你深入ESP-IDF的OTA底层,通过修改官方源码实现实时的下载进度显示,让你的设备在升级过程中也能与用户保持"沟通"。
1. 理解ESP32 OTA升级的核心机制
要定制OTA流程,首先需要理解ESP-IDF中HTTPS OTA的实现架构。不同于简单的API调用,esp_https_ota函数内部实际上是一个状态机,它封装了从建立连接到验证固件的完整流程。
1.1 OTA升级的六个关键阶段
- 初始化阶段:创建HTTP客户端并建立连接
- 头部解析阶段:获取固件元信息(包括大小、版本等)
- 数据下载阶段:分块接收固件数据
- 数据写入阶段:将接收到的数据写入Flash
- 校验阶段:验证固件完整性和签名
- 切换阶段:更新引导加载程序中的分区信息
在这六个阶段中,我们需要关注的焦点是第三阶段——数据下载。这个阶段持续时间最长,也最适合插入我们的进度监控代码。
1.2 进度监控的关键API
ESP-IDF提供了两个关键函数用于获取下载进度信息:
// 获取固件总大小(字节) int esp_https_ota_get_image_size(esp_https_ota_handle_t https_ota_handle); // 获取已下载数据量(字节) int esp_https_ota_get_image_len_read(esp_https_ota_handle_t https_ota_handle);这两个函数的组合使用,就能计算出当前的下载百分比。但要注意,esp_https_ota_get_image_size只有在头部解析完成后才能返回有效值,这意味着在下载的最初几秒内,进度显示可能会暂时为0%。
2. 构建自定义OTA函数:从复制到改造
直接修改ESP-IDF的官方库代码不是个好主意,这会导致后续框架升级时的维护噩梦。更合理的做法是创建我们自己的OTA函数,复用官方函数的核心逻辑。
2.1 函数框架搭建
首先,我们需要复制esp_https_ota的基本结构:
esp_err_t custom_https_ota(const esp_http_client_config_t *config) { if (!config) { ESP_LOGE(TAG, "Invalid HTTP client config"); return ESP_ERR_INVALID_ARG; } esp_https_ota_config_t ota_config = { .http_config = config, }; esp_https_ota_handle_t ota_handle = NULL; esp_err_t err = esp_https_ota_begin(&ota_config, &ota_handle); if (err != ESP_OK || ota_handle == NULL) { return err; } // 进度监控代码将插入在这里 err = esp_https_ota_perform(ota_handle); if (err != ESP_OK) { esp_https_ota_abort(ota_handle); return err; } return esp_https_ota_finish(ota_handle); }2.2 插入进度监控逻辑
真正的魔法发生在esp_https_ota_perform调用周围。这个函数实际上是一个循环的包装器,我们需要展开它:
int total_size = esp_https_ota_get_image_size(ota_handle); int last_reported = 0; // 上次报告的百分比 while (1) { err = esp_https_ota_perform(ota_handle); if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { break; } int downloaded = esp_https_ota_get_image_len_read(ota_handle); int percent = (total_size > 0) ? (downloaded * 100 / total_size) : 0; // 避免频繁打印(每5%变化打印一次) if (percent - last_reported >= 5 || percent == 100) { ESP_LOGI(TAG, "下载进度: %d%% (%d/%d bytes)", percent, downloaded, total_size); last_reported = percent; } vTaskDelay(pdMS_TO_TICKS(100)); // 适当延迟减少CPU占用 }提示:在实际产品中,你可能会通过LED闪烁频率、屏幕进度条或MQTT消息等方式展示进度,而不仅仅是日志打印。
3. 处理不同IDF版本的兼容性问题
ESP-IDF在4.4和5.3版本中对OTA API做了一些细微调整,这正是很多开发者直接复制代码时会遇到的坑。以下是主要差异点:
| 功能点 | IDF 4.4 | IDF 5.3 |
|---|---|---|
| 函数返回值类型 | esp_err_t | int |
| 错误码定义 | 使用ESP_ERR前缀 | 部分错误码移除了ESP_ERR前缀 |
| 句柄初始化检查 | 需显式检查handle是否为NULL | esp_https_ota_begin会返回错误 |
| 内存管理 | 需要手动释放部分中间资源 | 大部分资源自动管理 |
针对这些差异,我们可以使用条件编译来保持代码兼容性:
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) #define OTA_GET_SIZE(handle) esp_https_ota_get_image_size(handle) #define OTA_GET_LEN(handle) esp_https_ota_get_image_len_read(handle) #else #define OTA_GET_SIZE(handle) (int)esp_https_ota_get_image_size(handle) #define OTA_GET_LEN(handle) (int)esp_https_ota_get_image_len_read(handle) #endif4. 超越进度条:扩展你的OTA监控能力
单纯的百分比进度已经很有用,但我们还可以提供更多有价值的信息:
4.1 下载速度计算
static int calculate_speed(int downloaded, int prev_downloaded, int64_t last_time, int64_t current_time) { if (last_time == 0 || current_time <= last_time) { return 0; } int bytes_diff = downloaded - prev_downloaded; int time_diff_ms = (int)(current_time - last_time); return (bytes_diff * 1000) / time_diff_ms; // bytes/sec } // 在进度循环中使用: int64_t last_time = 0; int prev_downloaded = 0; ... int speed = calculate_speed(downloaded, prev_downloaded, last_time, esp_timer_get_time()); prev_downloaded = downloaded; last_time = esp_timer_get_time();4.2 剩余时间估算
static int estimate_remaining(int total, int downloaded, int speed) { if (speed <= 0 || downloaded >= total) { return 0; } return (total - downloaded) / speed; // 秒 } // 结合速度计算使用: int remaining = estimate_remaining(total_size, downloaded, speed); ESP_LOGI(TAG, "速度: %d KB/s, 剩余时间: %d秒", speed/1024, remaining);4.3 网络中断恢复
增强版的OTA应该能够处理网络波动:
int retry_count = 0; const int max_retries = 3; while (1) { err = esp_https_ota_perform(ota_handle); if (err == ESP_ERR_HTTPS_OTA_IN_PROGRESS) { retry_count = 0; // 重置重试计数器 ... // 正常进度处理 } else if (err == ESP_FAIL && retry_count < max_retries) { ESP_LOGW(TAG, "网络中断,尝试恢复(%d/%d)...", retry_count+1, max_retries); vTaskDelay(pdMS_TO_TICKS(5000)); // 等待5秒后重试 retry_count++; continue; } else { break; } }5. 实战:将进度显示集成到你的项目中
现在,让我们把这些碎片整合成一个完整的解决方案。以下是替换标准OTA调用的步骤:
- 在你的组件目录中创建
custom_ota组件 - 添加
custom_ota.h头文件:
#pragma once #include "esp_http_client.h" esp_err_t custom_https_ota_with_progress(const esp_http_client_config_t *config);- 实现文件
custom_ota.c:
#include "custom_ota.h" #include "esp_https_ota.h" #include "esp_timer.h" static const char *TAG = "custom_ota"; esp_err_t custom_https_ota_with_progress(const esp_http_client_config_t *config) { // 整合前面章节的所有代码 ... }- 在你的主程序中替换原来的OTA调用:
// 原来的调用 // esp_err_t err = esp_https_ota(&config); // 新的调用 esp_err_t err = custom_https_ota_with_progress(&config);- 根据你的硬件平台,将进度信息输出到合适的显示设备:
// 示例:OLED屏幕显示 void display_progress(int percent, int speed, int remaining) { char buf[64]; snprintf(buf, sizeof(buf), "升级中: %d%%", percent); oled_display_text(buf, 0, 0); snprintf(buf, sizeof(buf), "速度: %d KB/s", speed/1024); oled_display_text(buf, 0, 16); snprintf(buf, sizeof(buf), "剩余: %d秒", remaining); oled_display_text(buf, 0, 32); }在实际项目中实现这套系统后,我发现几个值得分享的经验:首先,进度更新频率需要仔细平衡——太频繁会影响下载性能,太稀疏则显得不连贯。其次,网络速度的计算最好采用滑动平均算法来避免瞬时波动造成的显示跳动。最后,对于资源受限的设备,可以考虑将进度信息通过UART输出,由上位机软件负责更复杂的可视化展示。