esp32: Kritische, fatale Fehlerquelle in manchen WROVER-E Modulen

Hinweis: Für hörberts, die zwischen Oktober 2021 und 7. Februar 2022 verkauft wurden, wird dringend ein sofortiges Firmware-Update empfohlen, Siehe: https://www.hoerbert.com/firmware

Alle hörberts nach dem 7.2.2022 sind nicht mehr betroffen

Ein schlummernder Fehler

hörberts neue Elektronik, die wir seit Oktober 2021 herstellen, und die hörbert um viele neue Funktionen bereichert, basiert auf einem sehr beliebten und funktionsreichen Prozessormodul. Es handelt sich dabei um das WROVER-E Modul der Firma Espressif. Das ist ein weit verbreitetes und millionenfach eingesetztes Modul weltweit. Wir setzen – wie könnte es anders sein – in hörbert das Modul mit dem größten erhältlichen eingebauten Flash-Speicher ein, um genug Platz für unsere Firmware -jetzt und in Zukunft- zu haben.

Genau so wie Du können wir dieser Platine nicht ansehen, dass im Prozessormodul ein Fehler darauf wartet, das Prozessormodul selbst zu sabotieren.

hörbert Platine v2.0

Es schlummert leider eine Fehlerquelle unter der Haube dieser Module.

Flash ist gleich gleich Flash

Auf den WROVER-E Modulen, die wir fertig bestückt zukaufen, sitzt unter der abschirmenden Metallhaube der Prozessor und zwei Speicherchips. Einer davon ist der Flash-Speicher, auf dem die Firmware gespeichert bleibt, auch wenn das Prozessormodul keinen Strom hat.
Wie im Markt üblich, verbaut der Hersteller unterschiedliche Flash-Speicherchips mit unterschiedlichen Größen und Funktionen von unterschiedlichen Herstellern.

So weit, so normal.

Gleich und doch anders

Alle von Espressif eingesetzten Flashchips verfügen über sehr ähnliche Funktionen und Spezifikationen.

Eine dieser Funktionen, die viele Flash-Speicher besitzen, ist ein Schreibschutz, den man durch spezielle Befehle einschalten kann. Die Befehle zum Aktivieren des Schreibschutzes unterscheiden sich zwischen den Produkten.

Und der Schreibschutz ist ein Teil des Problems. Und das auch nur bei einem von vermutlich 5 austauschbaren Typen der Flash-Chips.

In manchen unserer WROVER-E Modulen wird nämlich ein Flash-Chip der Firma XMC verbaut. Nein, wir können leider nicht wissen, in welchen Modulen und in wie vielen hörberts genau dieser Chip arbeitet, denn er ist Teil des Prozessormoduls, das wir nicht selbst herstellen können. Es handelt sich in unserem Fall um den Chip XM25QH128C. (->Datenblatt)

Durch eine unbekannte Ursache, die auch Espressif leider noch nicht gefunden hat, kann es passieren, dass der Flash-Chip zufällige, vielleicht sogar sinnlose Befehle erhält. Diese Befehle verstellen den Flash-Chip komplett. Da durch den Fehler leider auch der Schreibschutz dieses Flash-Chips aktiviert wird, wird es im Ergebnis unmöglich, ihn wieder zu löschen oder zu retten.
Das eigentliche Problem ist, dass die entsprechenden Status-Bits für den Schreibschutz unwiderruflich gesetzt werden.

Dadurch wird der Chip für alle weiteren Operationen nutzlos, und muss von uns physikalisch gegen einen neuen Chip ausgetauscht werden.

Die Lösung: Wie Du mir, so ich Dir!

Das Support-Team von Espressif hat uns eine Lösung in Form eines Codeschnipsels geschickt, den wir umgehend in eine neue hörbert-Firmware eingebunden haben. Schade ist: Die Lösung kann den Fehler nur im Vorfeld verhindern, bevor er zuschlägt. Super ist: Die Lösung verhindert, dass der Fehler zuschlagen kann.

Und so sieht die Lösung aus:

Da wir die Ursache des Fehlers (Datenmüll?) nicht kennen, aber nur die Auswirkung (Schreibschutz!) unser Problem ist, verhindern wir, dass der Schreibschutz gesetzt werden kann, und zwar für immer! Wir brauchen für hörberts Funktionalität diesen Schreibschutz nicht, und darum schlagen wir einfach der Fehlerquelle die Tür vor der Nase zu.

Also selbst, wenn der Flash-Chip in Zukunft wirre Befehle erhält, kann er sich nicht mehr selbst sperren.

Für Programmierer

Der Chip XM25QH128C wird auf dem WROVER-E Modul über SPI angebunden. Neben Schreib- und Lesebefehlen können auch die drei 8-Bit breiten Status-Register SR1, SR2 und SR3 gesetzt werden.
Verantwortlich für den Schreibschutz sind die Bits SRP1 (“Status Register Protect 1”) im Status-Register 2 sowie SRP0 (“Status Register Protect 2”) im Status-Register 1.

Das Setzen dieser beiden Bits auf “1” sperrt die Änderung aller drei Statusregister für alle Zeit, denn diese Status Register Protect-Bits sind “OTP” (one time programmable) Bits. Sind sie einmal gesetzt, können sie nicht wieder gelöscht werden. Darum stellen wir in unserer neuen Firmware alle drei Statusregister auf vernünftige Werte, die wir für unseren hörbert brauchen, und setzen dann beide Status Register Protect Bits. Das geht genau einmal.

In unserem Fall können wir die Register gefahrlos auf die Werte 0x600380 setzen. (S1=0x80, S2=0x03, S3=0x60) Diese Einstellungen sind für unsere Firmware und unsere Funktionen die richtigen Werte, und durch das Setzen der Bits SRP0 und SRP1 bleibt das auch so.

Wie finde ich mehr über den Flash-Chip heraus?

Die Chip-ID lässt sich mit Hilfe von esptool.py auslesen.
In unserem Fall ist es Manufacturer “20” (XMC) und die 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...

Die Statusregister lassen sich mit esptool.py ebenfalls auslesen. Hier ein Beispiel eines verkonfigurierten Chips mit den Werten der Statusregister 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...

Wie sieht der Fehler auf der Konsole aus?

Diesen Output sieht man, wenn der Fehler bereits zugeschlagen hat, und der Flash-Chip unbrauchbar wurde. Das WROVER-E Modul hängt in einer endlosen Bootschleife und führt kein anderes Programm aus, außer zu versuchen, den second stage Bootloader zu laden.
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

... und so weiter ...

Talk is cheap, show me the code!

Disclaimer: Benutze diesen Code nicht für Dein Projekt, wenn Du nicht verstehst, welche Status Bits Du setzen darfst, und welche nicht!

Dieser Code wurde uns glücklicherweise von Espressif als Grundlage für unseren eigenen Fix zur Unterstützung geschickt. Er benötigt das esp-idf von Espressif. Er läuft im RAM und prüft zuerst die Flash-ID 0x204018 ab. Danach liest er die Statusregister und ver-odert S2 mit dem unbedingt notwendigen Status Register Protect Bit SRP1, und setzt Bit SRP0, die zusammen den eigentlichen Fix ausmachen.

#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));
    }
}

Richtig verständlich wird die Methode, wenn man dazu die Status-Register des Flash-Chips betrachtet.

Die Statusregister des verwendeten Flash-Chips

* Preise inkl. 19% MwSt.