动态更新来自任意来源的 ESP32 固件





5.00/5 (2投票s)
您需要能够无需 WiFi 进行固件更新吗?这个项目就是为您准备的。
引言
我最近需要为 ESP32 提供固件更新功能,但我的 ESP32 不是从 WiFi 获取固件,而是通过串口 UART 从另一个连接的设备接收固件,该设备本身从 MQTT 服务器获取固件包。太棒了!开箱即用的 OTA 这里不适用,但您可以使用 OTA API 从您自己的数据流进行更新。
必备组件
- 你需要一个 ESP32。这段代码适用于基础 ESP32,但你可以通过修改 *platformio.ini* 来在例如 ESP32-S3 等设备上使用它。
- 你需要安装 VS Code 和 Platform IO。
- 要重新编译二进制文件,你需要本地安装 C++ 编译器和 CMake,以便你可以构建 zipsx 二进制文件(我使用 VS Code CMake 扩展和 Visual Studio 的 C++ 编译器)。这对于演示来说不是严格必需的,但如果你想在实际应用中压缩你的固件,或者如果你想更改演示提供的固件,则需要这样做。
- 如果你想重新编译二进制文件,你需要能够在本地创建 zip 文件。
准备更新二进制文件
这涉及几个步骤
- 在 Platform IO 中将构建配置切换到版本 B 条目并构建它。
- 在 *.pio* 构建文件夹下,找到版本 B 的 *firmware.bin*。
- 将 *firmware.bin* 压缩成 *firmware_rev_b.zip*。它必须是 zip 压缩包中的唯一文件。
- 运行 zipsx 处理 zip 文件以创建 *firmware_rev_b.stream*。这是从 zip 文件中提取的压缩二进制数据。不能流式传输 zip 文件,因为它们的目录条目位于文件的末尾。因此,我们不使用 zip 文件,而是仅使用固件文件的嵌入式压缩流。
- 对于演示(而非实际应用) - 使用我的在线 头文件生成工具,并将输出类型设置为 C/C++,以生成 *firmware_rev_b.h* 并将其复制到主项目中的 *include* 文件夹下。
Using the Code
注意:由于这使用了 OTA,你的 ESP32 必须有两个大小相同的应用程序分区才能方便更新。幸运的是,这是 ESP32 的默认配置,因此你不必更改你的闪存分区。
使用版本 A 配置来构建包含更新程序的主固件。setup()
中的所有代码都用于促进更新,因此你可以将该代码改编到你的项目中。
#include <Arduino.h>
// build the other project, and then compress
// and convert its firmware.bin to a header
#define FIRMWARE_REV_B_IMPLEMENTATION
#include <firmware_rev_b.h>
#include <htcw_zip.hpp>
#include "esp_ota_ops.h"
using namespace zip;
using namespace io;
esp_ota_handle_t handle = 0;
uint8_t write_buffer[8192];
size_t write_size = sizeof(write_buffer);
uint8_t* write_ptr = write_buffer;
void setup() {
Serial.begin(115200);
Serial.println("Hello from revision A");
Serial.print("Unpacking and updating firmware (");
Serial.print(sizeof(firmware_rev_b));
Serial.println(" bytes)");
io::const_buffer_stream in(firmware_rev_b,sizeof(firmware_rev_b));
archive arch;
esp_ota_begin(esp_ota_get_next_update_partition(NULL), OTA_SIZE_UNKNOWN, &handle);
zip_result r=inflate(&in,[](const uint8_t* buffer,size_t size, void* state){
if(size>write_size) {
size_t sz = sizeof(write_buffer)-write_size;
if(ESP_OK!=esp_ota_write(handle,write_buffer,sz)) {
Serial.println("OTA write error");
return (size_t)0;
} else {
Serial.print("OTA wrote ");
Serial.print(sz);
Serial.println(" bytes");
}
write_size = sizeof(write_buffer);
write_ptr = write_buffer;
}
memcpy(write_ptr,buffer,size);
write_ptr+=size;
write_size-=size;
return size;
},NULL,sizeof(firmware_rev_b));
if(zip_result::success==r) {
if(write_size<sizeof(write_buffer)) {
size_t sz = sizeof(write_buffer)-write_size;
if(ESP_OK!=esp_ota_write(handle,write_buffer,sz)) {
Serial.println("OTA write error");
while(1);
} else {
Serial.print("OTA wrote ");
Serial.print(sz);
Serial.println(" bytes");
}
}
if (ESP_OK == esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL))) {
Serial.println("Updated. Restarting");
esp_restart();
} else {
Serial.println("OTA unable to set boot partition");
Serial.println("Update error");
}
} else {
if(r!=zip_result::success) {
Serial.print("Unpacking error: ");
Serial.println((int)r);
}
Serial.println("Update error");
}
}
void loop() {
}
首先,我们设置包含文件。除了 OTA 支持、固件和 zip 支持外,大部分都是样板代码。你会注意到 `#define FIRMWARE_REV_B_IMPLEMENTATION`。这提供了实际的数组数据,并且需要在关联的包含文件之前指定,否则你会遇到链接错误。
*htcw_zip.hpp* 文件是 zip 提取和霍夫曼解压缩支持。为了“解压”(解压缩)“压缩”(压缩)的固件流,这是必要的。
*esp_ota_ops.h* 由 ESP-IDF 提供,并有助于实际调用 OTA API。
之后,是一些命名空间导入 - 一个用于我们的流支持(我们不使用 STL),另一个用于我们的 zip/解压缩支持。
接下来是 OTA 更新的“句柄”。与大多数句柄不同,它不是指针,而是一个整数,因此请相应地编写代码,例如将其初始化为 `0` 而不是 `NULL`。
我们使用该句柄在下一个可用的 OTA 分区上开始 OTA 更新。
接下来,我们有代码来辅助写入缓冲区。我们需要缓冲写入的原因是霍夫曼解压缩倾向于一次写入一个字节的数据,并且在 OTA 更新期间一次写入一个字节到闪存非常慢。相反,我们一次从解压缩例程收集 8KB 数据,然后写入。
在 `setup()` 中,我们将固件头文件中的数组包装在 `const_buffer_stream` 中,这为我们提供了数组的游标。在实际应用中,你可能会使用 `file_stream` 从 SD 卡等获取它,或者使用 `arduino_stream` 从串口读取它,或者使用 HTTP 等(后者至少在 Arduino 下是这样)。
声明一个匿名函数来处理 `inflate()` 回调,在其中我们填充 `write_buffer`,然后在每次它填满时将其写入。inflate() 函数每次生成更多解压缩数据时都会调用它自己的回调,这就是我们处理的内容。
之后,如果成功,我们将写入最终剩余的部分块(如果有),否则报告错误。写入最终块后,如果一切正常,我们将引导分区更改为我们刚刚写入的分区。
最后,如果所有这些都成功了,我们只需重新启动。
如果上述任何步骤失败,我们将报告错误。
关注点
我最初尝试在不从 zip 中提取流的情况下进行此操作,而只是使用 zip 本身,即使如果我想过这个问题,我也会意识到这是不可能的,因为中央目录信息位于文件的末尾。那浪费了不少时间。真是愚蠢。
历史
- 2024年3月24日 - 初次提交