Appearance
数据分析
分区表里面必须要有一个OTA数据分区, 类型为Data
第一次 OTA 升级后,OTA 数据分区更新,指定下一次启动哪个 OTA 应用程序分区。
- 如果在项目
PROJECT_VER
文件中设置PROJECT_VER
变量,则使用它的值。 - 否则,如果
$PROJECT_PATH/version.txt
存在,它的内容将用作PROJECT_VER
。 - 否则,如果项目位于Git存储库中,则使用
git describe
的输出.
应用回滚
应用程序回滚的主要目的是确保设备在更新后正常工作。如果新版应用程序出现严重错误,该功能可使设备回滚到之前正常运行的应用版本。在使能回滚并且 OTA 升级应用程序至新版本后,可能出现的结果如下:
- 应用程序运行正常,
esp_ota_mark_app_valid_cancel_rollback()
将正在运行的应用程序状态标记为ESP_OTA_IMG_VALID
,启动此应用程序无限制。 - 应用程序出现严重错误,无法继续工作,必须回滚到此前的版本,
esp_ota_mark_app_invalid_rollback_and_reboot()
将正在运行的版本标记为ESP_OTA_IMG_INVALID
然后复位。引导加载程序不会选取此版本,而是启动此前正常运行的版本。 - 如果 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE 使能,则无需调用函数便可复位,回滚至之前的应用版本。
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_INVALID
或ESP_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
,阻止其再次启动,之后回滚到之前的版本。
尽快完成, 防止意外复位, 自测以后设置状态是用户代码实现的
实际的更新
- 查看当前的版本, 看一看启动的是不是配置的版本
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)
获取上一次出错的版本