mirror of
https://github.com/espressif/esp-idf.git
synced 2026-07-17 16:33:02 +03:00
Before: The cache won't be disabled when XIP on psram. But during flash erasing/programming, read data will be courrupt. When XIP in psram is enabled, the image is not mapped to the cache so usually there will be no flash access. The only way to read from flash is via the driver or use mmap. The driver has protection during erasing, while th mmap region not. Now: Mmap APIs provide a flag to make mmap->unmap region mutually exclusive to flash erase/programming when XIP from psram. SPI Flash write APIs will benefit from this. When the flag is used, no concurrent access to mapped region will happen while writing; otherwise the cache will be disable to avoid data corruption. Most ESP-IDF APIs calls mmap with this flag. As for users calling mmap-like APIs directly, they can choose whether to enable this by a flag. Closes https://github.com/espressif/esp-idf/issues/14897
944 lines
37 KiB
C
944 lines
37 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <limits.h>
|
|
#include <errno.h>
|
|
#include "sdkconfig.h"
|
|
#include "esp_partition.h"
|
|
#include "esp_flash_partitions.h"
|
|
#include "esp_private/partition_linux.h"
|
|
#include "esp_log.h"
|
|
#include "spi_flash_mmap.h"
|
|
#include <dlfcn.h>
|
|
|
|
ESP_LOG_ATTR_TAG(TAG, "linux_spiflash");
|
|
|
|
typedef int (*ftruncate_func_t)(int fd, off_t length);
|
|
static ftruncate_func_t __orig_ftruncate = NULL;
|
|
|
|
void __attribute__((constructor)) init_orig_funcs(void)
|
|
{
|
|
__orig_ftruncate = (ftruncate_func_t) dlsym(RTLD_NEXT, "ftruncate");
|
|
if (__orig_ftruncate == NULL) {
|
|
ESP_LOGE(TAG, "Failed to load original ftruncate function: %s", dlerror());
|
|
abort();
|
|
}
|
|
}
|
|
|
|
static void *s_spiflash_mem_file_buf = NULL;
|
|
static int s_spiflash_mem_file_fd = -1;
|
|
static const esp_partition_mmap_handle_t s_default_partition_mmap_handle = 0;
|
|
|
|
// input control structure, always contains what was specified by caller
|
|
static esp_partition_file_mmap_ctrl_t s_esp_partition_file_mmap_ctrl_input = {0};
|
|
// actual control structure, contains what is actually used by the esp_partition
|
|
static esp_partition_file_mmap_ctrl_t s_esp_partition_file_mmap_ctrl_act = {0};
|
|
|
|
#ifdef CONFIG_ESP_PARTITION_ENABLE_STATS
|
|
// variables holding stats and controlling power-off emulation
|
|
static size_t s_esp_partition_stat_read_ops = 0;
|
|
static size_t s_esp_partition_stat_write_ops = 0;
|
|
static size_t s_esp_partition_stat_read_bytes = 0;
|
|
static size_t s_esp_partition_stat_write_bytes = 0;
|
|
static size_t s_esp_partition_stat_erase_ops = 0;
|
|
static size_t s_esp_partition_stat_total_time = 0;
|
|
static size_t s_esp_partition_emulated_power_off_counter = SIZE_MAX;
|
|
static uint8_t s_esp_partition_emulated_power_off_mode = 0;
|
|
|
|
// tracking erase count individually for each emulated sector
|
|
static size_t *s_esp_partition_stat_sector_erase_count = NULL;
|
|
|
|
// forward declaration of hooks
|
|
static void esp_partition_hook_read(const void *srcAddr, const size_t size);
|
|
static bool esp_partition_hook_write(const void *dstAddr, size_t *size);
|
|
static bool esp_partition_hook_erase(const void *dstAddr, size_t *size);
|
|
|
|
// redirect hooks to functions
|
|
#define ESP_PARTITION_HOOK_READ(srcAddr, size) esp_partition_hook_read(srcAddr, size)
|
|
#define ESP_PARTITION_HOOK_WRITE(dstAddr, size) esp_partition_hook_write(dstAddr, size)
|
|
#define ESP_PARTITION_HOOK_ERASE(dstAddr, size) esp_partition_hook_erase(dstAddr, size)
|
|
#else
|
|
// redirect hooks to "do nothing code"
|
|
#define ESP_PARTITION_HOOK_READ(srcAddr, size)
|
|
#define ESP_PARTITION_HOOK_WRITE(dstAddr, size) true
|
|
#define ESP_PARTITION_HOOK_ERASE(dstAddr, size) true
|
|
#endif
|
|
|
|
const char *esp_partition_type_to_str(const uint32_t type)
|
|
{
|
|
switch (type) {
|
|
case PART_TYPE_APP: return "app";
|
|
case PART_TYPE_DATA: return "data";
|
|
case PART_TYPE_BOOTLOADER: return "bootloader";
|
|
case PART_TYPE_PARTITION_TABLE: return "partition_table";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
const char *esp_partition_subtype_to_str(const uint32_t type, const uint32_t subtype)
|
|
{
|
|
switch (type) {
|
|
case PART_TYPE_BOOTLOADER:
|
|
switch (subtype) {
|
|
case PART_SUBTYPE_BOOTLOADER_PRIMARY: return "primary";
|
|
case PART_SUBTYPE_BOOTLOADER_OTA: return "ota";
|
|
case PART_SUBTYPE_BOOTLOADER_RECOVERY: return "recovery";
|
|
default: return "unknown";
|
|
}
|
|
case PART_TYPE_PARTITION_TABLE:
|
|
switch (subtype) {
|
|
case PART_SUBTYPE_PARTITION_TABLE_PRIMARY: return "primary";
|
|
case PART_SUBTYPE_PARTITION_TABLE_OTA: return "ota";
|
|
default: return "unknown";
|
|
}
|
|
case PART_TYPE_APP:
|
|
switch (subtype) {
|
|
case PART_SUBTYPE_FACTORY: return "factory";
|
|
case PART_SUBTYPE_OTA_FLAG: return "ota_flag";
|
|
case PART_SUBTYPE_OTA_MASK: return "ota_mask";
|
|
case PART_SUBTYPE_TEST: return "test";
|
|
default: return "unknown";
|
|
}
|
|
case PART_TYPE_DATA:
|
|
switch (subtype) {
|
|
case PART_SUBTYPE_DATA_OTA: return "data_ota";
|
|
case PART_SUBTYPE_DATA_RF: return "data_rf";
|
|
case PART_SUBTYPE_DATA_WIFI: return "data_wifi";
|
|
case PART_SUBTYPE_DATA_NVS_KEYS: return "nvs_keys";
|
|
case PART_SUBTYPE_DATA_EFUSE_EM: return "efuse_em";
|
|
default: return "unknown";
|
|
}
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
// Calculate required emulated flash size from a partition table binary.
|
|
// Returns 0 on failure.
|
|
static size_t esp_partition_calc_required_flash_size_from_file(const char *partition_file_path)
|
|
{
|
|
if (partition_file_path == NULL || partition_file_path[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
FILE *fp = fopen(partition_file_path, "rb");
|
|
if (fp == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
// Determine file size as an additional lower bound
|
|
if (fseek(fp, 0L, SEEK_END) != 0) {
|
|
fclose(fp);
|
|
return 0;
|
|
}
|
|
long file_size = ftell(fp);
|
|
if (file_size < 0) {
|
|
fclose(fp);
|
|
return 0;
|
|
}
|
|
if (fseek(fp, 0L, SEEK_SET) != 0) {
|
|
fclose(fp);
|
|
return 0;
|
|
}
|
|
|
|
size_t max_end = 0;
|
|
size_t max_entries = file_size / sizeof(esp_partition_info_t);
|
|
for (size_t i = 0; i < max_entries; i++) {
|
|
esp_partition_info_t entry;
|
|
size_t r = fread(&entry, 1, sizeof(entry), fp);
|
|
if (r != sizeof(entry) || entry.magic != ESP_PARTITION_MAGIC) {
|
|
break;
|
|
}
|
|
uint32_t end = entry.pos.offset + entry.pos.size;
|
|
if (end > max_end) {
|
|
max_end = end;
|
|
}
|
|
}
|
|
|
|
fclose(fp);
|
|
|
|
// Also ensure the flash holds the partition table itself at its offset
|
|
size_t min_from_table_blob = (size_t)file_size + ESP_PARTITION_TABLE_OFFSET;
|
|
size_t required = (max_end > min_from_table_blob) ? max_end : min_from_table_blob;
|
|
|
|
// Round up to emulated sector size
|
|
size_t sector = ESP_PARTITION_EMULATED_SECTOR_SIZE;
|
|
size_t rem = required % sector;
|
|
if (rem != 0) {
|
|
required += (sector - rem);
|
|
}
|
|
|
|
return required;
|
|
}
|
|
|
|
// Load pre-built partition data binaries into the emulated flash.
|
|
// Reads a manifest file (BUILD_DIR "/linux_flash_data.txt") where each line
|
|
// has format: "<hex_offset> <absolute_path_to_binary>".
|
|
// Each binary is copied into s_spiflash_mem_file_buf at the given offset.
|
|
static void esp_partition_load_flash_data(void)
|
|
{
|
|
const char *manifest_path = BUILD_DIR "/linux_flash_data.txt";
|
|
FILE *manifest = fopen(manifest_path, "r");
|
|
if (manifest == NULL) {
|
|
// No manifest file — nothing to pre-load (this is normal for projects
|
|
// that don't use esp_partition_register_target())
|
|
return;
|
|
}
|
|
|
|
char line[PATH_MAX + 32];
|
|
while (fgets(line, sizeof(line), manifest) != NULL) {
|
|
// Skip empty lines
|
|
size_t len = strlen(line);
|
|
if (len == 0) {
|
|
continue;
|
|
}
|
|
// Trim trailing newline
|
|
if (line[len - 1] == '\n') {
|
|
line[len - 1] = '\0';
|
|
len--;
|
|
}
|
|
if (len == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Parse "<hex_offset> <path>"
|
|
char *space = strchr(line, ' ');
|
|
if (space == NULL) {
|
|
ESP_LOGW(TAG, "Malformed line in flash data manifest: %s", line);
|
|
continue;
|
|
}
|
|
*space = '\0';
|
|
const char *offset_str = line;
|
|
const char *file_path = space + 1;
|
|
|
|
unsigned long offset = strtoul(offset_str, NULL, 0);
|
|
if (offset == 0 && offset_str[0] != '0') {
|
|
ESP_LOGW(TAG, "Invalid offset in flash data manifest: %s", offset_str);
|
|
continue;
|
|
}
|
|
|
|
FILE *data_file = fopen(file_path, "rb");
|
|
if (data_file == NULL) {
|
|
ESP_LOGW(TAG, "Failed to open flash data file %s: %s", file_path, strerror(errno));
|
|
continue;
|
|
}
|
|
|
|
// Get file size
|
|
if (fseek(data_file, 0L, SEEK_END) != 0) {
|
|
ESP_LOGW(TAG, "Failed to seek in flash data file %s: %s", file_path, strerror(errno));
|
|
fclose(data_file);
|
|
continue;
|
|
}
|
|
long data_size = ftell(data_file);
|
|
if (data_size < 0) {
|
|
ESP_LOGW(TAG, "Failed to get size of flash data file %s: %s", file_path, strerror(errno));
|
|
fclose(data_file);
|
|
continue;
|
|
}
|
|
if (fseek(data_file, 0L, SEEK_SET) != 0) {
|
|
ESP_LOGW(TAG, "Failed to seek in flash data file %s: %s", file_path, strerror(errno));
|
|
fclose(data_file);
|
|
continue;
|
|
}
|
|
|
|
// Verify the data fits within the emulated flash
|
|
if (offset + (size_t)data_size > s_esp_partition_file_mmap_ctrl_act.flash_file_size) {
|
|
ESP_LOGW(TAG, "Flash data file %s (offset: 0x%lx, size: %ld) exceeds emulated flash size (%" PRIu32 " B). Skipping.",
|
|
file_path, offset, data_size, (uint32_t) s_esp_partition_file_mmap_ctrl_act.flash_file_size);
|
|
fclose(data_file);
|
|
continue;
|
|
}
|
|
|
|
// Copy the data into the emulated flash at the specified offset
|
|
uint8_t *dst = (uint8_t *)s_spiflash_mem_file_buf + offset;
|
|
size_t bytes_read = fread(dst, 1, (size_t)data_size, data_file);
|
|
fclose(data_file);
|
|
|
|
if (bytes_read != (size_t)data_size) {
|
|
ESP_LOGW(TAG, "Partial read of flash data file %s: expected %ld bytes, got %zu", file_path, data_size, bytes_read);
|
|
} else {
|
|
ESP_LOGV(TAG, "Loaded flash data: %s at offset 0x%lx (%ld bytes)", file_path, offset, data_size);
|
|
}
|
|
}
|
|
|
|
fclose(manifest);
|
|
}
|
|
|
|
esp_err_t esp_partition_file_mmap(const uint8_t **part_desc_addr_start)
|
|
{
|
|
// temporary file is used only if control structure doesn't specify file name.
|
|
bool open_existing_file = false;
|
|
|
|
if (strlen(s_esp_partition_file_mmap_ctrl_input.flash_file_name) > 0) {
|
|
// Open existing file. If size or partition table file were specified, raise errors
|
|
if (s_esp_partition_file_mmap_ctrl_input.flash_file_size > 0) {
|
|
ESP_LOGE(TAG, "Flash emulation file size: %" PRIu32" was specified while together with the file name: %s (illegal). Use file size = 0",
|
|
(uint32_t) s_esp_partition_file_mmap_ctrl_input.flash_file_size,
|
|
s_esp_partition_file_mmap_ctrl_input.flash_file_name);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (strlen(s_esp_partition_file_mmap_ctrl_input.partition_file_name) > 0) {
|
|
ESP_LOGE(TAG, "Partition file name: %s was specified together with the flash emulation file name: %s (illegal). Use empty partition file name",
|
|
s_esp_partition_file_mmap_ctrl_input.partition_file_name,
|
|
s_esp_partition_file_mmap_ctrl_input.flash_file_name);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// copy flash file name to actual control struct
|
|
strlcpy(s_esp_partition_file_mmap_ctrl_act.flash_file_name, s_esp_partition_file_mmap_ctrl_input.flash_file_name, sizeof(s_esp_partition_file_mmap_ctrl_act.flash_file_name));
|
|
|
|
open_existing_file = true;
|
|
} else {
|
|
// name of temporary file and its size is available in s_esp_partition_file_mmap_ctrl.flash_file_name and s_esp_partition_file_mmap_ctrl_input.flash_file_size respectively
|
|
bool has_partfile = (strlen(s_esp_partition_file_mmap_ctrl_input.partition_file_name) > 0);
|
|
bool has_len = (s_esp_partition_file_mmap_ctrl_input.flash_file_size > 0);
|
|
|
|
// check if partition file is present, if not, use default
|
|
if (!has_partfile) {
|
|
strlcpy(s_esp_partition_file_mmap_ctrl_act.partition_file_name, BUILD_DIR "/partition_table/partition-table.bin", sizeof(s_esp_partition_file_mmap_ctrl_act.partition_file_name));
|
|
} else {
|
|
strlcpy(s_esp_partition_file_mmap_ctrl_act.partition_file_name, s_esp_partition_file_mmap_ctrl_input.partition_file_name, sizeof(s_esp_partition_file_mmap_ctrl_act.partition_file_name));
|
|
}
|
|
|
|
// derive the partition size from the s_esp_partition_file_mmap_ctrl_act.partition_file_name
|
|
size_t derived_size = esp_partition_calc_required_flash_size_from_file(s_esp_partition_file_mmap_ctrl_act.partition_file_name);
|
|
// if derived size is zero, use default partition size
|
|
s_esp_partition_file_mmap_ctrl_act.flash_file_size = (derived_size > 0) ? derived_size : ESP_PARTITION_DEFAULT_EMULATED_FLASH_SIZE;
|
|
|
|
// if the size of the temporary file is specified, check if the given partition size fits within it
|
|
if (has_len && s_esp_partition_file_mmap_ctrl_input.flash_file_size > derived_size) {
|
|
s_esp_partition_file_mmap_ctrl_act.flash_file_size = s_esp_partition_file_mmap_ctrl_input.flash_file_size;
|
|
}
|
|
|
|
// specify pattern file name for temporary flash file
|
|
strlcpy(s_esp_partition_file_mmap_ctrl_act.flash_file_name, "/tmp/idf-partition-XXXXXX", sizeof(s_esp_partition_file_mmap_ctrl_act.flash_file_name));
|
|
}
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
if (open_existing_file) {
|
|
|
|
s_spiflash_mem_file_fd = open(s_esp_partition_file_mmap_ctrl_act.flash_file_name, O_RDWR);
|
|
|
|
if (s_spiflash_mem_file_fd == -1) {
|
|
ESP_LOGE(TAG, "Failed to open SPI FLASH emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
do {
|
|
// seek to the end
|
|
off_t size = lseek(s_spiflash_mem_file_fd, 0L, SEEK_END);
|
|
if (size < 0) {
|
|
ESP_LOGE(TAG, "Failed to seek in SPI FLASH emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
ret = ESP_ERR_NOT_FINISHED;
|
|
break;
|
|
}
|
|
|
|
s_esp_partition_file_mmap_ctrl_act.flash_file_size = size;
|
|
|
|
// seek to beginning
|
|
size = lseek(s_spiflash_mem_file_fd, 0L, SEEK_SET);
|
|
if (size < 0) {
|
|
ESP_LOGE(TAG, "Failed to seek in SPI FLASH emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
ret = ESP_ERR_NOT_FINISHED;
|
|
break;
|
|
}
|
|
|
|
//create memory-mapping for the flash holder file
|
|
if ((s_spiflash_mem_file_buf = mmap(NULL, s_esp_partition_file_mmap_ctrl_act.flash_file_size, PROT_READ | PROT_WRITE, MAP_SHARED, s_spiflash_mem_file_fd, 0)) == MAP_FAILED) {
|
|
ESP_LOGE(TAG, "Failed to mmap() SPI FLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
ret = ESP_ERR_NOT_FINISHED;
|
|
break;
|
|
}
|
|
} while (false);
|
|
} else {
|
|
//create temporary file to hold complete SPIFLASH size
|
|
s_spiflash_mem_file_fd = mkstemp(s_esp_partition_file_mmap_ctrl_act.flash_file_name);
|
|
|
|
if (s_spiflash_mem_file_fd == -1) {
|
|
ESP_LOGE(TAG, "Failed to create SPI FLASH emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
return ESP_ERR_NOT_FINISHED;
|
|
}
|
|
|
|
do {
|
|
// resize file
|
|
if (__orig_ftruncate && __orig_ftruncate(s_spiflash_mem_file_fd, s_esp_partition_file_mmap_ctrl_act.flash_file_size) != 0) {
|
|
ESP_LOGE(TAG, "Failed to set size of SPI FLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
ret = ESP_ERR_INVALID_SIZE;
|
|
break;
|
|
}
|
|
|
|
ESP_LOGV(TAG, "SPIFLASH memory emulation file created: %s (size: %" PRIu32 " B)", s_esp_partition_file_mmap_ctrl_act.flash_file_name, (uint32_t) s_esp_partition_file_mmap_ctrl_act.flash_file_size);
|
|
|
|
// create memory-mapping for the flash holder file
|
|
if ((s_spiflash_mem_file_buf = mmap(NULL, s_esp_partition_file_mmap_ctrl_act.flash_file_size, PROT_READ | PROT_WRITE, MAP_SHARED, s_spiflash_mem_file_fd, 0)) == MAP_FAILED) {
|
|
ESP_LOGE(TAG, "Failed to mmap() SPI FLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
ret = ESP_ERR_NO_MEM;
|
|
break;
|
|
}
|
|
|
|
// initialize whole range with bit-1 (NOR FLASH default)
|
|
memset(s_spiflash_mem_file_buf, 0xFF, s_esp_partition_file_mmap_ctrl_act.flash_file_size);
|
|
|
|
// upload partition table to the mmap file at real offset as in SPIFLASH
|
|
FILE *f_partition_table = fopen(s_esp_partition_file_mmap_ctrl_act.partition_file_name, "r+");
|
|
if (f_partition_table == NULL) {
|
|
ESP_LOGE(TAG, "Failed to open partition table file %s: %s", s_esp_partition_file_mmap_ctrl_act.partition_file_name, strerror(errno));
|
|
ret = ESP_ERR_NOT_FOUND;
|
|
break;
|
|
}
|
|
|
|
if (fseek(f_partition_table, 0L, SEEK_END) != 0) {
|
|
ESP_LOGE(TAG, "Failed to seek in partition table file %s: %s", s_esp_partition_file_mmap_ctrl_act.partition_file_name, strerror(errno));
|
|
ret = ESP_ERR_INVALID_SIZE;
|
|
fclose(f_partition_table);
|
|
break;
|
|
}
|
|
|
|
int partition_table_file_size = ftell(f_partition_table);
|
|
ESP_LOGV(TAG, "Using partition table file %s (size: %d B):", s_esp_partition_file_mmap_ctrl_act.partition_file_name, partition_table_file_size);
|
|
|
|
// check whether partition table fits into the memory mapped file
|
|
if (partition_table_file_size + ESP_PARTITION_TABLE_OFFSET > s_esp_partition_file_mmap_ctrl_act.flash_file_size) {
|
|
ESP_LOGE(TAG, "Flash file: %s (size: %" PRIu32 " B) cannot hold partition table requiring %d B",
|
|
s_esp_partition_file_mmap_ctrl_act.flash_file_name,
|
|
(uint32_t) s_esp_partition_file_mmap_ctrl_act.flash_file_size,
|
|
(int) (partition_table_file_size + ESP_PARTITION_TABLE_OFFSET));
|
|
ret = ESP_ERR_INVALID_SIZE;
|
|
fclose(f_partition_table);
|
|
break;
|
|
}
|
|
|
|
//copy partition table from the file to emulated SPIFLASH memory space
|
|
if (fseek(f_partition_table, 0L, SEEK_SET) != 0) {
|
|
ESP_LOGE(TAG, "Failed to seek in partition table file %s: %s", s_esp_partition_file_mmap_ctrl_act.partition_file_name, strerror(errno));
|
|
ret = ESP_ERR_INVALID_SIZE;
|
|
fclose(f_partition_table);
|
|
break;
|
|
}
|
|
|
|
uint8_t *part_table_in_spiflash = s_spiflash_mem_file_buf + ESP_PARTITION_TABLE_OFFSET;
|
|
|
|
size_t res = fread(part_table_in_spiflash, 1, partition_table_file_size, f_partition_table);
|
|
fclose(f_partition_table);
|
|
if (res != partition_table_file_size) {
|
|
ESP_LOGE(TAG, "Failed to read partition table file %s", s_esp_partition_file_mmap_ctrl_act.partition_file_name);
|
|
ret = ESP_ERR_INVALID_STATE;
|
|
break;
|
|
}
|
|
|
|
// Load any pre-built partition data binaries into the emulated flash.
|
|
// The manifest file is generated at build time by esp_partition_register_target().
|
|
esp_partition_load_flash_data();
|
|
} while (false);
|
|
}
|
|
|
|
if (ret != ESP_OK) {
|
|
if (close(s_spiflash_mem_file_fd)) {
|
|
ESP_LOGE(TAG, "Failed to close() SPIFLASH memory emulation file: %s", strerror(errno));
|
|
}
|
|
s_spiflash_mem_file_fd = -1;
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_LOG_DEFAULT_LEVEL_VERBOSE
|
|
uint8_t *part_ptr = s_spiflash_mem_file_buf + ESP_PARTITION_TABLE_OFFSET;
|
|
|
|
ESP_LOGV(TAG, "");
|
|
ESP_LOGV(TAG, "Partition table successfully imported, partitions found:");
|
|
|
|
while (true) {
|
|
esp_partition_info_t *p_part_item = (esp_partition_info_t *)part_ptr;
|
|
if (p_part_item->magic != ESP_PARTITION_MAGIC ) {
|
|
break;
|
|
}
|
|
|
|
ESP_LOGV(TAG, " --------------");
|
|
ESP_LOGV(TAG, " label: %s", p_part_item->label);
|
|
ESP_LOGV(TAG, " type: %s", esp_partition_type_to_str(p_part_item->type));
|
|
ESP_LOGV(TAG, " subtype: %s", esp_partition_subtype_to_str(p_part_item->type, p_part_item->subtype));
|
|
ESP_LOGV(TAG, " offset: 0x%08" PRIX32, (uint32_t) p_part_item->pos.offset);
|
|
ESP_LOGV(TAG, " size: %" PRIu32, (uint32_t) p_part_item->pos.size);
|
|
ESP_LOGV(TAG, " flags: %" PRIu32, (uint32_t) p_part_item->flags);
|
|
|
|
part_ptr += sizeof(esp_partition_info_t);
|
|
}
|
|
|
|
ESP_LOGV(TAG, "");
|
|
#endif
|
|
|
|
#ifdef CONFIG_ESP_PARTITION_ENABLE_STATS
|
|
free(s_esp_partition_stat_sector_erase_count);
|
|
s_esp_partition_stat_sector_erase_count = malloc(sizeof(size_t) * s_esp_partition_file_mmap_ctrl_act.flash_file_size / ESP_PARTITION_EMULATED_SECTOR_SIZE);
|
|
#endif
|
|
|
|
//return mmapped file starting address
|
|
*part_desc_addr_start = s_spiflash_mem_file_buf;
|
|
|
|
// clear input control structure
|
|
memset(&s_esp_partition_file_mmap_ctrl_input, 0, sizeof(s_esp_partition_file_mmap_ctrl_input));
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t esp_partition_file_munmap(void)
|
|
{
|
|
if (s_spiflash_mem_file_buf == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
if (s_esp_partition_file_mmap_ctrl_act.flash_file_size == 0) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
if (s_spiflash_mem_file_fd == -1) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
esp_partition_unload_all();
|
|
|
|
#ifdef CONFIG_ESP_PARTITION_ENABLE_STATS
|
|
free(s_esp_partition_stat_sector_erase_count);
|
|
s_esp_partition_stat_sector_erase_count = NULL;
|
|
#endif
|
|
|
|
// unmap the flash emulation memory file
|
|
if (munmap(s_spiflash_mem_file_buf, s_esp_partition_file_mmap_ctrl_act.flash_file_size) != 0) {
|
|
ESP_LOGE(TAG, "Failed to munmap() SPIFLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
|
|
// close memory mapped file
|
|
if (close(s_spiflash_mem_file_fd)) {
|
|
ESP_LOGE(TAG, "Failed to close() SPIFLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
|
|
if (s_esp_partition_file_mmap_ctrl_input.remove_dump) {
|
|
// delete spi flash file
|
|
if (remove(s_esp_partition_file_mmap_ctrl_act.flash_file_name) != 0) {
|
|
ESP_LOGE(TAG, "Failed to remove() SPI FLASH memory emulation file %s: %s", s_esp_partition_file_mmap_ctrl_act.flash_file_name, strerror(errno));
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
}
|
|
|
|
// cleanup
|
|
memset(&s_esp_partition_file_mmap_ctrl_act, 0, sizeof(s_esp_partition_file_mmap_ctrl_act));
|
|
s_spiflash_mem_file_buf = NULL;
|
|
s_spiflash_mem_file_fd = -1;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t esp_partition_write(const esp_partition_t *partition, size_t dst_offset, const void *src, size_t size)
|
|
{
|
|
assert(partition != NULL && s_spiflash_mem_file_buf != NULL && src != NULL);
|
|
|
|
if (partition->readonly) {
|
|
return ESP_ERR_NOT_ALLOWED;
|
|
}
|
|
if (partition->encrypted) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (dst_offset > partition->size) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (dst_offset + size > partition->size) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
// Ensure write stays within mapped flash file size
|
|
if (s_esp_partition_file_mmap_ctrl_act.flash_file_size > 0) {
|
|
size_t start = (size_t)partition->address + dst_offset;
|
|
size_t max_len = s_esp_partition_file_mmap_ctrl_act.flash_file_size;
|
|
if ((start > max_len) || ((size + start) > max_len)) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
}
|
|
|
|
void *dst_addr = s_spiflash_mem_file_buf + partition->address + dst_offset;
|
|
ESP_LOGV(TAG, "esp_partition_write(): partition=%s dst_offset=%" PRIu32 " src=%p size=%" PRIu32 " (real dst address: %p)", partition->label, (uint32_t) dst_offset, src, (uint32_t) size, dst_addr);
|
|
|
|
// local size, can be modified by the write hook in case of simulated power-off
|
|
size_t new_size = size;
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
// hook gathers statistics and can emulate power-off
|
|
// in case of power - off it decreases new_size to the number of bytes written
|
|
// before power event occurred
|
|
if (!ESP_PARTITION_HOOK_WRITE(dst_addr, &new_size)) {
|
|
ret = ESP_ERR_FLASH_OP_FAIL;
|
|
}
|
|
|
|
for (size_t x = 0; x < new_size; x++) {
|
|
|
|
#ifdef CONFIG_ESP_PARTITION_ERASE_CHECK
|
|
// Check if address to be written was erased first
|
|
if((~((uint8_t *)dst_addr)[x] & ((uint8_t *)src)[x]) != 0) {
|
|
ESP_LOGW(TAG, "invalid flash operation detected");
|
|
ret = ESP_ERR_FLASH_OP_FAIL;
|
|
break;
|
|
}
|
|
#endif // CONFIG_ESP_PARTITION_ERASE_CHECK
|
|
|
|
// AND with destination byte (to emulate real NOR FLASH behavior)
|
|
((uint8_t *)dst_addr)[x] &= ((uint8_t *)src)[x];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
esp_err_t esp_partition_read(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size)
|
|
{
|
|
assert(partition != NULL && s_spiflash_mem_file_buf != NULL && dst != NULL);
|
|
|
|
if (partition->encrypted) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
if (src_offset > partition->size) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (src_offset + size > partition->size) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
// Ensure read stays within mapped flash file size
|
|
if (s_esp_partition_file_mmap_ctrl_act.flash_file_size > 0) {
|
|
size_t start = (size_t)partition->address + src_offset;
|
|
size_t max_len = s_esp_partition_file_mmap_ctrl_act.flash_file_size;
|
|
if ((start > max_len) || ((size + start) > max_len)) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
}
|
|
|
|
void *src_addr = s_spiflash_mem_file_buf + partition->address + src_offset;
|
|
ESP_LOGV(TAG, "esp_partition_read(): partition=%s src_offset=%" PRIu32 " dst=%p size=%" PRIu32 " (real src address: %p)", partition->label, (uint32_t) src_offset, dst, (uint32_t) size, src_addr);
|
|
|
|
memcpy(dst, src_addr, size);
|
|
|
|
ESP_PARTITION_HOOK_READ(src_addr, size); // statistics
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t esp_partition_read_raw(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size)
|
|
{
|
|
ESP_LOGV(TAG, "esp_partition_read_raw(): calling esp_partition_read()");
|
|
return esp_partition_read(partition, src_offset, dst, size);
|
|
}
|
|
|
|
esp_err_t esp_partition_write_raw(const esp_partition_t *partition, size_t dst_offset, const void *src, size_t size)
|
|
{
|
|
ESP_LOGV(TAG, "esp_partition_write_raw(): calling esp_partition_write()");
|
|
return esp_partition_write(partition, dst_offset, src, size);
|
|
}
|
|
|
|
esp_err_t esp_partition_erase_range(const esp_partition_t *partition, size_t offset, size_t size)
|
|
{
|
|
assert(partition != NULL && s_spiflash_mem_file_buf != NULL);
|
|
|
|
if (partition->readonly) {
|
|
return ESP_ERR_NOT_ALLOWED;
|
|
}
|
|
if (offset > partition->size || offset % partition->erase_size != 0) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (offset + size > partition->size || size % partition->erase_size != 0) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
// Ensure erase stays within mapped flash file size
|
|
if (s_esp_partition_file_mmap_ctrl_act.flash_file_size > 0) {
|
|
size_t start = (size_t)partition->address + offset;
|
|
size_t max_len = s_esp_partition_file_mmap_ctrl_act.flash_file_size;
|
|
if ((start > max_len) || ((size + start) > max_len)) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
}
|
|
|
|
void *target_addr = s_spiflash_mem_file_buf + partition->address + offset;
|
|
ESP_LOGV(TAG, "esp_partition_erase_range(): partition=%s offset=%" PRIu32 " size=%" PRIu32 " (real target address: %p)", partition->label, (uint32_t) offset, (uint32_t) size, target_addr);
|
|
|
|
// local size to be potentially updated by the hook in case of power-off event
|
|
size_t new_size = size;
|
|
|
|
// hook gathers statistics and can emulate power-off
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
if(!ESP_PARTITION_HOOK_ERASE(target_addr, &new_size)) {
|
|
ret = ESP_ERR_FLASH_OP_FAIL;
|
|
}
|
|
|
|
//set all bits to 1 (NOR FLASH default)
|
|
memset(target_addr, 0xFF, new_size);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Exposes direct pointer to the memory mapped file created by esp_partition_file_mmap
|
|
* No address alignment is performed
|
|
* Default handle is always returned
|
|
* Returns:
|
|
* ESP_ERR_INVALID_ARG - offset exceeds size of partition
|
|
* ESP_ERR_INVALID_SIZE - address range defined by offset + size is beyond the size of partition
|
|
* ESP_ERR_NOT_SUPPORTED - flash_chip of partition is not NULL
|
|
* ESP_OK - calculated out parameters hold pointer to the requested memory area and default handle respectively
|
|
*/
|
|
esp_err_t esp_partition_mmap(const esp_partition_t *partition, size_t offset, size_t size,
|
|
esp_partition_mmap_flag_t flags,
|
|
const void **out_ptr, esp_partition_mmap_handle_t *out_handle)
|
|
{
|
|
ESP_LOGV(TAG, "esp_partition_mmap(): partition=%s offset=%" PRIu32 " size=%" PRIu32 "", partition->label, (uint32_t) offset, (uint32_t) size);
|
|
|
|
assert(partition != NULL);
|
|
if (offset > partition->size) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (offset + size > partition->size) {
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
if (partition->flash_chip != NULL) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
// required starting address in flash aka offset from the flash beginning
|
|
size_t req_flash_addr = (size_t)(partition->address) + offset;
|
|
|
|
esp_err_t rc = ESP_OK;
|
|
|
|
// check if memory mapped file is already present, if not, map it now
|
|
if (s_spiflash_mem_file_buf == NULL) {
|
|
uint8_t *part_desc_addr_start = NULL;
|
|
rc = esp_partition_file_mmap((const uint8_t **) &part_desc_addr_start);
|
|
}
|
|
|
|
// adjust memory mapped pointer to the required offset
|
|
if (rc == ESP_OK) {
|
|
*out_ptr = (void *) (s_spiflash_mem_file_buf + req_flash_addr);
|
|
*out_handle = s_default_partition_mmap_handle;
|
|
} else {
|
|
*out_ptr = NULL;
|
|
*out_handle = 0;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
// Intentionally does nothing.
|
|
void esp_partition_munmap(esp_partition_mmap_handle_t handle __attribute__((unused)))
|
|
{
|
|
}
|
|
|
|
esp_partition_file_mmap_ctrl_t *esp_partition_get_file_mmap_ctrl_input(void)
|
|
{
|
|
return &s_esp_partition_file_mmap_ctrl_input;
|
|
}
|
|
|
|
esp_partition_file_mmap_ctrl_t *esp_partition_get_file_mmap_ctrl_act(void)
|
|
{
|
|
return &s_esp_partition_file_mmap_ctrl_act;
|
|
}
|
|
|
|
uint32_t esp_partition_get_main_flash_sector_size(void)
|
|
{
|
|
return ESP_PARTITION_EMULATED_SECTOR_SIZE;
|
|
}
|
|
|
|
#ifdef CONFIG_ESP_PARTITION_ENABLE_STATS
|
|
// timing data for ESP8266, 160MHz CPU frequency, 80MHz flash frequency
|
|
// all values in microseconds
|
|
// values are for block sizes starting at 4 bytes and going up to 4096 bytes
|
|
static size_t s_esp_partition_stat_read_times[] = {7, 5, 6, 7, 11, 18, 32, 60, 118, 231, 459};
|
|
static size_t s_esp_partition_stat_write_times[] = {19, 23, 35, 57, 106, 205, 417, 814, 1622, 3200, 6367};
|
|
static size_t s_esp_partition_stat_block_erase_time = 37142;
|
|
|
|
static size_t esp_partition_stat_time_interpolate(uint32_t bytes, size_t *lut)
|
|
{
|
|
const int lut_size = sizeof(s_esp_partition_stat_read_times) / sizeof(s_esp_partition_stat_read_times[0]);
|
|
int lz = __builtin_clz(bytes / 4);
|
|
int log_size = 32 - lz;
|
|
size_t x2 = 1 << (log_size + 2);
|
|
size_t upper_index = (log_size < lut_size - 1) ? log_size : lut_size - 1;
|
|
size_t y2 = lut[upper_index];
|
|
size_t x1 = 1 << (log_size + 1);
|
|
size_t y1 = lut[log_size - 1];
|
|
return (bytes - x1) * (y2 - y1) / (x2 - x1) + y1;
|
|
}
|
|
|
|
// Registers read access statistics of emulated SPI FLASH device (Linux host)
|
|
// Function increases nmuber of read operations, accumulates number of read bytes
|
|
// and accumulates emulated read operation time (size dependent)
|
|
static void esp_partition_hook_read(const void *srcAddr, const size_t size)
|
|
{
|
|
ESP_LOGV(TAG, "esp_partition_hook_read()");
|
|
|
|
// stats
|
|
++s_esp_partition_stat_read_ops;
|
|
s_esp_partition_stat_read_bytes += size;
|
|
s_esp_partition_stat_total_time += esp_partition_stat_time_interpolate((uint32_t) size, s_esp_partition_stat_read_times);
|
|
}
|
|
|
|
// Registers write access statistics of emulated SPI FLASH device (Linux host)
|
|
// If enabled by the esp_partition_fail_after, function emulates power-off event during write operations by
|
|
// decrementing the s_esp_partition_emulated_power_off_counter for each 4 bytes written
|
|
// If zero threshold is reached, false is returned. In this case the size parameter contains number of successfully written bytes
|
|
// Else the function increases nmuber of write operations, accumulates number
|
|
// of bytes written and accumulates emulated write operation time (size dependent) and returns true.
|
|
static bool esp_partition_hook_write(const void *dstAddr, size_t *size)
|
|
{
|
|
ESP_LOGV(TAG, "%s", __FUNCTION__);
|
|
|
|
bool ret_val = true;
|
|
|
|
// one power down cycle per 4 bytes written
|
|
size_t write_cycles = *size / 4;
|
|
|
|
// check whether power off simulation is active for write
|
|
if (s_esp_partition_emulated_power_off_counter != SIZE_MAX &&
|
|
ESP_PARTITION_FAIL_AFTER_MODE_WRITE) {
|
|
|
|
// check if power down happens during this call
|
|
if (s_esp_partition_emulated_power_off_counter > write_cycles) {
|
|
// OK
|
|
s_esp_partition_emulated_power_off_counter -= write_cycles;
|
|
} else {
|
|
// failure in this call
|
|
|
|
// update number of bytes written to the in/out parameter
|
|
*size = s_esp_partition_emulated_power_off_counter * 4;
|
|
|
|
// disable power on cycles for further calls
|
|
s_esp_partition_emulated_power_off_counter = SIZE_MAX;
|
|
// final result value will be false
|
|
ret_val = false;
|
|
}
|
|
}
|
|
|
|
if(ret_val) {
|
|
// stats
|
|
++s_esp_partition_stat_write_ops;
|
|
s_esp_partition_stat_write_bytes += write_cycles * 4;
|
|
s_esp_partition_stat_total_time += esp_partition_stat_time_interpolate((uint32_t) (*size), s_esp_partition_stat_write_times);
|
|
}
|
|
|
|
return ret_val;
|
|
}
|
|
|
|
// Registers erase access statistics of emulated SPI FLASH device (Linux host)
|
|
// If enabled by 'esp_partition_fail_after' parameter, the function emulates a power-off event during erase
|
|
// operation by decrementing the s_esp_partition_emulated_power_off_counterpower for each erased virtual sector.
|
|
// If zero threshold is reached, false is returned. In out parameter size is updated with number of bytes erased until power-off
|
|
// Else, for statistics purpose, the impacted virtual sectors are identified based on
|
|
// ESP_PARTITION_EMULATED_SECTOR_SIZE and their respective counts of erase operations are incremented
|
|
// Total number of erase operations is increased by the number of impacted virtual sectors
|
|
static bool esp_partition_hook_erase(const void *dstAddr, size_t *size)
|
|
{
|
|
ESP_LOGV(TAG, "%s", __FUNCTION__);
|
|
|
|
if (*size == 0) {
|
|
return true;
|
|
}
|
|
|
|
// cycle over virtual sectors
|
|
ptrdiff_t offset = dstAddr - s_spiflash_mem_file_buf;
|
|
size_t first_sector_idx = offset / ESP_PARTITION_EMULATED_SECTOR_SIZE;
|
|
size_t last_sector_idx = (offset + *size - 1) / ESP_PARTITION_EMULATED_SECTOR_SIZE;
|
|
size_t sector_count = 1 + last_sector_idx - first_sector_idx;
|
|
|
|
bool ret_val = true;
|
|
|
|
// check whether power off simulation is active for erase
|
|
if (s_esp_partition_emulated_power_off_counter != SIZE_MAX &&
|
|
ESP_PARTITION_FAIL_AFTER_MODE_ERASE) {
|
|
|
|
// check if power down happens during this call
|
|
if (s_esp_partition_emulated_power_off_counter > sector_count) {
|
|
// OK
|
|
s_esp_partition_emulated_power_off_counter -= sector_count;
|
|
} else {
|
|
// failure in this call - reduce sector_count to the number of remaining power on cycles
|
|
sector_count = s_esp_partition_emulated_power_off_counter;
|
|
// disable power on cycles for further calls
|
|
s_esp_partition_emulated_power_off_counter = SIZE_MAX;
|
|
// update number of bytes to be really erased before power-off event
|
|
*size = sector_count * ESP_PARTITION_EMULATED_SECTOR_SIZE;
|
|
// final result value will be false
|
|
ret_val = false;
|
|
}
|
|
}
|
|
|
|
// update statistics for all sectors until power down cycle
|
|
for (size_t sector_index = first_sector_idx; sector_index < first_sector_idx + sector_count; sector_index++) {
|
|
++s_esp_partition_stat_erase_ops;
|
|
s_esp_partition_stat_sector_erase_count[sector_index]++;
|
|
s_esp_partition_stat_total_time += s_esp_partition_stat_block_erase_time;
|
|
}
|
|
|
|
return ret_val;
|
|
}
|
|
|
|
void esp_partition_clear_stats(void)
|
|
{
|
|
s_esp_partition_stat_read_bytes = 0;
|
|
s_esp_partition_stat_write_bytes = 0;
|
|
s_esp_partition_stat_erase_ops = 0;
|
|
s_esp_partition_stat_read_ops = 0;
|
|
s_esp_partition_stat_write_ops = 0;
|
|
s_esp_partition_stat_total_time = 0;
|
|
|
|
memset(s_esp_partition_stat_sector_erase_count, 0, sizeof(size_t) * s_esp_partition_file_mmap_ctrl_act.flash_file_size / ESP_PARTITION_EMULATED_SECTOR_SIZE);
|
|
}
|
|
|
|
size_t esp_partition_get_read_ops(void)
|
|
{
|
|
return s_esp_partition_stat_read_ops;
|
|
}
|
|
|
|
size_t esp_partition_get_write_ops(void)
|
|
{
|
|
return s_esp_partition_stat_write_ops;
|
|
}
|
|
|
|
size_t esp_partition_get_erase_ops(void)
|
|
{
|
|
return s_esp_partition_stat_erase_ops;
|
|
}
|
|
|
|
size_t esp_partition_get_read_bytes(void)
|
|
{
|
|
return s_esp_partition_stat_read_bytes;
|
|
}
|
|
|
|
size_t esp_partition_get_write_bytes(void)
|
|
{
|
|
return s_esp_partition_stat_write_bytes;
|
|
}
|
|
|
|
size_t esp_partition_get_total_time(void)
|
|
{
|
|
return s_esp_partition_stat_total_time;
|
|
}
|
|
|
|
void esp_partition_fail_after(size_t count, uint8_t mode)
|
|
{
|
|
s_esp_partition_emulated_power_off_counter = count;
|
|
s_esp_partition_emulated_power_off_mode = mode;
|
|
}
|
|
|
|
size_t esp_partition_get_sector_erase_count(size_t sector)
|
|
{
|
|
return s_esp_partition_stat_sector_erase_count[sector];
|
|
}
|
|
#endif
|