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。

升级流程如下:

  1. 其中 FactoryApp 内存有出厂时的默认固件。
  2. 首次进行 OTA 升级时,应用向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。
  3. 系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。
  4. 同理,若某次升级后已经在执行 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和文件名
xecuting action: flash Serial port /dev/ttyACM0 Connecting.... Detecting chip type... ESP32-S3 Running ninja in directory /home/jichu/esp32/lvgl_display/build Executing "ninja flash"... [1/1] cd /home/jichu/esp32/lvgl_displa...isplay/build/bootloader/bootloader.bin Bootloader binary size 0x5240 bytes. 0x2dc0 bytes (36%) free. [2/5] Linking CXX executable lvgl_display.elf FAILED: lvgl_display.elf : && /home/jichu/.espressif/tools/xtensa-esp-elf/esp-13.2.0_20230928/xtensa-esp-elf/bin/xtensa-esp32s3-elf-g++ -mlongcalls -Wl,--cref -Wl,--defsym=IDF_TARGET_ESP32S3=0 -Wl,--Map=/home/jichu/esp32/lvgl_display/build/lvgl_display.map -Wl,--no-warn-rwx-segments -fno-rtti -fno-lto -Wl,--gc-sections -Wl,--warn-common -T esp32s3.peripherals.ld -T esp32s3.rom.ld -T esp32s3.rom.api.ld -T esp32s3.rom.libgcc.ld -T esp32s3.rom.newlib.ld -T esp32s3.rom.version.ld -T memory.ld -T sections.ld CMakeFiles/lvgl_display.elf.dir/project_elf_src_esp32s3.c.obj -o lvgl_display.elf -L/home/jichu/esp32/esp-idf/components/soc/esp32s3/ld -L/home/jichu/esp32/esp-idf/components/esp_rom/esp32s3/ld -L/home/jichu/esp32/lvgl_display/build/esp-idf/esp_system/ld -L/home/jichu/esp32/esp-idf/components/esp_phy/lib/esp32s3 -L/home/jichu/esp32/esp-idf/components/esp_coex/lib/esp32s3 -L/home/jichu/esp32/esp-idf/components/esp_wifi/lib/esp32s3 -L/home/jichu/esp32/esp-idf/components/bt/controller/lib_esp32c3_family/esp32s3 esp-idf/xtensa/libxtensa.a esp-idf/esp_ringbuf/libesp_ringbuf.a esp-idf/efuse/libefuse.a esp-idf/esp_mm/libesp_mm.a esp-idf/driver/libdriver.a esp-idf/esp_pm/libesp_pm.a esp-idf/mbedtls/libmbedtls.a esp-idf/esp_bootloader_format/libesp_bootloader_format.a esp-idf/esp_app_format/libesp_app_format.a esp-idf/bootloader_support/libbootloader_support.a esp-idf/esp_partition/libesp_partition.a esp-idf/app_update/libapp_update.a esp-idf/spi_flash/libspi_flash.a esp-idf/pthread/libpthread.a esp-idf/esp_system/libesp_system.a esp-idf/esp_rom/libesp_rom.a esp-idf/hal/libhal.a esp-idf/log/liblog.a esp-idf/heap/libheap.a esp-idf/soc/libsoc.a esp-idf/esp_hw_support/libesp_hw_support.a esp-idf/freertos/libfreertos.a esp-idf/newlib/libnewlib.a esp-idf/cxx/libcxx.a esp-idf/esp_common/libesp_common.a esp-idf/esp_timer/libesp_timer.a esp-idf/app_trace/libapp_trace.a esp-idf/esp_event/libesp_event.a esp-idf/nvs_flash/libnvs_flash.a esp-idf/esp_phy/libesp_phy.a esp-idf/vfs/libvfs.a esp-idf/lwip/liblwip.a esp-idf/esp_netif/libesp_netif.a esp-idf/wpa_supplicant/libwpa_supplicant.a esp-idf/esp_coex/libesp_coex.a esp-idf/esp_wifi/libesp_wifi.a esp-idf/bt/libbt.a esp-idf/unity/libunity.a esp-idf/cmock/libcmock.a esp-idf/console/libconsole.a esp-idf/http_parser/libhttp_parser.a esp-idf/esp-tls/libesp-tls.a esp-idf/esp_adc/libesp_adc.a esp-idf/esp_eth/libesp_eth.a esp-idf/esp_gdbstub/libesp_gdbstub.a esp-idf/esp_hid/libesp_hid.a esp-idf/tcp_transport/libtcp_transport.a esp-idf/esp_http_client/libesp_http_client.a esp-idf/esp_http_server/libesp_http_server.a esp-idf/esp_https_ota/libesp_https_ota.a esp-idf/esp_lcd/libesp_lcd.a esp-idf/protobuf-c/libprotobuf-c.a esp-idf/protocomm/libprotocomm.a esp-idf/esp_local_ctrl/libesp_local_ctrl.a esp-idf/espcoredump/libespcoredump.a esp-idf/wear_levelling/libwear_levelling.a esp-idf/sdmmc/libsdmmc.a esp-idf/fatfs/libfatfs.a esp-idf/json/libjson.a esp-idf/mqtt/libmqtt.a esp-idf/nvs_sec_provider/libnvs_sec_provider.a esp-idf/perfmon/libperfmon.a esp-idf/spiffs/libspiffs.a esp-idf/touch_element/libtouch_element.a esp-idf/usb/libusb.a esp-idf/wifi_p
03-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值