Skip to content

数据分析

分区表里面必须要有一个OTA数据分区, 类型为Data

第一次 OTA 升级后,OTA 数据分区更新,指定下一次启动哪个 OTA 应用程序分区。

image-20240625174631671

  1. 如果在项目 PROJECT_VER 文件中设置PROJECT_VER变量,则使用它的值。
  2. 否则,如果 $PROJECT_PATH/version.txt 存在,它的内容将用作 PROJECT_VER
  3. 否则,如果项目位于Git存储库中,则使用 git describe的输出.

应用回滚

应用程序回滚的主要目的是确保设备在更新后正常工作。如果新版应用程序出现严重错误,该功能可使设备回滚到之前正常运行的应用版本。在使能回滚并且 OTA 升级应用程序至新版本后,可能出现的结果如下:

c
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
//获取当前的状态
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
    if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
        //这一个状态表示当前的程序是第一次启动
        // run diagnostic function ...
        bool diagnostic_is_ok = diagnostic();
        if (diagnostic_is_ok) {
            ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution ...");
            esp_ota_mark_app_valid_cancel_rollback(); //标记一下当前的程序是有效的
        } else {
            ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ...");
            esp_ota_mark_app_invalid_rollback_and_reboot();//标记一下当前的程序是无效的, 并且重启, 重启后会回到上一个版本
        }
    }
}

程序的状态是放在otadata 分区, 该分区有一个 ota_seq 计数器,该计数器是 OTA 应用分区的指针,指向下次启动时选取应用所在的分区 (ota_0, ota_1, ...)。

Kconfig 中的 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE 可以帮助用户追踪新版应用程序的第一次启动。应用程序需调用 esp_ota_mark_app_valid_cancel_rollback() 函数确认可以运行,否则将会在重启时回滚至旧版本。该功能可让用户在启动阶段控制应用程序的可操作性。新版应用程序仅有一次机会尝试是否能成功启动。

状态引导加载程序选取启动应用程序的限制
ESP_OTA_IMG_VALID没有限制,可以选取。
ESP_OTA_IMG_UNDEFINED没有限制,可以选取。
ESP_OTA_IMG_INVALID不会选取。
ESP_OTA_IMG_ABORTED不会选取。
ESP_OTA_IMG_NEW如使能 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, 则仅会选取一次。在引导加载程序中,状态立即变为 ESP_OTA_IMG_PENDING_VERIFY
ESP_OTA_IMG_PENDING_VERIFY如使能 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, 则不会选取,状态变为 ESP_OTA_IMG_ABORTED

默认的时候这一个是没有启用的, ESP_OTA_IMG_PENDING_VERIFY是不会到达的

如果失能, 第一次启动的时候状态从ESP_OTA_IMG_NEW, 变为ESP_OTA_IMG_PENDING_VERIFY, 如果不使用esp_ota_mark_app_valid_cancel_rollback();进行设置之后会变为ESP_OTA_IMG_PENDING_VERIFY不能在进行启动

自动回滚过程

CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE 使能时,回滚过程如下:

  • 新版应用程序下载成功,esp_ota_set_boot_partition() 函数将分区设为可启动,状态设为 ESP_OTA_IMG_NEW。该状态表示应用程序为新版本,第一次启动需要监测。
  • 重新启动 esp_restart()
  • 引导加载程序检查 ESP_OTA_IMG_PENDING_VERIFY 状态,如有设置,则将其写入 ESP_OTA_IMG_ABORTED
  • 引导加载程序选取一个新版应用程序来引导,这样应用程序状态就不会设置为 ESP_OTA_IMG_INVALIDESP_OTA_IMG_ABORTED
  • 引导加载程序检查所选取的新版应用程序,若状态设置为 ESP_OTA_IMG_NEW,则写入 ESP_OTA_IMG_PENDING_VERIFY。该状态表示,需确认应用程序的可操作性,如不确认,发生重启,则状态会重写为 ESP_OTA_IMG_ABORTED (见上文),该应用程序不可再启动,将回滚至上一版本。
  • 新版应用程序启动,应进行自测。
  • 若通过自测,则必须调用函数 esp_ota_mark_app_valid_cancel_rollback(),因为新版应用程序在等待确认其可操作性(ESP_OTA_IMG_PENDING_VERIFY 状态)。
  • 若未通过自测,则调用函数 esp_ota_mark_app_invalid_rollback_and_reboot(),回滚至之前能正常工作的应用程序版本,同时将无效的新版本应用程序设置为 ESP_OTA_IMG_INVALID
  • 如果新版应用程序可操作性没有确认,则状态一直为 ESP_OTA_IMG_PENDING_VERIFY。下一次启动时,状态变更为 ESP_OTA_IMG_ABORTED,阻止其再次启动,之后回滚到之前的版本。

尽快完成, 防止意外复位, 自测以后设置状态是用户代码实现的

实际的更新

  1. 查看当前的版本, 看一看启动的是不是配置的版本
c
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 task");

    const esp_partition_t *configured = esp_ota_get_boot_partition();   //获取配置的分区
    const esp_partition_t *running = esp_ota_get_running_partition();   //获取当前运行的分区

    if (configured != running) {
        //版本不一致, 可能出错, 提示一下
        ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08"PRIx32", but running from offset 0x%08"PRIx32,
                 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%08"PRIx32")",
             running->type, running->subtype, running->address);
	//设置一下http的服务器
    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,
        .keep_alive_enable = true,
    };

#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
    //获取http client
    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);
    //从Flash里面获取一个可以使用的分区
    update_partition = esp_ota_get_next_update_partition(NULL); //获取下一个可以进行写入的位置
    assert(update_partition != NULL);
    ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%"PRIx32,
             update_partition->subtype, update_partition->address);

    int binary_file_length = 0; //记录当前的bin文件大小
    /*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); //获取数据, 数据在ota_write_data里面
        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
                    //获取新的image文件的头部信息
                    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的头部信息
                    image_header_was_checked = true;
                    //擦除分区update_handle是一个传出参数, 表示状态
                    err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);
                    if (err != ESP_OK) {
                        ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
                        http_cleanup(client);
                        esp_ota_abort(update_handle);
                        task_fatal_error();
                    }
                    ESP_LOGI(TAG, "esp_ota_begin succeeded");
                } else {
                    ESP_LOGE(TAG, "received package is not fit len");
                    http_cleanup(client);
                    esp_ota_abort(update_handle);
                    task_fatal_error();
                }
            }
            //写入数据, 除了第一次直接回到这里来, 数据直接写入
            err = esp_ota_write( update_handle, (const void *)ota_write_data, data_read);
            if (err != ESP_OK) {
                http_cleanup(client);
                esp_ota_abort(update_handle);
                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;
            }
        }
    }
    //关闭http连接
    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);
        esp_ota_abort(update_handle);
        task_fatal_error();
    }

    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");
        } else {
            ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
        }
        http_cleanup(client);
        task_fatal_error();
    }
	//设置新的启动
    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 ;
}

API

c
const esp_partition_t *esp_ota_get_boot_partition(void)

Get partition info of currently configured boot app.获取当前配置的启动APP

If esp_ota_set_boot_partition() has been called, the partition which was set by that function will be returned.使用esp_ota_set_boot_partition这一个进行设置, 没有设置的时候一般是当前运行的APP

c
const esp_partition_t *esp_ota_get_running_partition(void)

获取运行的APP

c
const esp_partition_t *esp_ota_get_next_update_partition(const esp_partition_t *start_from)

获取下一个OTA分区, 从当前的running分区开始

c
esp_err_t esp_ota_begin(const esp_partition_t *partition, size_t image_size, esp_ota_handle_t *out_handle)

开始启动OTA擦除, 需要设置image的大小从而开始擦除out_handle是一个传出参数

c
esp_err_t esp_ota_write(esp_ota_handle_t handle, const void *data, size_t size)

开始进行写入

c
esp_err_t esp_ota_end(esp_ota_handle_t handle)

写入结束

c
esp_err_t esp_ota_abort(esp_ota_handle_t handle)

写入出错的时候中断使用这一个

c
esp_err_t esp_ota_set_boot_partition(const esp_partition_t *partition

设置启动的分区

c
esp_err_t esp_ota_get_partition_description(const esp_partition_t *partition, esp_app_desc_t *app_desc)

从app的部分获取app的版本信息

c
const esp_partition_t *esp_ota_get_last_invalid_partition(void)

获取上一次出错的版本