From 9dff050f290a2e92072d791a051804d7f0ed3606 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 23 Oct 2026 14:41:38 +0200 Subject: [PATCH] esp32s3: boot images with large flash segments The ROM rejects an image with an IROM or DROM segment of 1 MiB or more. Keep IROM and DROM out of the segments the ROM loads. They follow the RAM part of the image, and the startup code maps them with the MMU. machine.Flash uses the MMU entries after DROM and gets its physical flash offset from the linker script. Based on tinygo-org/tinygo#5567 by rdon-key. --- builder/esp.go | 177 +++++++++++++++++++++++++-- src/device/esp/esp32s3.S | 90 ++++++++++---- src/machine/machine_esp32xx_flash.go | 10 +- targets/esp32c3.ld | 1 + targets/esp32s3.ld | 91 +++++++++----- 5 files changed, 306 insertions(+), 63 deletions(-) diff --git a/builder/esp.go b/builder/esp.go index 9f6545623e..fa96dd5b5e 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,140 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + var esp32s3IromSegment *espImageSegment + 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 { + 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 + + // First offset at or after minOffset with the same low 16 bits as + // addr, since the MMU maps whole 64 KB pages. + 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, + ) + } + + // 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 { + 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 +409,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 +464,22 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + if chip == "esp32s3" && len(flashSegments) > 0 { + 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 := esp32s3DromOffset + 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 cc654b5baa..1256c68760 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -18,7 +18,7 @@ // 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 pages for IROM and DROM +// h. Write MMU entries mapping flash pages for IROM, DROM and machine.Flash // i. Clear bus-shut bits // j. Enable caches + isync @@ -87,11 +87,24 @@ .long 0x600C4064 .Ldcache_ctrl1_reg: .long 0x600C4004 -// End of the flash area for MMU mapping. -.Lflash_map_end: - .long _flash_map_end - 1 -.Ldrom_base: - .long 0x3C000000 +// Flash MMU layout from targets/esp32s3.ld. The builder patches the physical +// IROM and DROM offsets into _irom_flash_addr and _drom_flash_addr. +.Lirom_flash_addr: + .long _irom_flash_addr +.Lirom_mmu_pages: + .long _irom_mmu_pages +.Ldrom_flash_addr: + .long _drom_flash_addr +.Ldrom_mmu_index: + .long _drom_mmu_index +.Ldrom_mmu_pages: + .long _drom_mmu_pages +.Lflash_data_phys: + .long __flash_data_phys +.Lflash_data_mmu_index: + .long _flash_data_mmu_index +.Lflash_data_mmu_pages: + .long _flash_data_mmu_pages .global call_start_cpu0 call_start_cpu0: @@ -260,28 +273,63 @@ 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. IROM and DROM share one table of 512 entries. IROM uses the + // entries before DROM, see mmu_ll_get_entry_id in esp-idf mmu_ll.h. + l32r a6, .Ldrom_mmu_index + slli a6, a6, 2 // a6 = IROM table size in bytes + movi a7, 0x7FF + addi a7, a7, 1 + sub a7, a7, a6 // a7 = DROM table size in bytes mov a5, a1 l32r a4, .LCache_Set_IDROM_MMU_Size callx4 a4 - // 4i. Identity map flash up to _flash_map_end. IROM and DROM share entries, - // see mmu_ll_get_entry_id in esp-idf hal/esp32s3/include/hal/mmu_ll.h. - l32r a2, .Lflash_map_end // a2 = last mapped byte (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) - l32r a10, .Lmmu_table_base // a10 = current MMU entry pointer -.Lmmu_loop: + // 4i. Map IROM, DROM and the machine.Flash area. Each loop writes a2 + // entries from a10 with physical pages counting up from a9. + l32r a8, .Lmmu_table_base + + mov a10, a8 + l32r a9, .Lirom_flash_addr + l32i a9, a9, 0 + srli a9, a9, 16 + l32r a2, .Lirom_mmu_pages +.Lirom_map_loop: + beqz a2, .Lirom_map_done s32i a9, a10, 0 addi a9, a9, 1 addi a10, a10, 4 - blt a9, a2, .Lmmu_loop + addi a2, a2, -1 + j .Lirom_map_loop +.Lirom_map_done: + + l32r a10, .Ldrom_mmu_index + addx4 a10, a10, a8 + l32r a9, .Ldrom_flash_addr + l32i a9, a9, 0 + srli a9, a9, 16 + l32r a2, .Ldrom_mmu_pages +.Ldrom_map_loop: + beqz a2, .Ldrom_map_done + s32i a9, a10, 0 + addi a9, a9, 1 + addi a10, a10, 4 + addi a2, a2, -1 + j .Ldrom_map_loop +.Ldrom_map_done: + + l32r a10, .Lflash_data_mmu_index + addx4 a10, a10, a8 + l32r a9, .Lflash_data_phys + srli a9, a9, 16 + l32r a2, .Lflash_data_mmu_pages +.Lflash_data_map_loop: + beqz a2, .Lflash_data_map_done + s32i a9, a10, 0 + addi a9, a9, 1 + addi a10, a10, 4 + addi a2, a2, -1 + j .Lflash_data_map_loop +.Lflash_data_map_done: memw // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. diff --git a/src/machine/machine_esp32xx_flash.go b/src/machine/machine_esp32xx_flash.go index 8368021f17..c1b5a93990 100644 --- a/src/machine/machine_esp32xx_flash.go +++ b/src/machine/machine_esp32xx_flash.go @@ -85,12 +85,14 @@ func readAddress(off int64) uintptr { return FlashDataStart() + uintptr(off) } -const flashDROMStart = 0x3C000000 +// Physical flash offset of FlashDataStart, set by the linker script. +// +//go:extern __flash_data_phys +var flashDataPhys [0]byte // return the correct physical address to be used for write/erase func writeAddress(off int64) uint32 { - // DROM maps 1:1 with flash physical offset, starting at 0x3C000000. - return uint32(readAddress(off) - flashDROMStart) + return uint32(uintptr(unsafe.Pointer(&flashDataPhys))) + uint32(off) } func (f flashBlockDevice) writeAt(p []byte, off int64) (n int, err error) { @@ -116,7 +118,7 @@ func (f flashBlockDevice) writeAt(p []byte, off int64) (n int, err error) { func (f flashBlockDevice) eraseBlocks(start, length int64) error { address := writeAddress(start * f.EraseBlockSize()) - if uintptr(unsafe.Add(unsafe.Pointer(uintptr(address)+flashDROMStart), length*f.EraseBlockSize())) > FlashDataEnd() { + if readAddress((start+length)*f.EraseBlockSize()) > FlashDataEnd() { return errFlashCannotErasePastEOF } diff --git a/targets/esp32c3.ld b/targets/esp32c3.ld index 982f1cff5e..d57506ba8f 100644 --- a/targets/esp32c3.ld +++ b/targets/esp32c3.ld @@ -170,6 +170,7 @@ SECTIONS /* The 64 bytes skip the image footer written by builder/esp.go. */ PROVIDE(__flash_data_start = ALIGN(_irom_end - 0x42000000 + 0x3C000000 + 64, 4096)); PROVIDE(__flash_data_end = 0x3C000000 + 4M); + PROVIDE(__flash_data_phys = __flash_data_start - 0x3C000000); /DISCARD/ : { diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index dceb72bac6..d627f3b1d1 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. */ @@ -76,6 +59,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 @@ -126,24 +116,58 @@ SECTIONS _iram_end = .; } >IRAM - /* Dummy section to put the IROM segment at the correct flash offset. */ + /* Size of the RAM part of the image. Header, segments, padding to 16 bytes + * and the SHA-256 digest, as written by builder/esp.go. */ + _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; + + /* The MMU maps 64 KB pages, so the low 16 bits of the IROM VMA must match + * its flash offset. IROM follows the RAM part of the image. */ .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 + . += _ram_image_size % 0x10000; + } >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.*) + . = ALIGN(4); _irom_end = .; } >IROM + /* DROM uses the MMU entries after IROM, with the same low 16 bit rule. */ + _irom_mmu_pages = + ((_irom_end - ORIGIN(IROM) + 0xFFFF) / 0x10000); + + _drom_flash_offset = + _ram_image_size + SIZEOF(.text); + + .rodata_dummy (NOLOAD): ALIGN(4) + { + . += _irom_mmu_pages * 0x10000; + . += _drom_flash_offset % 0x10000; + } >DROM + + .rodata : ALIGN(4) + { + _drom_start = .; + *(.rodata*) + . = ALIGN(4); + _drom_end = .; + } >DROM + /DISCARD/ : { *(.eh_frame) @@ -178,11 +202,18 @@ _heap_end = 0x3fceb710; _stack_size = 4K; -/* IROM and DROM both map flash 1:1 from offset 0, see esp32s3.S. - * The 64 bytes skip the image footer written by builder/esp.go. */ -PROVIDE(__flash_data_start = ALIGN(_irom_end - 0x42000000 + 0x3C000000 + 64, 4096)); -PROVIDE(__flash_data_end = 0x3C000000 + 4M); -_flash_map_end = MAX(_irom_end - 0x42000000 + 0x3C000000, __flash_data_end); +/* MMU entries for esp32s3.S. DROM follows IROM in the shared table, and the + * machine.Flash area follows DROM. It starts at the first 4K sector after DROM. */ +_drom_mmu_index = (_drom_start - 0x3C000000) / 0x10000; +_drom_mmu_pages = (_drom_end - 0x3C000000 + 0xFFFF) / 0x10000 - _drom_mmu_index; +_drom_flash_start = _drom_flash_offset - _drom_flash_offset % 0x10000 + _drom_start % 0x10000; +_drom_flash_end = (_drom_flash_start < _drom_flash_offset ? _drom_flash_start + 0x10000 : _drom_flash_start) + (_drom_end - _drom_start); +__flash_data_phys = ALIGN(_drom_flash_end, 4096); +_flash_data_phys_end = MAX(__flash_data_phys, 4M); +_flash_data_mmu_index = _drom_mmu_index + _drom_mmu_pages; +_flash_data_mmu_pages = (_flash_data_phys_end + 0xFFFF) / 0x10000 - __flash_data_phys / 0x10000; +PROVIDE(__flash_data_start = 0x3C000000 + _flash_data_mmu_index * 0x10000 + __flash_data_phys % 0x10000); +PROVIDE(__flash_data_end = __flash_data_start + (_flash_data_phys_end - __flash_data_phys)); /* From ESP-IDF: * components/esp_rom/esp32/ld/esp32.rom.newlib-funcs.ld