Merge branch 'bugfix/esp_flash_write_performance_regression_4.0' into 'release/v4.0'

esp_flash: fix the write performance regression (Backport v4.0)

See merge request espressif/esp-idf!9616
This commit is contained in:
Angus Gratton 2020-07-29 15:30:13 +08:00
commit 0dc46879c5
7 changed files with 312 additions and 45 deletions

View file

@ -48,8 +48,8 @@ typedef struct {
/** Called before any erase/write operations to check whether the region is limited by the OS */
esp_err_t (*region_protected)(void* arg, size_t start_addr, size_t size);
/** Delay for at least 'ms' milliseconds. Called in between 'start' and 'end'. */
esp_err_t (*delay_ms)(void *arg, unsigned ms);
/** Delay for at least 'us' microseconds. Called in between 'start' and 'end'. */
esp_err_t (*delay_us)(void *arg, unsigned us);
} esp_flash_os_functions_t;
/** @brief Structure to describe a SPI flash chip connected to the system.

View file

@ -193,14 +193,14 @@ esp_err_t spi_flash_chip_generic_get_write_protect(esp_flash_t *chip, bool *out_
* progress bit) to be cleared.
*
* @param chip Pointer to SPI flash chip to use. If NULL, esp_flash_default_chip is substituted.
* @param timeout_ms Time to wait before timeout, in ms.
* @param timeout_us Time to wait before timeout, in us.
*
* @return
* - ESP_OK if success
* - ESP_ERR_TIMEOUT if not idle before timeout
* - or other error passed from the ``wait_idle`` or ``read_status`` function of host driver
*/
esp_err_t spi_flash_chip_generic_wait_idle(esp_flash_t *chip, uint32_t timeout_ms);
esp_err_t spi_flash_chip_generic_wait_idle(esp_flash_t *chip, uint32_t timeout_us);
/**
* @brief Set the specified SPI read mode according to the data in the chip
@ -247,7 +247,7 @@ extern const spi_flash_chip_t esp_flash_chip_generic;
* spi_flash_chip_generic_wait_idle() and may be useful when implementing
* alternative drivers.
*
* timeout_ms will be decremented if the function needs to wait until the host hardware is idle.
* timeout_us will be decremented if the function needs to wait until the host hardware is idle.
*
* @param chip Pointer to SPI flash chip to use. If NULL, esp_flash_default_chip is substituted.
*
@ -256,7 +256,7 @@ extern const spi_flash_chip_t esp_flash_chip_generic;
* - ESP_ERR_TIMEOUT if not idle before timeout
* - or other error passed from the ``set_write_protect`` or ``common_command`` function of host driver
*/
esp_err_t spi_flash_generic_wait_host_idle(esp_flash_t *chip, uint32_t *timeout_ms);
esp_err_t spi_flash_generic_wait_host_idle(esp_flash_t *chip, uint32_t *timeout_us);
/// Function pointer type for reading status register with QE bit.
typedef esp_err_t (*esp_flash_rdsr_func_t)(esp_flash_t* chip, uint32_t* out_sr);

View file

@ -20,12 +20,15 @@
static const char TAG[] = "chip_generic";
#define SPI_FLASH_GENERIC_CHIP_ERASE_TIMEOUT 4000
#define SPI_FLASH_GENERIC_SECTOR_ERASE_TIMEOUT 500
#define SPI_FLASH_GENERIC_BLOCK_ERASE_TIMEOUT 1000
#define SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS 200
#define SPI_FLASH_GENERIC_CHIP_ERASE_TIMEOUT_MS 4000
#define SPI_FLASH_GENERIC_SECTOR_ERASE_TIMEOUT_MS 500 //according to GD25Q127 + 100ms
#define SPI_FLASH_GENERIC_BLOCK_ERASE_TIMEOUT_MS 1300 //according to GD25Q127 + 100ms
#define SPI_FLASH_GENERIC_PAGE_PROGRAM_TIMEOUT_MS 500
#define HOST_DELAY_INTERVAL_US 1
#define CHIP_WAIT_IDLE_INTERVAL_US 20
#define DEFAULT_IDLE_TIMEOUT 200
#define DEFAULT_PAGE_PROGRAM_TIMEOUT 500
esp_err_t spi_flash_chip_generic_probe(esp_flash_t *chip, uint32_t flash_id)
{
@ -54,7 +57,7 @@ esp_err_t spi_flash_chip_generic_reset(esp_flash_t *chip)
return err;
}
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
return err;
}
@ -80,7 +83,7 @@ esp_err_t spi_flash_chip_generic_erase_chip(esp_flash_t *chip)
err = chip->chip_drv->set_chip_write_protect(chip, false);
if (err == ESP_OK) {
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
}
if (err == ESP_OK) {
chip->host->erase_chip(chip->host);
@ -91,7 +94,7 @@ esp_err_t spi_flash_chip_generic_erase_chip(esp_flash_t *chip)
return err;
}
}
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_CHIP_ERASE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_CHIP_ERASE_TIMEOUT_MS * 1000);
}
return err;
}
@ -100,7 +103,7 @@ esp_err_t spi_flash_chip_generic_erase_sector(esp_flash_t *chip, uint32_t start_
{
esp_err_t err = chip->chip_drv->set_chip_write_protect(chip, false);
if (err == ESP_OK) {
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
}
if (err == ESP_OK) {
chip->host->erase_sector(chip->host, start_address);
@ -111,7 +114,7 @@ esp_err_t spi_flash_chip_generic_erase_sector(esp_flash_t *chip, uint32_t start_
return err;
}
}
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_SECTOR_ERASE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_SECTOR_ERASE_TIMEOUT_MS * 1000);
}
return err;
}
@ -120,7 +123,7 @@ esp_err_t spi_flash_chip_generic_erase_block(esp_flash_t *chip, uint32_t start_a
{
esp_err_t err = chip->chip_drv->set_chip_write_protect(chip, false);
if (err == ESP_OK) {
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
}
if (err == ESP_OK) {
chip->host->erase_block(chip->host, start_address);
@ -131,7 +134,7 @@ esp_err_t spi_flash_chip_generic_erase_block(esp_flash_t *chip, uint32_t start_a
return err;
}
}
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_BLOCK_ERASE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_BLOCK_ERASE_TIMEOUT_MS * 1000);
}
return err;
}
@ -158,13 +161,13 @@ esp_err_t spi_flash_chip_generic_page_program(esp_flash_t *chip, const void *buf
{
esp_err_t err;
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
if (err == ESP_OK) {
// Perform the actual Page Program command
chip->host->program_page(chip->host, buffer, address, length);
err = chip->chip_drv->wait_idle(chip, DEFAULT_PAGE_PROGRAM_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_GENERIC_PAGE_PROGRAM_TIMEOUT_MS * 1000);
}
return err;
}
@ -205,7 +208,7 @@ esp_err_t spi_flash_chip_generic_set_write_protect(esp_flash_t *chip, bool write
{
esp_err_t err = ESP_OK;
err = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
err = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
if (err == ESP_OK) {
chip->host->set_write_protect(chip->host, write_protect);
@ -234,25 +237,31 @@ esp_err_t spi_flash_chip_generic_get_write_protect(esp_flash_t *chip, bool *out_
return err;
}
esp_err_t spi_flash_generic_wait_host_idle(esp_flash_t *chip, uint32_t *timeout_ms)
esp_err_t spi_flash_generic_wait_host_idle(esp_flash_t *chip, uint32_t *timeout_us)
{
while (chip->host->host_idle(chip->host) && *timeout_ms > 0) {
if (*timeout_ms > 1) {
chip->os_func->delay_ms(chip->os_func_data, 1);
while (chip->host->host_idle(chip->host) && *timeout_us > 0) {
#if HOST_DELAY_INTERVAL_US > 0
if (*timeout_us > 1) {
int delay = MIN(HOST_DELAY_INTERVAL_US, *timeout_us);
chip->os_func->delay_us(chip->os_func_data, delay);
*timeout_us -= delay;
} else {
return ESP_ERR_TIMEOUT;
}
(*timeout_ms)--;
#endif
}
return (*timeout_ms > 0) ? ESP_OK : ESP_ERR_TIMEOUT;
return ESP_OK;
}
esp_err_t spi_flash_chip_generic_wait_idle(esp_flash_t *chip, uint32_t timeout_ms)
esp_err_t spi_flash_chip_generic_wait_idle(esp_flash_t *chip, uint32_t timeout_us)
{
timeout_ms++; // allow at least one pass before timeout, last one has no sleep cycle
timeout_us++; // allow at least one pass before timeout, last one has no sleep cycle
uint8_t status = 0;
while (timeout_ms > 0) {
const int interval = CHIP_WAIT_IDLE_INTERVAL_US;
while (timeout_us > 0) {
esp_err_t err = spi_flash_generic_wait_host_idle(chip, &timeout_ms);
esp_err_t err = spi_flash_generic_wait_host_idle(chip, & timeout_us);
if (err != ESP_OK) {
return err;
}
@ -264,13 +273,14 @@ esp_err_t spi_flash_chip_generic_wait_idle(esp_flash_t *chip, uint32_t timeout_m
if ((status & SR_WIP) == 0) {
break; // Write in progress is complete
}
if (timeout_ms > 1) {
chip->os_func->delay_ms(chip->os_func_data, 1);
if (timeout_us > 0 && interval > 0) {
int delay = MIN(interval, timeout_us);
chip->os_func->delay_us(chip->os_func_data, delay);
timeout_us -= delay;
}
timeout_ms--;
}
return (timeout_ms > 0) ? ESP_OK : ESP_ERR_TIMEOUT;
return (timeout_us > 0) ? ESP_OK : ESP_ERR_TIMEOUT;
}
esp_err_t spi_flash_chip_generic_config_host_io_mode(esp_flash_t *chip)
@ -490,7 +500,7 @@ esp_err_t spi_flash_common_set_io_mode(esp_flash_t *chip, esp_flash_wrsr_func_t
return ret;
}
ret = chip->chip_drv->wait_idle(chip, DEFAULT_IDLE_TIMEOUT);
ret = chip->chip_drv->wait_idle(chip, SPI_FLASH_DEFAULT_IDLE_TIMEOUT_MS * 1000);
if (ret != ESP_OK) {
return ret;
}

View file

@ -77,9 +77,9 @@ static esp_err_t spi23_end(void *arg)
return ESP_OK;
}
static IRAM_ATTR esp_err_t delay_ms(void *arg, unsigned ms)
static IRAM_ATTR esp_err_t delay_us(void *arg, unsigned us)
{
ets_delay_us(1000 * ms);
ets_delay_us(us);
return ESP_OK;
}
@ -115,14 +115,14 @@ static app_func_arg_t spi3_arg = {
const DRAM_ATTR esp_flash_os_functions_t esp_flash_spi1_default_os_functions = {
.start = spi1_start,
.end = spi1_end,
.delay_ms = delay_ms,
.delay_us = delay_us,
.region_protected = main_flash_region_protected,
};
const esp_flash_os_functions_t esp_flash_spi23_default_os_functions = {
.start = spi23_start,
.end = spi23_end,
.delay_ms = delay_ms,
.delay_us = delay_us,
};
esp_err_t esp_flash_init_os_functions(esp_flash_t *chip, int host_id)

View file

@ -37,16 +37,16 @@ static IRAM_ATTR esp_err_t end(void *arg)
return ESP_OK;
}
static IRAM_ATTR esp_err_t delay_ms(void *arg, unsigned ms)
static IRAM_ATTR esp_err_t delay_us(void *arg, unsigned us)
{
ets_delay_us(1000 * ms);
ets_delay_us(us);
return ESP_OK;
}
const DRAM_ATTR esp_flash_os_functions_t esp_flash_noos_functions = {
.start = start,
.end = end,
.delay_ms = delay_ms,
.delay_us = delay_us,
.region_protected = NULL,
};

View file

@ -460,7 +460,7 @@ static void test_toggle_qe(esp_flash_t* chip)
ESP_LOGW(TAG, "cannot clear QE bit for known permanent QE (Winbond) chips.");
} else {
ESP_LOGE(TAG, "cannot clear QE bit, please make sure force clearing QE option is enabled in `spi_flash_common_set_io_mode`, and this chip is not a permanent QE one.");
}
}
chip->read_mode = io_mode_before;
return;
}
@ -610,6 +610,138 @@ static void test_write_large_buffer(esp_flash_t *chip, const uint8_t *source, si
read_and_check(chip, part, source, length);
}
typedef struct {
uint32_t us_start;
size_t len;
const char* name;
} time_meas_ctx_t;
static void time_measure_start(time_meas_ctx_t* ctx)
{
ctx->us_start = esp_timer_get_time();
}
static uint32_t time_measure_end(time_meas_ctx_t* ctx)
{
uint32_t time_us = esp_timer_get_time() - ctx->us_start;
ESP_LOGI(TAG, "%s: typical: %.2lf kB/s", ctx->name, ctx->len / (time_us/1000.));
return ctx->len * 1000 / (time_us / 1000);
}
#define TEST_TIMES 20
#define TEST_SECTORS 4
static uint32_t measure_erase(const esp_partition_t* part)
{
const int total_len = SPI_FLASH_SEC_SIZE * TEST_SECTORS;
time_meas_ctx_t time_ctx = {.name = "erase", .len = total_len};
time_measure_start(&time_ctx);
esp_err_t err = esp_flash_erase_region(part->flash_chip, part->address, total_len);
TEST_ESP_OK(err);
return time_measure_end(&time_ctx);
}
// should called after measure_erase
static uint32_t measure_write(const char* name, const esp_partition_t* part, const uint8_t* data_to_write, int seg_len)
{
const int total_len = SPI_FLASH_SEC_SIZE;
time_meas_ctx_t time_ctx = {.name = name, .len = total_len * TEST_TIMES};
time_measure_start(&time_ctx);
for (int i = 0; i < TEST_TIMES; i ++) {
// Erase one time, but write 100 times the same data
size_t len = total_len;
int offset = 0;
while (len) {
int len_write = MIN(seg_len, len);
esp_err_t err = esp_flash_write(part->flash_chip, data_to_write + offset, part->address + offset, len_write);
TEST_ESP_OK(err);
offset += len_write;
len -= len_write;
}
}
return time_measure_end(&time_ctx);
}
static uint32_t measure_read(const char* name, const esp_partition_t* part, uint8_t* data_read, int seg_len)
{
const int total_len = SPI_FLASH_SEC_SIZE;
time_meas_ctx_t time_ctx = {.name = name, .len = total_len * TEST_TIMES};
time_measure_start(&time_ctx);
for (int i = 0; i < TEST_TIMES; i ++) {
size_t len = total_len;
int offset = 0;
while (len) {
int len_read = MIN(seg_len, len);
esp_err_t err = esp_flash_read(part->flash_chip, data_read + offset, part->address + offset, len_read);
TEST_ESP_OK(err);
offset += len_read;
len -= len_read;
}
}
return time_measure_end(&time_ctx);
}
#define MEAS_WRITE(n) (measure_write("write in "#n"-byte chunks", &test_part, data_to_write, n))
#define MEAS_READ(n) (measure_read("read in "#n"-byte chunks", &test_part, data_read, n))
static void test_flash_read_write_performance(esp_flash_t* chip)
{
const esp_partition_t *part = get_test_data_partition();
// Copy to new partition variable and replace the chip member
// Actually there's no "partition" in the external flash on runners. We just don't bother creating a new partition variable.
esp_partition_t test_part;
memcpy(&test_part, part, sizeof(esp_partition_t));
test_part.flash_chip = chip;
const int total_len = SPI_FLASH_SEC_SIZE;
uint8_t *data_to_write = heap_caps_malloc(total_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
uint8_t *data_read = heap_caps_malloc(total_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
srand(777);
for (int i = 0; i < total_len; i++) {
data_to_write[i] = rand();
}
uint32_t erase_1 = measure_erase(&test_part);
uint32_t speed_WR_4B = MEAS_WRITE(4);
uint32_t speed_RD_4B = MEAS_READ(4);
uint32_t erase_2 = measure_erase(&test_part);
uint32_t speed_WR_2KB = MEAS_WRITE(2048);
uint32_t speed_RD_2KB = MEAS_READ(2048);
TEST_ASSERT_EQUAL_HEX8_ARRAY(data_to_write, data_read, total_len);
// Not actually checking in this version
#define CHECK_DATA(bus, suffix) ((void)speed_##suffix)
#define CHECK_ERASE(bus, var) ((void)var)
// Erase time may vary a lot, can increase threshold if this fails with a reasonable speed
#define CHECK_PERFORMANCE(bus) do {\
CHECK_DATA(bus, WR_4B); \
CHECK_DATA(bus, RD_4B); \
CHECK_DATA(bus, WR_2KB); \
CHECK_DATA(bus, RD_2KB); \
CHECK_ERASE(bus, erase_1); \
CHECK_ERASE(bus, erase_2); \
} while (0)
CHECK_PERFORMANCE(0);
free(data_to_write);
free(data_read);
}
FLASH_TEST_CASE("Test esp_flash read/write performance", test_flash_read_write_performance);
FLASH_TEST_CASE_3("Test esp_flash read/write performance", test_flash_read_write_performance);
#ifdef CONFIG_SPIRAM_USE_MALLOC
/* Utility: Read into a small internal RAM buffer using esp_flash_read() and compare what
@ -676,5 +808,4 @@ static void test_flash_read_large_psram_buffer_low_internal_mem(esp_flash_t *chi
FLASH_TEST_CASE("esp_flash_read large PSRAM buffer low memory", test_flash_read_large_psram_buffer_low_internal_mem);
#endif

View file

@ -1,4 +1,5 @@
#include <stdio.h>
#include <sys/param.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
@ -9,6 +10,7 @@
#include "driver/timer.h"
#include "esp_intr_alloc.h"
#include "test_utils.h"
#include "esp_log.h"
struct flash_test_ctx {
uint32_t offset;
@ -16,6 +18,8 @@ struct flash_test_ctx {
SemaphoreHandle_t done;
};
static const char TAG[] = "test_spi_flash";
static void flash_test_task(void *arg)
{
struct flash_test_ctx *ctx = (struct flash_test_ctx *) arg;
@ -168,6 +172,128 @@ TEST_CASE("spi flash functions can run along with IRAM interrupts", "[spi_flash]
free(read_arg.buf);
}
typedef struct {
uint32_t us_start;
size_t len;
const char* name;
} time_meas_ctx_t;
static void time_measure_start(time_meas_ctx_t* ctx)
{
ctx->us_start = esp_timer_get_time();
}
static uint32_t time_measure_end(time_meas_ctx_t* ctx)
{
uint32_t time_us = esp_timer_get_time() - ctx->us_start;
ESP_LOGI(TAG, "%s: typical: %.2lf kB/s", ctx->name, ctx->len / (time_us/1000.));
return ctx->len * 1000 / (time_us / 1000);
}
#define TEST_TIMES 20
#define TEST_SECTORS 4
static uint32_t measure_erase(const esp_partition_t* part)
{
const int total_len = SPI_FLASH_SEC_SIZE * TEST_SECTORS;
time_meas_ctx_t time_ctx = {.name = "erase", .len = total_len};
time_measure_start(&time_ctx);
esp_err_t err = spi_flash_erase_range(part->address, total_len);
TEST_ESP_OK(err);
return time_measure_end(&time_ctx);
}
// should called after measure_erase
static uint32_t measure_write(const char* name, const esp_partition_t* part, const uint8_t* data_to_write, int seg_len)
{
const int total_len = SPI_FLASH_SEC_SIZE;
time_meas_ctx_t time_ctx = {.name = name, .len = total_len * TEST_TIMES};
time_measure_start(&time_ctx);
for (int i = 0; i < TEST_TIMES; i ++) {
// Erase one time, but write 100 times the same data
size_t len = total_len;
int offset = 0;
while (len) {
int len_write = MIN(seg_len, len);
esp_err_t err = spi_flash_write(part->address + offset, data_to_write + offset, len_write);
TEST_ESP_OK(err);
offset += len_write;
len -= len_write;
}
}
return time_measure_end(&time_ctx);
}
static uint32_t measure_read(const char* name, const esp_partition_t* part, uint8_t* data_read, int seg_len)
{
const int total_len = SPI_FLASH_SEC_SIZE;
time_meas_ctx_t time_ctx = {.name = name, .len = total_len * TEST_TIMES};
time_measure_start(&time_ctx);
for (int i = 0; i < TEST_TIMES; i ++) {
size_t len = total_len;
int offset = 0;
while (len) {
int len_read = MIN(seg_len, len);
esp_err_t err = spi_flash_read(part->address + offset, data_read + offset, len_read);
TEST_ESP_OK(err);
offset += len_read;
len -= len_read;
}
}
return time_measure_end(&time_ctx);
}
#define MEAS_WRITE(n) (measure_write("write in "#n"-byte chunks", part, data_to_write, n))
#define MEAS_READ(n) (measure_read("read in "#n"-byte chunks", part, data_read, n))
TEST_CASE("Test spi_flash read/write performance", "[spi_flash]")
{
const esp_partition_t *part = get_test_data_partition();
const int total_len = SPI_FLASH_SEC_SIZE;
uint8_t *data_to_write = heap_caps_malloc(total_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
uint8_t *data_read = heap_caps_malloc(total_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
srand(777);
for (int i = 0; i < total_len; i++) {
data_to_write[i] = rand();
}
uint32_t erase_1 = measure_erase(part);
uint32_t speed_WR_4B = MEAS_WRITE(4);
uint32_t speed_RD_4B = MEAS_READ(4);
uint32_t erase_2 = measure_erase(part);
uint32_t speed_WR_2KB = MEAS_WRITE(2048);
uint32_t speed_RD_2KB = MEAS_READ(2048);
TEST_ASSERT_EQUAL_HEX8_ARRAY(data_to_write, data_read, total_len);
// Not actually checking in this version
#define CHECK_DATA(suffix) ((void)speed_##suffix)
#define CHECK_ERASE(var) ((void)var)
CHECK_DATA(WR_4B);
CHECK_DATA(RD_4B);
CHECK_DATA(WR_2KB);
CHECK_DATA(RD_2KB);
// Erase time may vary a lot, can increase threshold if this fails with a reasonable speed
CHECK_ERASE(erase_1);
CHECK_ERASE(erase_2);
free(data_to_write);
free(data_read);
}
#if portNUM_PROCESSORS > 1
TEST_CASE("spi_flash deadlock with high priority busy-waiting task", "[spi_flash][esp_flash]")