SOC-ESP32S3部分:27-设备OTA
飞书文档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 256esp_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){// 如果配置过,就直接连接wifiESP_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和微信AIRKISSsmartconfig_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");// 停止 SmartConfigesp_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 模式为站点模式并启动 WiFiESP_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 bootloaderBefore 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和文件名