ESP32学习笔记(24)——OTA(空中升级)接口使用(原生API)

Posted Leung_ManWah

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32学习笔记(24)——OTA(空中升级)接口使用(原生API)相关的知识,希望对你有一定的参考价值。

一、概述

ESP32应用程序可以在运行时通过Wi-Fi或以太网从特定的服务器下载新映像,然后将其闪存到某些分区中,从而进行升级。

在ESP-IDF中有两种方式可以进行空中(OTA)升级:

  • 使用 app_update 组件提供的原生API
  • 使用 esp_https_ota 组件提供的简化API,它在原生OTA API上添加了一个抽象层,以便使用HTTPS协议进行升级。

分别在 native_ota_examplesimple_ota_example 下的OTA示例中演示了这两种方法。

1.1 OTA工作流程

1.2 OTA数据分区

ESP32 SPI Flash 内有与升级相关的(至少)四个分区:OTA dataFactory AppOTA_0OTA_1。其中 FactoryApp 内存有出厂时的默认固件。

首次进行 OTA 升级时,OTA Demo 向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。

系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。

同理,若某次升级后 ESP32 已经在执行 OTA_0 内的固件,此时再升级时 OTA Demo 就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。

为了简单起见,OTA示例通过在menuconfig中启用CONFIG_PARTITION_TABLE_TWO_OTA选项来选择预定义的分区表,该选项支持三个应用程序分区:工厂分区、OTA_0分区和OTA_1分区。有关分区表的更多信息,请参阅分区表.

二、API说明

以下 OTA 接口位于 app_update/include/esp_ota_ops.h

2.1 esp_ota_begin

2.2 esp_ota_write

2.3 esp_ota_end

2.4 esp_ota_set_boot_partition

2.5 esp_ota_get_boot_partition

2.6 esp_ota_get_running_partition

2.7 esp_ota_get_next_update_partition

三、编程流程

3.1 OTA详细过程逻辑

3.2 OTA分区操作流程


节选自 esp-idf\\examples\\system\\ota\\native_ota_example 中的例程

static void ota_example_task(void *pvParameter)
{
    esp_err_t err;
    /* update handle : set by esp_ota_begin(), must be freed via esp_ota_end() */
    esp_ota_handle_t update_handle = 0 ;
    const esp_partition_t *update_partition = NULL;

    ESP_LOGI(TAG, "Starting OTA example");
    //获取OTA app存放的位置
    const esp_partition_t *configured = esp_ota_get_boot_partition();
    //获取当前系统执行的固件所在的Flash分区
    const esp_partition_t *running = esp_ota_get_running_partition();

    if (configured != running) {
        ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x",
                 configured->address, running->address);
        ESP_LOGW(TAG, "(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)");
    }
    ESP_LOGI(TAG, "Running partition type %d subtype %d (offset 0x%08x)",
             running->type, running->subtype, running->address);

    esp_http_client_config_t config = {
        .url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL,
        .cert_pem = (char *)server_cert_pem_start,
        .timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT,
    };

#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN
    char url_buf[OTA_URL_SIZE];
    if (strcmp(config.url, "FROM_STDIN") == 0) {
        example_configure_stdin_stdout();
        fgets(url_buf, OTA_URL_SIZE, stdin);
        int len = strlen(url_buf);
        url_buf[len - 1] = '\\0';
        config.url = url_buf;
    } else {
        ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url");
        abort();
    }
#endif

#ifdef CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK
    config.skip_cert_common_name_check = true;
#endif

    esp_http_client_handle_t client = esp_http_client_init(&config);
    if (client == NULL) {
        ESP_LOGE(TAG, "Failed to initialise HTTP connection");
        task_fatal_error();
    }
    //连http服务器
    err = esp_http_client_open(client, 0);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
        esp_http_client_cleanup(client);
        task_fatal_error();
    }
    esp_http_client_fetch_headers(client);

    //获取当前系统下一个(紧邻当前使用的OTA_X分区)可用于烧录升级固件的Flash分区
    update_partition = esp_ota_get_next_update_partition(NULL);
    ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%x",
             update_partition->subtype, update_partition->address);
    assert(update_partition != NULL);

    int binary_file_length = 0;
    /*deal with all receive packet*/
    bool image_header_was_checked = false;
    while (1) {
        int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE);
        if (data_read < 0) {
            ESP_LOGE(TAG, "Error: SSL data read error");
            http_cleanup(client);
            task_fatal_error();
        } else if (data_read > 0) {
            if (image_header_was_checked == false) {
                esp_app_desc_t new_app_info;
                if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
                    // check current version with downloading
                    memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));
                    ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version);

                    esp_app_desc_t running_app_info;
                    if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) {
                        ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version);
                    }

                    const esp_partition_t* last_invalid_app = esp_ota_get_last_invalid_partition();
                    esp_app_desc_t invalid_app_info;
                    if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) {
                        ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version);
                    }

                    // check current version with last invalid partition
                    if (last_invalid_app != NULL) {
                        if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {
                            ESP_LOGW(TAG, "New version is the same as invalid version.");
                            ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
                            ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");
                            http_cleanup(client);
                            infinite_loop();
                        }
                    }
#ifndef CONFIG_EXAMPLE_SKIP_VERSION_CHECK
                    if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0) {
                        ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update.");
                        http_cleanup(client);
                        infinite_loop();
                    }
#endif

                    image_header_was_checked = true;
                     //OTA写开始
                    err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
                    if (err != ESP_OK) {
                        ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
                        http_cleanup(client);
                        task_fatal_error();
                    }
                    ESP_LOGI(TAG, "esp_ota_begin succeeded");
                } else {
                    ESP_LOGE(TAG, "received package is not fit len");
                    http_cleanup(client);
                    task_fatal_error();
                }
            }
            //写flash
            err = esp_ota_write( update_handle, (const void *)ota_write_data, data_read);
            if (err != ESP_OK) {
                http_cleanup(client);
                task_fatal_error();
            }
            binary_file_length += data_read;
            ESP_LOGD(TAG, "Written image length %d", binary_file_length);
        } else if (data_read == 0) {
           /*
            * As esp_http_client_read never returns negative error code, we rely on
            * `errno` to check for underlying transport connectivity closure if any
            */
            if (errno == ECONNRESET || errno == ENOTCONN) {
                ESP_LOGE(TAG, "Connection closed, errno = %d", errno);
                break;
            }
            if (esp_http_client_is_complete_data_received(client) == true) {
                ESP_LOGI(TAG, "Connection closed");
                break;
            }
        }
    }
    ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length);
    if (esp_http_client_is_complete_data_received(client) != true) {
        ESP_LOGE(TAG, "Error in receiving complete file");
        http_cleanup(client);
        task_fatal_error();
    }

    //OTA写结束
    err = esp_ota_end(update_handle);
    if (err != ESP_OK) {
        if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
            ESP_LOGE(TAG, "Image validation failed, image is corrupted");
        }
        ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
        http_cleanup(client);
        task_fatal_error();
    }

    //升级完成更新OTA data区数据,重启时根据OTA data区数据到Flash分区加载执行目标(新)固件
    err = esp_ota_set_boot_partition(update_partition);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
        http_cleanup(client);
        task_fatal_error();
    }
    ESP_LOGI(TAG, "Prepare to restart system!");
    esp_restart();
    return ;
}

四、测试流程

4.1 配置Flash大小

打开项目配置菜单 idf.py menuconfig
选择串行Flash配置 Serial flasher config --->

根据模块Flash大小,更改 Flash size,我使用的是 ESP32-WROVER-B 模组,所以修改为 4 MB

4.2 配置分区表

选择分区表配置 Partition Table --->

选择 Factory app, two OTA definitions

4.3 配置服务器信息


看服务器类型修改 httpshttp

4.4 配置连接方式


选择WIFI连接方式,并修改要连接路由器的SSID和密码

4.5 配置APP版本号

Note: native_ota_example 中没有版本号大小检查,它看到不同的版本就会下载。当本地设备是比OTA服务器版本号更高的时候,也会下载OTA服务器的旧版进行更新。这个需要自行添加版本号大小检查。

4.5.1 通过version.txt控制

4.5.2 通过menuconfig控制

选择应用管理 Application manager --->

勾选 Get the project version from Kconfig 并在下面填写版本号。

4.6 开启HTTP服务器(可选)

编译链内 Python 有一个内置的 HTTP 服务器,我们这里可以直接使用它。我们将会使用示例 get-started/hello_world 作为需要更新的固件。

4.6.1 生成目标固件

使用示例 get-started/hello_world,此处执行idf.py build 会生成二进制文件get-started\\hello_world\\build\\hello-world.bin。

4.6.2 创建HTTP服务器

  • 使用工具链python创建服务器
    进入存放需要升级的固件(hello-world.bin)目录。在此处创建HTTP服务器。
    此处执行 python -m http.server --bind 192.168.61.67 8070会生成一个HTTP本地服务器。该服务器下对应\\build文件夹下所有内容。(有的文章执行的python命令不同,是由于python版本不同造成的。)
  • 使用HFS创建服务器
    链接:https://pan.baidu.com/s/1MIAI5m4WQJdAZpqRAdvsXg 提取码:9m8y

4.7 开启HTTPS服务器(可选)

  1. Shirt + 鼠标右键 打开 Linux shell
  2. 创建一个新的自签名证书和密钥
    openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem -days 365 -nodes
    依次输入:(国家)、(洲/省)、(城/镇)、(组织名)、(单位名)、(httpd-ssl.conf中的ServerName 名称)、(邮箱)
  3. 将生成的证书 ca_cert.pem 复制到OTA示例目录中的 server_certs 目录下,已存在则替换内容
  4. 创建HTTPS服务器
    openssl s_server -WWW -key ca_key.pem -cert ca_cert.pem -port 8070

4.8 查看打印



• 由 Leung 写于 2021 年 6 月 11 日

• 参考:ESP32 学习日志(4)——OTA升级(1)-示例解析
    第二十章 ESP32的空中升级(OTA)
    ESP32 OTA详解-中文翻译版

以上是关于ESP32学习笔记(24)——OTA(空中升级)接口使用(原生API)的主要内容,如果未能解决你的问题,请参考以下文章

ESP32入门基础之空中升级(OTA)

ESP32入门基础之空中升级(OTA)

给你的 ESP32 进行空中升级

给你的 ESP32 进行空中升级

IoT如何实现 ESP32 固件的 OTA 在线升级更新

乐鑫Esp32学习之旅30 对接华为IoT物联网平台适配踩坑,使用ESP-IDF 编程使用HTTPS请求固件跳过证书校验,实现OTA远程升级文件。(附带源码)