From f4ada9eab75e70f1c5dd584fedd9dc1c13869fa7 Mon Sep 17 00:00:00 2001 From: Konstantin Sharlaimov Date: Sat, 25 Jul 2026 20:24:47 +0200 Subject: [PATCH 1/2] feat(esp32s3): add PSRAM support for QSPI and Octal chips. Add runtime initialization, MMU layout, linker script mappings, and targets for ESP32-S3 Octal PSRAM. --- GNUmakefile | 2 + src/device/esp/esp32s3.S | 49 ++- src/examples/psram/main.go | 60 +++ src/runtime/runtime_esp32_nopsram.go | 6 + src/runtime/runtime_esp32_psram.go | 100 +++++ src/runtime/runtime_esp32_psram_octal.go | 472 +++++++++++++++++++++++ src/runtime/runtime_esp32s3.go | 3 + targets/esp32s3-psram-octal.json | 6 + targets/esp32s3.ld | 58 ++- 9 files changed, 730 insertions(+), 26 deletions(-) create mode 100644 src/examples/psram/main.go create mode 100644 src/runtime/runtime_esp32_nopsram.go create mode 100644 src/runtime/runtime_esp32_psram.go create mode 100644 src/runtime/runtime_esp32_psram_octal.go create mode 100644 targets/esp32s3-psram-octal.json diff --git a/GNUmakefile b/GNUmakefile index 627640a520..56f91ffabb 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1019,6 +1019,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-box-3 examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-psram-octal examples/psram + @$(MD5SUM) test.bin endif # esp32c3-supermini $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index 0d4d1e21bb..bd85fb247c 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -18,7 +18,8 @@ // e. Disable caches // f. Cache_MMU_Init() — reset all MMU entries to invalid // g. Cache_Set_IDROM_MMU_Size() — set IROM/DROM entry split -// h. Write MMU entries mapping flash page 0 for IROM and DROM +// h. Map flash page 0 for IROM (hand-rolled loop) and DROM (ROM's +// Cache_Dbus_MMU_Set) // i. Clear bus-shut bits // j. Enable caches + isync @@ -76,6 +77,8 @@ .long 0x40001998 .LCache_Set_IDROM_MMU_Size: .long 0x40001914 +.LCache_Dbus_MMU_Set: + .long 0x400019b0 .LCache_Enable_ICache: .long 0x40001878 .LCache_Enable_DCache: @@ -87,13 +90,13 @@ .long 0x600C4064 .Ldcache_ctrl1_reg: .long 0x600C4004 -// End-of-section symbols for multi-page MMU mapping. +// Window bases and end-of-section symbols for MMU mapping. .Lirom_end: .long _irom_end -.Ldrom_end: - .long _drom_end .Lirom_base: .long 0x42000000 +.Ldrom_end: + .long _drom_end .Ldrom_base: .long 0x3C000000 @@ -272,13 +275,16 @@ call_start_cpu0: l32r a4, .LCache_Set_IDROM_MMU_Size callx4 a4 - // 4i. Map flash pages for IROM and DROM using identity mapping. - // MMU table at 0x600C5000: entries 0-255 = ICache, 256-511 = DCache. - // Entry value N = flash page N (SOC_MMU_VALID = 0 on S3). - // Each 64KB page needs one 4-byte entry. + // 4i. Map flash pages for IROM and DROM. Entry value N = flash page N + // (SOC_MMU_VALID = 0 on S3), one 4-byte entry per 64KB page. // // IROM: map pages 0..N where N = (_irom_end - 0x42000000) >> 16 // DROM: map pages 0..M where M = (_drom_end - 0x3C000000) >> 16 + // + // IROM is a hand-rolled store loop; DROM instead calls ROM's + // Cache_Dbus_MMU_Set. The equivalent Cache_Ibus_MMU_Set for IROM + // boots but then hangs at entry for reasons not yet understood -- + // do not swap the IROM loop for it without a new hypothesis. l32r a8, .Lmmu_table_base // a8 = 0x600C5000 @@ -297,18 +303,23 @@ call_start_cpu0: blt a9, a2, .Lirom_loop // --- DROM pages --- - l32r a2, .Ldrom_end // a2 = _drom_end (VMA in 0x3Cxxxxxx) - l32r a3, .Ldrom_base // a3 = 0x3C000000 - sub a2, a2, a3 // a2 = byte offset past DROM base + l32r a4, .Ldrom_end // a4 = _drom_end (VMA in 0x3Cxxxxxx) + l32r a5, .Ldrom_base // a5 = 0x3C000000 + sub a2, a4, a5 // a2 = byte offset past DROM base srli a2, a2, 16 // a2 = last page index - addi a2, a2, 1 // a2 = number of pages to map - movi a9, 0 // a9 = page counter (and entry value) - addmi a10, a8, 0x400 // a10 = 0x600C5400 (DCache entry 256) -.Ldrom_loop: - s32i a9, a10, 0 - addi a9, a9, 1 - addi a10, a10, 4 - blt a9, a2, .Ldrom_loop + addi a2, a2, 1 // a2 = number of pages to map (preserved) + + // Cache_Dbus_MMU_Set(ext_ram=0, vaddr=0x3C000000, paddr=0, psize=64, + // num=a2, fixed=0) + movi a6, 0 + l32r a7, .Ldrom_base + movi a8, 0 + movi a9, 64 + mov a10, a2 + movi a11, 0 + mov a5, a1 + l32r a4, .LCache_Dbus_MMU_Set + callx4 a4 memw // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. diff --git a/src/examples/psram/main.go b/src/examples/psram/main.go new file mode 100644 index 0000000000..e7b334543e --- /dev/null +++ b/src/examples/psram/main.go @@ -0,0 +1,60 @@ +package main + +// This example demonstrates how to use external PSRAM (Octal or Quad SPI) on +// supported targets (e.g., -target=esp32s3-psram-qspi). +// +// Variables placed in the `.psram` section via the `//go:section .psram` +// pragma are stored in external PSRAM rather than internal SRAM. +// Note: importing "unsafe" is required to enable `//go:section`. + +import ( + "fmt" + "time" + "unsafe" +) + +// Allocate a 1MB buffer explicitly in external PSRAM. +// +//go:section .psram +var psramBuffer [1024 * 1024]byte + +func main() { + time.Sleep(2 * time.Second) + + fmt.Println("=== PSRAM Example ===") + fmt.Printf("psramBuffer start address: 0x%X\n", uintptr(unsafe.Pointer(&psramBuffer[0]))) + fmt.Printf("psramBuffer size: %d bytes\n", len(psramBuffer)) + + fmt.Println("Writing test pattern to PSRAM...") + for i := range psramBuffer { + psramBuffer[i] = byte(i ^ 0xAA) + } + + fmt.Println("Verifying test pattern...") + errors := 0 + for i := range psramBuffer { + if psramBuffer[i] != byte(i^0xAA) { + errors++ + } + } + + if errors == 0 { + fmt.Println("SUCCESS: PSRAM 1MB read/write verification passed!") + } else { + fmt.Printf("FAIL: %d readback errors detected\n", errors) + printed := 0 + for i := range psramBuffer { + if psramBuffer[i] != byte(i^0xAA) { + fmt.Printf(" mismatch at idx %d (offset 0x%X): got 0x%02X, want 0x%02X\n", i, i, psramBuffer[i], byte(i^0xAA)) + printed++ + if printed >= 8 { + break + } + } + } + } + + for { + time.Sleep(time.Second) + } +} diff --git a/src/runtime/runtime_esp32_nopsram.go b/src/runtime/runtime_esp32_nopsram.go new file mode 100644 index 0000000000..ee9ee52e25 --- /dev/null +++ b/src/runtime/runtime_esp32_nopsram.go @@ -0,0 +1,6 @@ +//go:build esp32s3 && !numa_psram_octal + +package runtime + +// initPSRAM is a no-op when PSRAM support is disabled. +func initPSRAM() {} diff --git a/src/runtime/runtime_esp32_psram.go b/src/runtime/runtime_esp32_psram.go new file mode 100644 index 0000000000..c4d6fc3385 --- /dev/null +++ b/src/runtime/runtime_esp32_psram.go @@ -0,0 +1,100 @@ +//go:build esp32s3 && numa_psram_octal + +// Shared PSRAM support for ESP32-S3: linker symbols, MMU/ROM-call helpers, +// and address-map documentation common to both the Quad SPI driver +// (runtime_esp32_psram_qspi.go, build tag numa_psram_qspi) and the Octal +// SPI driver (runtime_esp32_psram_octal.go, build tag numa_psram_octal). +// Everything protocol-specific (SPI1 command sequencing, chip ID/mode +// register parsing, SPI0 cache-phase configuration) lives in those files. +// +// Address map (esp-idf soc/ext_mem_defs.h; verified on hardware while +// bringing up the Octal driver): +// +// 0x600C5000 MMU table, 512 x 32-bit entries, one per 64KB page. +// A SINGLE table shared by IBUS (0x42000000 window) and DBUS +// (0x3C000000 window), indexed by linear address: +// entry = (vaddr & SOC_MMU_LINEAR_ADDR_MASK 0x1FFFFFF) >> 16. +// There is no ICache/DCache half-split: 0x3C800000 and +// 0x42800000 both decode to entry 128. +// Entry format: bits[13:0] physical page number, bit 14 = +// invalid (SOC_MMU_INVALID), bit 15 = target type, 0 = flash, +// 1 = PSRAM (SOC_MMU_ACCESS_SPIRAM). +// +// Flash XIP identity-maps linear page N -> flash page N for both IROM +// and DROM (esp32s3.S), so the flash image occupies entries +// 0..(image pages - 1), up to 16M. PSRAM lives in the upper half of the +// DBUS window (0x3D000000, entries 256-511; targets/esp32s3.ld), +// disjoint from all flash entries. +// +// DCache (data cache) is the L1/L2 cache in front of the DBUS window. +// It is disabled (ROM Cache_Disable_DCache, which also invalidates all +// tag memory) while MMU entries are modified and re-enabled after, so +// that no stale cache lines survive the remap. +package runtime + +import "unsafe" + +//go:extern _spsram +var _spsram [0]byte + +//go:extern _epsram +var _epsram [0]byte + +//go:extern _psram_start +var _psram_start [0]byte + +//go:extern _psram_end +var _psram_end [0]byte + +// ROM functions baked into every ESP32-S3 chip (fixed addresses provided +// via PROVIDE() in targets/esp32s3.ld); see esp32s3/rom/cache.h. + +//export Cache_Dbus_MMU_Set +func romCacheDbusMMUSet(extRam uint32, vaddr uint32, paddr uint32, psize uint32, num uint32, fixed uint32) int32 + +// Cache_Disable_DCache / Cache_Enable_DCache: the documented-safe pairing +// around an MMU change (Cache_Disable_DCache also invalidates all DCache +// tag memory). Cache_Suspend_DCache/Resume, the alternative pairing, is +// explicitly documented as unsafe to use while changing the MMU. + +//export Cache_Disable_DCache +func romCacheDisableDCache() uint32 + +//export Cache_Enable_DCache +func romCacheEnableDCache(autoload uint32) + +const mmuAccessSpiram = 0x8000 // SOC_MMU_TYPE / SOC_MMU_ACCESS_SPIRAM: routes entry to PSRAM instead of flash. + +// psramWindow returns the linker-provided PSRAM vaddr window +// (targets/esp32s3.ld) and its capacity in 64KB MMU pages. +func psramWindow() (start, end uintptr, pageCap int) { + start = uintptr(unsafe.Pointer(&_psram_start)) + end = uintptr(unsafe.Pointer(&_psram_end)) + pageCap = int((end - start) / 65536) + return +} + +// zeroInitPSRAM zero-initializes .psram BSS after PSRAM has been mapped +// into the MMU, verifying with a known pattern first that writes actually +// reach the chip (a hardware or timing issue could otherwise silently +// drop them). +func zeroInitPSRAM() { + spsram := uintptr(unsafe.Pointer(&_spsram)) + epsram := uintptr(unsafe.Pointer(&_epsram)) + if epsram <= spsram { + return + } + + *(*uint32)(unsafe.Pointer(spsram)) = 0x5A5A5A5A + if *(*uint32)(unsafe.Pointer(spsram)) != 0x5A5A5A5A { + panic("PSRAM readback test failed") + } + *(*uint32)(unsafe.Pointer(spsram)) = 0 + if *(*uint32)(unsafe.Pointer(spsram)) != 0 { + panic("PSRAM readback test failed") + } + + for addr := spsram + 4; addr < epsram; addr += 4 { + *(*uint32)(unsafe.Pointer(addr)) = 0 + } +} diff --git a/src/runtime/runtime_esp32_psram_octal.go b/src/runtime/runtime_esp32_psram_octal.go new file mode 100644 index 0000000000..829cf975b5 --- /dev/null +++ b/src/runtime/runtime_esp32_psram_octal.go @@ -0,0 +1,472 @@ +//go:build esp32s3 && numa_psram_octal + +// Octal SPI (OPI) PSRAM support for ESP32-S3 (e.g. ESP32-S3-WROOM-1U-N16R8, +// combo modules with in-package Octal PSRAM). Octal PSRAM uses 8 data +// lines and a DTR (double transfer rate / DDR) protocol, unlike the 4-line +// single-rate Quad PSRAM handled by runtime_esp32_psram_qspi.go; the two +// are not interchangeable and use different GPIOs (Octal adds D4-D7+DQS +// on GPIO33-37, on top of the CS1/D0-D3 pins shared with flash). Shared +// linker symbols, MMU/ROM-call helpers, and the address map are +// documented in runtime_esp32_psram.go. +// +// Unlike its own Quad PSRAM driver, ESP-IDF does not hand-roll the Octal +// DTR command sequence in C -- it delegates entirely to a mask-ROM +// function (esp_rom_opiflash_exec_cmd, esp32s3/rom/opi_flash.h) that every +// ESP32-S3 chip has at a fixed address. There is no accessible reference +// for what that ROM function does internally, so this file calls it +// directly (matching esp_psram_impl_octal.c's call sites) rather than +// guessing at the DDR-mode register sequence. +// +// This driver performs ESP-IDF's DQS/MSPI-delay timing calibration +// (mspi_timing_psram_tuning / s_select_best_tuning_config_dtr). It performs an +// iterative sweep of 14 MSPI input delay and extra dummy cycle candidate +// parameters at 80 MHz DTR (160 MHz MSPI core clock) using reference pattern +// reads written during 40 MHz bring-up, selecting the optimal configuration +// to achieve 80 MHz DTR (160 MB/s bandwidth). +// +// GPIO33-37 (the Octal-only D4-D7+DQS lines) are configured by calling the +// ROM's esp_rom_opiflash_pin_config(), which reads the efuse OPI-pin +// strapping. The ROM does not do this on its own at boot: its boot-time +// pin setup follows the flash boot mode (DIO on modules with a plain Quad +// flash chip, which only needs D0/D1), and a tinygo image has no +// 2nd-stage bootloader to do it either. +package runtime + +import ( + "device/esp" + "unsafe" +) + +// ROM functions baked into every ESP32-S3 chip (fixed addresses provided +// via PROVIDE() in targets/esp32s3.ld). Declared per esp32s3/rom/opi_flash.h. + +//export esp_rom_opiflash_exec_cmd +func romOpiflashExecCmd(spiNum int32, mode int32, cmd uint32, cmdBitLen int32, addr uint32, addrBitLen int32, dummyBits int32, mosiData *byte, mosiBitLen int32, misoData *byte, misoBitLen int32, csMask uint32, isWriteErase bool) + +//export esp_rom_spi_set_dtr_swap_mode +func romSpiSetDtrSwapMode(spiNum int32, wrSwap bool, rdSwap bool) + +//export esp_rom_opiflash_pin_config +func romOpiflashPinConfig() + +// g_rom_spiflash_dummy_len_plus[1]: per-spi_num extra dummy cycle count that +// esp_rom_opiflash_exec_cmd() adds on top of the caller-supplied dummyBits. +// +//go:extern g_rom_spiflash_dummy_len_plus +var romDummyLenPlusSymbol [0]byte + +const ( + opiSpiNum = 1 // SPI1: command engine used for PSRAM setup, matches esp_psram_impl_octal.c's spi_num=1. + opiDtrMode = 7 // ESP_ROM_SPIFLASH_OPI_DTR_MODE (esp_rom_spiflash.h enum: QIO=0,QOUT,DIO,DOUT,FASTRD,SLOWRD,OPI_STR,OPI_DTR=7). + opiCS1Mask = 2 // BIT(1): esp_psram_impl_octal.c passes this literal cs_mask value for every PSRAM command. + + // Octal PSRAM command opcodes and phase lengths, matching the + // OPI_PSRAM_*/OCT_PSRAM_* constants in esp_psram_impl_octal.c. + opiRegRead = 0x4040 + opiRegWrite = 0xC0C0 + opiSyncRead = 0x0000 + opiSyncWrite = 0x8080 + opiCmdBitLen = 16 + opiAddrBitLen = 32 + opiRegDummy = 2 * (5 - 1) // 8 dummy cycles for MRx register read. + opiRdDummy = 2 * (10 - 1) // 18 dummy cycles for sync read. + opiWrDummy = 2 * (5 - 1) // 8 dummy cycles for sync write. + + opiVendorIDAP = 0xD // s_print_psram_info / OCT_PSRAM_VENDOR_ID_AP. + opiVendorIDUnilc = 0x1A // OCT_PSRAM_VENDOR_ID_UNILC. + + csHoldTime = 3 // OCT_PSRAM_CS_HOLD_TIME. + csSetupTime = 3 // OCT_PSRAM_CS_SETUP_TIME. + csHoldDelay = 2 // OCT_PSRAM_CS_HOLD_DELAY. + + // SPI0/SPI1 SMEM/FMEM clock divider for a fixed 40 MHz bring-up/fallback + // speed. SCLK = source/(N+1), H=((N+1)/2)-1, L=N. Also the speed + // calibratePSRAMTiming reverts to if the 80 MHz DTR timing sweep can't + // find a working config -- same din_mode/din_num/extra-dummy of 0 as + // this bring-up path, which already passed checkPSRAMConnected. + spiClkCntN = 1 + spiClkCntH = 0 + spiClkCntL = 1 +) + +// setPSRAMClockDivider sets SPI0's SRAM (PSRAM) clock and SPI1's FMEM (flash) +// clock divider. SCLK = source/(N+1), H=((N+1)/2)-1, L=N. +// +// WARNING: this writes to SPI1.CLOCK as well, not just to the SRAM clock. During +// PSRAM bring-up and timing calibration this is intentional (SPI1 drives PSRAM +// command sequences); any caller that only wants to touch the SRAM clock must use +// the individual SPI0.SetSRAM_CLK_* calls directly. +// +// Lives in IRAM: calibratePSRAMTiming calls this with flash cache disabled, +// so the code itself can't be fetched from flash-mapped memory at that point. +// +//go:section .iram +func setPSRAMClockDivider(n, h, l uint32) { + esp.SPI0.SetSRAM_CLK_SCLK_EQU_SYSCLK(0) + esp.SPI0.SetSRAM_CLK_SCLKCNT_N(n) + esp.SPI0.SetSRAM_CLK_SCLKCNT_H(h) + esp.SPI0.SetSRAM_CLK_SCLKCNT_L(l) + esp.SPI1.SetCLOCK_CLK_EQU_SYSCLK(0) + esp.SPI1.SetCLOCK_CLKCNT_N(n) + esp.SPI1.SetCLOCK_CLKCNT_H(h) + esp.SPI1.SetCLOCK_CLKCNT_L(l) +} + +// initPSRAM performs the Octal PSRAM bring-up sequence, mirroring +// esp_psram_impl_octal.c's esp_psram_impl_enable (minus DQS/timing +// calibration, see file doc comment): +// 1. CS1 pin select, CS timing, fixed low-speed clock. +// 2. Mode register (MR0) init: fixed read latency. +// 3. Connectivity check via a sync write/read of a reference word. +// 4. Read vendor ID (MR1) and density (MR2), validate and size the MMU window. +// 5. Configure SPI0's cache-facing read/write command phases for Octal DTR. +// 6. Map PSRAM into the MMU, zero-initialize .psram BSS. +func initPSRAM() { + psramStart, _, psramMmuPagesCap := psramWindow() + + // Enable SPI0/SPI1 peripheral clock and clear resets. + esp.SYSTEM.SetPERIP_CLK_EN0_SPI01_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI01_RST(0) + + // Ensure MMU table power is enabled and force-on. + esp.EXTMEM.SetCACHE_MMU_POWER_CTRL_CACHE_MMU_MEM_FORCE_ON(1) + esp.EXTMEM.SetCACHE_MMU_POWER_CTRL_CACHE_MMU_MEM_FORCE_PU(1) + + // CS1 pin (GPIO26): function 0 is the dedicated SPICS1 signal and the + // IO_MUX reset default; function 1 switches the pin to plain GPIO, + // disconnecting it from SPI1 (soc/io_mux_reg.h: FUNC_SPICS1_SPICS1 = 0). + // FUN_DRV(3) sets max drive strength (3 = 40 mA) for the CS1 and + // SPICLK pads; Octal OPI at 80 MHz DTR needs stronger drive than the + // Quad SPI driver's FUN_WPU pull-up approach (the Quad driver operates + // at single-data-rate 40 MHz and the pull-up ensures CS1 isn't + // floating when deasserted). + esp.IO_MUX.SetGPIO26_MCU_SEL(0) + esp.IO_MUX.SetGPIO26_FUN_DRV(3) + esp.SPI0.SetDATE_SPI_SMEM_SPICLK_FUN_DRV(3) + + // GPIO33-37 (Octal D4-D7+DQS) are not configured by the ROM at boot + // (see file doc comment); the efuse.h "GPIO33-37 powered by VDDSPI" + // note is about voltage domain, not pin function. This ROM call does + // the actual per-efuse pad configuration. + romOpiflashPinConfig() + + // CS/hold timing, shared by SPI0 and SPI1 for PSRAM + // (esp_psram_impl_octal.c s_set_psram_cs_timing). + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_SETUP(1) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD(1) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD_TIME(csHoldTime) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_SETUP_TIME(csSetupTime) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD_DELAY(csHoldDelay) + + // Fixed low-speed clock for SPI0 and SPI1 (no DQS/timing calibration). + setPSRAMClockDivider(spiClkCntN, spiClkCntH, spiClkCntL) + + // Variable-dummy DDR mode on SPI1, no DTR read/write swap + // (esp_psram_impl_octal.c esp_psram_impl_enable). + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(1) + romSpiSetDtrSwapMode(opiSpiNum, false, false) + + // Mode register MR0: fixed latency, read_latency=2, drive_str=0 + // (esp_psram_impl_octal.c: mode_reg.mr0 = {lt:1, read_latency:2, drive_str:0}). + mr0 := readPSRAMModeReg(0x0) + mr0 = (mr0 &^ 0x3f) | (2 << 2) | (1 << 5) + writePSRAMModeReg(0x0, mr0) + + if !checkPSRAMConnected() { + panic("PSRAM init failed: sync read/write mismatch, chip not detected") + } + + mr1 := readPSRAMModeReg(0x1) // vendor id + mr2 := readPSRAMModeReg(0x2) // density / dev id / gb + vendorID := mr1 & 0x1f + if vendorID != opiVendorIDAP && vendorID != opiVendorIDUnilc { + panic("PSRAM init failed: unrecognized vendor id") + } + + // Density encoding (esp_psram_impl_octal.c:372-376): 0x1=4MB, 0x3=8MB, + // 0x5=16MB, in 64KB pages. Chips reporting 0x7 (32MB) or 0x6 (64MB) + // fall through unmatched and are simply capped at psramMmuPagesCap, + // same as any chip bigger than the window: targets/esp32s3.ld only + // reserves a 16M PSRAM window, so there's nothing a bigger case value + // could map to. + density := mr2 & 0x7 + mmuPages := psramMmuPagesCap + switch density { + case 0x1: + if mmuPages > 64 { + mmuPages = 64 // 4MB + } + case 0x3: + if mmuPages > 128 { + mmuPages = 128 // 8MB + } + case 0x5: + if mmuPages > 256 { + mmuPages = 256 // 16MB + } + } + + // Perform DQS / MSPI delay timing calibration to upgrade Octal PSRAM to + // 80 MHz DTR. On failure it reverts to the untuned 40 MHz bring-up + // speed instead of running at an unverified timing; either way, the + // cache-facing register setup below is unaffected by which speed won. + calibratePSRAMTiming() + + // Disable DCache before touching the SPI0 cache-facing registers and + // the MMU table below (see the Cache_Disable_DCache doc comment in + // runtime_esp32_psram.go). + romCacheDisableDCache() + + // Enable SPI0's CS1 output for its cache-triggered SRAM (PSRAM) + // transactions. SPI_MEM_CS1_DIS on SPI0.MISC defaults to 1 (disabled; + // soc/spi_mem_reg.h bitpos:[1]), so without this the chip is never + // selected for SPI0 accesses. Matches ESP-IDF's quad PSRAM driver: + // "ENABLE SPI0 CS1 TO PSRAM (CS0--FLASH; CS1--SRAM)" + // (esp32s2/esp_psram_impl_quad.c:546); the generated SPI0_Type has no + // named CS1_DIS accessor, hence ClearBits. + esp.SPI0.MISC.ClearBits(1 << 1) + + // Configure SPI0's cache-facing read/write command phases for Octal + // DTR PSRAM access (esp_psram_impl_octal.c s_config_psram_spi_phases). + esp.SPI0.SetCACHE_SCTRL_CACHE_SRAM_USR_WCMD(1) + esp.SPI0.SetSRAM_DWR_CMD_CACHE_SRAM_USR_WR_CMD_BITLEN(opiCmdBitLen - 1) + esp.SPI0.SetSRAM_DWR_CMD_CACHE_SRAM_USR_WR_CMD_VALUE(opiSyncWrite) + + esp.SPI0.SetCACHE_SCTRL_CACHE_SRAM_USR_RCMD(1) + esp.SPI0.SetSRAM_DRD_CMD_CACHE_SRAM_USR_RD_CMD_BITLEN(opiCmdBitLen - 1) + esp.SPI0.SetSRAM_DRD_CMD_CACHE_SRAM_USR_RD_CMD_VALUE(opiSyncRead) + + esp.SPI0.SetCACHE_SCTRL_SRAM_ADDR_BITLEN(opiAddrBitLen - 1) + esp.SPI0.SetCACHE_SCTRL_CACHE_USR_SCMD_4BYTE(1) + + esp.SPI0.SetCACHE_SCTRL_USR_RD_SRAM_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_USR_WR_SRAM_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_RDUMMY_CYCLELEN(opiRdDummy - 1) + esp.SPI0.SetSPI_SMEM_DDR_SPI_SMEM_VAR_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_WDUMMY_CYCLELEN(opiWrDummy - 1) + + esp.SPI0.SetSPI_SMEM_DDR_WDAT_SWP(0) + esp.SPI0.SetSPI_SMEM_DDR_RDAT_SWP(0) + esp.SPI0.SetSPI_SMEM_DDR_EN(1) + + esp.SPI0.SetSRAM_CMD_SDUMMY_OUT(1) + esp.SPI0.SetSRAM_CMD_SCMD_OCT(1) + esp.SPI0.SetSRAM_CMD_SADDR_OCT(1) + esp.SPI0.SetSRAM_CMD_SDOUT_OCT(1) + esp.SPI0.SetSRAM_CMD_SDIN_OCT(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_OCT(1) + + // Map PSRAM into the MMU: PSRAM physical page 0 at _psram_start, 64KB + // pages, fixed=0 (physical pages grow linearly with virtual pages). + if romCacheDbusMMUSet(mmuAccessSpiram, uint32(psramStart), 0, 64, uint32(mmuPages), 0) != 0 { + panic("PSRAM init failed: Cache_Dbus_MMU_Set error") + } + + // Re-enable DCache. Cache_Disable_DCache above already invalidated all + // tag memory. + romCacheEnableDCache(0) + + zeroInitPSRAM() +} + +//export rom_spi_flash_disable_cache +func romSpiFlashDisableCache(cpuid uint32, savedState *uint32) + +//export rom_spi_flash_restore_cache +func romSpiFlashRestoreCache(cpuid uint32, savedState uint32) + +//go:section .iram +func calibratePSRAMTiming() (ok bool) { + // Disable both caches so that no flash fetches can be initiated by the + // cache controller during clock switches. + var cacheState uint32 + romSpiFlashDisableCache(0, &cacheState) + + origCoreClkSel := esp.SPI0.GetCORE_CLK_SEL() + + // Disable variable dummy mode on SPI1 during timing calibration (matching ESP-IDF). + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(0) + + // 1. Write reference data pattern (64 bytes) to PSRAM address 0 at low speed (40 MHz). + var refData [16]uint32 + seed := uint32(0xa5ff005a) + for i := range refData { + seed = seed*1664525 + 1013904223 + refData[i] = seed + } + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncWrite, opiCmdBitLen, 0, opiAddrBitLen, opiWrDummy, (*byte)(unsafe.Pointer(&refData[0])), 512, nil, 0, opiCS1Mask, false) + + // 2. Read the original flash clock configuration to scale the divider when the core clock increases. + origFlashClock := esp.SPI0.CLOCK.Get() + var origFlashDiv uint32 = 1 + if (origFlashClock & (1 << 31)) == 0 { + origFlashDiv = ((origFlashClock >> 16) & 0xff) + 1 + } + newFlashDiv := origFlashDiv * 2 + newFlashClock := ((newFlashDiv - 1) << 16) | (((newFlashDiv/2 - 1) & 0xff) << 8) | (newFlashDiv - 1) + + // Apply scaled divider to SPI0 and SPI1 flash clock registers. + esp.SPI0.CLOCK.Set(newFlashClock) + esp.SPI1.CLOCK.Set(newFlashClock) + + // Switch MSPI core clock to 160 MHz. + esp.SPI0.SetCORE_CLK_SEL(2) // 160 MHz core clock + + // Set SPI0/SPI1 SRAM clocks to 80 MHz (divider = 2). + setPSRAMClockDivider(1, 0, 1) + + // Declare candidate delay parameters inside IRAM to avoid RODATA flash access. + psramTuningParams := [14]struct { + dinMode uint8 + dinNum uint8 + extraDummyLen uint8 + }{ + {0, 0, 0}, + {4, 2, 2}, + {2, 1, 2}, + {4, 1, 2}, + {1, 0, 1}, + {4, 0, 2}, // default config index 5 + {0, 0, 1}, + {4, 2, 3}, + {2, 1, 3}, + {4, 1, 3}, + {1, 0, 2}, + {4, 0, 3}, + {0, 0, 2}, + {4, 2, 4}, + } + + romDummyPlus1 := (*uint8)(unsafe.Pointer(&romDummyLenPlusSymbol)) + origRomDummyPlus1 := *romDummyPlus1 + + // 3. Sweep all 14 timing configurations and record successful reads. + var success [14]bool + for i := 0; i < 14; i++ { + p := psramTuningParams[i] + dinModeReg := (uint32(p.dinMode) & 7) * 0x01249249 + dinNumReg := (uint32(p.dinNum) & 3) * 0x00015555 + esp.SPI0.SPI_SMEM_DIN_MODE.Set(dinModeReg) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(dinNumReg) + + if p.extraDummyLen > 0 { + esp.SPI1.TIMING_CALI.Set(2 | ((uint32(p.extraDummyLen) & 7) << 2)) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(2 | ((uint32(p.extraDummyLen) & 7) << 2)) + } else { + esp.SPI1.TIMING_CALI.Set(0) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + } + *romDummyPlus1 = origRomDummyPlus1 + p.extraDummyLen + + // Inline clear of SPI1 FIFO (W0..W15 registers). + esp.SPI1.W0.Set(0) + esp.SPI1.W1.Set(0) + esp.SPI1.W2.Set(0) + esp.SPI1.W3.Set(0) + esp.SPI1.W4.Set(0) + esp.SPI1.W5.Set(0) + esp.SPI1.W6.Set(0) + esp.SPI1.W7.Set(0) + esp.SPI1.W8.Set(0) + esp.SPI1.W9.Set(0) + esp.SPI1.W10.Set(0) + esp.SPI1.W11.Set(0) + esp.SPI1.W12.Set(0) + esp.SPI1.W13.Set(0) + esp.SPI1.W14.Set(0) + esp.SPI1.W15.Set(0) + + var readData [16]uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncRead, opiCmdBitLen, 0, opiAddrBitLen, opiRdDummy, nil, 0, (*byte)(unsafe.Pointer(&readData[0])), 512, opiCS1Mask, false) + + if readData == refData { + success[i] = true + } + } + + // 5. Select best calibration index from passing candidates, in order of + // preference; this covers all 14 measured candidates, so a working but + // non-preferred config is never silently dropped in favor of an + // unverified default. + priority := [14]uint32{4, 10, 6, 12, 0, 5, 1, 2, 3, 7, 8, 9, 11, 13} + var bestIdx uint32 + ok = false + for _, idx := range priority { + if success[idx] { + bestIdx = idx + ok = true + break + } + } + + if ok { + // 6. Apply best calibration parameters to SPI0 (cache) and + // clear SPI1's leftover sweep timing. + best := psramTuningParams[bestIdx] + dinModeReg := (uint32(best.dinMode) & 7) * 0x01249249 + dinNumReg := (uint32(best.dinNum) & 3) * 0x00015555 + esp.SPI0.SPI_SMEM_DIN_MODE.Set(dinModeReg) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(dinNumReg) + + if best.extraDummyLen > 0 { + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(2 | ((uint32(best.extraDummyLen) & 7) << 2)) + } else { + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + } + esp.SPI1.TIMING_CALI.Set(0) + } else { + // No candidate passed the 80 MHz DTR read-back: revert clocks and + // din_mode/din_num/timing-cali to the same untuned 40 MHz state + // used for bring-up (spiClkCntN/H/L), which already passed + // checkPSRAMConnected, instead of running PSRAM at an unverified + // timing. + esp.SPI0.CLOCK.Set(origFlashClock) + esp.SPI1.CLOCK.Set(origFlashClock) + esp.SPI0.SetCORE_CLK_SEL(origCoreClkSel) + + // Restore SPI0 SRAM clock for 40 MHz PSRAM. + setPSRAMClockDivider(spiClkCntN, spiClkCntH, spiClkCntL) + + esp.SPI0.SPI_SMEM_DIN_MODE.Set(0) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(0) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + esp.SPI1.TIMING_CALI.Set(0) + } + + // Restore variable dummy mode on SPI1. + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(1) + + // Restore ROM global dummy plus value. + *romDummyPlus1 = origRomDummyPlus1 + + // Re-enable caches. + romSpiFlashRestoreCache(0, cacheState) + return ok +} + +// readPSRAMModeReg reads a 16-bit PSRAM mode register (MR0..MR8) via the +// ROM's Octal DTR command executor, matching esp_psram_impl_octal.c's +// s_get_psram_mode_reg / s_init_psram_mode_reg read calls. +func readPSRAMModeReg(addr uint32) uint32 { + var val uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiRegRead, opiCmdBitLen, addr, opiAddrBitLen, opiRegDummy, nil, 0, (*byte)(unsafe.Pointer(&val)), 16, opiCS1Mask, false) + return val +} + +// writePSRAMModeReg writes a 16-bit PSRAM mode register, matching +// esp_psram_impl_octal.c's s_init_psram_mode_reg write calls. +func writePSRAMModeReg(addr uint32, val uint32) { + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiRegWrite, opiCmdBitLen, addr, opiAddrBitLen, 0, (*byte)(unsafe.Pointer(&val)), 16, nil, 0, opiCS1Mask, false) +} + +// checkPSRAMConnected writes a reference word to PSRAM address 0 and reads +// it back, matching esp_psram_impl_octal.c's s_check_psram_connected. +func checkPSRAMConnected() bool { + refData := uint32(0x5a6b7c8d) + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncWrite, opiCmdBitLen, 0, opiAddrBitLen, opiWrDummy, (*byte)(unsafe.Pointer(&refData)), 32, nil, 0, opiCS1Mask, false) + + var got uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncRead, opiCmdBitLen, 0, opiAddrBitLen, opiRdDummy, nil, 0, (*byte)(unsafe.Pointer(&got)), 32, opiCS1Mask, false) + + return got == refData +} diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 7c8b208a96..483f505e75 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -74,6 +74,9 @@ func main() { clearbss() } + // Initialize PSRAM if numa_psram_* build tag is enabled. + initPSRAM() + // Initialize main system timer used for time.Now. initTimer() diff --git a/targets/esp32s3-psram-octal.json b/targets/esp32s3-psram-octal.json new file mode 100644 index 0000000000..cc2a55ee6a --- /dev/null +++ b/targets/esp32s3-psram-octal.json @@ -0,0 +1,6 @@ +{ + "inherits": ["esp32s3-generic"], + "build-tags": ["numa_psram_octal"], + "flash-method": "esp32flash", + "serial": "uart" +} diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index 6a08f29983..be0b86069b 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -6,11 +6,21 @@ * - SRAM1 (416KB): dual-mapped as IRAM 0x40378000-0x403DFFFF and * DRAM 0x3FC88000-0x3FCEFFFF. * - * Flash is memory-mapped via the cache: - * - DROM (read-only data): 0x3C000000, up to 32MB - * - IROM (executable code): 0x42000000, up to 32MB - * The MMU uses 64KB pages, so the bottom 16 bits of the virtual address - * and the flash offset must match. Dummy sections handle this alignment. + * Flash is memory-mapped via the cache, backed by the 512-entry MMU table + * (see esp32s3.S). The table is a SINGLE array shared by the instruction + * bus (IROM window at 0x42000000) and the data bus (DROM window at + * 0x3C000000), indexed by LINEAR address: + * + * entry = (vaddr & 0x1FFFFFF) >> 16 + * + * so 0x42800000 and 0x3C800000 decode to the SAME entry (128). There is no + * ICache/DCache half-split (verified on hardware via the CACHE_MMU_FAULT_* + * registers and ROM's Cache_Dbus_MMU_Set during Octal PSRAM bring-up). + * 512 entries x 64KB = 32M of linear space per window. Flash XIP + * identity-maps linear page N -> flash page N for both buses, so the image + * occupies entries 0..(image pages - 1). The MMU uses 64KB pages, so the + * bottom 16 bits of the virtual address and the flash offset must match; + * dummy sections handle this alignment. */ MEMORY @@ -18,8 +28,17 @@ MEMORY DRAM (rw) : ORIGIN = 0x3FC88000, LENGTH = 416K IRAM (x) : ORIGIN = 0x40378000, LENGTH = 416K /* SRAM1 only (SRAM0 used by ICache) */ - DROM (r) : ORIGIN = 0x3C000000, LENGTH = 32M /* Flash data bus (read-only) */ - IROM (rx) : ORIGIN = 0x42000000, LENGTH = 32M /* Flash instruction bus */ + /* DROM and IROM share MMU entries 0-255 by linear address (see above); + * the flash image (rodata + text, identity-mapped) may use the full + * 16M. The DBUS cache window extends to 0x3E000000 + * (SOC_DRAM0_CACHE_ADDRESS_HIGH), so PSRAM lives in its upper half. */ + DROM (r) : ORIGIN = 0x3C000000, LENGTH = 16M /* Flash data bus (read-only) */ + IROM (rx) : ORIGIN = 0x42000000, LENGTH = 16M /* Flash instruction bus; shares entries 0-255 with DROM */ + + /* External PSRAM: upper half of the DBUS cache window, MMU entries + * 256-511 (linear pages of 0x3D000000..0x3DFFFFFF), mapped at runtime + * by runtime_esp32_psram*.go. Disjoint from all flash entries. */ + PSRAM (rw): ORIGIN = 0x3D000000, LENGTH = 16M } /* The entry point. It is set in the image flashed to the chip, so must be @@ -139,12 +158,23 @@ SECTIONS _irom_end = .; } >IROM + .psram (NOLOAD) : ALIGN(4) + { + _spsram = ABSOLUTE(.); + *(.psram .psram.*) + . = ALIGN(4); + _epsram = ABSOLUTE(.); + } > PSRAM + /DISCARD/ : { *(.eh_frame) } } +_psram_start = ORIGIN(PSRAM); +_psram_end = ORIGIN(PSRAM) + LENGTH(PSRAM); + /* For the garbage collector. * _heap_start must be after the DRAM shadow of the IRAM section. * IRAM and DRAM share the same physical SRAM1, with addresses offset by @@ -165,10 +195,14 @@ _heap_start = _iram_end - 0x6F0000 - (__init_end - __init_start); * - 0x3fcef3d4-0x3fcef66c : PHY function table (g_phyFuns, 0x298 bytes) * - 0x3fcef81c-0x3fcef954 : ROM global variables (phy_param_rom … pTxRx) * - 0x3fcefe6d : g_scan_forever + * - 0x3fcefffd : g_rom_spiflash_dummy_len_plus[1], read/adjusted + * by esp_rom_opiflash_exec_cmd() during Octal PSRAM timing calibration * The Go GC stores metadata at [metadataStart, heapEnd) and will corrupt * any blob data that overlaps this range. */ _heap_end = 0x3fceb710; +PROVIDE( g_rom_spiflash_dummy_len_plus = 0x3fcefffd ); + _stack_size = 4K; /* From ESP-IDF: @@ -301,6 +335,16 @@ ets_install_putc1 = 0x400005dc; ets_install_uart_printf = 0x400005e8; ets_install_putc2 = 0x400005f4; PROVIDE( ets_delay_us = 0x40000600 ); +PROVIDE( esp_rom_opiflash_pin_config = 0x40000894 ); +PROVIDE( esp_rom_opiflash_exec_cmd = 0x400008b8 ); +PROVIDE( esp_rom_spi_set_dtr_swap_mode = 0x4000093c ); +PROVIDE( Cache_Disable_ICache = 0x4000186c ); +PROVIDE( Cache_Enable_ICache = 0x40001878 ); +PROVIDE( Cache_Disable_DCache = 0x40001884 ); +PROVIDE( Cache_Enable_DCache = 0x40001890 ); +PROVIDE( Cache_Dbus_MMU_Set = 0x400019b0 ); +PROVIDE( rom_spi_flash_disable_cache = 0x40000c0c ); +PROVIDE( rom_spi_flash_restore_cache = 0x40000c18 ); ets_get_stack_info = 0x4000060c; ets_install_lock = 0x40000618; ets_backup_dma_copy = 0x40000624; From ac70d2f9e3d12b0c841cbf3a4cc11cb4459e7417 Mon Sep 17 00:00:00 2001 From: rdon-key Date: Fri, 31 Jul 2026 19:54:59 +0900 Subject: [PATCH 2/2] fix(esp32s3): support large flash-mapped segments. The ROM bootloader rejects an image whose DROM segment exceeds 1MB, so any application with more than 1MB of read-only data failed to boot. DROM and IROM are now kept out of the ROM-loaded segment table entirely and appended to the image at 64KB-aligned flash offsets. Those offsets are patched into .data before the checksum and hash are computed, and the startup code programs the flash cache MMU from them, which decouples virtual pages from flash pages. Because the ESP32-S3 IBUS and DBUS windows share one 512-entry MMU table, .text and .rodata must occupy disjoint linear ranges. The linker script now places .rodata one whole page past the end of .text and sizes the DROM region so that an overlarge link fails at link time instead of silently colliding with the PSRAM window. Co-Authored-By: Konstantin Sharlaimov --- builder/esp.go | 236 +++++++++++++++----- builder/esp_test.go | 343 +++++++++++++++++++++++++++++ src/device/esp/esp32s3.S | 122 ++++++---- src/runtime/runtime_esp32_psram.go | 17 +- targets/esp32s3.ld | 90 ++++---- 5 files changed, 669 insertions(+), 139 deletions(-) create mode 100644 builder/esp_test.go diff --git a/builder/esp.go b/builder/esp.go index 739035c089..a855311d93 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -23,6 +23,30 @@ type espImageSegment struct { data []byte } +// ESP32-S3 flash-mapped virtual address windows (esp-idf +// soc/esp32s3/ext_mem_defs.h, narrowed to what targets/esp32s3.ld uses). +// Segments in these ranges are XIP'd from flash through the cache MMU +// instead of being loaded into RAM by the ROM bootloader. +const ( + esp32s3DromLow = 0x3C000000 // DBUS window; its upper half (0x3D000000+) is PSRAM, not flash. + esp32s3DromHigh = 0x3D000000 + esp32s3IromLow = 0x42000000 // IBUS window. + esp32s3IromHigh = 0x44000000 +) + +const ( + // esp32FlashBase is the flash offset esptool writes the ESP32 image to. + // ESP32-S3 images are written at offset 0 instead (see + // flashBinUsingEsp32), so there image offsets are flash offsets. + esp32FlashBase = 0x1000 + + // espFlashPageSize is the flash cache MMU page size. The MMU supports + // page sizes down to 256 B, but 64 KiB is the reset/default value and is + // what the startup code relies on. If the startup code ever changes the + // page size, this constant must change with it. + espFlashPageSize = 0x10000 +) + // makeESPFirmwareImage converts an input ELF file to an image file for an ESP32 or // ESP8266 chip. This is a special purpose image format just for the ESP chip // family, and is parsed by the on-chip mask ROM bootloader. @@ -79,16 +103,30 @@ func makeESPFirmwareImage(infile, outfile, format string) error { chip = format[:len(format)-len("-img")] } - // For ESP32 (original): separate RAM segments (loadable by ROM bootloader) - // from flash-mapped segments (DROM/IROM, require MMU setup by startup code). - // The ROM bootloader on ESP32 does NOT handle flash-mapped segments — - // it tries to memcpy to the virtual address, which crashes. + // Separate RAM segments loaded by the ROM bootloader from flash-mapped + // segments initialized by the TinyGo startup code. var flashSegments []*espImageSegment - if chip == "esp32" { + switch chip { + case "esp32": + var ramSegments []*espImageSegment + for _, seg := range segments { + if (seg.addr >= 0x3F400000 && seg.addr < 0x3F800000) || + (seg.addr >= 0x400D0000 && seg.addr < 0x40400000) { + flashSegments = append(flashSegments, seg) + } else { + ramSegments = append(ramSegments, seg) + } + } + segments = ramSegments + + case "esp32s3": + // The DBUS cache window runs to 0x3E000000, but its upper half is + // reserved for PSRAM (targets/esp32s3.ld), which is never backed by + // flash. Only the DROM half below 0x3D000000 is flash-mapped. var ramSegments []*espImageSegment for _, seg := range segments { - if (seg.addr >= 0x3F400000 && seg.addr < 0x3F800000) || // DROM - (seg.addr >= 0x400D0000 && seg.addr < 0x40400000) { // IROM + if (seg.addr >= esp32s3DromLow && seg.addr < esp32s3DromHigh) || + (seg.addr >= esp32s3IromLow && seg.addr < esp32s3IromHigh) { flashSegments = append(flashSegments, seg) } else { ramSegments = append(ramSegments, seg) @@ -100,53 +138,54 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // ESP32 flash XIP: compute where the DROM segment will be placed in flash // (page-aligned, right after the RAM segments) and patch the // _drom_flash_addr variable so the startup code can program the cache MMU. - // This must happen before the checksum/hash are computed so the patched - // value is covered by both. - const esp32FlashBase = 0x1000 // esptool flashes the image at 0x1000 - // The ESP32 flash cache MMU supports configurable page sizes down to 256 B. 64 KiB is the reset/default size. - // If the startup code ever changes the MMU page size, this constant must change too. - const esp32PageSize = 0x10000 // 64KB MMU pages var esp32DromFlashAddr uint32 if chip == "esp32" && len(flashSegments) > 0 { - // Compute the size of the RAM portion of the image (everything the ROM - // bootloader loads, up to and including the appended SHA256 hash). - ramImageSize := 0 - if makeImage { - ramImageSize += 4096 + esp32DromFlashAddr = uint32(alignUpFlashPage(esp32FlashBase + ramImageSize(segments, makeImage))) + + syms, err := inf.Symbols() + if err != nil { + return fmt.Errorf("ESP32: %w", err) } - ramImageSize += 24 // image header (8) + trailer fields (16) - for _, seg := range segments { - ramImageSize += 8 + len(seg.data) // segment header + data (4-aligned) + if err := patchFlashAddr(syms, segments, "_drom_flash_addr", esp32DromFlashAddr); err != nil { + return fmt.Errorf("ESP32: %w", err) } - ramImageSize += 16 - ramImageSize%16 // footer padding + checksum byte - ramImageSize += 32 // appended SHA256 hash - - // DROM flash address must be 64KB page-aligned. - esp32DromFlashAddr = uint32(esp32FlashBase+ramImageSize+esp32PageSize-1) &^ (esp32PageSize - 1) - - // Patch _drom_flash_addr in whichever RAM segment contains it. - syms, _ := inf.Symbols() - var dromSymAddr uint64 - for _, s := range syms { - if s.Name == "_drom_flash_addr" { - dromSymAddr = s.Value - break - } + } + + // ESP32-S3 flash XIP: the ROM bootloader rejects images with a DROM + // segment over 1MB, so IROM and DROM are kept out of the segment table + // (see above) and appended at page-aligned flash offsets instead. Those + // offsets are patched into the RAM image for the startup code to program + // the cache MMU with. + var esp32s3Irom, esp32s3Drom *espImageSegment + var esp32s3IromFlashAddr, esp32s3DromFlashAddr uint32 + if chip == "esp32s3" && len(flashSegments) > 0 { + var err error + esp32s3Irom, err = singleFlashSegment(flashSegments, esp32s3IromLow, esp32s3IromHigh, "IROM") + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - if dromSymAddr == 0 { - return fmt.Errorf("ESP32: _drom_flash_addr symbol not found") + esp32s3Drom, err = singleFlashSegment(flashSegments, esp32s3DromLow, esp32s3DromHigh, "DROM") + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - patched := false - for _, seg := range segments { - if dromSymAddr >= uint64(seg.addr) && dromSymAddr+4 <= uint64(seg.addr)+uint64(len(seg.data)) { - off := int(dromSymAddr - uint64(seg.addr)) - binary.LittleEndian.PutUint32(seg.data[off:], esp32DromFlashAddr) - patched = true - break - } + + // The image is flashed at offset 0, so image offsets are flash + // offsets. IROM goes right after the RAM image and DROM right after + // IROM, each rounded up to an MMU page. The linker script rounds the + // virtual addresses up the same way, so the offsets within a page + // match on both sides of the mapping. + esp32s3IromFlashAddr = uint32(alignUpFlashPage(ramImageSize(segments, makeImage))) + esp32s3DromFlashAddr = esp32s3IromFlashAddr + uint32(alignUpFlashPage(len(esp32s3Irom.data))) + + syms, err := inf.Symbols() + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - if !patched { - return fmt.Errorf("ESP32: _drom_flash_addr (0x%x) not in any RAM segment", dromSymAddr) + if err := patchFlashAddr(syms, segments, "_irom_flash_addr", esp32s3IromFlashAddr); err != nil { + return fmt.Errorf("ESP32-S3: %w", err) + } + if err := patchFlashAddr(syms, segments, "_drom_flash_addr", esp32s3DromFlashAddr); err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } } @@ -265,9 +304,9 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // For ESP32: append flash-mapped segments (DROM/IROM) at page-aligned flash // offsets after the RAM portion. The startup code maps them via the flash // cache MMU (DROM at esp32DromFlashAddr, patched into _drom_flash_addr). - if len(flashSegments) > 0 { + if chip == "esp32" && len(flashSegments) > 0 { const flashBase = esp32FlashBase - const pageSize = esp32PageSize + const pageSize = espFlashPageSize dromFlashAddr := esp32DromFlashAddr // Separate DROM and IROM segments. @@ -314,6 +353,27 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + // For ESP32-S3: append the XIP segments at the flash offsets patched into + // the image above. Both are page-aligned and in ascending order, so each + // one only needs padding up to its own offset. + if chip == "esp32s3" && len(flashSegments) > 0 { + for _, region := range []struct { + name string + offset uint32 + segment *espImageSegment + }{ + {"IROM", esp32s3IromFlashAddr, esp32s3Irom}, + {"DROM", esp32s3DromFlashAddr, esp32s3Drom}, + } { + if outf.Len() > int(region.offset) { + return fmt.Errorf("ESP32-S3: image is %d bytes, overlapping %s at flash offset 0x%x", + outf.Len(), region.name, region.offset) + } + outf.Write(make([]byte, int(region.offset)-outf.Len())) + outf.Write(region.segment.data) + } + } + // QEMU (or more precisely, qemu-system-xtensa from Espressif) expects the // image to be a certain size. if makeImage { @@ -327,3 +387,81 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // Write the image to the output file. return os.WriteFile(outfile, outf.Bytes(), 0666) } + +// alignUpFlashPage rounds size up to the next flash cache MMU page boundary. +func alignUpFlashPage(size int) int { + return (size + espFlashPageSize - 1) &^ (espFlashPageSize - 1) +} + +// ramImageSize returns the size of the part of the image that the ROM +// bootloader loads: the header, the segment headers and their data, the +// footer holding the checksum, and the appended SHA256 hash. +// +// Flash-mapped (XIP) segments are appended after this portion, and their +// flash offsets have to be known before the image is written, because they +// are patched into the image itself. This function must therefore predict +// exactly what makeESPFirmwareImage writes; keep the two in sync. +func ramImageSize(segments []*espImageSegment, makeImage bool) int { + size := 0 + if makeImage { + size += 4096 // padding in front of the image header + } + size += 24 // image header (8) + trailer fields (16) + for _, segment := range segments { + size += 8 + len(segment.data) // segment header + data (4-aligned) + } + size += 16 - size%16 // footer padding + checksum byte + size += 32 // appended SHA256 hash + return size +} + +// patchFlashAddr stores value in the 32-bit variable named by symbol, which +// must live in one of the RAM segments. The startup code reads it to program +// the flash cache MMU. Patching must happen before the checksum and hash are +// computed, so that the patched value is covered by both. +func patchFlashAddr(syms []elf.Symbol, segments []*espImageSegment, symbol string, value uint32) error { + var symbolAddr uint64 + found := false + for _, sym := range syms { + if sym.Name == symbol { + symbolAddr = sym.Value + found = true + break + } + } + if !found { + return fmt.Errorf("symbol %s not found", symbol) + } + + for _, segment := range segments { + start := uint64(segment.addr) + end := start + uint64(len(segment.data)) + if symbolAddr >= start && symbolAddr+4 <= end { + binary.LittleEndian.PutUint32(segment.data[symbolAddr-start:], value) + return nil + } + } + return fmt.Errorf("symbol %s (0x%x) not in a RAM segment", symbol, symbolAddr) +} + +// singleFlashSegment returns the one segment inside the [low, high) virtual +// address window, named name in error messages. The startup code maps each +// XIP region as a single run of MMU pages and the linker script sizes the +// regions accordingly, so anything other than exactly one segment per window +// means the two have drifted apart and the image would not boot. +func singleFlashSegment(segments []*espImageSegment, low, high uint32, name string) (*espImageSegment, error) { + var found *espImageSegment + for _, segment := range segments { + if segment.addr < low || segment.addr >= high { + continue + } + if found != nil { + return nil, fmt.Errorf("expected a single %s segment, found more than one", name) + } + found = segment + } + if found == nil { + return nil, fmt.Errorf("%s segment not found", name) + } + return found, nil +} diff --git a/builder/esp_test.go b/builder/esp_test.go new file mode 100644 index 0000000000..e3acc9dfbf --- /dev/null +++ b/builder/esp_test.go @@ -0,0 +1,343 @@ +package builder + +// Tests for the ESP32-S3 image layout in makeESPFirmwareImage. The ESP32-S3 +// keeps DROM/IROM out of the ROM-loaded segment table (the ROM bootloader +// rejects images with a DROM segment over 1MB) and appends them at +// 64KB-aligned flash offsets, patching those offsets into the RAM image so +// the startup code can program the cache MMU. +// +// The input ELF is synthesized here rather than compiled, so these tests run +// without an Xtensa toolchain. + +import ( + "bytes" + "crypto/sha256" + "debug/elf" + "encoding/binary" + "os" + "path/filepath" + "testing" +) + +// elf32Section describes one allocated section of the synthetic ELF. +type elf32Section struct { + name string + addr uint32 + data []byte + flags elf.SectionFlag +} + +// writeTestELF writes a minimal 32-bit little-endian Xtensa ELF containing +// the given allocated PROGBITS sections plus a symbol table defining syms +// (name -> address). It returns the file path. +func writeTestELF(t *testing.T, entry uint32, sections []elf32Section, syms map[string]uint32) string { + t.Helper() + + const ( + ehSize = 52 // Elf32_Ehdr + shSize = 40 // Elf32_Shdr + symSize = 16 // Elf32_Sym + ) + + // Section 0 is the mandatory null entry, followed by the allocated + // sections, then .symtab, .strtab and .shstrtab. + symtabIdx := len(sections) + 1 + strtabIdx := symtabIdx + 1 + shstrtabIdx := strtabIdx + 1 + numSections := shstrtabIdx + 1 + + // Build the section name table. + var shstrtab []byte + shName := func(name string) uint32 { + off := uint32(len(shstrtab)) + shstrtab = append(shstrtab, name...) + shstrtab = append(shstrtab, 0) + return off + } + shstrtab = append(shstrtab, 0) + + // Build the symbol table and its string table. Symbols are attributed to + // the first section, which is enough for elf.File.Symbols. + strtab := []byte{0} + symtab := make([]byte, symSize) // index 0 is the null symbol + for name, value := range syms { + nameOff := uint32(len(strtab)) + strtab = append(strtab, name...) + strtab = append(strtab, 0) + + var sym [symSize]byte + binary.LittleEndian.PutUint32(sym[0:], nameOff) + binary.LittleEndian.PutUint32(sym[4:], value) + binary.LittleEndian.PutUint32(sym[8:], 4) + sym[12] = byte(elf.ST_INFO(elf.STB_GLOBAL, elf.STT_OBJECT)) + binary.LittleEndian.PutUint16(sym[14:], 1) + symtab = append(symtab, sym[:]...) + } + + // Lay out the file: header, section headers, then section contents. + offset := uint32(ehSize + numSections*shSize) + type placed struct { + nameOff uint32 + offset uint32 + } + allocated := make([]placed, len(sections)) + for i, section := range sections { + allocated[i] = placed{nameOff: shName(section.name), offset: offset} + offset += uint32(len(section.data)) + } + symtabName, symtabOff := shName(".symtab"), offset + offset += uint32(len(symtab)) + strtabName, strtabOff := shName(".strtab"), offset + offset += uint32(len(strtab)) + shstrtabName, shstrtabOff := shName(".shstrtab"), offset + + buf := &bytes.Buffer{} + header := make([]byte, ehSize) + copy(header, []byte{0x7f, 'E', 'L', 'F'}) + header[4] = byte(elf.ELFCLASS32) + header[5] = byte(elf.ELFDATA2LSB) + header[6] = byte(elf.EV_CURRENT) + binary.LittleEndian.PutUint16(header[16:], uint16(elf.ET_EXEC)) + binary.LittleEndian.PutUint16(header[18:], uint16(elf.EM_XTENSA)) + binary.LittleEndian.PutUint32(header[20:], uint32(elf.EV_CURRENT)) + binary.LittleEndian.PutUint32(header[24:], entry) + binary.LittleEndian.PutUint32(header[32:], ehSize) // e_shoff + binary.LittleEndian.PutUint16(header[40:], ehSize) // e_ehsize + binary.LittleEndian.PutUint16(header[46:], shSize) // e_shentsize + binary.LittleEndian.PutUint16(header[48:], uint16(numSections)) + binary.LittleEndian.PutUint16(header[50:], uint16(shstrtabIdx)) + buf.Write(header) + + writeSectionHeader := func(nameOff uint32, typ elf.SectionType, flags elf.SectionFlag, addr, off, size, link, info, entsize uint32) { + var sh [shSize]byte + binary.LittleEndian.PutUint32(sh[0:], nameOff) + binary.LittleEndian.PutUint32(sh[4:], uint32(typ)) + binary.LittleEndian.PutUint32(sh[8:], uint32(flags)) + binary.LittleEndian.PutUint32(sh[12:], addr) + binary.LittleEndian.PutUint32(sh[16:], off) + binary.LittleEndian.PutUint32(sh[20:], size) + binary.LittleEndian.PutUint32(sh[24:], link) + binary.LittleEndian.PutUint32(sh[28:], info) + binary.LittleEndian.PutUint32(sh[32:], 4) + binary.LittleEndian.PutUint32(sh[36:], entsize) + buf.Write(sh[:]) + } + + writeSectionHeader(0, elf.SHT_NULL, 0, 0, 0, 0, 0, 0, 0) + for i, section := range sections { + writeSectionHeader(allocated[i].nameOff, elf.SHT_PROGBITS, section.flags, + section.addr, allocated[i].offset, uint32(len(section.data)), 0, 0, 0) + } + writeSectionHeader(symtabName, elf.SHT_SYMTAB, 0, 0, symtabOff, uint32(len(symtab)), + uint32(strtabIdx), 1, symSize) + writeSectionHeader(strtabName, elf.SHT_STRTAB, 0, 0, strtabOff, uint32(len(strtab)), 0, 0, 0) + writeSectionHeader(shstrtabName, elf.SHT_STRTAB, 0, 0, shstrtabOff, uint32(len(shstrtab)), 0, 0, 0) + + for _, section := range sections { + buf.Write(section.data) + } + buf.Write(symtab) + buf.Write(strtab) + buf.Write(shstrtab) + + path := filepath.Join(t.TempDir(), "test.elf") + if err := os.WriteFile(path, buf.Bytes(), 0666); err != nil { + t.Fatal(err) + } + return path +} + +// esp32s3TestImage builds an ESP32-S3 image from a synthetic ELF with the +// given .text and .rodata sizes, and returns the image bytes. +func esp32s3TestImage(t *testing.T, textSize, rodataSize int) []byte { + t.Helper() + + // Mirrors targets/esp32s3.ld: .text starts at ORIGIN(IROM) and .rodata + // starts one whole 64KB page past the end of .text. + const iromBase = 0x42000000 + const dromBase = 0x3C000000 + const pageSize = 0x10000 + dromAddr := uint32(dromBase + (textSize+pageSize-1)/pageSize*pageSize) + + text := bytes.Repeat([]byte{0x11}, textSize) + rodata := bytes.Repeat([]byte{0x22}, rodataSize) + iram := bytes.Repeat([]byte{0x33}, 256) + // .data holds _irom_flash_addr and _drom_flash_addr, both zero until the + // image builder patches them. + data := make([]byte, 16) + + const dataAddr = 0x3FC88000 + sections := []elf32Section{ + {name: ".rodata", addr: dromAddr, data: rodata, flags: elf.SHF_ALLOC}, + {name: ".data", addr: dataAddr, data: data, flags: elf.SHF_ALLOC | elf.SHF_WRITE}, + {name: ".iram", addr: 0x40378000, data: iram, flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}, + {name: ".text", addr: iromBase, data: text, flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}, + } + syms := map[string]uint32{ + "_irom_flash_addr": dataAddr + 8, + "_drom_flash_addr": dataAddr + 12, + } + + infile := writeTestELF(t, 0x40378000, sections, syms) + outfile := filepath.Join(t.TempDir(), "test.bin") + if err := makeESPFirmwareImage(infile, outfile, "esp32s3"); err != nil { + t.Fatal("makeESPFirmwareImage failed:", err) + } + image, err := os.ReadFile(outfile) + if err != nil { + t.Fatal(err) + } + return image +} + +// TestESP32S3ImageLayout checks that DROM and IROM stay out of the ROM-loaded +// segment table, that they land at 64KB-aligned flash offsets, and that those +// offsets are patched into the RAM image. +func TestESP32S3ImageLayout(t *testing.T) { + const pageSize = 0x10000 + + for _, tc := range []struct { + name string + textSize int + rodataSize int + }{ + {"small", 0x800, 0x400}, + {"multipage text", 0x30000, 0x400}, + // A DROM segment over 1MB is what the ROM bootloader used to reject. + {"large rodata", 0x1000, 0x780000}, + } { + t.Run(tc.name, func(t *testing.T) { + image := esp32s3TestImage(t, tc.textSize, tc.rodataSize) + + // The ROM only sees the RAM segments: .data and .iram. + if got := image[1]; got != 2 { + t.Errorf("segment count = %d, want 2 (.data and .iram only)", got) + } + + // Both flash regions must be page-aligned, in IROM/DROM order, + // and must not overlap the RAM image. + iromAddr := binary.LittleEndian.Uint32(image[0x18+8+8:]) + dromAddr := binary.LittleEndian.Uint32(image[0x18+8+12:]) + if iromAddr%pageSize != 0 || dromAddr%pageSize != 0 { + t.Errorf("flash offsets not 64KB-aligned: irom=%#x drom=%#x", iromAddr, dromAddr) + } + if dromAddr < iromAddr+uint32(tc.textSize) { + t.Errorf("DROM at %#x overlaps IROM at %#x (%#x bytes)", dromAddr, iromAddr, tc.textSize) + } + ramImageEnd := checkRAMImage(t, image) + if int(iromAddr) < ramImageEnd { + t.Errorf("IROM at %#x overlaps the RAM image ending at %#x", iromAddr, ramImageEnd) + } + + // The image is flashed at offset 0 on the ESP32-S3, so the patched + // flash offsets are also offsets into the image, and the startup + // code reads the section contents through them. + if int(iromAddr)+tc.textSize > len(image) || int(dromAddr)+tc.rodataSize > len(image) { + t.Fatalf("image too short: %d bytes, irom=%#x drom=%#x", len(image), iromAddr, dromAddr) + } + if got := image[iromAddr]; got != 0x11 { + t.Errorf("byte at IROM offset %#x = %#x, want 0x11 (.text)", iromAddr, got) + } + if got := image[dromAddr]; got != 0x22 { + t.Errorf("byte at DROM offset %#x = %#x, want 0x22 (.rodata)", dromAddr, got) + } + }) + } +} + +// checkRAMImage walks the ROM-loaded part of the image the way the ROM +// bootloader does, verifying the segment table, the trailing checksum byte +// and the appended SHA256 hash. This also covers the flash offsets patched +// into .data, which must be in place before either is computed. It returns +// the offset just past the hash, where the XIP segments start. +func checkRAMImage(t *testing.T, image []byte) int { + t.Helper() + + if image[0] != 0xE9 { + t.Fatalf("image magic = %#x, want 0xe9", image[0]) + } + + offset := 0x18 // image header + checksum := byte(0xEF) + for i := 0; i < int(image[1]); i++ { + length := int(binary.LittleEndian.Uint32(image[offset+4:])) + offset += 8 + if offset+length > len(image) { + t.Fatalf("segment %d runs past the end of the image", i) + } + for _, b := range image[offset : offset+length] { + checksum ^= b + } + offset += length + } + + offset += 15 - offset%16 // footer padding + if got := image[offset]; got != checksum { + t.Errorf("checksum byte = %#x, want %#x", got, checksum) + } + offset++ + + want := sha256.Sum256(image[:offset]) + if got := image[offset : offset+sha256.Size]; !bytes.Equal(got, want[:]) { + t.Errorf("appended SHA256 = %x, want %x", got, want[:]) + } + return offset + sha256.Size +} + +// TestESP32S3ImageErrors checks that an ELF the startup code could not boot +// from is rejected, rather than silently producing an unbootable image. +func TestESP32S3ImageErrors(t *testing.T) { + const dataAddr = 0x3FC88000 + data := elf32Section{name: ".data", addr: dataAddr, data: make([]byte, 16), + flags: elf.SHF_ALLOC | elf.SHF_WRITE} + text := elf32Section{name: ".text", addr: 0x42000000, data: make([]byte, 64), + flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR} + rodata := elf32Section{name: ".rodata", addr: 0x3C010000, data: make([]byte, 64), + flags: elf.SHF_ALLOC} + bothSyms := map[string]uint32{ + "_irom_flash_addr": dataAddr + 8, + "_drom_flash_addr": dataAddr + 12, + } + + for _, tc := range []struct { + name string + sections []elf32Section + syms map[string]uint32 + }{ + {"no IROM", []elf32Section{rodata, data}, bothSyms}, + {"no DROM", []elf32Section{text, data}, bothSyms}, + { + // The startup code maps each region as one run of MMU pages, so + // a second output section in the window cannot be represented. + name: "two DROM sections", + sections: []elf32Section{text, rodata, data, + {name: ".rodata2", addr: 0x3C020000, data: make([]byte, 64), flags: elf.SHF_ALLOC}}, + syms: bothSyms, + }, + { + name: "missing symbol", + sections: []elf32Section{text, rodata, data}, + syms: map[string]uint32{"_drom_flash_addr": dataAddr + 12}, + }, + { + // A flash offset that is not in a RAM segment cannot be patched, + // so the startup code would read an uninitialized value. + name: "symbol outside the RAM segments", + sections: []elf32Section{text, rodata, data}, + syms: map[string]uint32{ + "_irom_flash_addr": 0x3FC99000, + "_drom_flash_addr": dataAddr + 12, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + infile := writeTestELF(t, 0x40378000, tc.sections, tc.syms) + outfile := filepath.Join(t.TempDir(), "test.bin") + if err := makeESPFirmwareImage(infile, outfile, "esp32s3"); err == nil { + t.Error("expected an error, got none") + } else { + t.Log(err) + } + }) + } +} diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index bd85fb247c..f66bb9fc15 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -18,8 +18,8 @@ // e. Disable caches // f. Cache_MMU_Init() — reset all MMU entries to invalid // g. Cache_Set_IDROM_MMU_Size() — set IROM/DROM entry split -// h. Map flash page 0 for IROM (hand-rolled loop) and DROM (ROM's -// Cache_Dbus_MMU_Set) +// h. Write MMU entries mapping the IROM and DROM flash pages, using the +// physical flash offsets patched into .data by builder/esp.go // i. Clear bus-shut bits // j. Enable caches + isync @@ -77,8 +77,6 @@ .long 0x40001998 .LCache_Set_IDROM_MMU_Size: .long 0x40001914 -.LCache_Dbus_MMU_Set: - .long 0x400019b0 .LCache_Enable_ICache: .long 0x40001878 .LCache_Enable_DCache: @@ -90,15 +88,23 @@ .long 0x600C4064 .Ldcache_ctrl1_reg: .long 0x600C4004 -// Window bases and end-of-section symbols for MMU mapping. +// Flash section bounds, window bases, and builder-patched physical offsets. +.Lirom_start: + .long _irom_start .Lirom_end: .long _irom_end .Lirom_base: .long 0x42000000 +.Ldrom_start: + .long _drom_start .Ldrom_end: .long _drom_end .Ldrom_base: .long 0x3C000000 +.Lirom_flash_addr_ptr: + .long _irom_flash_addr +.Ldrom_flash_addr_ptr: + .long _drom_flash_addr .global call_start_cpu0 call_start_cpu0: @@ -267,59 +273,79 @@ call_start_cpu0: l32r a4, .LCache_MMU_Init callx4 a4 - // 4h. Set IDROM MMU size: even 256/256 split. - // Each entry is 4 bytes, so 256 entries = 0x400 bytes per region. - movi a6, 0x400 // irom_mmu_size (256 entries × 4 bytes) - movi a7, 0x400 // drom_mmu_size (256 entries × 4 bytes) + // 4h. Split the shared 512-entry MMU table between IROM and DROM. + // DROM begins at the virtual page immediately after IROM. + l32r a2, .Ldrom_start + l32r a3, .Ldrom_base + sub a2, a2, a3 + srli a2, a2, 16 // a2 = first DROM MMU entry + + slli a6, a2, 2 // IROM table size in bytes + movi a7, 0x400 // movi takes a 12-bit signed immediate, + add a7, a7, a7 // so build 0x800 (full table) with an add + sub a7, a7, a6 // DROM table size in bytes mov a5, a1 l32r a4, .LCache_Set_IDROM_MMU_Size callx4 a4 - // 4i. Map flash pages for IROM and DROM. Entry value N = flash page N - // (SOC_MMU_VALID = 0 on S3), one 4-byte entry per 64KB page. + // 4i. Map physical flash pages into the shared MMU table. One 4-byte + // entry per 64KB page; entry value = physical flash page number + // (SOC_MMU_VALID = 0 on S3, SOC_MMU_ACCESS_SPIRAM bit clear for + // flash). The physical base of each region is patched into + // _irom_flash_addr/_drom_flash_addr by builder/esp.go, so virtual + // pages are independent of flash pages. // - // IROM: map pages 0..N where N = (_irom_end - 0x42000000) >> 16 - // DROM: map pages 0..M where M = (_drom_end - 0x3C000000) >> 16 + // Both regions use the hand-rolled store loop below. ROM's + // Cache_Ibus_MMU_Set boots but then hangs at entry for reasons not + // yet understood -- do not swap the IROM loop for it without a new + // hypothesis. (The runtime PSRAM driver does use the ROM's + // Cache_Dbus_MMU_Set, but only after the caches are up.) + l32r a8, .Lmmu_table_base + + // map_flash_pages maps one XIP region into the shared MMU table: // - // IROM is a hand-rolled store loop; DROM instead calls ROM's - // Cache_Dbus_MMU_Set. The equivalent Cache_Ibus_MMU_Set for IROM - // boots but then hangs at entry for reasons not yet understood -- - // do not swap the IROM loop for it without a new hypothesis. - - l32r a8, .Lmmu_table_base // a8 = 0x600C5000 - - // --- IROM pages --- - l32r a2, .Lirom_end // a2 = _irom_end (VMA in 0x42xxxxxx) - l32r a3, .Lirom_base // a3 = 0x42000000 - sub a2, a2, a3 // a2 = byte offset past IROM base - srli a2, a2, 16 // a2 = last page index - addi a2, a2, 1 // a2 = number of pages to map - movi a9, 0 // a9 = page counter (and entry value) - mov a10, a8 // a10 = current MMU entry pointer -.Lirom_loop: + // base_lit literal holding the base VMA of the region's cache window + // start_lit literal holding the region's start VMA (page-aligned) + // end_lit literal holding the region's end VMA + // addr_lit literal holding the address of the physical flash offset + // that builder/esp.go patched into .data + // + // Requires a8 = MMU table base. Clobbers a2, a3, a9, a10, a11. The + // region must be non-empty; targets/esp32s3.ld asserts that, because the + // page count below underflows for a zero-length region. + .macro map_flash_pages base_lit, start_lit, end_lit, addr_lit + // First table entry, from the region's virtual page in its window. + l32r a2, \start_lit + l32r a3, \base_lit + sub a2, a2, a3 + srli a2, a2, 16 + slli a10, a2, 2 + add a10, a8, a10 + + // Number of pages covering [start, end). + l32r a2, \end_lit + l32r a3, \start_lit + sub a2, a2, a3 + addi a2, a2, -1 + srli a2, a2, 16 + addi a2, a2, 1 + + // First physical flash page. + l32r a3, \addr_lit + l32i a9, a3, 0 + srli a9, a9, 16 + + movi a11, 0 +1: s32i a9, a10, 0 addi a9, a9, 1 addi a10, a10, 4 - blt a9, a2, .Lirom_loop - - // --- DROM pages --- - l32r a4, .Ldrom_end // a4 = _drom_end (VMA in 0x3Cxxxxxx) - l32r a5, .Ldrom_base // a5 = 0x3C000000 - sub a2, a4, a5 // a2 = byte offset past DROM base - srli a2, a2, 16 // a2 = last page index - addi a2, a2, 1 // a2 = number of pages to map (preserved) + addi a11, a11, 1 + blt a11, a2, 1b + .endm - // Cache_Dbus_MMU_Set(ext_ram=0, vaddr=0x3C000000, paddr=0, psize=64, - // num=a2, fixed=0) - movi a6, 0 - l32r a7, .Ldrom_base - movi a8, 0 - movi a9, 64 - mov a10, a2 - movi a11, 0 - mov a5, a1 - l32r a4, .LCache_Dbus_MMU_Set - callx4 a4 + map_flash_pages .Lirom_base, .Lirom_start, .Lirom_end, .Lirom_flash_addr_ptr + map_flash_pages .Ldrom_base, .Ldrom_start, .Ldrom_end, .Ldrom_flash_addr_ptr memw // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. diff --git a/src/runtime/runtime_esp32_psram.go b/src/runtime/runtime_esp32_psram.go index c4d6fc3385..e65f1422d9 100644 --- a/src/runtime/runtime_esp32_psram.go +++ b/src/runtime/runtime_esp32_psram.go @@ -20,11 +20,18 @@ // invalid (SOC_MMU_INVALID), bit 15 = target type, 0 = flash, // 1 = PSRAM (SOC_MMU_ACCESS_SPIRAM). // -// Flash XIP identity-maps linear page N -> flash page N for both IROM -// and DROM (esp32s3.S), so the flash image occupies entries -// 0..(image pages - 1), up to 16M. PSRAM lives in the upper half of the -// DBUS window (0x3D000000, entries 256-511; targets/esp32s3.ld), -// disjoint from all flash entries. +// Flash XIP takes the low entries: IROM starts at entry 0 and DROM +// follows immediately after it (esp32s3.S, targets/esp32s3.ld), so +// .text + .rodata together occupy a prefix of entries 0..255, capped at +// 16M by the length of the DROM linker region. PSRAM lives in the upper +// half of the DBUS window (0x3D000000, entries 256-511), disjoint from +// all flash entries. +// +// The IROM/DROM boundary is variable, since it follows the size of +// .text. The PSRAM window sits above it either way: the boundary can +// only move down, never above 0x3D000000, so the vaddr the drivers pass +// to romCacheDbusMMUSet always stays inside whatever DROM range +// Cache_Set_IDROM_MMU_Size left behind. // // DCache (data cache) is the L1/L2 cache in front of the DBUS window. // It is disabled (ROM Cache_Disable_DCache, which also invalidates all diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index be0b86069b..87d8f64b41 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -16,11 +16,19 @@ * so 0x42800000 and 0x3C800000 decode to the SAME entry (128). There is no * ICache/DCache half-split (verified on hardware via the CACHE_MMU_FAULT_* * registers and ROM's Cache_Dbus_MMU_Set during Octal PSRAM bring-up). - * 512 entries x 64KB = 32M of linear space per window. Flash XIP - * identity-maps linear page N -> flash page N for both buses, so the image - * occupies entries 0..(image pages - 1). The MMU uses 64KB pages, so the - * bottom 16 bits of the virtual address and the flash offset must match; - * dummy sections handle this alignment. + * 512 entries x 64KB = 32M of linear space per window. + * + * Because the two windows share entries, IROM and DROM must occupy disjoint + * linear ranges: .text starts at ORIGIN(IROM) (entry 0) and .rodata starts + * one whole 64KB page past the end of .text (.rodata_dummy pads for this), + * so DROM takes the entries right after IROM. + * + * Virtual pages are NOT tied to flash pages. The image builder + * (builder/esp.go) keeps DROM/IROM out of the ROM-loaded segment table -- + * the ROM bootloader rejects images with a DROM segment over 1MB -- and + * appends them at 64KB-aligned flash offsets after the RAM image, patching + * those offsets into _irom_flash_addr/_drom_flash_addr. The startup code + * (esp32s3.S) programs the MMU entries from them. */ MEMORY @@ -28,11 +36,14 @@ MEMORY DRAM (rw) : ORIGIN = 0x3FC88000, LENGTH = 416K IRAM (x) : ORIGIN = 0x40378000, LENGTH = 416K /* SRAM1 only (SRAM0 used by ICache) */ - /* DROM and IROM share MMU entries 0-255 by linear address (see above); - * the flash image (rodata + text, identity-mapped) may use the full - * 16M. The DBUS cache window extends to 0x3E000000 + /* DROM and IROM share MMU entries 0-255 by linear address (see above), + * so their combined size must stay below 16M. The DROM region length + * enforces that: .rodata_dummy first burns the IROM pages out of this + * region, so a link that would push .rodata into the PSRAM window at + * 0x3D000000 overflows DROM and fails at link time. + * The DBUS cache window extends to 0x3E000000 * (SOC_DRAM0_CACHE_ADDRESS_HIGH), so PSRAM lives in its upper half. */ - DROM (r) : ORIGIN = 0x3C000000, LENGTH = 16M /* Flash data bus (read-only) */ + DROM (r) : ORIGIN = 0x3C000000, LENGTH = 16M /* Flash data bus (read-only); holds IROM padding + .rodata */ IROM (rx) : ORIGIN = 0x42000000, LENGTH = 16M /* Flash instruction bus; shares entries 0-255 with DROM */ /* External PSRAM: upper half of the DBUS cache window, MMU entries @@ -48,23 +59,6 @@ ENTRY(call_start_cpu0) SECTIONS { - /* Dummy section so that .rodata starts right after the image header - * and DROM segment header in the flash image. - */ - .rodata_dummy (NOLOAD): ALIGN(4) - { - . += 0x18; /* esp_image_header_t at start of flash */ - . += 0x8; /* DROM segment header (8 bytes) */ - } > DROM - - /* Constant global variables, stored in flash (DROM). */ - .rodata : ALIGN(4) - { - *(.rodata*) - . = ALIGN (4); - _drom_end = .; - } >DROM - /* Put the stack at the bottom of DRAM, so that the application will * crash on stack overflow instead of silently corrupting memory. */ @@ -92,6 +86,15 @@ SECTIONS _sdata = ABSOLUTE(.); *(.data .data.*) *(.dram*) + + /* Physical flash offsets patched by the ESP image builder. The + * startup code reads them with l32i, so they must be 4-aligned. */ + . = ALIGN (4); + _irom_flash_addr = ABSOLUTE(.); + LONG(0); + _drom_flash_addr = ABSOLUTE(.); + LONG(0); + . = ALIGN (4); _edata = ABSOLUTE(.); } >DRAM @@ -140,24 +143,37 @@ SECTIONS _iram_end = .; } >IRAM - /* Dummy section to put the IROM segment at the correct flash offset. */ - .text_dummy (NOLOAD): ALIGN(4) - { - . += 0x18; /* esp_image_header_t */ - . += SIZEOF(.rodata) + ((SIZEOF(.rodata) != 0) ? 0x8 : 0); /* DROM segment (optional) */ - . += SIZEOF(.data) + ((SIZEOF(.data) != 0) ? 0x8 : 0); /* DRAM segment (optional) */ - . += SIZEOF(.iram) + 0x8; /* IRAM segment */ - . += 0x8; /* IROM segment header */ - } > IROM - - /* IROM segment: main code executed from flash via cache. */ + /* IROM occupies the first entries in the shared flash MMU table. */ .text : ALIGN(4) { + _irom_start = .; *(.literal .text) *(.literal.* .text.*) _irom_end = .; } >IROM + /* DROM starts after all MMU pages reserved for IROM. */ + .rodata_dummy (NOLOAD): ALIGN(4) + { + . += ((SIZEOF(.text) + 0xFFFF) / 0x10000) * 0x10000; + } >DROM + + .rodata : ALIGN(4) + { + _drom_start = .; + *(.rodata*) + . = ALIGN(4); + _drom_end = .; + } >DROM + + /* The MMU mapping loops in esp32s3.S compute a page count as + * ((end - start - 1) >> 16) + 1, which underflows to 65536 pages for an + * empty region and would scribble over the whole MMU window. The image + * builder also requires exactly one segment per region. + */ + ASSERT(_irom_end > _irom_start, "IROM (.text) must not be empty") + ASSERT(_drom_end > _drom_start, "DROM (.rodata) must not be empty") + .psram (NOLOAD) : ALIGN(4) { _spsram = ABSOLUTE(.);