From 3784d499afd7b15db58d7f3e50a75761c74fe153 Mon Sep 17 00:00:00 2001 From: Wang Ning Date: Wed, 25 Mar 2020 21:44:33 +0800 Subject: [PATCH] Add translation for ota tool and partation tool --- docs/en/api-reference/system/ota.rst | 1 + docs/zh_CN/api-guides/bootloader.rst | 2 + docs/zh_CN/api-guides/partition-tables.rst | 118 +++++++- docs/zh_CN/api-reference/system/ota.rst | 303 ++++++++++++++++++++- 4 files changed, 412 insertions(+), 12 deletions(-) diff --git a/docs/en/api-reference/system/ota.rst b/docs/en/api-reference/system/ota.rst index e5c397c2e..e2dd0b09e 100644 --- a/docs/en/api-reference/system/ota.rst +++ b/docs/en/api-reference/system/ota.rst @@ -1,5 +1,6 @@ Over The Air Updates (OTA) ========================== +:link_to_translation:`zh_CN:[中文]` OTA Process Overview -------------------- diff --git a/docs/zh_CN/api-guides/bootloader.rst b/docs/zh_CN/api-guides/bootloader.rst index 86ee7efa8..3d6987762 100644 --- a/docs/zh_CN/api-guides/bootloader.rst +++ b/docs/zh_CN/api-guides/bootloader.rst @@ -43,6 +43,8 @@ ota_0, 0, ota_0, , 512K ota_1, 0, ota_1, , 512K +.. _bootloader_boot_from_test_firmware: + 从测试固件启动 -------------- 用户可以编写在生产环境中测试用的特殊固件,然后在需要的时候运行。此时需要在分区表中专门申请一块分区用于保存该测试固件(详情请参阅 :doc:`分区表 `)。如果想要触发测试固件,还需要设置 :ref:`CONFIG_BOOTLOADER_APP_TEST`。 diff --git a/docs/zh_CN/api-guides/partition-tables.rst b/docs/zh_CN/api-guides/partition-tables.rst index 52c9d0780..e6c49f959 100644 --- a/docs/zh_CN/api-guides/partition-tables.rst +++ b/docs/zh_CN/api-guides/partition-tables.rst @@ -13,7 +13,7 @@ 分区表中的每个条目都包括以下几个部分:Name(标签)、Type(app、data 等)、SubType 以及在 flash 中的偏移量(分区的加载地址)。 -在使用分区表时,最简单的方法就是打开项目配置菜单(``idf.py menuconfig``),并在 :ref:`CONFIG_PARTITION_TABLE_TYPE` 下选择一张预定义的分区表: +在使用分区表时,最简单的方法就是打开项目配置菜单(``idf.py menuconfig``),并在 :ref:`CONFIG_PARTITION_TABLE_TYPE` 下选择一个预定义的分区表: - "Single factory app, no OTA" - "Factory app, two OTA definitions" @@ -32,7 +32,7 @@ phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 1M, -- flash 的 0x10000 (64KB) 偏移地址处存放一个标记为 "factory" 的二进制应用程序,且 Bootloader 将默认加载这个应用程序。 +- flash 的 0x10000 (64KB) 偏移地址处存放一个标记为 "factory" 的二进制应用程序,且启动加载器将默认加载这个应用程序。 - 分区表中还定义了两个数据区域,分别用于存储 NVS 库专用分区和 PHY 初始化数据。 以下是 "Factory app, two OTA definitions" 选项的分区表信息摘要: @@ -47,7 +47,7 @@ ota_1, app, ota_1, 0x210000, 1M, - 分区表中定义了三个应用程序分区,这三个分区的类型都被设置为 “app”,但具体 app 类型不同。其中,位于 0x10000 偏移地址处的为出厂应用程序(factory),其余两个为 OTA 应用程序(ota_0,ota_1)。 -- 新增了一个名为 “otadata” 的数据分区,用于保存 OTA 升级时候需要的数据。Bootloader 会查询该分区的数据,以判断该从哪个 OTA 应用程序分区加载程序。如果 “otadata” 分区为空,则会执行出厂程序。 +- 新增了一个名为 “otadata” 的数据分区,用于保存 OTA 升级时需要的数据。启动加载器会查询该分区的数据,以判断该从哪个 OTA 应用程序分区加载程序。如果 “otadata” 分区为空,则会执行出厂程序。 创建自定义分区表 ---------------- @@ -81,7 +81,7 @@ Type 字段可以指定为 app (0) 或者 data (1),也可以直接使用数字 如果您的应用程序需要保存数据,请在 0x40-0xFE 内添加一个自定义分区类型。 -注意,bootloader 将忽略 app (0) 和 data (1) 以外的其他分区类型。 +注意,启动加载器将忽略 app (0) 和 data (1) 以外的其他分区类型。 SubType 字段 @@ -91,12 +91,12 @@ SubType 字段长度为 8 bit,内容与具体 Type 有关。目前,esp-idf * 当 Type 定义为 ``app`` 时,SubType 字段可以指定为 factory (0),ota_0 (0x10) ... ota_15 (0x1F) 或者 test (0x20)。 - - factory (0) 是默认的 app 分区。Bootloader 将默认加在该应用程序。但如果存在类型为 data/ota 分区,则 Bootloader 将加载 data/ota 分区中的数据,进而判断启动哪个 OTA 镜像文件。 + - factory (0) 是默认的 app 分区。启动加载器将默认加载该应用程序。但如果存在类型为 data/ota 分区,则启动加载器将加载 data/ota 分区中的数据,进而判断启动哪个 OTA 镜像文件。 - OTA 升级永远都不会更新 factory 分区中的内容。 - 如果您希望在 OTA 项目中预留更多 flash,可以删除 factory 分区,转而使用 ota_0 分区。 - - ota_0 (0x10) ... ota_15 (0x1F) 为 OTA 应用程序分区,Bootloader 将根据 OTA 数据分区中的数据来决定加载哪个 OTA 应用程序分区中的程序。在使用 OTA 功能时,应用程序应至少拥有 2 个 OTA 应用程序分区(ota_0 和 ota_1)。更多详细信息,请参考 :doc:`OTA 文档 ` 。 - - test (0x2) 为预留 app 子类型,用于工厂测试过程。注意,目前,esp-idf 并不支持这种子类型。 + - ota_0 (0x10) ... ota_15 (0x1F) 为 OTA 应用程序分区,启动加载器将根据 OTA 数据分区中的数据来决定加载哪个 OTA 应用程序分区中的程序。在使用 OTA 功能时,应用程序应至少拥有 2 个 OTA 应用程序分区(ota_0 和 ota_1)。更多详细信息,请参考 :doc:`OTA 文档 ` 。 + - test (0x2) 为预留 app 子类型,用于工厂测试流程。如果没有其他有效 app 分区,test 将作为备选启动分区使用。也可以在每次启动时配置启动加载器读取 GPIO,如果 GPIO 被拉低则启动该分区。详细信息请查阅 :ref:`bootloader_boot_from_test_firmware`。 * 当 Type 定义为 ``data`` 时,SubType 字段可以指定为 ota (0),phy (1),nvs (2) 或者 nvs_keys (4)。 @@ -117,12 +117,12 @@ SubType 字段长度为 8 bit,内容与具体 Type 有关。目前,esp-idf - 用于存储加密密钥(如果启用了 `NVS 加密` 功能)。 - 此分区应至少设定为 4096 字节。 -其它数据子类型已预留给 esp-idf 的未来使用。 +其它数据子类型已预留给 esp-idf 未来使用。 Offset 和 Size 字段 ~~~~~~~~~~~~~~~~~~~ -分区若为指定偏移地址,则会紧跟着前一个分区之后开始。若此分区为首个分区,则将紧跟着分区表开始。 +分区若偏移地址为空,则会紧跟着前一个分区之后开始;若为首个分区,则将紧跟着分区表开始。 app 分区的偏移地址必须要与 0x10000 (64K) 对齐,如果将偏移字段留空,``gen_esp32part.py`` 工具会自动计算得到一个满足对齐要求的偏移地址。如果 app 分区的偏移地址没有与 0x10000 (64K) 对齐,则该工具会报错。 @@ -144,7 +144,7 @@ Flags 字段 烧写到 ESP32 中的分区表采用二进制格式,而不是 CSV 文件本身。此时,:component_file:`partition_table/gen_esp32part.py` 工具可以实现 CSV 和二进制文件之间的转换。 -如果您在项目配置菜单(``idf.py menuconfig``)中设置了分区表 CSV 文件的名称,然后构建项目获执行 ``idf.py partition_table``。这时,转换将在编译过程中自动完成。 +如果您在项目配置菜单(``idf.py menuconfig``)中设置了分区表 CSV 文件的名称,然后构建项目或执行 ``idf.py partition_table``。这时,转换将在编译过程中自动完成。 手动将 CSV 文件转换为二进制文件: @@ -163,7 +163,7 @@ MD5 校验和 二进制格式的分区表中含有一个 MD5 校验和。这个 MD5 校验和是根据分区表内容计算的,可在设备启动阶段,用于验证分区表的完整性。 -注意,一些版本较老的 bootloader 无法支持 MD5 校验,如果发现 MD5 校验和则将报错 ``invalid magic number 0xebeb``。此时,用户可通过 ``gen_esp32part.py`` 的 ``--disable-md5sum`` 选项或者 ``menuconfig`` 的 :ref:`CONFIG_PARTITION_TABLE_MD5` 选项关闭 MD5 校验。 +注意,一些版本较老的启动加载器无法支持 MD5 校验,如果发现 MD5 校验和则将报错 ``invalid magic number 0xebeb``。此时,用户可通过 ``gen_esp32part.py`` 的 ``--disable-md5sum`` 选项或者 ``menuconfig`` 的 :ref:`CONFIG_PARTITION_TABLE_MD5` 选项关闭 MD5 校验。 烧写分区表 ---------- @@ -177,4 +177,100 @@ MD5 校验和 分区表的更新并不会擦除根据之前分区表存储的数据。此时,您可以使用 ``idf.py erase_flash`` 命令或者 ``esptool.py erase_flash`` 命令来擦除 flash 中的所有内容。 + +分区工具 (parttool.py) +---------------------- + +`partition_table` 组件中有分区工具 :component_file:`parttool.py`,可以在目标设备上完成分区相关操作。该工具有如下用途: + + - 读取分区,将内容存储到文件中 (read_partition) + - 将文件中的内容写至分区 (write_partition) + - 擦除分区 (erase_partition) + - 检索特定分区的偏移和大小等信息 (get_partition_info) + +用户若想通过编程方式完成相关操作,可从另一个 Python 脚本导入并使用分区工具,或者从 Shell 脚本调用分区工具。前者可使用工具的 Python API,后者可使用命令行界面。 + +Python API +~~~~~~~~~~~ + +首先请确保已导入 `parttool` 模块。 + +.. code-block:: python + + import sys + import os + + idf_path = os.environ["IDF_PATH"] # 从环境中获取 IDF_PATH 的值 + parttool_dir = os.path.join(idf_path, "components", "partition_table") # parttool.py 位于 $IDF_PATH/components/partition_table 下 + + sys.path.append(parttool_dir) # 使能 Python 寻找 parttool 模块 + from parttool import * # 导入 parttool 模块内的所有名称 + +要使用分区工具的 Python API,第一步是创建 `ParttoolTarget`: + +.. code-block:: python + + # 创建 partool.py 的目标设备,并将目标设备连接到串行端口 /dev/ttyUSB1 + target = ParttoolTarget("/dev/ttyUSB1") + +现在,可使用创建的 `ParttoolTarget` 在目标设备上完成操作: + +.. code-block:: python + + # 擦除名为 'storage' 的分区 + target.erase_partition(PartitionName("storage")) + + # 读取类型为 'data'、子类型为 'spiffs' 的分区,保存至文件 'spiffs.bin' + target.read_partition(PartitionType("data", "spiffs"), "spiffs.bin") + + # 将 'factory.bin' 文件的内容写至 'factory' 分区 + target.write_partition(PartitionName("factory"), "factory.bin") + + # 打印默认启动分区的大小 + storage = target.get_partition_info(PARTITION_BOOT_DEFAULT) + print(storage.size) + +使用 `PartitionName`、`PartitionType` 或 PARTITION_BOOT_DEFAULT 指定要操作的分区。顾名思义,这三个参数可以指向拥有特定名称的分区、特定类型和子类型的分区或默认启动分区。 + +更多关于 Python API 的信息,请查看分区工具的代码注释。 + +命令行界面 +~~~~~~~~~~ + +`parttool.py` 的命令行界面具有如下结构: + +.. code-block:: bash + + parttool.py [command-args] [subcommand] [subcommand-args] + + - command-args - 执行主命令 (parttool.py) 所需的实际参数,多与目标设备有关 + - subcommand - 要执行的操作 + - subcommand-args - 所选操作的实际参数 + +.. code-block:: bash + + # 擦除名为 'storage' 的分区 + parttool.py --port "/dev/ttyUSB1" erase_partition --partition-name=storage + + # 读取类型为 'data'、子类型为 'spiffs' 的分区,保存到 'spiffs.bin' 文件 + parttool.py --port "/dev/ttyUSB1" read_partition --partition-type=data --partition-subtype=spiffs "spiffs.bin" + + # 将 'factory.bin' 文件中的内容写入到 'factory' 分区 + parttool.py --port "/dev/ttyUSB1" write_partition --partition-name=factory "factory.bin" + + # 打印默认启动分区的大小 + parttool.py --port "/dev/ttyUSB1" get_partition_info --partition-boot-default --info size + +更多信息可用 `--help` 指令查看: + +.. code-block:: bash + + # 显示可用的子命令和主命令描述 + parttool.py --help + + # 显示子命令的描述 + parttool.py [subcommand] --help + + .. _secure boot: security/secure-boot-v1.rst + diff --git a/docs/zh_CN/api-reference/system/ota.rst b/docs/zh_CN/api-reference/system/ota.rst index 41ed0bcb7..b3ac7f396 100644 --- a/docs/zh_CN/api-reference/system/ota.rst +++ b/docs/zh_CN/api-reference/system/ota.rst @@ -1 +1,302 @@ -.. include:: ../../../en/api-reference/system/ota.rst \ No newline at end of file +空中升级 (OTA) +============== +:link_to_translation:`en:[English]` + +OTA 流程概览 +------------ + +OTA 升级机制可以让设备在固件正常运行时根据接收数据更新(如通过 Wi-Fi 或蓝牙)。 + +要运行 OTA 机制,需配置设备的 :doc:` 分区表 <../../api-guides/partition-tables>`,该分区表至少包括两个 OTA 应用程序分区(即 `ota_0` 和 `ota_1`)和一个 OTA 数据分区。 + +OTA 功能启动后,向当前未用于启动的 OTA 应用分区写入新的应用固件镜像。镜像验证后,OTA 数据分区更新,指定在下一次启动时使用该镜像。 + +.. _ota_data_partition: + +OTA 数据分区 +------------ + +所有使用 OTA 功能项目,其 :doc:` 分区表 <../../api-guides/partition-tables>`必须包含一个 OTA 数据分区 (类型为 ``data``,子类型为 ``ota``)。 + +工厂启动设置下,OTA 数据分区中应没有数据(所有字节擦写成 0xFF)。如果分区表中有工厂应用程序,ESP-IDF 软件启动加载器会启动工厂应用程序。如果分区表中没有工厂应用程序,则启动第一个可用的 OTA 分区(通常是 ``ota_0``)。 + +第一次 OTA 升级后,OTA 数据分区更新,指定下一次启动哪个 OTA 应用程序分区。 + +OTA 数据分区是两个 0x2000 字节大小的 flash 扇区,防止写入时电源故障引发问题。两个扇区单独擦除、写入匹配数据,若存在不一致,则用计数器字段判定哪个扇区为最新数据。 + +.. _app_rollback: + +应用程序回滚 +------------ + +应用程序回滚的主要目的是确保设备更新后正常运转。该功能可使设备在更新新版本后出现严重错误时,回滚到之前正常运行的应用版本。回滚使能,OTA 升级,应用更新至新版本,之后可能有以下三种情况: + +* 应用程序运行正常 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` 将应用程序状态标记为 ``ESP_OTA_IMG_VALID``,启动无限制。 +* 应用程序出现严重错误,无法继续工作,必须回滚到此前的版本,:cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` 将正在运行的版本标记为 ``ESP_OTA_IMG_INVALID`` 然后复位。启动加载器不会选取此版本,而是此前正常运行的版本。 +* 如果 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 使能,则无需调用函数便可复位,回滚至之前的应用版本。 + +注解:应用程序的状态不是写到程序的二进制镜像,而是写到 ``otadata`` 分区。该分区有一个 ``ota_seq`` 计数器,该计数器是 OTA 应用分区的指针,指向下次启动时选取应用所在的分区 (ota_0, ota_1, ...)。 + +应用程序 OTA 状态 +^^^^^^^^^^^^^^^^^ + +状态控制了选取启动应用程序的过程: + +============================= ======================================================== +状态 启动加载器选取启动应用程序的限制 +============================= ======================================================== + ESP_OTA_IMG_VALID 没有限制,可以选取。 + ESP_OTA_IMG_UNDEFINED 没有限制,可以选取。 + ESP_OTA_IMG_INVALID 不会选取。 + ESP_OTA_IMG_ABORTED 不会选取。 + ESP_OTA_IMG_NEW 如使能 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`, + 则仅会选取一次。在启动加载器中,状态立即变为 + ``ESP_OTA_IMG_PENDING_VERIFY``。 + ESP_OTA_IMG_PENDING_VERIFY 如使能 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`, + 则不会选取,状态变为``ESP_OTA_IMG_ABORTED``。 +============================= ======================================================== + +如果 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 没有使能(默认情况),则 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` 和 :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` 为可选功能,``ESP_OTA_IMG_NEW`` 和 ``ESP_OTA_IMG_PENDING_VERIFY`` 不会使用。 + +Kconfig 中的 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 可以帮助用户追踪新版应用程序的第一次启动。应用程序需调用 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` 函数确认可以运行,否则将会在重启时回滚至旧版本。该功能可让用户在启动阶段控制应用程序的可操作性。新版应用程序仅有一次机会尝试是否能成功启动。 + +回滚过程 +^^^^^^^^ + +:ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 使能时,回滚过程如下: + +* 新版应用程序下载成功,:cpp:func:`esp_ota_set_boot_partition` 函数将分区设为可启动,状态设为 ``ESP_OTA_IMG_NEW``。该状态表示应用程序为新版本,第一次启动需要监测。 +* 重新启动 :cpp:func:`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`` (见上文),该应用程序不可再启动,将回滚至上一版本。 +* 新版应用程序启动,应进行自测。 +* 若通过自测,则必须调用函数 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback`,因为新版应用程序在等待确认其可操作性 (``ESP_OTA_IMG_PENDING_VERIFY`` 状态)。 +* 若未通过自测,则调用函数 :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot`,回滚至之前的版本,同时无效的新版本设置为 ``ESP_OTA_IMG_INVALID``。 +* 如果新版应用程序可操作性没有确认,则状态一直为 ``ESP_OTA_IMG_PENDING_VERIFY``。下一次启动时,状态变更为 ``ESP_OTA_IMG_ABORTED``,阻止其再次启动,之后回滚到之前的版本。 + +意外复位 +^^^^^^^^ + +如果在新版应用第一次启动时发生断电或意外崩溃,则会回滚至之前正常运行的版本。 + +建议:尽快完成自测,防止因断电回滚。 + +只有 ``OTA`` 分区可以回滚。工厂分区不会回滚。 + +启动无效/中止的应用程序 +^^^^^^^^^^^^^^^^^^^^^^^ + +用户可以启动此前设置为 ``ESP_OTA_IMG_INVALID`` 或 ``ESP_OTA_IMG_ABORTED`` 的应用程序: + +* 获取最后一个无效应用分区 :cpp:func:`esp_ota_get_last_invalid_partition`。 +* 将获取的分区传递给 :cpp:func:`esp_ota_set_boot_partition`,更新 ``otadata``。 +* 重启 :cpp:func:`esp_restart`。启动加载器会启动指定应用程序。 + +要确定是否在应用程序启动时进行自测,可以调用 :cpp:func:`esp_ota_get_state_partition` 函数。如果结果为 ``ESP_OTA_IMG_PENDING_VERIFY``,则需要自测,后续确认应用程序的可操作性。 + +如何设置状态 +^^^^^^^^^^^^ + +下文简单描述了如何设置应用程序状态: + +* ``ESP_OTA_IMG_VALID`` 由函数 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` 设置。 +* 如果 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 没有使能,``ESP_OTA_IMG_UNDEFINED`` 由函数 :cpp:func:`esp_ota_set_boot_partition` 设置。 +* 如果 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 没有使能,``ESP_OTA_IMG_NEW`` 由函数 :cpp:func:`esp_ota_set_boot_partition` 设置。 +* ``ESP_OTA_IMG_INVALID`` 由函数 :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` 设置。 +* 如果应用程序的可操作性无法确认,发生重启(:ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 使能),则设置 ``ESP_OTA_IMG_ABORTED``。 +* 如果 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 使能,选取的应用程序状态为 ``ESP_OTA_IMG_NEW``,则在启动加载器中设置 ``ESP_OTA_IMG_PENDING_VERIFY``。 + +.. _anti-rollback: + +防回滚 +------ + +防回滚机制可以防止回滚到安全版本号低于芯片 eFuse 中烧录程序的应用程序版本。 + +设置 :ref:`CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK`,启动防回滚机制。在启动加载器中选取可启动的应用程序,会额外检查芯片和应用程序镜像的安全版本号。可启动固件中的应用安全版本号必须等于或高于芯片中的应用安全版本号。 + +:ref:`CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK` 和 :ref:`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` 一起使用。此时,只有安全版本号等于或高于芯片中的应用安全版本号时才会回滚。 + + +典型的防回滚机制 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- 新发布的固件解决了此前版本的安全问题。 +- 开发者在确保固件可以运行之后,增加安全版本号,发布固件。 +- 下载新版应用程序。 +- 运行函数 :cpp:func:`esp_ota_set_boot_partition`,将新版应用程序设为可启动。如果新版应用程序的安全版本号低于芯片中的应用安全版本号,新版应用程序会被擦除,无法更新到新固件。 +- 重新启动。 +- 在启动加载器中选取安全版本号等于或高于芯片中应用安全版本号的应用程序。如果 otadata 处于初始阶段,通过串行通道加载了安全版本号高于芯片中应用安全版本号的固件,则启动加载器中 eFuse 的安全版本号会立即更新。 +- 新版应用程序启动,之后进行可操作性检测,如果通过检测,则调用函数 :cpp:func:`esp_ota_mark_app_valid_cancel_rollback`,将应用程序标记为 ``ESP_OTA_IMG_VALID``,更新芯片中应用程序的安全版本号。注意,如果调用函数 :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot`,可能会因为设备中没有可启动的应用程序而回滚失败,返回 ``ESP_ERR_OTA_ROLLBACK_FAILED`` 错误,应用程序状态一直为 ``ESP_OTA_IMG_PENDING_VERIFY``。 +- 如果运行的应用程序处于 ``ESP_OTA_IMG_VALID`` 状态,则可再次更新。 + +建议: + +如果想避免因服务器应用程序的安全版本号低于运行的应用程序,造成不必要的下载和擦除,必须从镜像的第一个包中获取 ``new_app_info.secure_version``,和 eFuse 的安全版本号比较。如果 ``esp_efuse_check_secure_version(new_app_info.secure_version)`` 函数为真,则下载继续,反之则中断。 + +.. code-block:: c + + .... + bool image_header_was_checked = false; + while (1) { + int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE); + ... + 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 + if (esp_efuse_check_secure_version(new_app_info.secure_version) == false) { + ESP_LOGE(TAG, "This a new app can not be downloaded due to a secure version is lower than stored in efuse."); + http_cleanup(client); + task_fatal_error(); + } + + image_header_was_checked = true; + + esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); + } + } + esp_ota_write( update_handle, (const void *)ota_write_data, data_read); + } + } + ... + +限制: + +- ``secure_version`` 字段最多有 32 位。也就是说,防回滚最多可以做 32 次。用户可以使用 :ref:`CONFIG_BOOTLOADER_APP_SEC_VER_SIZE_EFUSE_FIELD` 减少该 eFuse 字段的长度。 +- 防回滚仅在 eFuse 编码机制设置为 ``NONE`` 时生效。 +- 分区表不应有工厂分区,应仅有两个应用程序分区。 + +``security_version``: + +- 存储在应用程序镜像中的 ``esp_app_desc`` 里。版本号用 :ref:`CONFIG_BOOTLOADER_APP_SECURE_VERSION` 设置。 + +.. only:: esp32 + + - ESP32 中版本号存储在 eFuse 的 ``EFUSE_BLK3_RDATA4_REG`` 里(若 eFuse 的位烧写为 1,则永远无法恢复为 0)。寄存器设置了多少位,应用程序的安全版本号就为多少。 + +.. only:: esp32 + + .. _secure-ota-updates: + + 没有安全启动的安全 OTA 升级 + --------------------------- + + 即便硬件安全启动没有使能,也可验证已签名的 OTA 升级,具体可参考 :ref:`signed-app-verify`。 + + +OTA 工具 (otatool.py) +--------------------- + +`app_update` 组件中有 :component_file:`otatool.py` 工具,用于在目标设备上完成下列 OTA 分区相关操作: + + - 读取 otadata 分区 (read_otadata) + - 擦除 otadata 分区,将设备复位至工厂应用程序 (erase_otadata) + - 切换 OTA 分区 (switch_ota_partition) + - 擦除 OTA 分区 (erase_ota_partition) + - 写入 OTA 分区 (write_ota_partition) + - 读取 OTA 分区 (read_ota_partition) + +用户若想通过编程方式完成相关操作,可从另一个 Python 脚本导入并使用分区工具,或者从 Shell 脚本调用分区工具。前者可使用工具的 Python API,后者可使用命令行界面。 + +Python API +^^^^^^^^^^ + +首先,确保已导入 `otatool` 模块。 + +.. code-block:: python + + import sys + import os + + idf_path = os.environ["IDF_PATH"] # 从环境中获取 IDF_PATH 的值 + otatool_dir = os.path.join(idf_path, "components", "app_update") # otatool.py 位于 $IDF_PATH/components/app_update 下 + + sys.path.append(otatool_dir) # 使能 Python 寻找 otatool 模块 + from otatool import * # 导入 otatool 模块内的所有名称 + +要使用 OTA 工具的 Python API,第一步是创建 `OtatoolTarget`: + +.. code-block:: python + + # 创建 partool.py 的目标设备,并将目标设备连接到串行端口 /dev/ttyUSB1 + target = OtatoolTarget("/dev/ttyUSB1") + +现在,可使用创建的 `OtatoolTarget` 在目标设备上完成操作: + +.. code-block:: python + + # 擦除 otadata,将设备复位至工厂应用程序 + target.erase_otadata() + + # 擦除 OTA 应用程序分区 0 + target.erase_ota_partition(0) + + # 将启动分区切换至 OTA 应用程序分区 1 + target.switch_ota_partition(1) + + # 读取 OTA 分区 'ota_3',将内容保存至文件 'ota_3.bin' + target.read_ota_partition("ota_3", "ota_3.bin") + +要操作的 OTA 分区通过应用程序分区序号或分区名称指定。 + +更多关于 Python API 的信息,请查看 OTA 工具的代码注释。 + +命令行界面 +^^^^^^^^^^ + +`otatool.py` 的命令行界面具有如下结构: + +.. code-block:: bash + + otatool.py [command-args] [subcommand] [subcommand-args] + + - command-args - 执行主命令 (otatool.py) 所需的实际参数,多与目标设备有关 + - subcommand - 要执行的操作 + - subcommand-args - 所选操作的实际参数 + +.. code-block:: bash + + # 擦除 otadata,将设备复位至工厂应用程序 + otatool.py --port "/dev/ttyUSB1" erase_otadata + + # 擦除 OTA 应用程序分区 0 + otatool.py --port "/dev/ttyUSB1" erase_ota_partition --slot 0 + + # 将启动分区切换至 OTA 应用程序分区 1 + otatool.py --port "/dev/ttyUSB1" switch_ota_partition --slot 1 + + # 读取 OTA 分区 'ota_3',将内容保存至文件 'ota_3.bin' + otatool.py --port "/dev/ttyUSB1" read_ota_partition --name=ota_3 + + +更多信息可用 `--help` 指令查看: + +.. code-block:: bash + + # 显示可用的子命令和主命令描述 + otatool.py --help + + # 显示子命令的描述 + otatool.py [subcommand] --help + + +相关文档 +-------- + +* :doc:`分区表 <../../api-guides/partition-tables>` +* :doc:`SPI Flash 和分区 API <../storage/spi_flash>` +* :doc:`ESP HTTPS OTA ` + +应用程序示例 +------------ + +端对端的 OTA 固件升级示例请参考 :example:`system/ota`。 + +API 参考 +-------- + +.. include-build-file:: inc/esp_ota_ops.inc +