esp32 : Source d’erreur critique et fatale dans certains modules WROVER-E

Remarque : Pour les hörberts vendus entre octobre 2021 et le 7 février 2022, une mise à jour immédiate du firmware est fortement recommandée, Voir : https://www.hoerbert.com/firmware

Tous les hörberts fabriqués après le 7 février 2022 ne sont pas concernés.

Un défaut dormant

La nouvelle électronique de hörbert, que nous fabriquons depuis octobre 2021 et qui enrichit hörbert de nombreuses nouvelles fonctions, est basée sur un module de processeur très populaire et riche en fonctionnalités. Il s’agit du module WROVER-E de la société Espressif. Il s’agit d’un module très répandu et utilisé des millions de fois dans le monde. Nous utilisons – comment pourrait-il en être autrement – le module avec la plus grande mémoire flash intégrée disponible dans hörbert, afin d’avoir suffisamment d’espace pour notre firmware – maintenant et à l’avenir.

Tout comme vous maintenant, nous ne pouvons pas voir dans cette carte qu’un bug attend de saboter le module processeur lui-même.

carte hörbert V2.0

Il y a malheureusement une source d’erreur qui sommeille sous le capot de ces modules.

Tous les modules flash sont identiques

Sur les modules WROVER-E que nous achetons déjà équipés, le processeur et deux puces de mémoire se trouvent sous le capot métallique de protection. L’une d’elles est la mémoire flash, qui conserve le firmware même lorsque le module processeur n’est pas alimenté.
Comme il est d’usage sur le marché, le fabricant intègre différentes puces de mémoire flash, de tailles et de fonctions différentes, provenant de différents fabricants.

Jusque-là, rien de plus normal.

Identiques et pourtant différentes

Toutes les puces flash utilisées par Espressif ont des fonctions et des spécifications très similaires.

L’une de ces fonctions, présente dans de nombreuses mémoires flash, est la protection en écriture, qui peut être activée par des commandes spéciales. Les commandes permettant d’activer la protection en écriture diffèrent selon les produits.

Et la protection en écriture est une partie du problème. Et cela ne concerne qu’un seul des 5 types de puces flash probablement interchangeables.

En effet, certains de nos modules WROVER-E sont équipés d’une puce flash de la société XMC. Non, nous ne pouvons malheureusement pas savoir dans quels modules et dans combien de hörberts exactement cette puce fonctionne, car elle fait partie du module processeur que nous ne pouvons pas fabriquer nous-mêmes. Dans notre cas, il s’agit de la puce XM25QH128C. (->fiche technique)

En raison d’une cause inconnue, qu’Espressif n’a malheureusement pas encore trouvée, il peut arriver que la puce flash reçoive des instructions aléatoires, voire inutiles. Ces commandes déréglent complètement la puce flash. Malheureusement, comme cette erreur active également la protection en écriture de cette puce flash, il devient impossible de l’effacer ou de la récupérer.
Le véritable problème est que les bits correspondants à la protection en écriture sont activés de manière irréversible.

Cela rend la puce inutile pour toute opération ultérieure et nous devons la remplacer physiquement par une nouvelle puce.

La solution : comme toi, je te rends service !

L’équipe de support d’Espressif nous a envoyé une solution sous la forme d’un extrait de code que nous avons immédiatement intégré dans un nouveau firmware hörbert. Ce qui est dommage, c’est que la solution ne peut empêcher l’erreur qu’en amont, avant qu’elle ne frappe. Ce qui est super, c’est que la solution empêche l’erreur de se produire.

Et voici à quoi ressemble la solution :

Étant donné que nous ne connaissons pas la cause de l’erreur (données perdues ?), mais que seul l’effet (protection en écriture !) est notre problème, nous empêchons la protection en écriture d’être activée, et ce pour toujours ! Nous n’avons pas besoin de cette protection en écriture pour la fonctionnalité de hörbert, et c’est pourquoi nous fermons simplement la porte à la source de l’erreur.

Ainsi, même si la puce flash reçoit à l’avenir des commandes confuses, elle ne pourra plus se verrouiller elle-même.

Pour les programmeurs

La puce XM25QH128C est connectée au module WROVER-E via SPI. Outre les commandes d’écriture et de lecture, les trois registres SR1, SR2 et SR3 de 8 bits de large peuvent être définis.
Les bits SRP1 (“Status Register Protect 1”) dans le registre 2 et SRP0 (“Status Register Protect 2”) dans le registre 1 sont responsables de la protection en écriture.

Le fait de mettre ces deux bits à “1” bloque la modification des trois registres  pour toujours, car ces bits Status Register Protect sont des bits “OTP” (one time programmable). Une fois qu’ils sont activés, ils ne peuvent plus être supprimés. C’est pourquoi, dans notre nouveau firmware, nous réglons les trois registres sur des valeurs raisonnables dont nous avons besoin pour notre hörbert, puis nous réglons les deux bits Status Register Protect. Cela ne se fait qu’une fois.

Dans notre cas, nous pouvons sans risque régler les registres sur les valeurs 0x600380. (S1=0x80, S2=0x03, S3=0x60) Ces réglages sont les valeurs correctes pour notre firmware et nos fonctions, et en définissant les bits SRP0 et SRP1, cela reste ainsi.

Comment puis-je en savoir plus sur la puce flash ?

L’identifiant de la puce peut être lu à l’aide de esptool.py.
Dans notre cas, il s’agit de Manufacturer “20” (XMC) et de Chip ID 4018 (128 MBit Flash).

> esptool.py flash_id
esptool.py v3.1-dev
Found 2 serial ports
Serial port /dev/ttyUSB0
Connecting........_
Detecting chip type... ESP32
Chip is ESP32-D0WD-V3 (revision 3)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: 94:3c:c6:c1:55:e4
Uploading stub...
Running stub...
Stub running...
Manufacturer: 20
Device: 4018
Detected flash size: 16MB
Hard resetting via RTS pin...

Les registres peuvent également être lus avec esptool.py. Voici un exemple d’une puce configurée avec les valeurs des registres 0xe37bfc (S1=0xfc, S2=0x7b, S3=0xe3)

> esptool.py read_flash_status --bytes 3
esptool.py v3.1-dev
Found 2 serial ports
Serial port /dev/ttyUSB0
Connecting...
Detecting chip type... ESP32
Chip is ESP32-D0WD-V3 (revision 3)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: 94:3c:c6:c1:55:e4
Stub is already running. No upload is necessary.
Status value: 0xe37bfc
Hard resetting via RTS pin...

A quoi ressemble l’erreur sur la console ?

Cette sortie s’affiche lorsque l’erreur a déjà frappé et que la puce flash est devenue inutilisable. Le module WROVER-E est bloqué dans une boucle de démarrage sans fin et n’exécute aucun autre programme que d’essayer de charger le second stage bootloader.
ets Jul 29 2019 12:21:46

rst:0x1 (POWERON_RESET),boot:0x33 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:380
ho 0 tail 12 room 4
load:0x07800000,len:3378177
ets Jul 29 2019 12:21:46

rst:0x10 (RTCWDT_RTC_RESET),boot:0x33 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:380
ho 0 tail 12 room 4
load:0x07800000,len:3378177
ets Jul 29 2019 12:21:46

rst:0x10 (RTCWDT_RTC_RESET),boot:0x33 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:380
ho 0 tail 12 room 4
load:0x07800000,len:3378177
ets Jul 29 2019 12:21:46

rst:0x10 (RTCWDT_RTC_RESET),boot:0x33 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:380
ho 0 tail 12 room 4
load:0x07800000,len:3378177

... et ainsi de suite ...

Talk is cheap, show me the code!

Avertissement : N’utilisez pas ce code pour votre projet si vous ne comprenez pas quels bits vous pouvez ou ne pouvez pas mettre !

Heureusement, ce code nous a été envoyé par Espressif comme base pour notre propre correction. Il nécessite l’esp-idf d’Espressif. Il s’exécute en RAM et vérifie d’abord l’identifiant Flash 0x204018. Ensuite, il lit les registres et modifie S2 avec le bit indispensable Status Register Protect SRP1, et définit le bit SRP0, qui constituent ensemble la correction proprement dite.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/spi_reg.h"
#include "esp32/rom/spi_flash.h"
#include "esp_spi_flash.h"
#include "esp_task_wdt.h"
#include "soc/spi_struct.h"

extern uint8_t g_rom_spiflash_dummy_len_plus[];
#define SPIFLASH SPI1

/**
 *  Copy from execute_flash_command() since it is static function
 */
IRAM_ATTR uint32_t bootloader_execute_flash_command(uint8_t command, uint32_t mosi_data, uint8_t mosi_len, uint8_t miso_len)
{
    uint32_t old_ctrl_reg = SPIFLASH.ctrl.val;
#if CONFIG_IDF_TARGET_ESP32
    SPIFLASH.ctrl.val = SPI_WP_REG_M; // keep WP high while idle, otherwise leave DIO mode
#else
    SPIFLASH.ctrl.val = SPI_MEM_WP_REG_M; // keep WP high while idle, otherwise leave DIO mode
#endif
    SPIFLASH.user.usr_dummy = 0;
    SPIFLASH.user.usr_addr = 0;
    SPIFLASH.user.usr_command = 1;
    SPIFLASH.user2.usr_command_bitlen = 7;

    SPIFLASH.user2.usr_command_value = command;
    SPIFLASH.user.usr_miso = miso_len > 0;
#if CONFIG_IDF_TARGET_ESP32
    SPIFLASH.miso_dlen.usr_miso_dbitlen = miso_len ? (miso_len - 1) : 0;
#else
    SPIFLASH.miso_dlen.usr_miso_bit_len = miso_len ? (miso_len - 1) : 0;
#endif
    SPIFLASH.user.usr_mosi = mosi_len > 0;
#if CONFIG_IDF_TARGET_ESP32
    SPIFLASH.mosi_dlen.usr_mosi_dbitlen = mosi_len ? (mosi_len - 1) : 0;
#else
    SPIFLASH.mosi_dlen.usr_mosi_bit_len = mosi_len ? (mosi_len - 1) : 0;
#endif
    SPIFLASH.data_buf[0] = mosi_data;

    if (g_rom_spiflash_dummy_len_plus[1]) {
        /* When flash pins are mapped via GPIO matrix, need a dummy cycle before reading via MISO */
        if (miso_len > 0) {
            SPIFLASH.user.usr_dummy = 1;
            SPIFLASH.user1.usr_dummy_cyclelen = g_rom_spiflash_dummy_len_plus[1] - 1;
        } else {
            SPIFLASH.user.usr_dummy = 0;
            SPIFLASH.user1.usr_dummy_cyclelen = 0;
        }
    }

    SPIFLASH.cmd.usr = 1;
    while (SPIFLASH.cmd.usr != 0) {
    }

    SPIFLASH.ctrl.val = old_ctrl_reg;
    return SPIFLASH.data_buf[0];
}

/**
 * SR3 should be wrote at first before writing SR1 SR2
 */
IRAM_ATTR esp_rom_spiflash_result_t esp_xmc_flash_read_status_sr3(esp_rom_spiflash_chip_t *spi, uint32_t *status)
{
    esp_rom_spiflash_result_t ret;
    esp_rom_spiflash_wait_idle(spi);
    ret = esp_rom_spiflash_read_user_cmd(status, 0x15);
    return ret;
}

IRAM_ATTR esp_rom_spiflash_result_t esp_xmc_flash_read_sr(esp_rom_spiflash_chip_t *spi_flash, uint32_t *sr1_status, uint32_t *sr2_status, uint32_t *sr3_status)
{
    esp_rom_spiflash_result_t ret = esp_rom_spiflash_read_status(spi_flash, sr1_status);
    if (ret != ESP_OK) {
        return ret;
    }
    ret = esp_rom_spiflash_read_statushigh(spi_flash, sr2_status);
    if (ret != ESP_OK) {
        return ret;
    }
    ret = esp_xmc_flash_read_status_sr3(spi_flash, sr3_status);
    if (ret != ESP_OK) {
        return ret;
    }

    *sr2_status >>= 8;
    return ESP_ROM_SPIFLASH_RESULT_OK;
}

/**
 *  Copy from esp_rom_spiflash_enable_write() since it is static function
 */
IRAM_ATTR esp_rom_spiflash_result_t esp_xmc_flash_enable_write(esp_rom_spiflash_chip_t *spi)
{
    uint32_t flash_status = 0;
    esp_rom_spiflash_wait_idle(spi);
    WRITE_PERI_REG(PERIPHS_SPI_FLASH_CMD, SPI_FLASH_WREN);     // Enable write operation
    while (READ_PERI_REG(PERIPHS_SPI_FLASH_CMD) != 0);
    while (ESP_ROM_SPIFLASH_WRENABLE_FLAG != (flash_status & ESP_ROM_SPIFLASH_WRENABLE_FLAG)) {  //Waiting for flash
        esp_rom_spiflash_read_status(spi, &flash_status);
    }
    return ESP_ROM_SPIFLASH_RESULT_OK;
}

IRAM_ATTR esp_err_t esp_xmc_flash_write_sr(esp_rom_spiflash_chip_t *spi_flash, uint8_t sr1_status, uint8_t sr2_status, uint8_t sr3_status)
{
    const uint8_t SPIFLASH_WRSR1 = 0x01;
    const uint8_t SPIFLASH_WRSR2 = 0x31;
    const uint8_t SPIFLASH_WRSR3 = 0x11;

    esp_xmc_flash_enable_write(spi_flash);
    esp_rom_spiflash_wait_idle(spi_flash);
    bootloader_execute_flash_command(SPIFLASH_WRSR3, sr3_status, 8, 0); //SR3 should be wrote at first

    esp_xmc_flash_enable_write(spi_flash);
    esp_rom_spiflash_wait_idle(spi_flash);
    bootloader_execute_flash_command(SPIFLASH_WRSR1, sr1_status, 8, 0);

    esp_xmc_flash_enable_write(spi_flash);
    esp_rom_spiflash_wait_idle(spi_flash);
    bootloader_execute_flash_command(SPIFLASH_WRSR2, sr2_status, 8, 0);
    esp_rom_spiflash_wait_idle(spi_flash);
    return ESP_OK;
}

/**
 * One time programming, the status register value will be programmed into 0x600380(little-ending) and can't be reversed
 */
IRAM_ATTR esp_err_t esp_xmc_16m_flash_sr_otp(esp_rom_spiflash_chip_t *spi_flash)
{
    if (spi_flash->device_id != 0x204018) {
        return ESP_ERR_NOT_FOUND;
    }
    uint32_t sr1_status = 0;
    uint32_t sr2_status = 0;
    uint32_t sr3_status = 0;
//    const uint8_t SR1_TB_MASK = 0x20;       //Top/Botton protect bit (SR1 bit5), Should be cleared
//    const uint8_t SR1_BP_MASK = 0x1c;      //Block protect bits, include BP0(SR1 bit4), BP1(SR1 bit3), BP2(SR1 bit2), should be cleared
//    const uint8_t SR1_SEC_MASK = 0x40;    //Sector protect bit(SR1 bit6), should be cleared
//    const uint8_t SR1_WEL_MASK = 0x02;   //Write enable latch(SR1 bit1), should be cleared
    const uint8_t SR1_SRP0_MASK = 0x80;   //SRP0, Should be set
    const uint8_t SR2_SRP1_MASK = 0x01;  //SRP1, Should be set
    const uint8_t SR2_QE_MASK = 0x02;   //Quad Enable,

    esp_rom_spiflash_result_t ret = esp_xmc_flash_read_sr(spi_flash, &sr1_status, &sr2_status, &sr3_status);
    if (ret != ESP_ROM_SPIFLASH_RESULT_OK) {
        return ESP_FAIL;
    }
    if ((sr1_status & SR1_SRP0_MASK) && (sr2_status & SR2_SRP1_MASK)) {
        //SPR0 and SPR1 has been set
        return ESP_OK;
    }
    uint8_t new_sr1_status = 0x80;  //Set SRP0, clear other protect bits
    uint8_t new_sr2_status = ((uint8_t)sr2_status | SR2_SRP1_MASK | SR2_QE_MASK);
    uint8_t new_sr3_status = 0x60;
    esp_xmc_flash_write_sr(spi_flash, new_sr1_status, new_sr2_status, new_sr3_status);
    return ESP_OK;
}

IRAM_ATTR void app_main(void)  //app_main should be put into IRAM
{
    esp_err_t ret;
    TaskHandle_t cur_core_idle_handle = xTaskGetIdleTaskHandle();
    ret = esp_task_wdt_delete(cur_core_idle_handle);  //Disable watchdog
    if (ret != ESP_OK) {
        return;
    }
    g_flash_guard_default_ops.start(); //Disable cache
    esp_xmc_16m_flash_sr_otp(&g_rom_flashchip);
    g_flash_guard_default_ops.end();  //Enable cache
    ret = esp_task_wdt_add(cur_core_idle_handle);
    if (ret != ESP_OK) {
        return;
    }

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Pour bien comprendre la méthode, il suffit d’examiner les registres de la puce flash.

Les registres de la puce flash utilisée

* Prix incl. 19% TVA incluse