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