Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 127 additions & 8 deletions builder/esp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
101 changes: 67 additions & 34 deletions src/device/esp/esp32s3.S
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 23 additions & 28 deletions targets/esp32s3.ld
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading