bootloader: Add factory reset and start test app

Added feature:
- reset firmware to Factory app.(by long pressing of the button)
- boot Test app. (by long pressing of the button)
- Added feature erase data partitions from factory reset.

TW10281
TW10280
This commit is contained in:
Konstantin Kondrashov 2018-04-23 17:19:04 +05:00
parent b7c37024d5
commit ae10abff2f
7 changed files with 365 additions and 7 deletions

View File

@ -62,6 +62,71 @@ config BOOTLOADER_VDDSDIO_BOOST_1_9V
bool "1.9V"
endchoice
config BOOTLOADER_FACTORY_RESET
bool "GPIO triggers factory reset"
default N
help
Allows to reset the device to factory settings:
- clear one or more data partitions;
- boot from "factory" partition.
The factory reset will occur if there is a GPIO input pulled low while device starts up.
See settings below.
config BOOTLOADER_NUM_PIN_FACTORY_RESET
int "Number of the GPIO input for factory reset"
depends on BOOTLOADER_FACTORY_RESET
range 0 39
default 4
help
The selected GPIO will be configured as an input with internal pull-up enabled.
To trigger a factory reset, this GPIO must be pulled low on reset.
Note that GPIO34-39 do not have an internal pullup and an external one must be provided.
config BOOTLOADER_OTA_DATA_ERASE
bool "Clear OTA data on factory reset (select factory partition)"
depends on BOOTLOADER_FACTORY_RESET
help
The device will boot from "factory" partition (or OTA slot 0 if no factory partition is present) after a factory reset.
config BOOTLOADER_DATA_FACTORY_RESET
string "Comma-separated names of partitions to clear on factory reset"
depends on BOOTLOADER_FACTORY_RESET
default "nvs"
help
Allows customers to select which data partitions will be erased while factory reset.
Specify the names of partitions as a comma-delimited with optional spaces for readability. (Like this: "nvs, phy_init, ...")
Make sure that the name specified in the partition table and here are the same.
Partitions of type "app" cannot be specified here.
config BOOTLOADER_APP_TEST
bool "GPIO triggers boot from test app partition"
default N
help
Allows to run the test app from "TEST" partition.
A boot from "test" partition will occur if there is a GPIO input pulled low while device starts up.
See settings below.
config BOOTLOADER_NUM_PIN_APP_TEST
int "Number of the GPIO input to boot TEST partition"
depends on BOOTLOADER_APP_TEST
range 0 39
default 18
help
The selected GPIO will be configured as an input with internal pull-up enabled.
To trigger a test app, this GPIO must be pulled low on reset.
After the GPIO input is deactivated and the device reboots, the old application will boot.
(factory or OTA[x]).
Note that GPIO34-39 do not have an internal pullup and an external one must be provided.
config BOOTLOADER_HOLD_TIME_GPIO
int "Hold time of GPIO for reset/test mode (seconds)"
depends on BOOTLOADER_FACTORY_RESET || BOOTLOADER_APP_TEST
default 5
help
The GPIO must be held low continuously for this period of time after reset
before a factory reset or test partition boot (as applicable) is performed.
endmenu # Bootloader

View File

@ -17,6 +17,7 @@
#include "esp_log.h"
#include "rom/gpio.h"
#include "rom/spi_flash.h"
#include "bootloader_config.h"
#include "bootloader_init.h"
#include "bootloader_utility.h"
@ -83,7 +84,35 @@ static int selected_boot_partition(const bootloader_state_t *bs)
if (boot_index == INVALID_INDEX) {
return boot_index; // Unrecoverable failure (not due to corrupt ota data or bad partition contents)
} else {
// Check for reset to the factory firmware or for launch OTA[x] firmware.
// Factory firmware.
#ifdef CONFIG_BOOTLOADER_FACTORY_RESET
if (bootloader_common_check_long_hold_gpio(CONFIG_BOOTLOADER_NUM_PIN_FACTORY_RESET, CONFIG_BOOTLOADER_HOLD_TIME_GPIO) == 1) {
ESP_LOGI(TAG, "Detect a condition of the factory reset");
bool ota_data_erase = false;
#ifdef CONFIG_BOOTLOADER_OTA_DATA_ERASE
ota_data_erase = true;
#endif
const char *list_erase = CONFIG_BOOTLOADER_DATA_FACTORY_RESET;
ESP_LOGI(TAG, "Data partitions to erase: %s", list_erase);
if (bootloader_common_erase_part_type_data(list_erase, ota_data_erase) == false) {
ESP_LOGE(TAG, "Not all partitions were erased");
}
return bootloader_utility_get_selected_boot_partition(bs);
}
#endif
// TEST firmware.
#ifdef CONFIG_BOOTLOADER_APP_TEST
if (bootloader_common_check_long_hold_gpio(CONFIG_BOOTLOADER_NUM_PIN_APP_TEST, CONFIG_BOOTLOADER_HOLD_TIME_GPIO) == 1) {
ESP_LOGI(TAG, "Detect a boot condition of the test firmware");
if (bs->test.offset != 0) {
boot_index = TEST_APP_INDEX;
return boot_index;
} else {
ESP_LOGE(TAG, "Test firmware is not found in partition table");
return INVALID_INDEX;
}
}
#endif
// Customer implementation.
// if (gpio_pin_1 == true && ...){
// boot_index = required_boot_partition;

View File

@ -13,6 +13,14 @@
// limitations under the License.
#pragma once
#include "esp_flash_data_types.h"
/// Type of hold a GPIO in low state
typedef enum {
GPIO_LONG_HOLD = 1, /*!< The long hold GPIO */
GPIO_SHORT_HOLD = -1, /*!< The short hold GPIO */
GPIO_NOT_HOLD = 0 /*!< If the GPIO input is not low */
} esp_comm_gpio_hold_t;
/**
* @brief Calculate crc for the OTA data partition.
@ -29,3 +37,34 @@ uint32_t bootloader_common_ota_select_crc(const esp_ota_select_entry_t *s);
* @return Returns true on valid, false otherwise.
*/
bool bootloader_common_ota_select_valid(const esp_ota_select_entry_t *s);
/**
* @brief Check if the GPIO input is a long hold or a short hold.
*
* Number of the GPIO input will be configured as an input with internal pull-up enabled.
* If the GPIO input is held low continuously for delay_sec period then it is a long hold.
* If the GPIO input is held low for less period then it is a short hold.
*
* @param[in] num_pin Number of the GPIO input.
* @param[in] delay_sec Input must be driven low for at least this long, continuously.
* @return esp_comm_gpio_hold_t Defines type of hold a GPIO in low state.
*/
esp_comm_gpio_hold_t bootloader_common_check_long_hold_gpio(uint32_t num_pin, uint32_t delay_sec);
/**
* @brief Erase the partition data that is specified in the transferred list.
*
* @param[in] list_erase String containing a list of cleared partitions. Like this "nvs, phy". The string must be null-terminal.
* @param[in] ota_data_erase If true then the OTA data partition will be cleared (if there is it in partition table).
* @return Returns true on success, false otherwise.
*/
bool bootloader_common_erase_part_type_data(const char *list_erase, bool ota_data_erase);
/**
* @brief Determines if the list contains the label
*
* @param[in] list A string of names delimited by commas or spaces. Like this "nvs, phy, data". The string must be null-terminated.
* @param[in] label The substring that will be searched in the list.
* @return Returns true if the list contains the label, false otherwise.
*/
bool bootloader_common_label_search(const char *list, char *label);

View File

@ -12,8 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdbool.h>
#include <assert.h>
#include "string.h"
#include "sdkconfig.h"
#include "esp_err.h"
#include "esp_log.h"
#include "rom/spi_flash.h"
#include "rom/crc.h"
#include "rom/ets_sys.h"
#include "rom/gpio.h"
#include "esp_flash_data_types.h"
#include "esp_secure_boot.h"
#include "esp_flash_partitions.h"
#include "bootloader_flash.h"
#include "bootloader_common.h"
static const char* TAG = "boot_comm";
uint32_t bootloader_common_ota_select_crc(const esp_ota_select_entry_t *s)
{
@ -24,3 +38,118 @@ bool bootloader_common_ota_select_valid(const esp_ota_select_entry_t *s)
{
return s->ota_seq != UINT32_MAX && s->crc == bootloader_common_ota_select_crc(s);
}
esp_comm_gpio_hold_t bootloader_common_check_long_hold_gpio(uint32_t num_pin, uint32_t delay_sec)
{
gpio_pad_select_gpio(num_pin);
gpio_pad_pullup(num_pin);
uint32_t tm_start = esp_log_early_timestamp();
if (GPIO_INPUT_GET(num_pin) == 1) {
return GPIO_NOT_HOLD;
}
do {
if (GPIO_INPUT_GET(num_pin) != 0) {
return GPIO_SHORT_HOLD;
}
} while (delay_sec > ((esp_log_early_timestamp() - tm_start) / 1000L));
return GPIO_LONG_HOLD;
}
// Search for a label in the list. list = "nvs1, nvs2, otadata, nvs"; label = "nvs".
bool bootloader_common_label_search(const char *list, char *label)
{
if (list == NULL || label == NULL) {
return false;
}
const char *sub_list_start_like_label = strstr(list, label);
while (sub_list_start_like_label != NULL) {
// ["," or " "] + label + ["," or " " or "\0"]
// first character before the label found there must be a delimiter ["," or " "].
int idx_first = sub_list_start_like_label - list;
if (idx_first == 0 || (idx_first != 0 && (list[idx_first - 1] == ',' || list[idx_first - 1] == ' '))) {
// next character after the label found there must be a delimiter ["," or " " or "\0"].
int len_label = strlen(label);
if (sub_list_start_like_label[len_label] == 0 ||
sub_list_start_like_label[len_label] == ',' ||
sub_list_start_like_label[len_label] == ' ') {
return true;
}
}
// [start_delim] + label + [end_delim] was not found.
// Position is moving to next delimiter if it is not the end of list.
int pos_delim = strcspn(sub_list_start_like_label, ", ");
if (pos_delim == strlen(sub_list_start_like_label)) {
break;
}
sub_list_start_like_label = strstr(&sub_list_start_like_label[pos_delim], label);
}
return false;
}
bool bootloader_common_erase_part_type_data(const char *list_erase, bool ota_data_erase)
{
const esp_partition_info_t *partitions;
const char *marker;
esp_err_t err;
int num_partitions;
bool ret = true;
#ifdef CONFIG_SECURE_BOOT_ENABLED
if (esp_secure_boot_enabled()) {
ESP_LOGI(TAG, "Verifying partition table signature...");
err = esp_secure_boot_verify_signature(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to verify partition table signature.");
return false;
}
ESP_LOGD(TAG, "Partition table signature verified");
}
#endif
partitions = bootloader_mmap(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
if (!partitions) {
ESP_LOGE(TAG, "bootloader_mmap(0x%x, 0x%x) failed", ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
return false;
}
ESP_LOGD(TAG, "mapped partition table 0x%x at 0x%x", ESP_PARTITION_TABLE_ADDR, (intptr_t)partitions);
err = esp_partition_table_basic_verify(partitions, true, &num_partitions);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to verify partition table");
ret = false;
} else {
ESP_LOGI(TAG, "## Label Usage Offset Length Cleaned");
for (int i = 0; i < num_partitions; i++) {
const esp_partition_info_t *partition = &partitions[i];
char label[sizeof(partition->label) + 1] = {0};
if (partition->type == PART_TYPE_DATA) {
bool fl_ota_data_erase = false;
if (ota_data_erase == true && partition->subtype == PART_SUBTYPE_DATA_OTA) {
fl_ota_data_erase = true;
}
// partition->label is not null-terminated string.
strncpy(label, (char *)&partition->label, sizeof(partition->label));
if (fl_ota_data_erase == true || (bootloader_common_label_search(list_erase, label) == true)) {
err = esp_rom_spiflash_erase_area(partition->pos.offset, partition->pos.size);
if (err != ESP_OK) {
ret = false;
marker = "err";
} else {
marker = "yes";
}
} else {
marker = "no";
}
ESP_LOGI(TAG, "%2d %-16s data %08x %08x [%s]", i, partition->label,
partition->pos.offset, partition->pos.size, marker);
}
}
}
bootloader_munmap(partitions);
return ret;
}

View File

@ -67,7 +67,6 @@ static void set_cache_and_start_app(uint32_t drom_addr,
bool bootloader_utility_load_partition_table(bootloader_state_t* bs)
{
const esp_partition_info_t *partitions;
const int ESP_PARTITION_TABLE_DATA_LEN = 0xC00; /* length of actual data (signature is appended to this) */
const char *partition_usage;
esp_err_t err;
int num_partitions;
@ -75,7 +74,7 @@ bool bootloader_utility_load_partition_table(bootloader_state_t* bs)
#ifdef CONFIG_SECURE_BOOT_ENABLED
if(esp_secure_boot_enabled()) {
ESP_LOGI(TAG, "Verifying partition table signature...");
err = esp_secure_boot_verify_signature(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_DATA_LEN);
err = esp_secure_boot_verify_signature(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to verify partition table signature.");
return false;
@ -84,9 +83,9 @@ bool bootloader_utility_load_partition_table(bootloader_state_t* bs)
}
#endif
partitions = bootloader_mmap(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_DATA_LEN);
partitions = bootloader_mmap(ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
if (!partitions) {
ESP_LOGE(TAG, "bootloader_mmap(0x%x, 0x%x) failed", ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_DATA_LEN);
ESP_LOGE(TAG, "bootloader_mmap(0x%x, 0x%x) failed", ESP_PARTITION_TABLE_ADDR, ESP_PARTITION_TABLE_MAX_LEN);
return false;
}
ESP_LOGD(TAG, "mapped partition table 0x%x at 0x%x", ESP_PARTITION_TABLE_ADDR, (intptr_t)partitions);
@ -292,7 +291,14 @@ bool bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_
{
int index = start_index;
esp_partition_pos_t part;
if(start_index == TEST_APP_INDEX) {
if (try_load_partition(&bs->test, result)) {
return true;
} else {
ESP_LOGE(TAG, "No bootable test partition in the partition table");
return false;
}
}
/* work backwards from start_index, down to the factory app */
for(index = start_index; index >= FACTORY_INDEX; index--) {
part = index_to_partition(bs, index);

View File

@ -4,6 +4,7 @@
#include <esp_types.h>
#include <stdio.h>
#include "string.h"
#include "rom/ets_sys.h"
#include "freertos/FreeRTOS.h"
@ -12,7 +13,7 @@
#include "freertos/queue.h"
#include "freertos/xtensa_api.h"
#include "unity.h"
#include "bootloader_common.h"
#include "esp_partition.h"
#include "esp_ota_ops.h"
#include "esp_image_format.h"
@ -47,3 +48,47 @@ TEST_CASE("Verify unit test app image", "[bootloader_support]")
TEST_ASSERT_TRUE(data.image_len <= running->size);
}
void check_label_search (int num_test, const char *list, const char *t_label, bool result)
{
// gen_esp32part.py trims up to 16 characters
// and the string may not have a null terminal symbol.
// below is cutting as it does the generator.
char label[16 + 1] = {0};
strncpy(label, t_label, sizeof(label) - 1);
bool ret = bootloader_common_label_search(list, label);
if (ret != result) {
printf("%d) %s | %s \n", num_test, list, label);
}
TEST_ASSERT_MESSAGE(ret == result, "Test failed");
}
TEST_CASE("Test label_search", "[bootloader_support]")
{
TEST_ASSERT_FALSE(bootloader_common_label_search(NULL, NULL));
TEST_ASSERT_FALSE(bootloader_common_label_search("nvs", NULL));
check_label_search(1, "nvs", "nvs", true);
check_label_search(2, "nvs, ", "nvs", true);
check_label_search(3, "nvs1", "nvs", false);
check_label_search(3, "nvs1, ", "nvs", false);
check_label_search(4, "nvs1nvs1, phy", "nvs1", false);
check_label_search(5, "nvs1, nvs1, phy", "nvs1", true);
check_label_search(6, "nvs12, nvs12, phy", "nvs1", false);
check_label_search(7, "nvs12, nvs1, phy", "nvs1", true);
check_label_search(8, "nvs12, nvs3, phy, nvs1","nvs1", true);
check_label_search(9, "nvs1nvs1, phy, nvs", "nvs", true);
check_label_search(10, "nvs1nvs1, phy, nvs1", "nvs", false);
check_label_search(11, "nvs1, nvs, phy, nvs1", "nvs", true);
check_label_search(12, "nvs1, nvs2, phy, nvs","nvs", true);
check_label_search(13, "ota_data, backup_nvs", "nvs", false);
check_label_search(14, "nvs1, nvs2, ota, nvs", "vs1", false);
check_label_search(20, "12345678901234, phy, nvs1", "12345678901234", true);
check_label_search(21, "123456789012345, phy, nvs1", "123456789012345", true);
check_label_search(22, "1234567890123456, phy, nvs1", "1234567890123456", true);
check_label_search(23, "12345678901234567, phy, nvs1", "12345678901234567", false);
check_label_search(24, "1234567890123456, phy, nvs1", "12345678901234567", true);
check_label_search(25, "phy, 1234567890123456, nvs1", "12345678901234567", true);
}

View File

@ -9,7 +9,52 @@ Bootloader performs the following functions:
Bootloader is located at the address `0x1000` in the flash.
FACTORY reset
---------------------------
The user can write a basic working firmware and load it into the factory partition.
Next, update the firmware via OTA (over the air). The updated firmware will be loaded into an OTA app partition slot and the OTA data partition is updated to boot from this partition.
If you want to be able to roll back to the factory firmware and clear the settings, then you need to set :ref:`CONFIG_BOOTLOADER_FACTORY_RESET`.
The factory reset mechanism allows to reset the device to factory settings:
- Clear one or more data partitions.
- Boot from "factory" partition.
:ref:`CONFIG_BOOTLOADER_DATA_FACTORY_RESET` allows customers to select which data partitions will be erased when the factory reset is executed.
Can specify the names of partitions through comma-delimited with optional spaces for readability. (Like this: "nvs, phy_init, nvs_custom, ...").
Make sure that the name specified in the partition table and here are the same.
Partitions of type "app" cannot be specified here.
:ref:`CONFIG_BOOTLOADER_OTA_DATA_ERASE` - the device will boot from "factory" partition after a factory reset. The OTA data partition will be cleared.
:ref:`CONFIG_BOOTLOADER_NUM_PIN_FACTORY_RESET`- number of the GPIO input for factory reset uses to trigger a factory reset, this GPIO must be pulled low on reset to trigger this.
:ref:`CONFIG_BOOTLOADER_HOLD_TIME_GPIO`- this is hold time of GPIO for reset/test mode (by default 5 seconds). The GPIO must be held low continuously for this period of time after reset before a factory reset or test partition boot (as applicable) is performed.
Partition table.::
# Name, Type, SubType, Offset, Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs, data, nvs, 0x9000, 0x4000
otadata, data, ota, 0xd000, 0x2000
phy_init, data, phy, 0xf000, 0x1000
factory, 0, 0, 0x10000, 1M
test, 0, test, , 512K
ota_0, 0, ota_0, , 512K
ota_1, 0, ota_1, , 512K
Boot from TEST firmware
------------------------
The user can write a special firmware for testing in production, and run it as needed. The partition table also needs a dedicated partition for this testing firmware (See `partition table`).
To trigger a test app you need to set :ref:`CONFIG_BOOTLOADER_APP_TEST`.
:ref:`CONFIG_BOOTLOADER_NUM_PIN_APP_TEST` - number of the GPIO input to boot TEST partition. The selected GPIO will be configured as an input with internal pull-up enabled. To trigger a test app, this GPIO must be pulled low on reset.
After the GPIO input is deactivated and the device reboots, the old application will boot (factory or any OTA slot).
:ref:`CONFIG_BOOTLOADER_HOLD_TIME_GPIO` - this is hold time of GPIO for reset/test mode (by default 5 seconds). The GPIO must be held low continuously for this period of time after reset before a factory reset or test partition boot (as applicable) is performed.
Customer bootloader
---------------------
The current bootloader implementation allows the customer to override it. To do this, you must copy the folder `/esp-idf/components/bootloader` and then edit `/your_project/components/bootloader/subproject/main/bootloader_main.c`.
In the bootloader space, you can not use the drivers and functions from other components. If necessary, then the required functionality should be placed in the folder bootloader (note that this will increase its size).
It is necessary to monitor its size because there can be overlays in memory with a partition table leading to damage. At the moment the bootloader is limited to the partition table from the address `0x8000`.