From c3df3552d69a3209049e3a853ff3da93666dd2b1 Mon Sep 17 00:00:00 2001 From: rdon-key Date: Fri, 31 Jul 2026 19:54:59 +0900 Subject: [PATCH 1/2] esp32s3: support large flash-mapped segments The ESP32-S3 ROM bootloader cannot load large IROM and DROM segments directly. Separate flash-mapped segments from RAM-loaded segments and place them at dedicated flash offsets. Update the startup code to configure the cache MMU and map IROM and DROM across multiple 64 KB pages. --- builder/esp.go | 135 ++++++++++++++++++++++++++++++++++++--- src/device/esp/esp32s3.S | 101 +++++++++++++++++++---------- targets/esp32s3.ld | 51 +++++++-------- 3 files changed, 217 insertions(+), 70 deletions(-) diff --git a/builder/esp.go b/builder/esp.go index 9f6545623e..929243b88c 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -79,16 +79,27 @@ 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": 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 >= 0x3C000000 && seg.addr < 0x3E000000) || + (seg.addr >= 0x42000000 && seg.addr < 0x44000000) { flashSegments = append(flashSegments, seg) } else { ramSegments = append(ramSegments, seg) @@ -147,6 +158,98 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + var esp32s3IromSegment *espImageSegment + var esp32s3DromSegment *espImageSegment + var esp32s3IromFlashAddr uint32 + var esp32s3DromFlashAddr uint32 + + if chip == "esp32s3" && len(flashSegments) > 0 { + for _, seg := range flashSegments { + switch { + case seg.addr >= 0x42000000 && seg.addr < 0x44000000: + if esp32s3IromSegment != nil { + return fmt.Errorf("ESP32-S3: multiple IROM segments") + } + esp32s3IromSegment = seg + case seg.addr >= 0x3C000000 && seg.addr < 0x3E000000: + if esp32s3DromSegment != nil { + return fmt.Errorf("ESP32-S3: multiple DROM segments") + } + esp32s3DromSegment = seg + } + } + + if esp32s3IromSegment == nil { + return fmt.Errorf("ESP32-S3: IROM segment not found") + } + if esp32s3DromSegment == nil { + return fmt.Errorf("ESP32-S3: DROM segment not found") + } + + ramImageSize := 24 + if makeImage { + ramImageSize += 4096 + } + for _, seg := range segments { + ramImageSize += 8 + len(seg.data) + } + ramImageSize += 16 - ramImageSize%16 + ramImageSize += 32 + + const pageSize = 0x10000 + alignUp := func(value int) int { + return (value + pageSize - 1) &^ (pageSize - 1) + } + + esp32s3IromFlashAddr = uint32(alignUp(ramImageSize)) + esp32s3DromFlashAddr = esp32s3IromFlashAddr + + uint32(alignUp(len(esp32s3IromSegment.data))) + + syms, err := inf.Symbols() + if err != nil { + return fmt.Errorf("ESP32-S3: failed to read symbols: %w", err) + } + + patchSymbol := func(name string, value uint32) error { + var symbolAddr uint64 + found := false + + for _, sym := range syms { + if sym.Name == name { + symbolAddr = sym.Value + found = true + break + } + } + if !found { + return fmt.Errorf("ESP32-S3: symbol %s not found", name) + } + + for _, seg := range segments { + start := uint64(seg.addr) + end := start + uint64(len(seg.data)) + if symbolAddr >= start && symbolAddr+4 <= end { + offset := int(symbolAddr - start) + binary.LittleEndian.PutUint32(seg.data[offset:], value) + return nil + } + } + + return fmt.Errorf( + "ESP32-S3: symbol %s (0x%x) not in a RAM segment", + name, + symbolAddr, + ) + } + + if err := patchSymbol("_irom_flash_addr", esp32s3IromFlashAddr); err != nil { + return err + } + if err := patchSymbol("_drom_flash_addr", esp32s3DromFlashAddr); err != nil { + return err + } + } + // Calculate checksum over the segment data. This is used in the image // footer. checksum := uint8(0xef) @@ -264,7 +367,7 @@ 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 dromFlashAddr := esp32DromFlashAddr @@ -319,6 +422,22 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + if chip == "esp32s3" && len(flashSegments) > 0 { + iromOffset := int(esp32s3IromFlashAddr) + if outf.Len() > iromOffset { + return fmt.Errorf("ESP32-S3: RAM image overlaps IROM") + } + outf.Write(make([]byte, iromOffset-outf.Len())) + outf.Write(esp32s3IromSegment.data) + + dromOffset := int(esp32s3DromFlashAddr) + if outf.Len() > dromOffset { + return fmt.Errorf("ESP32-S3: IROM overlaps DROM") + } + outf.Write(make([]byte, dromOffset-outf.Len())) + outf.Write(esp32s3DromSegment.data) + } + // QEMU (or more precisely, qemu-system-xtensa from Espressif) expects the // image to be a certain size. if makeImage { diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index 0d4d1e21bb..4195a20713 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -87,15 +87,23 @@ .long 0x600C4064 .Ldcache_ctrl1_reg: .long 0x600C4004 -// End-of-section symbols for multi-page MMU mapping. +// Flash section bounds and builder-patched physical offsets. +.Lirom_start: + .long _irom_start .Lirom_end: .long _irom_end +.Ldrom_start: + .long _drom_start .Ldrom_end: .long _drom_end .Lirom_base: .long 0x42000000 .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: @@ -264,51 +272,76 @@ 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 + add a7, a7, a7 // a7 = 0x800, full MMU table size + 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 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. - // - // IROM: map pages 0..N where N = (_irom_end - 0x42000000) >> 16 - // DROM: map pages 0..M where M = (_drom_end - 0x3C000000) >> 16 - - 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 + // 4i. Map physical flash pages into the shared MMU table. + l32r a8, .Lmmu_table_base + + // --- IROM --- + // Number of pages covering [_irom_start, _irom_end). + l32r a2, .Lirom_end + l32r a3, .Lirom_start + sub a2, a2, a3 + addi a2, a2, -1 + srli a2, a2, 16 + addi a2, a2, 1 + + // First physical IROM page. + l32r a3, .Lirom_flash_addr_ptr + l32i a9, a3, 0 + srli a9, a9, 16 + + mov a10, a8 + movi a11, 0 .Lirom_loop: s32i a9, a10, 0 addi a9, a9, 1 addi a10, a10, 4 - 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 - 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) + addi a11, a11, 1 + blt a11, a2, .Lirom_loop + + // --- DROM --- + // First DROM table entry is encoded by its virtual page. + l32r a2, .Ldrom_start + l32r a3, .Ldrom_base + sub a2, a2, a3 + srli a2, a2, 16 + slli a10, a2, 2 + add a10, a8, a10 + + // Number of pages covering [_drom_start, _drom_end). + l32r a2, .Ldrom_end + l32r a3, .Ldrom_start + sub a2, a2, a3 + addi a2, a2, -1 + srli a2, a2, 16 + addi a2, a2, 1 + + // First physical DROM page. + l32r a3, .Ldrom_flash_addr_ptr + l32i a9, a3, 0 + srli a9, a9, 16 + + movi a11, 0 .Ldrom_loop: s32i a9, a10, 0 addi a9, a9, 1 addi a10, a10, 4 - blt a9, a2, .Ldrom_loop + addi a11, a11, 1 + blt a11, a2, .Ldrom_loop memw // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index ff6a22fe6d..571da44d90 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -29,23 +29,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. */ @@ -73,6 +56,13 @@ SECTIONS _sdata = ABSOLUTE(.); *(.data .data.*) *(.dram*) + + /* Physical flash offsets patched by the ESP image builder. */ + _irom_flash_addr = ABSOLUTE(.); + LONG(0); + _drom_flash_addr = ABSOLUTE(.); + LONG(0); + . = ALIGN (4); _edata = ABSOLUTE(.); } >DRAM @@ -121,24 +111,29 @@ 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 + /DISCARD/ : { *(.eh_frame) From e3b24990263d6ddee0b060b4272f3d06223ff694 Mon Sep 17 00:00:00 2001 From: rdon-key Date: Mon, 12 Oct 2026 19:18:49 +0900 Subject: [PATCH 2/2] esp32s3: avoid unnecessary flash image padding The ESP32-S3 image builder aligns IROM and DROM to 64 KB boundaries, introducing approximately 124 KB of unnecessary padding. The flash MMU only requires the lower 16 bits of the physical flash offset and virtual address to match. Calculate the flash offsets accordingly and update the linker script and startup MMU mapping to support the resulting layout. --- builder/esp.go | 58 +++++++++++++++++++++++++++++++++++----- src/device/esp/esp32s3.S | 17 +++++++----- targets/esp32s3.ld | 48 +++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/builder/esp.go b/builder/esp.go index 929243b88c..cff9004118 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -162,6 +162,8 @@ func makeESPFirmwareImage(infile, outfile, format string) error { var esp32s3DromSegment *espImageSegment var esp32s3IromFlashAddr uint32 var esp32s3DromFlashAddr uint32 + var esp32s3IromOffset int + var esp32s3DromOffset int if chip == "esp32s3" && len(flashSegments) > 0 { for _, seg := range flashSegments { @@ -197,13 +199,55 @@ func makeESPFirmwareImage(infile, outfile, format string) error { ramImageSize += 32 const pageSize = 0x10000 - alignUp := func(value int) int { - return (value + pageSize - 1) &^ (pageSize - 1) + + // Return the first offset at or after minOffset whose low 16 bits + // match the virtual address. The MMU maps whole 64KB pages, so the + // offset within the page must be identical in flash and virtual + // address space. + offsetForVMA := func(minOffset int, addr uint32) int { + offset := (minOffset &^ (pageSize - 1)) + + int(addr&uint32(pageSize-1)) + if offset < minOffset { + offset += pageSize + } + return offset + } + + esp32s3IromOffset = offsetForVMA( + ramImageSize, + esp32s3IromSegment.addr, + ) + esp32s3DromOffset = offsetForVMA( + esp32s3IromOffset+len(esp32s3IromSegment.data), + esp32s3DromSegment.addr, + ) + + // Normal .bin images should not contain excessive padding. + // Allow small gaps required for section alignment. + if !makeImage && esp32s3IromOffset != ramImageSize { + return fmt.Errorf( + "ESP32-S3: unexpected padding before IROM: image ends at 0x%x, IROM starts at 0x%x", + ramImageSize, + esp32s3IromOffset, + ) + } + + iromEnd := esp32s3IromOffset + len(esp32s3IromSegment.data) + dromPadding := esp32s3DromOffset - iromEnd + + if !makeImage && dromPadding >= 16 { + return fmt.Errorf( + "ESP32-S3: excessive padding before DROM: %d bytes", + dromPadding, + ) } - esp32s3IromFlashAddr = uint32(alignUp(ramImageSize)) - esp32s3DromFlashAddr = esp32s3IromFlashAddr + - uint32(alignUp(len(esp32s3IromSegment.data))) + // Startup code needs the physical page containing each segment, + // not the exact byte offset within that page. + esp32s3IromFlashAddr = + uint32(esp32s3IromOffset) &^ uint32(pageSize-1) + esp32s3DromFlashAddr = + uint32(esp32s3DromOffset) &^ uint32(pageSize-1) syms, err := inf.Symbols() if err != nil { @@ -423,14 +467,14 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } if chip == "esp32s3" && len(flashSegments) > 0 { - iromOffset := int(esp32s3IromFlashAddr) + iromOffset := esp32s3IromOffset if outf.Len() > iromOffset { return fmt.Errorf("ESP32-S3: RAM image overlaps IROM") } outf.Write(make([]byte, iromOffset-outf.Len())) outf.Write(esp32s3IromSegment.data) - dromOffset := int(esp32s3DromFlashAddr) + dromOffset := esp32s3DromOffset if outf.Len() > dromOffset { return fmt.Errorf("ESP32-S3: IROM overlaps DROM") } diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index 4195a20713..498825e060 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -291,9 +291,11 @@ call_start_cpu0: l32r a8, .Lmmu_table_base // --- IROM --- - // Number of pages covering [_irom_start, _irom_end). + // Number of virtual pages occupied by IROM. IROM starts within + // virtual page zero, so count from the IROM virtual base rather + // than from _irom_start. l32r a2, .Lirom_end - l32r a3, .Lirom_start + l32r a3, .Lirom_base sub a2, a2, a3 addi a2, a2, -1 srli a2, a2, 16 @@ -318,18 +320,21 @@ call_start_cpu0: l32r a2, .Ldrom_start l32r a3, .Ldrom_base sub a2, a2, a3 - srli a2, a2, 16 - slli a10, a2, 2 + srli a11, a2, 16 // a11 = first DROM virtual page + slli a10, a11, 2 add a10, a8, a10 - // Number of pages covering [_drom_start, _drom_end). + // One-past-last DROM virtual page. l32r a2, .Ldrom_end - l32r a3, .Ldrom_start + l32r a3, .Ldrom_base sub a2, a2, a3 addi a2, a2, -1 srli a2, a2, 16 addi a2, a2, 1 + // Number of pages covering [_drom_start, _drom_end). + sub a2, a2, a11 + // First physical DROM page. l32r a3, .Ldrom_flash_addr_ptr l32i a9, a3, 0 diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index 571da44d90..153c12901c 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -111,19 +111,63 @@ SECTIONS _iram_end = .; } >IRAM + /* + * Size of the ESP image containing the RAM-loaded segments. + * Flash-mapped IROM/DROM data is appended after this image. + * + * 0x18: image header + * 0x08: segment header for each non-empty RAM segment + * 0x20: SHA256 digest + * + * The image builder always advances to the next 16-byte boundary + * before appending the digest. + */ + _ram_image_payload_size = + 0x18 + + SIZEOF(.data) + + ((SIZEOF(.data) != 0) ? 0x08 : 0) + + SIZEOF(.iram) + + ((SIZEOF(.iram) != 0) ? 0x08 : 0); + + _ram_image_size = + _ram_image_payload_size + + 16 - (_ram_image_payload_size % 16) + + 0x20; + + /* + * Keep the low 16 bits of the IROM VMA equal to its physical flash + * offset. The MMU maps 64KB pages, so the segment itself does not + * need to start on a page boundary. + */ + .text_dummy (NOLOAD): ALIGN(4) + { + . += _ram_image_size % 0x10000; + } >IROM + /* IROM occupies the first entries in the shared flash MMU table. */ .text : ALIGN(4) { _irom_start = .; *(.literal .text) *(.literal.* .text.*) + . = ALIGN(4); _irom_end = .; } >IROM - /* DROM starts after all MMU pages reserved for IROM. */ + /* + * DROM uses MMU entries immediately after those occupied by IROM. + * Its low 16 VMA bits must also match its physical flash offset. + */ + _irom_mmu_pages = + ((_irom_end - ORIGIN(IROM) + 0xFFFF) / 0x10000); + + _drom_flash_offset = + _ram_image_size + SIZEOF(.text); + .rodata_dummy (NOLOAD): ALIGN(4) { - . += ((SIZEOF(.text) + 0xFFFF) / 0x10000) * 0x10000; + . += _irom_mmu_pages * 0x10000; + . += _drom_flash_offset % 0x10000; } >DROM .rodata : ALIGN(4)