Skip to content
Closed
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
179 changes: 171 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 @@ -147,6 +158,142 @@ 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

// 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,
)
}

// 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)
Expand Down Expand Up @@ -264,7 +411,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 @@ -319,6 +466,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 {
Expand Down
106 changes: 72 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,81 @@ 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 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_base
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 a11, a2, 16 // a11 = first DROM virtual page
slli a10, a11, 2
add a10, a8, a10

// One-past-last DROM virtual page.
l32r a2, .Ldrom_end
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
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
Loading
Loading