Description
Answers checklist.
- I have read the documentation ESP-IDF Programming Guide and the issue is not addressed there.
- I have updated my IDF branch (master or release) to the latest version and checked that the issue is present there.
- I have searched the issue tracker for a similar issue and not found a similar issue.
General issue report
While working on implementing XiP from PSRAM for the rust esp-hal project I reverse engineered the Cache_Flash_To_SPIRAM_Copy
from the ESP32S3 ROM (revision 0). I have reproduced my cleaned up version of the function (reverse engineered with the help of Ghidra) below:
#define SPARE_PAGE 0x1ff
#define SPARE_PAGE_ADDR (volatile void *)(0x3dff0000)
uint32_t Cache_Flash_To_SPIRAM_Copy(
uint32_t bus, uint32_t bus_addr, uint32_t start_page, uint32_t *page0_page
)
{
volatile uint32_t *mmu_table = (volatile uint32_t *)DR_REG_MMU_TABLE;
uint32_t saved_mapping = mmu_table[SPARE_PAGE];
uint32_t start, end;
if (bus == CACHE_IBUS) {
start = CACHE_IROM_MMU_START;
end = CACHE_IROM_MMU_END;
} else {
start = CACHE_DROM_MMU_START;
end = CACHE_DROM_MMU_END;
}
Cache_WriteBack_All();
for (uint32_t i = start >> 2; i < end >> 2; i++) {
uint32_t mapping = mmu_table[i];
if ((mapping & (SOC_MMU_INVALID | SOC_MMU_TYPE)) == (SOC_MMU_VALID | SOC_MMU_ACCESS_FLASH)) {
bool is_page0 = false;
if ((mapping & SOC_MMU_VALID_VAL_MASK) == 0) {
if (*page0_page != INVALID_PHY_PAGE) {
mmu_table[i] = *page0_page |
SOC_MMU_ACCESS_SPIRAM;
continue;
}
is_page0 = true;
}
mmu_table[SPARE_PAGE] = start_page | SOC_MMU_ACCESS_SPIRAM;
Cache_Invalidate_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
memcpy(SPARE_PAGE_ADDR,
(void *)(i * MMU_PAGE_SIZE + bus_addr),
MMU_PAGE_SIZE);
Cache_WriteBack_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
mmu_table[i] = start_page | SOC_MMU_ACCESS_SPIRAM;
// BUG: The ordering here is wrong, the ROM increments start_page
// before assigning it to *page0_page, so every page0 mapping after
// the first actually ends up pointing at the wrong place which will
// end up containing non-page0 content (or, if all the page0
// mappings happen to be the last ones in the MMU table, it will
// point into an uninitialised page)
start_page += 1;
if (is_page0) *page0_page = start_page;
}
};
mmu_table[SPARE_PAGE] = saved_mapping;
Cache_Invalidate_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
return start_page;
}
As can be seen from the comment and code above, the start_page
variable is incremented before the page0_page
in-out parameter is updated. This means that the start_page
variable will be referring to the PSRAM page immediately after the location of the page which contains flash page 0 data.
This means that only the first page which is mapped to page 0 in flash will be handled correctly and all subsequent pages will end up pointing at a copy of a differnet flash page or at an uninitialised PSRAM page.
In cases where the page 0 mapping points at an uninitialised PSRAM page, the unused page may even be allocated for other purposes.
I believe a fix for this may be to re-implement the functionality of this function entirely in the IDF or to ensure that this function is never called if there is more than 1 flash page 0 mapping.
I confirmed this bug in a bare-metal environment by mapping flash page 0 multiple times and calling this function.