飞书文档https://x509p6c8to.feishu.cn/wiki/Hd9TwkuZ3iEQiUkjaoic5p7Knuh
ESO32S3应用程序可以在运行时通过网络从服务器下载新的固件,然后将其存储到某个分区中,从而实现固件的升级功能。
在ESP-IDF中有两种方式可以进行空中(OTA)升级:
使用 app_update 组件提供的原生API
使用 esp_https_ota 组件提供的简化API,它在原生OTA API上添加了一个抽象层,以便使用HTTPS协议进行升级。
分别在 esp-idf/examples/system/ota下的native_ota_example 和 simple_ota_example 下的OTA示例中演示了这两种方法。
在实现OTA功能时,要求我们要重新设计分区表,添加两个OTA分区,例如下方所示,具体设置方法在后面代码中会有讲解。
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
ota_0, app, ota_0, , 1M,
ota_1, app, ota_1, , 1M,
与升级相关的(至少)四个分区:OTA data、Factory App、OTA_0、OTA_1。
升级流程如下:
- 其中 FactoryApp 内存有出厂时的默认固件。
- 首次进行 OTA 升级时,应用向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。
- 系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。
- 同理,若某次升级后已经在执行 OTA_0 内的固件,此时再升级时应用就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0、OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。
本地服务器搭建
在升级前,我们可以先搭建一个本地HTTPS文件服务器,由于第三方服务器可能不可靠,随时有关停可能,所以本地服务器可以方便我们快速验证,由于走的是HTTPS升级,所以我们还需要生成一个证书,这个证书同时放在服务器端和设备中,升级时设备会验证服务器端的证书是否合法,这也是企业级解决方案必须具备的功能。
生成证书
我们可以使用openssl生成一个自己的证书,这里面需要自行安装openssl工具,如果不想自己安装,也可以直接使用文档提供的证书,在实际开发中,这部分证书是由服务器同事进行管理的。
openssl req -x509 -newkey rsa:2048 -keyout xiaozhi_key.pem -out xiaozhi_cert.pem -days 3650 -nodes
依次输入:
(国家)、
(洲/省)、
(城/镇)、
(组织名)、
(单位名)、
(httpd-ssl.conf中的ServerName 名称)、
(邮箱)
这里其实可以随意填写任意字符,不影响后续操作。
以上指令会生成一个密钥xiaozhi_key.pem,一个证书xiaozhi_cert.pem文件
拿到证书后,我们就可以搭建本地HTTP服务器
本地HTTP服务器搭建
我们直接使用开源的hfs进行搭建,链接如下,需要魔法访问
https://github.com/rejetto/hfs/releases
根据对应平台下载
解压后,双击hfs.exe打开
这里填写https的端口为8088,其它值也是可以的,只要你电脑没占用此端口即可,把这个端口记录下来,因为后续设备需要用到
然后上传上面生成的证书和密钥,上传完成一定要点击底部的保存
然后查看本机ip,在windos的命令行窗口,输入ipconfig,例如我的ip是192.168.3.24,把这个ip记录下来
然后添加需要升级的文件到服务器中,这里同时要配置链接,根据上面的ip和端口输入https://192.168.3.24:8088
添加完成后点击底部的ADD,找到需要升级的固件,例如这里使用最简单的工程固件hello_world.bin
固件在工程编译成功后的build文件内,例如:hello_world/build/hello_world.bin
上传成功后,记得点击SAVE,到这里配置就完成了,你在浏览器中访问https://192.168.3.24:8088/
看到上述文件代表配置成功,如果打不开此页面,估计了漏了步骤,可以重复多做几次,一般可能的原因:
- 端口占用
- IP错误
- 有页面配置未保存
可以百度下”hfs服务器搭建“,结合其它文章多搭建几次即可。
设备端实现
代码部分先添加上述的证书,把xiaozhi_cert.pem文件放到工程main内
然后修改CMakeLists.txt
demo/main/CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
EMBED_TXTFILES xiaozhi_cert.pem
)
最终代码如下
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_eap_client.h"
#include "esp_netif.h"
#include "esp_smartconfig.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
static const char *TAG = "simple_ota_example";
static EventGroupHandle_t s_wifi_event_group;
static const int CONNECTED_BIT = BIT0;
static const int ESPTOUCH_DONE_BIT = BIT1;
static void smartconfig_example_task(void *parm);
static bool is_connect_wifi = false;
extern const uint8_t server_cert_pem_start[] asm("_binary_xiaozhi_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_xiaozhi_cert_pem_end");
#define OTA_URL_SIZE 256
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADER_SENT:
ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
case HTTP_EVENT_REDIRECT:
ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT");
break;
}
return ESP_OK;
}
void simple_ota_run()
{
ESP_LOGI(TAG, "Starting OTA example task");
esp_http_client_config_t config = {
.url = "https://192.168.3.24:8088/hello_world.bin",
.cert_pem = (char *)server_cert_pem_start,
.event_handler = _http_event_handler,
.skip_cert_common_name_check = true, //自己生成的测试证书中没有域名信息,所以不检查
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(TAG, "Attempting to download update from %s", config.url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA Succeed, Rebooting...");
esp_restart();
} else {
ESP_LOGE(TAG, "Firmware upgrade failed");
}
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
static void http_get_task(void *pvParameters)
{
while (1)
{
if (is_connect_wifi)
{
simple_ota_run();
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
// WiFi 站点模式启动后,创建 SmartConfig 任务
xTaskCreate(smartconfig_example_task, "smartconfig_example_task", 4096, NULL, 3, NULL);
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
is_connect_wifi = false;
// WiFi 断开连接时,重新连接并清除连接标志位
esp_wifi_connect();
xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
// 获取到 IP 地址后,设置连接标志位
xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
is_connect_wifi = true;
}
else if (event_base == SC_EVENT && event_id == SC_EVENT_SCAN_DONE)
{
// SmartConfig 扫描完成事件
ESP_LOGI(TAG, "Scan done");
}
else if (event_base == SC_EVENT && event_id == SC_EVENT_FOUND_CHANNEL)
{
// SmartConfig 找到信道事件
ESP_LOGI(TAG, "Found channel");
}
else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD)
{
// SmartConfig 获取到 SSID 和密码事件
ESP_LOGI(TAG, "Got SSID and password");
smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
wifi_config_t wifi_config;
uint8_t ssid[33] = {0};
uint8_t password[65] = {0};
uint8_t rvd_data[33] = {0};
bzero(&wifi_config, sizeof(wifi_config_t));
memcpy(wifi_config.sta.ssid, evt->ssid, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.sta.password, evt->password, sizeof(wifi_config.sta.password));
memcpy(ssid, evt->ssid, sizeof(evt->ssid));
memcpy(password, evt->password, sizeof(evt->password));
ESP_LOGI(TAG, "SSID:%s", ssid);
ESP_LOGI(TAG, "PASSWORD:%s", password);
if (evt->type == SC_TYPE_ESPTOUCH_V2)
{
// 如果使用的是 ESPTouch V2,获取额外的数据
ESP_ERROR_CHECK(esp_smartconfig_get_rvd_data(rvd_data, sizeof(rvd_data)));
ESP_LOGI(TAG, "RVD_DATA:");
for (int i = 0; i < 33; i++)
{
printf("%02x ", rvd_data[i]);
}
printf("\n");
}
// 断开当前 WiFi 连接,设置新的 WiFi 配置并重新连接
ESP_ERROR_CHECK(esp_wifi_disconnect());
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
esp_wifi_connect();
}
else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE)
{
// SmartConfig 发送 ACK 完成事件,设置 SmartConfig 完成标志位
xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
}
}
static void smartconfig_example_task(void *parm)
{
EventBits_t uxBits;
wifi_config_t myconfig = {0};
ESP_LOGI(TAG, "creat smartconfig_example_task");
// 获取wifi配置信息
esp_wifi_get_config(ESP_IF_WIFI_STA, &myconfig);
if (strlen((char *)myconfig.sta.ssid) > 0)
{
// 如果配置过,就直接连接wifi
ESP_LOGI(TAG, "alrealy set, SSID is :%s,start connect", myconfig.sta.ssid);
esp_wifi_connect();
}
else
{
// 如果没有配置过,就进行配网操作
ESP_LOGI(TAG, "have no set, start to config");
ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_AIRKISS)); // 支持APP ESPTOUCH和微信AIRKISS
smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_smartconfig_start(&cfg));
}
while (1)
{
// 等待连接标志位或 SmartConfig 完成标志位
uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
if (uxBits & CONNECTED_BIT)
{
// 连接到 AP 后的日志
ESP_LOGI(TAG, "WiFi Connected to ap");
// 联网成功后,可以关闭线程
vTaskDelete(NULL);
}
if (uxBits & ESPTOUCH_DONE_BIT)
{
// SmartConfig 完成后的日志
ESP_LOGI(TAG, "smartconfig over");
// 停止 SmartConfig
esp_smartconfig_stop();
// 删除 SmartConfig 任务
vTaskDelete(NULL);
}
}
}
void app_main(void)
{
// 初始化 NVS 闪存
ESP_LOGI(TAG, "OTA example app_main start");
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 1.OTA app partition table has a smaller NVS partition size than the non-OTA
// partition table. This size mismatch may cause NVS initialization to fail.
// 2.NVS partition contains data in new format and cannot be recognized by this version of code.
// If this happens, we erase NVS partition and initialize NVS again.
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
// 初始化网络接口
ESP_ERROR_CHECK(esp_netif_init());
// 创建事件组
s_wifi_event_group = xEventGroupCreate();
// 创建默认事件循环
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 创建默认的 WiFi 站点模式网络接口
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// 初始化 WiFi 配置
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册事件处理函数
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
// 设置 WiFi 模式为站点模式并启动 WiFi
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
xTaskCreate(&http_get_task, "http_get_task", 8192, NULL, 5, NULL);
}
然后修改分区表,选择支持OTA的分区表
(Top) → Partition Table → Partition Table
Espressif IoT Development Framework Configuration
( ) Single factory app, no OTA
( ) Single factory app (large), no OTA
(X) Factory app, two OTA definitions
( ) Two large size OTA partitions
( ) Custom partition table CSV
因为OTA分区表的分区如下,是大于默认工程设置的2M Flash大小的
*******************************************************************************
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,16K,
otadata,data,ota,0xd000,8K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
ota_0,app,ota_0,0x110000,1M,
ota_1,app,ota_1,0x210000,1M,
*******************************************************************************
所以还需要修改Flash大小,这个可以按ESP32S3的实际大小修改,例如我们改为4M
(Top) → Serial flasher config
Espressif IoT Development Framework Configuration
[ ] Disable download stub
[ ] Enable Octal Flash
[*] Choose flash mode automatically (please read help)
Flash SPI mode (DIO) --->
Flash Sampling Mode (STR Mode) --->
Flash SPI speed (80 MHz) --->
Flash size (4 MB) --->
[ ] Detect flash size when flashing bootloader
Before flashing (Reset to bootloader) --->
After flashing (Reset after flashing) --->
修改完成后,重新编译,代码会先进行配网操作,注意,配置的网络需要和你的电脑运行的服务器连接到同一个网络,保证网段是一样的,例如都是192.168.3.xx,配网完网络后,设备连接路由后会开启OTA,OTA结束后,重启日志如下
如果出现OTA失败,一般就三个原因,逐个排查即可。
- IP网段不一样,设备连接的路由和电脑连接路由不一样
- 证书没设置好
- 电脑禁用了某个端口
- 服务器端口没改过来
- 服务器IP没改过来
- 文件名没改过来,可以在浏览器上测试能否下载,https://192.168.3.24:8088/hello_world.bin,改为你自己的IP和文件名