Skip to content
Merged
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
177 changes: 169 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,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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
90 changes: 69 additions & 21 deletions src/device/esp/esp32s3.S
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions src/machine/machine_esp32xx_flash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions targets/esp32c3.ld
Original file line number Diff line number Diff line change
Expand Up @@ -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/ :
{
Expand Down
Loading
Loading