diff --git a/builder/esp.go b/builder/esp.go index 739035c089..ec27df1102 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) @@ -150,6 +161,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) @@ -265,7 +368,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 @@ -314,6 +417,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 6a08f29983..81a1c32ca5 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)