diff --git a/GNUmakefile b/GNUmakefile index 627640a520..56f91ffabb 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1019,6 +1019,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-box-3 examples/blinky1 @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32s3-psram-octal examples/psram + @$(MD5SUM) test.bin endif # esp32c3-supermini $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 diff --git a/builder/esp.go b/builder/esp.go index 739035c089..a855311d93 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -23,6 +23,30 @@ type espImageSegment struct { data []byte } +// ESP32-S3 flash-mapped virtual address windows (esp-idf +// soc/esp32s3/ext_mem_defs.h, narrowed to what targets/esp32s3.ld uses). +// Segments in these ranges are XIP'd from flash through the cache MMU +// instead of being loaded into RAM by the ROM bootloader. +const ( + esp32s3DromLow = 0x3C000000 // DBUS window; its upper half (0x3D000000+) is PSRAM, not flash. + esp32s3DromHigh = 0x3D000000 + esp32s3IromLow = 0x42000000 // IBUS window. + esp32s3IromHigh = 0x44000000 +) + +const ( + // esp32FlashBase is the flash offset esptool writes the ESP32 image to. + // ESP32-S3 images are written at offset 0 instead (see + // flashBinUsingEsp32), so there image offsets are flash offsets. + esp32FlashBase = 0x1000 + + // espFlashPageSize is the flash cache MMU page size. The MMU supports + // page sizes down to 256 B, but 64 KiB is the reset/default value and is + // what the startup code relies on. If the startup code ever changes the + // page size, this constant must change with it. + espFlashPageSize = 0x10000 +) + // makeESPFirmwareImage converts an input ELF file to an image file for an ESP32 or // ESP8266 chip. This is a special purpose image format just for the ESP chip // family, and is parsed by the on-chip mask ROM bootloader. @@ -79,16 +103,30 @@ 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": + // The DBUS cache window runs to 0x3E000000, but its upper half is + // reserved for PSRAM (targets/esp32s3.ld), which is never backed by + // flash. Only the DROM half below 0x3D000000 is flash-mapped. 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 >= esp32s3DromLow && seg.addr < esp32s3DromHigh) || + (seg.addr >= esp32s3IromLow && seg.addr < esp32s3IromHigh) { flashSegments = append(flashSegments, seg) } else { ramSegments = append(ramSegments, seg) @@ -100,53 +138,54 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // ESP32 flash XIP: compute where the DROM segment will be placed in flash // (page-aligned, right after the RAM segments) and patch the // _drom_flash_addr variable so the startup code can program the cache MMU. - // This must happen before the checksum/hash are computed so the patched - // value is covered by both. - const esp32FlashBase = 0x1000 // esptool flashes the image at 0x1000 - // The ESP32 flash cache MMU supports configurable page sizes down to 256 B. 64 KiB is the reset/default size. - // If the startup code ever changes the MMU page size, this constant must change too. - const esp32PageSize = 0x10000 // 64KB MMU pages var esp32DromFlashAddr uint32 if chip == "esp32" && len(flashSegments) > 0 { - // Compute the size of the RAM portion of the image (everything the ROM - // bootloader loads, up to and including the appended SHA256 hash). - ramImageSize := 0 - if makeImage { - ramImageSize += 4096 + esp32DromFlashAddr = uint32(alignUpFlashPage(esp32FlashBase + ramImageSize(segments, makeImage))) + + syms, err := inf.Symbols() + if err != nil { + return fmt.Errorf("ESP32: %w", err) } - ramImageSize += 24 // image header (8) + trailer fields (16) - for _, seg := range segments { - ramImageSize += 8 + len(seg.data) // segment header + data (4-aligned) + if err := patchFlashAddr(syms, segments, "_drom_flash_addr", esp32DromFlashAddr); err != nil { + return fmt.Errorf("ESP32: %w", err) } - ramImageSize += 16 - ramImageSize%16 // footer padding + checksum byte - ramImageSize += 32 // appended SHA256 hash - - // DROM flash address must be 64KB page-aligned. - esp32DromFlashAddr = uint32(esp32FlashBase+ramImageSize+esp32PageSize-1) &^ (esp32PageSize - 1) - - // Patch _drom_flash_addr in whichever RAM segment contains it. - syms, _ := inf.Symbols() - var dromSymAddr uint64 - for _, s := range syms { - if s.Name == "_drom_flash_addr" { - dromSymAddr = s.Value - break - } + } + + // ESP32-S3 flash XIP: the ROM bootloader rejects images with a DROM + // segment over 1MB, so IROM and DROM are kept out of the segment table + // (see above) and appended at page-aligned flash offsets instead. Those + // offsets are patched into the RAM image for the startup code to program + // the cache MMU with. + var esp32s3Irom, esp32s3Drom *espImageSegment + var esp32s3IromFlashAddr, esp32s3DromFlashAddr uint32 + if chip == "esp32s3" && len(flashSegments) > 0 { + var err error + esp32s3Irom, err = singleFlashSegment(flashSegments, esp32s3IromLow, esp32s3IromHigh, "IROM") + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - if dromSymAddr == 0 { - return fmt.Errorf("ESP32: _drom_flash_addr symbol not found") + esp32s3Drom, err = singleFlashSegment(flashSegments, esp32s3DromLow, esp32s3DromHigh, "DROM") + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - patched := false - for _, seg := range segments { - if dromSymAddr >= uint64(seg.addr) && dromSymAddr+4 <= uint64(seg.addr)+uint64(len(seg.data)) { - off := int(dromSymAddr - uint64(seg.addr)) - binary.LittleEndian.PutUint32(seg.data[off:], esp32DromFlashAddr) - patched = true - break - } + + // The image is flashed at offset 0, so image offsets are flash + // offsets. IROM goes right after the RAM image and DROM right after + // IROM, each rounded up to an MMU page. The linker script rounds the + // virtual addresses up the same way, so the offsets within a page + // match on both sides of the mapping. + esp32s3IromFlashAddr = uint32(alignUpFlashPage(ramImageSize(segments, makeImage))) + esp32s3DromFlashAddr = esp32s3IromFlashAddr + uint32(alignUpFlashPage(len(esp32s3Irom.data))) + + syms, err := inf.Symbols() + if err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } - if !patched { - return fmt.Errorf("ESP32: _drom_flash_addr (0x%x) not in any RAM segment", dromSymAddr) + if err := patchFlashAddr(syms, segments, "_irom_flash_addr", esp32s3IromFlashAddr); err != nil { + return fmt.Errorf("ESP32-S3: %w", err) + } + if err := patchFlashAddr(syms, segments, "_drom_flash_addr", esp32s3DromFlashAddr); err != nil { + return fmt.Errorf("ESP32-S3: %w", err) } } @@ -265,9 +304,9 @@ 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 + const pageSize = espFlashPageSize dromFlashAddr := esp32DromFlashAddr // Separate DROM and IROM segments. @@ -314,6 +353,27 @@ func makeESPFirmwareImage(infile, outfile, format string) error { } } + // For ESP32-S3: append the XIP segments at the flash offsets patched into + // the image above. Both are page-aligned and in ascending order, so each + // one only needs padding up to its own offset. + if chip == "esp32s3" && len(flashSegments) > 0 { + for _, region := range []struct { + name string + offset uint32 + segment *espImageSegment + }{ + {"IROM", esp32s3IromFlashAddr, esp32s3Irom}, + {"DROM", esp32s3DromFlashAddr, esp32s3Drom}, + } { + if outf.Len() > int(region.offset) { + return fmt.Errorf("ESP32-S3: image is %d bytes, overlapping %s at flash offset 0x%x", + outf.Len(), region.name, region.offset) + } + outf.Write(make([]byte, int(region.offset)-outf.Len())) + outf.Write(region.segment.data) + } + } + // QEMU (or more precisely, qemu-system-xtensa from Espressif) expects the // image to be a certain size. if makeImage { @@ -327,3 +387,81 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // Write the image to the output file. return os.WriteFile(outfile, outf.Bytes(), 0666) } + +// alignUpFlashPage rounds size up to the next flash cache MMU page boundary. +func alignUpFlashPage(size int) int { + return (size + espFlashPageSize - 1) &^ (espFlashPageSize - 1) +} + +// ramImageSize returns the size of the part of the image that the ROM +// bootloader loads: the header, the segment headers and their data, the +// footer holding the checksum, and the appended SHA256 hash. +// +// Flash-mapped (XIP) segments are appended after this portion, and their +// flash offsets have to be known before the image is written, because they +// are patched into the image itself. This function must therefore predict +// exactly what makeESPFirmwareImage writes; keep the two in sync. +func ramImageSize(segments []*espImageSegment, makeImage bool) int { + size := 0 + if makeImage { + size += 4096 // padding in front of the image header + } + size += 24 // image header (8) + trailer fields (16) + for _, segment := range segments { + size += 8 + len(segment.data) // segment header + data (4-aligned) + } + size += 16 - size%16 // footer padding + checksum byte + size += 32 // appended SHA256 hash + return size +} + +// patchFlashAddr stores value in the 32-bit variable named by symbol, which +// must live in one of the RAM segments. The startup code reads it to program +// the flash cache MMU. Patching must happen before the checksum and hash are +// computed, so that the patched value is covered by both. +func patchFlashAddr(syms []elf.Symbol, segments []*espImageSegment, symbol string, value uint32) error { + var symbolAddr uint64 + found := false + for _, sym := range syms { + if sym.Name == symbol { + symbolAddr = sym.Value + found = true + break + } + } + if !found { + return fmt.Errorf("symbol %s not found", symbol) + } + + for _, segment := range segments { + start := uint64(segment.addr) + end := start + uint64(len(segment.data)) + if symbolAddr >= start && symbolAddr+4 <= end { + binary.LittleEndian.PutUint32(segment.data[symbolAddr-start:], value) + return nil + } + } + return fmt.Errorf("symbol %s (0x%x) not in a RAM segment", symbol, symbolAddr) +} + +// singleFlashSegment returns the one segment inside the [low, high) virtual +// address window, named name in error messages. The startup code maps each +// XIP region as a single run of MMU pages and the linker script sizes the +// regions accordingly, so anything other than exactly one segment per window +// means the two have drifted apart and the image would not boot. +func singleFlashSegment(segments []*espImageSegment, low, high uint32, name string) (*espImageSegment, error) { + var found *espImageSegment + for _, segment := range segments { + if segment.addr < low || segment.addr >= high { + continue + } + if found != nil { + return nil, fmt.Errorf("expected a single %s segment, found more than one", name) + } + found = segment + } + if found == nil { + return nil, fmt.Errorf("%s segment not found", name) + } + return found, nil +} diff --git a/builder/esp_test.go b/builder/esp_test.go new file mode 100644 index 0000000000..e3acc9dfbf --- /dev/null +++ b/builder/esp_test.go @@ -0,0 +1,343 @@ +package builder + +// Tests for the ESP32-S3 image layout in makeESPFirmwareImage. The ESP32-S3 +// keeps DROM/IROM out of the ROM-loaded segment table (the ROM bootloader +// rejects images with a DROM segment over 1MB) and appends them at +// 64KB-aligned flash offsets, patching those offsets into the RAM image so +// the startup code can program the cache MMU. +// +// The input ELF is synthesized here rather than compiled, so these tests run +// without an Xtensa toolchain. + +import ( + "bytes" + "crypto/sha256" + "debug/elf" + "encoding/binary" + "os" + "path/filepath" + "testing" +) + +// elf32Section describes one allocated section of the synthetic ELF. +type elf32Section struct { + name string + addr uint32 + data []byte + flags elf.SectionFlag +} + +// writeTestELF writes a minimal 32-bit little-endian Xtensa ELF containing +// the given allocated PROGBITS sections plus a symbol table defining syms +// (name -> address). It returns the file path. +func writeTestELF(t *testing.T, entry uint32, sections []elf32Section, syms map[string]uint32) string { + t.Helper() + + const ( + ehSize = 52 // Elf32_Ehdr + shSize = 40 // Elf32_Shdr + symSize = 16 // Elf32_Sym + ) + + // Section 0 is the mandatory null entry, followed by the allocated + // sections, then .symtab, .strtab and .shstrtab. + symtabIdx := len(sections) + 1 + strtabIdx := symtabIdx + 1 + shstrtabIdx := strtabIdx + 1 + numSections := shstrtabIdx + 1 + + // Build the section name table. + var shstrtab []byte + shName := func(name string) uint32 { + off := uint32(len(shstrtab)) + shstrtab = append(shstrtab, name...) + shstrtab = append(shstrtab, 0) + return off + } + shstrtab = append(shstrtab, 0) + + // Build the symbol table and its string table. Symbols are attributed to + // the first section, which is enough for elf.File.Symbols. + strtab := []byte{0} + symtab := make([]byte, symSize) // index 0 is the null symbol + for name, value := range syms { + nameOff := uint32(len(strtab)) + strtab = append(strtab, name...) + strtab = append(strtab, 0) + + var sym [symSize]byte + binary.LittleEndian.PutUint32(sym[0:], nameOff) + binary.LittleEndian.PutUint32(sym[4:], value) + binary.LittleEndian.PutUint32(sym[8:], 4) + sym[12] = byte(elf.ST_INFO(elf.STB_GLOBAL, elf.STT_OBJECT)) + binary.LittleEndian.PutUint16(sym[14:], 1) + symtab = append(symtab, sym[:]...) + } + + // Lay out the file: header, section headers, then section contents. + offset := uint32(ehSize + numSections*shSize) + type placed struct { + nameOff uint32 + offset uint32 + } + allocated := make([]placed, len(sections)) + for i, section := range sections { + allocated[i] = placed{nameOff: shName(section.name), offset: offset} + offset += uint32(len(section.data)) + } + symtabName, symtabOff := shName(".symtab"), offset + offset += uint32(len(symtab)) + strtabName, strtabOff := shName(".strtab"), offset + offset += uint32(len(strtab)) + shstrtabName, shstrtabOff := shName(".shstrtab"), offset + + buf := &bytes.Buffer{} + header := make([]byte, ehSize) + copy(header, []byte{0x7f, 'E', 'L', 'F'}) + header[4] = byte(elf.ELFCLASS32) + header[5] = byte(elf.ELFDATA2LSB) + header[6] = byte(elf.EV_CURRENT) + binary.LittleEndian.PutUint16(header[16:], uint16(elf.ET_EXEC)) + binary.LittleEndian.PutUint16(header[18:], uint16(elf.EM_XTENSA)) + binary.LittleEndian.PutUint32(header[20:], uint32(elf.EV_CURRENT)) + binary.LittleEndian.PutUint32(header[24:], entry) + binary.LittleEndian.PutUint32(header[32:], ehSize) // e_shoff + binary.LittleEndian.PutUint16(header[40:], ehSize) // e_ehsize + binary.LittleEndian.PutUint16(header[46:], shSize) // e_shentsize + binary.LittleEndian.PutUint16(header[48:], uint16(numSections)) + binary.LittleEndian.PutUint16(header[50:], uint16(shstrtabIdx)) + buf.Write(header) + + writeSectionHeader := func(nameOff uint32, typ elf.SectionType, flags elf.SectionFlag, addr, off, size, link, info, entsize uint32) { + var sh [shSize]byte + binary.LittleEndian.PutUint32(sh[0:], nameOff) + binary.LittleEndian.PutUint32(sh[4:], uint32(typ)) + binary.LittleEndian.PutUint32(sh[8:], uint32(flags)) + binary.LittleEndian.PutUint32(sh[12:], addr) + binary.LittleEndian.PutUint32(sh[16:], off) + binary.LittleEndian.PutUint32(sh[20:], size) + binary.LittleEndian.PutUint32(sh[24:], link) + binary.LittleEndian.PutUint32(sh[28:], info) + binary.LittleEndian.PutUint32(sh[32:], 4) + binary.LittleEndian.PutUint32(sh[36:], entsize) + buf.Write(sh[:]) + } + + writeSectionHeader(0, elf.SHT_NULL, 0, 0, 0, 0, 0, 0, 0) + for i, section := range sections { + writeSectionHeader(allocated[i].nameOff, elf.SHT_PROGBITS, section.flags, + section.addr, allocated[i].offset, uint32(len(section.data)), 0, 0, 0) + } + writeSectionHeader(symtabName, elf.SHT_SYMTAB, 0, 0, symtabOff, uint32(len(symtab)), + uint32(strtabIdx), 1, symSize) + writeSectionHeader(strtabName, elf.SHT_STRTAB, 0, 0, strtabOff, uint32(len(strtab)), 0, 0, 0) + writeSectionHeader(shstrtabName, elf.SHT_STRTAB, 0, 0, shstrtabOff, uint32(len(shstrtab)), 0, 0, 0) + + for _, section := range sections { + buf.Write(section.data) + } + buf.Write(symtab) + buf.Write(strtab) + buf.Write(shstrtab) + + path := filepath.Join(t.TempDir(), "test.elf") + if err := os.WriteFile(path, buf.Bytes(), 0666); err != nil { + t.Fatal(err) + } + return path +} + +// esp32s3TestImage builds an ESP32-S3 image from a synthetic ELF with the +// given .text and .rodata sizes, and returns the image bytes. +func esp32s3TestImage(t *testing.T, textSize, rodataSize int) []byte { + t.Helper() + + // Mirrors targets/esp32s3.ld: .text starts at ORIGIN(IROM) and .rodata + // starts one whole 64KB page past the end of .text. + const iromBase = 0x42000000 + const dromBase = 0x3C000000 + const pageSize = 0x10000 + dromAddr := uint32(dromBase + (textSize+pageSize-1)/pageSize*pageSize) + + text := bytes.Repeat([]byte{0x11}, textSize) + rodata := bytes.Repeat([]byte{0x22}, rodataSize) + iram := bytes.Repeat([]byte{0x33}, 256) + // .data holds _irom_flash_addr and _drom_flash_addr, both zero until the + // image builder patches them. + data := make([]byte, 16) + + const dataAddr = 0x3FC88000 + sections := []elf32Section{ + {name: ".rodata", addr: dromAddr, data: rodata, flags: elf.SHF_ALLOC}, + {name: ".data", addr: dataAddr, data: data, flags: elf.SHF_ALLOC | elf.SHF_WRITE}, + {name: ".iram", addr: 0x40378000, data: iram, flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}, + {name: ".text", addr: iromBase, data: text, flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}, + } + syms := map[string]uint32{ + "_irom_flash_addr": dataAddr + 8, + "_drom_flash_addr": dataAddr + 12, + } + + infile := writeTestELF(t, 0x40378000, sections, syms) + outfile := filepath.Join(t.TempDir(), "test.bin") + if err := makeESPFirmwareImage(infile, outfile, "esp32s3"); err != nil { + t.Fatal("makeESPFirmwareImage failed:", err) + } + image, err := os.ReadFile(outfile) + if err != nil { + t.Fatal(err) + } + return image +} + +// TestESP32S3ImageLayout checks that DROM and IROM stay out of the ROM-loaded +// segment table, that they land at 64KB-aligned flash offsets, and that those +// offsets are patched into the RAM image. +func TestESP32S3ImageLayout(t *testing.T) { + const pageSize = 0x10000 + + for _, tc := range []struct { + name string + textSize int + rodataSize int + }{ + {"small", 0x800, 0x400}, + {"multipage text", 0x30000, 0x400}, + // A DROM segment over 1MB is what the ROM bootloader used to reject. + {"large rodata", 0x1000, 0x780000}, + } { + t.Run(tc.name, func(t *testing.T) { + image := esp32s3TestImage(t, tc.textSize, tc.rodataSize) + + // The ROM only sees the RAM segments: .data and .iram. + if got := image[1]; got != 2 { + t.Errorf("segment count = %d, want 2 (.data and .iram only)", got) + } + + // Both flash regions must be page-aligned, in IROM/DROM order, + // and must not overlap the RAM image. + iromAddr := binary.LittleEndian.Uint32(image[0x18+8+8:]) + dromAddr := binary.LittleEndian.Uint32(image[0x18+8+12:]) + if iromAddr%pageSize != 0 || dromAddr%pageSize != 0 { + t.Errorf("flash offsets not 64KB-aligned: irom=%#x drom=%#x", iromAddr, dromAddr) + } + if dromAddr < iromAddr+uint32(tc.textSize) { + t.Errorf("DROM at %#x overlaps IROM at %#x (%#x bytes)", dromAddr, iromAddr, tc.textSize) + } + ramImageEnd := checkRAMImage(t, image) + if int(iromAddr) < ramImageEnd { + t.Errorf("IROM at %#x overlaps the RAM image ending at %#x", iromAddr, ramImageEnd) + } + + // The image is flashed at offset 0 on the ESP32-S3, so the patched + // flash offsets are also offsets into the image, and the startup + // code reads the section contents through them. + if int(iromAddr)+tc.textSize > len(image) || int(dromAddr)+tc.rodataSize > len(image) { + t.Fatalf("image too short: %d bytes, irom=%#x drom=%#x", len(image), iromAddr, dromAddr) + } + if got := image[iromAddr]; got != 0x11 { + t.Errorf("byte at IROM offset %#x = %#x, want 0x11 (.text)", iromAddr, got) + } + if got := image[dromAddr]; got != 0x22 { + t.Errorf("byte at DROM offset %#x = %#x, want 0x22 (.rodata)", dromAddr, got) + } + }) + } +} + +// checkRAMImage walks the ROM-loaded part of the image the way the ROM +// bootloader does, verifying the segment table, the trailing checksum byte +// and the appended SHA256 hash. This also covers the flash offsets patched +// into .data, which must be in place before either is computed. It returns +// the offset just past the hash, where the XIP segments start. +func checkRAMImage(t *testing.T, image []byte) int { + t.Helper() + + if image[0] != 0xE9 { + t.Fatalf("image magic = %#x, want 0xe9", image[0]) + } + + offset := 0x18 // image header + checksum := byte(0xEF) + for i := 0; i < int(image[1]); i++ { + length := int(binary.LittleEndian.Uint32(image[offset+4:])) + offset += 8 + if offset+length > len(image) { + t.Fatalf("segment %d runs past the end of the image", i) + } + for _, b := range image[offset : offset+length] { + checksum ^= b + } + offset += length + } + + offset += 15 - offset%16 // footer padding + if got := image[offset]; got != checksum { + t.Errorf("checksum byte = %#x, want %#x", got, checksum) + } + offset++ + + want := sha256.Sum256(image[:offset]) + if got := image[offset : offset+sha256.Size]; !bytes.Equal(got, want[:]) { + t.Errorf("appended SHA256 = %x, want %x", got, want[:]) + } + return offset + sha256.Size +} + +// TestESP32S3ImageErrors checks that an ELF the startup code could not boot +// from is rejected, rather than silently producing an unbootable image. +func TestESP32S3ImageErrors(t *testing.T) { + const dataAddr = 0x3FC88000 + data := elf32Section{name: ".data", addr: dataAddr, data: make([]byte, 16), + flags: elf.SHF_ALLOC | elf.SHF_WRITE} + text := elf32Section{name: ".text", addr: 0x42000000, data: make([]byte, 64), + flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR} + rodata := elf32Section{name: ".rodata", addr: 0x3C010000, data: make([]byte, 64), + flags: elf.SHF_ALLOC} + bothSyms := map[string]uint32{ + "_irom_flash_addr": dataAddr + 8, + "_drom_flash_addr": dataAddr + 12, + } + + for _, tc := range []struct { + name string + sections []elf32Section + syms map[string]uint32 + }{ + {"no IROM", []elf32Section{rodata, data}, bothSyms}, + {"no DROM", []elf32Section{text, data}, bothSyms}, + { + // The startup code maps each region as one run of MMU pages, so + // a second output section in the window cannot be represented. + name: "two DROM sections", + sections: []elf32Section{text, rodata, data, + {name: ".rodata2", addr: 0x3C020000, data: make([]byte, 64), flags: elf.SHF_ALLOC}}, + syms: bothSyms, + }, + { + name: "missing symbol", + sections: []elf32Section{text, rodata, data}, + syms: map[string]uint32{"_drom_flash_addr": dataAddr + 12}, + }, + { + // A flash offset that is not in a RAM segment cannot be patched, + // so the startup code would read an uninitialized value. + name: "symbol outside the RAM segments", + sections: []elf32Section{text, rodata, data}, + syms: map[string]uint32{ + "_irom_flash_addr": 0x3FC99000, + "_drom_flash_addr": dataAddr + 12, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + infile := writeTestELF(t, 0x40378000, tc.sections, tc.syms) + outfile := filepath.Join(t.TempDir(), "test.bin") + if err := makeESPFirmwareImage(infile, outfile, "esp32s3"); err == nil { + t.Error("expected an error, got none") + } else { + t.Log(err) + } + }) + } +} diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index 0d4d1e21bb..f66bb9fc15 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -18,7 +18,8 @@ // 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 page 0 for IROM and DROM +// h. Write MMU entries mapping the IROM and DROM flash pages, using the +// physical flash offsets patched into .data by builder/esp.go // i. Clear bus-shut bits // j. Enable caches + isync @@ -87,15 +88,23 @@ .long 0x600C4064 .Ldcache_ctrl1_reg: .long 0x600C4004 -// End-of-section symbols for multi-page MMU mapping. +// Flash section bounds, window bases, and builder-patched physical offsets. +.Lirom_start: + .long _irom_start .Lirom_end: .long _irom_end -.Ldrom_end: - .long _drom_end .Lirom_base: .long 0x42000000 +.Ldrom_start: + .long _drom_start +.Ldrom_end: + .long _drom_end .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 +273,79 @@ 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 // movi takes a 12-bit signed immediate, + add a7, a7, a7 // so build 0x800 (full table) with an add + 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. + // 4i. Map physical flash pages into the shared MMU table. One 4-byte + // entry per 64KB page; entry value = physical flash page number + // (SOC_MMU_VALID = 0 on S3, SOC_MMU_ACCESS_SPIRAM bit clear for + // flash). The physical base of each region is patched into + // _irom_flash_addr/_drom_flash_addr by builder/esp.go, so virtual + // pages are independent of flash pages. // - // 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 -.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) -.Ldrom_loop: + // Both regions use the hand-rolled store loop below. ROM's + // Cache_Ibus_MMU_Set boots but then hangs at entry for reasons not + // yet understood -- do not swap the IROM loop for it without a new + // hypothesis. (The runtime PSRAM driver does use the ROM's + // Cache_Dbus_MMU_Set, but only after the caches are up.) + l32r a8, .Lmmu_table_base + + // map_flash_pages maps one XIP region into the shared MMU table: + // + // base_lit literal holding the base VMA of the region's cache window + // start_lit literal holding the region's start VMA (page-aligned) + // end_lit literal holding the region's end VMA + // addr_lit literal holding the address of the physical flash offset + // that builder/esp.go patched into .data + // + // Requires a8 = MMU table base. Clobbers a2, a3, a9, a10, a11. The + // region must be non-empty; targets/esp32s3.ld asserts that, because the + // page count below underflows for a zero-length region. + .macro map_flash_pages base_lit, start_lit, end_lit, addr_lit + // First table entry, from the region's virtual page in its window. + l32r a2, \start_lit + l32r a3, \base_lit + sub a2, a2, a3 + srli a2, a2, 16 + slli a10, a2, 2 + add a10, a8, a10 + + // Number of pages covering [start, end). + l32r a2, \end_lit + l32r a3, \start_lit + sub a2, a2, a3 + addi a2, a2, -1 + srli a2, a2, 16 + addi a2, a2, 1 + + // First physical flash page. + l32r a3, \addr_lit + l32i a9, a3, 0 + srli a9, a9, 16 + + movi a11, 0 +1: s32i a9, a10, 0 addi a9, a9, 1 addi a10, a10, 4 - blt a9, a2, .Ldrom_loop + addi a11, a11, 1 + blt a11, a2, 1b + .endm + + map_flash_pages .Lirom_base, .Lirom_start, .Lirom_end, .Lirom_flash_addr_ptr + map_flash_pages .Ldrom_base, .Ldrom_start, .Ldrom_end, .Ldrom_flash_addr_ptr memw // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. diff --git a/src/examples/psram/main.go b/src/examples/psram/main.go new file mode 100644 index 0000000000..e7b334543e --- /dev/null +++ b/src/examples/psram/main.go @@ -0,0 +1,60 @@ +package main + +// This example demonstrates how to use external PSRAM (Octal or Quad SPI) on +// supported targets (e.g., -target=esp32s3-psram-qspi). +// +// Variables placed in the `.psram` section via the `//go:section .psram` +// pragma are stored in external PSRAM rather than internal SRAM. +// Note: importing "unsafe" is required to enable `//go:section`. + +import ( + "fmt" + "time" + "unsafe" +) + +// Allocate a 1MB buffer explicitly in external PSRAM. +// +//go:section .psram +var psramBuffer [1024 * 1024]byte + +func main() { + time.Sleep(2 * time.Second) + + fmt.Println("=== PSRAM Example ===") + fmt.Printf("psramBuffer start address: 0x%X\n", uintptr(unsafe.Pointer(&psramBuffer[0]))) + fmt.Printf("psramBuffer size: %d bytes\n", len(psramBuffer)) + + fmt.Println("Writing test pattern to PSRAM...") + for i := range psramBuffer { + psramBuffer[i] = byte(i ^ 0xAA) + } + + fmt.Println("Verifying test pattern...") + errors := 0 + for i := range psramBuffer { + if psramBuffer[i] != byte(i^0xAA) { + errors++ + } + } + + if errors == 0 { + fmt.Println("SUCCESS: PSRAM 1MB read/write verification passed!") + } else { + fmt.Printf("FAIL: %d readback errors detected\n", errors) + printed := 0 + for i := range psramBuffer { + if psramBuffer[i] != byte(i^0xAA) { + fmt.Printf(" mismatch at idx %d (offset 0x%X): got 0x%02X, want 0x%02X\n", i, i, psramBuffer[i], byte(i^0xAA)) + printed++ + if printed >= 8 { + break + } + } + } + } + + for { + time.Sleep(time.Second) + } +} diff --git a/src/runtime/runtime_esp32_nopsram.go b/src/runtime/runtime_esp32_nopsram.go new file mode 100644 index 0000000000..ee9ee52e25 --- /dev/null +++ b/src/runtime/runtime_esp32_nopsram.go @@ -0,0 +1,6 @@ +//go:build esp32s3 && !numa_psram_octal + +package runtime + +// initPSRAM is a no-op when PSRAM support is disabled. +func initPSRAM() {} diff --git a/src/runtime/runtime_esp32_psram.go b/src/runtime/runtime_esp32_psram.go new file mode 100644 index 0000000000..e65f1422d9 --- /dev/null +++ b/src/runtime/runtime_esp32_psram.go @@ -0,0 +1,107 @@ +//go:build esp32s3 && numa_psram_octal + +// Shared PSRAM support for ESP32-S3: linker symbols, MMU/ROM-call helpers, +// and address-map documentation common to both the Quad SPI driver +// (runtime_esp32_psram_qspi.go, build tag numa_psram_qspi) and the Octal +// SPI driver (runtime_esp32_psram_octal.go, build tag numa_psram_octal). +// Everything protocol-specific (SPI1 command sequencing, chip ID/mode +// register parsing, SPI0 cache-phase configuration) lives in those files. +// +// Address map (esp-idf soc/ext_mem_defs.h; verified on hardware while +// bringing up the Octal driver): +// +// 0x600C5000 MMU table, 512 x 32-bit entries, one per 64KB page. +// A SINGLE table shared by IBUS (0x42000000 window) and DBUS +// (0x3C000000 window), indexed by linear address: +// entry = (vaddr & SOC_MMU_LINEAR_ADDR_MASK 0x1FFFFFF) >> 16. +// There is no ICache/DCache half-split: 0x3C800000 and +// 0x42800000 both decode to entry 128. +// Entry format: bits[13:0] physical page number, bit 14 = +// invalid (SOC_MMU_INVALID), bit 15 = target type, 0 = flash, +// 1 = PSRAM (SOC_MMU_ACCESS_SPIRAM). +// +// Flash XIP takes the low entries: IROM starts at entry 0 and DROM +// follows immediately after it (esp32s3.S, targets/esp32s3.ld), so +// .text + .rodata together occupy a prefix of entries 0..255, capped at +// 16M by the length of the DROM linker region. PSRAM lives in the upper +// half of the DBUS window (0x3D000000, entries 256-511), disjoint from +// all flash entries. +// +// The IROM/DROM boundary is variable, since it follows the size of +// .text. The PSRAM window sits above it either way: the boundary can +// only move down, never above 0x3D000000, so the vaddr the drivers pass +// to romCacheDbusMMUSet always stays inside whatever DROM range +// Cache_Set_IDROM_MMU_Size left behind. +// +// DCache (data cache) is the L1/L2 cache in front of the DBUS window. +// It is disabled (ROM Cache_Disable_DCache, which also invalidates all +// tag memory) while MMU entries are modified and re-enabled after, so +// that no stale cache lines survive the remap. +package runtime + +import "unsafe" + +//go:extern _spsram +var _spsram [0]byte + +//go:extern _epsram +var _epsram [0]byte + +//go:extern _psram_start +var _psram_start [0]byte + +//go:extern _psram_end +var _psram_end [0]byte + +// ROM functions baked into every ESP32-S3 chip (fixed addresses provided +// via PROVIDE() in targets/esp32s3.ld); see esp32s3/rom/cache.h. + +//export Cache_Dbus_MMU_Set +func romCacheDbusMMUSet(extRam uint32, vaddr uint32, paddr uint32, psize uint32, num uint32, fixed uint32) int32 + +// Cache_Disable_DCache / Cache_Enable_DCache: the documented-safe pairing +// around an MMU change (Cache_Disable_DCache also invalidates all DCache +// tag memory). Cache_Suspend_DCache/Resume, the alternative pairing, is +// explicitly documented as unsafe to use while changing the MMU. + +//export Cache_Disable_DCache +func romCacheDisableDCache() uint32 + +//export Cache_Enable_DCache +func romCacheEnableDCache(autoload uint32) + +const mmuAccessSpiram = 0x8000 // SOC_MMU_TYPE / SOC_MMU_ACCESS_SPIRAM: routes entry to PSRAM instead of flash. + +// psramWindow returns the linker-provided PSRAM vaddr window +// (targets/esp32s3.ld) and its capacity in 64KB MMU pages. +func psramWindow() (start, end uintptr, pageCap int) { + start = uintptr(unsafe.Pointer(&_psram_start)) + end = uintptr(unsafe.Pointer(&_psram_end)) + pageCap = int((end - start) / 65536) + return +} + +// zeroInitPSRAM zero-initializes .psram BSS after PSRAM has been mapped +// into the MMU, verifying with a known pattern first that writes actually +// reach the chip (a hardware or timing issue could otherwise silently +// drop them). +func zeroInitPSRAM() { + spsram := uintptr(unsafe.Pointer(&_spsram)) + epsram := uintptr(unsafe.Pointer(&_epsram)) + if epsram <= spsram { + return + } + + *(*uint32)(unsafe.Pointer(spsram)) = 0x5A5A5A5A + if *(*uint32)(unsafe.Pointer(spsram)) != 0x5A5A5A5A { + panic("PSRAM readback test failed") + } + *(*uint32)(unsafe.Pointer(spsram)) = 0 + if *(*uint32)(unsafe.Pointer(spsram)) != 0 { + panic("PSRAM readback test failed") + } + + for addr := spsram + 4; addr < epsram; addr += 4 { + *(*uint32)(unsafe.Pointer(addr)) = 0 + } +} diff --git a/src/runtime/runtime_esp32_psram_octal.go b/src/runtime/runtime_esp32_psram_octal.go new file mode 100644 index 0000000000..829cf975b5 --- /dev/null +++ b/src/runtime/runtime_esp32_psram_octal.go @@ -0,0 +1,472 @@ +//go:build esp32s3 && numa_psram_octal + +// Octal SPI (OPI) PSRAM support for ESP32-S3 (e.g. ESP32-S3-WROOM-1U-N16R8, +// combo modules with in-package Octal PSRAM). Octal PSRAM uses 8 data +// lines and a DTR (double transfer rate / DDR) protocol, unlike the 4-line +// single-rate Quad PSRAM handled by runtime_esp32_psram_qspi.go; the two +// are not interchangeable and use different GPIOs (Octal adds D4-D7+DQS +// on GPIO33-37, on top of the CS1/D0-D3 pins shared with flash). Shared +// linker symbols, MMU/ROM-call helpers, and the address map are +// documented in runtime_esp32_psram.go. +// +// Unlike its own Quad PSRAM driver, ESP-IDF does not hand-roll the Octal +// DTR command sequence in C -- it delegates entirely to a mask-ROM +// function (esp_rom_opiflash_exec_cmd, esp32s3/rom/opi_flash.h) that every +// ESP32-S3 chip has at a fixed address. There is no accessible reference +// for what that ROM function does internally, so this file calls it +// directly (matching esp_psram_impl_octal.c's call sites) rather than +// guessing at the DDR-mode register sequence. +// +// This driver performs ESP-IDF's DQS/MSPI-delay timing calibration +// (mspi_timing_psram_tuning / s_select_best_tuning_config_dtr). It performs an +// iterative sweep of 14 MSPI input delay and extra dummy cycle candidate +// parameters at 80 MHz DTR (160 MHz MSPI core clock) using reference pattern +// reads written during 40 MHz bring-up, selecting the optimal configuration +// to achieve 80 MHz DTR (160 MB/s bandwidth). +// +// GPIO33-37 (the Octal-only D4-D7+DQS lines) are configured by calling the +// ROM's esp_rom_opiflash_pin_config(), which reads the efuse OPI-pin +// strapping. The ROM does not do this on its own at boot: its boot-time +// pin setup follows the flash boot mode (DIO on modules with a plain Quad +// flash chip, which only needs D0/D1), and a tinygo image has no +// 2nd-stage bootloader to do it either. +package runtime + +import ( + "device/esp" + "unsafe" +) + +// ROM functions baked into every ESP32-S3 chip (fixed addresses provided +// via PROVIDE() in targets/esp32s3.ld). Declared per esp32s3/rom/opi_flash.h. + +//export esp_rom_opiflash_exec_cmd +func romOpiflashExecCmd(spiNum int32, mode int32, cmd uint32, cmdBitLen int32, addr uint32, addrBitLen int32, dummyBits int32, mosiData *byte, mosiBitLen int32, misoData *byte, misoBitLen int32, csMask uint32, isWriteErase bool) + +//export esp_rom_spi_set_dtr_swap_mode +func romSpiSetDtrSwapMode(spiNum int32, wrSwap bool, rdSwap bool) + +//export esp_rom_opiflash_pin_config +func romOpiflashPinConfig() + +// g_rom_spiflash_dummy_len_plus[1]: per-spi_num extra dummy cycle count that +// esp_rom_opiflash_exec_cmd() adds on top of the caller-supplied dummyBits. +// +//go:extern g_rom_spiflash_dummy_len_plus +var romDummyLenPlusSymbol [0]byte + +const ( + opiSpiNum = 1 // SPI1: command engine used for PSRAM setup, matches esp_psram_impl_octal.c's spi_num=1. + opiDtrMode = 7 // ESP_ROM_SPIFLASH_OPI_DTR_MODE (esp_rom_spiflash.h enum: QIO=0,QOUT,DIO,DOUT,FASTRD,SLOWRD,OPI_STR,OPI_DTR=7). + opiCS1Mask = 2 // BIT(1): esp_psram_impl_octal.c passes this literal cs_mask value for every PSRAM command. + + // Octal PSRAM command opcodes and phase lengths, matching the + // OPI_PSRAM_*/OCT_PSRAM_* constants in esp_psram_impl_octal.c. + opiRegRead = 0x4040 + opiRegWrite = 0xC0C0 + opiSyncRead = 0x0000 + opiSyncWrite = 0x8080 + opiCmdBitLen = 16 + opiAddrBitLen = 32 + opiRegDummy = 2 * (5 - 1) // 8 dummy cycles for MRx register read. + opiRdDummy = 2 * (10 - 1) // 18 dummy cycles for sync read. + opiWrDummy = 2 * (5 - 1) // 8 dummy cycles for sync write. + + opiVendorIDAP = 0xD // s_print_psram_info / OCT_PSRAM_VENDOR_ID_AP. + opiVendorIDUnilc = 0x1A // OCT_PSRAM_VENDOR_ID_UNILC. + + csHoldTime = 3 // OCT_PSRAM_CS_HOLD_TIME. + csSetupTime = 3 // OCT_PSRAM_CS_SETUP_TIME. + csHoldDelay = 2 // OCT_PSRAM_CS_HOLD_DELAY. + + // SPI0/SPI1 SMEM/FMEM clock divider for a fixed 40 MHz bring-up/fallback + // speed. SCLK = source/(N+1), H=((N+1)/2)-1, L=N. Also the speed + // calibratePSRAMTiming reverts to if the 80 MHz DTR timing sweep can't + // find a working config -- same din_mode/din_num/extra-dummy of 0 as + // this bring-up path, which already passed checkPSRAMConnected. + spiClkCntN = 1 + spiClkCntH = 0 + spiClkCntL = 1 +) + +// setPSRAMClockDivider sets SPI0's SRAM (PSRAM) clock and SPI1's FMEM (flash) +// clock divider. SCLK = source/(N+1), H=((N+1)/2)-1, L=N. +// +// WARNING: this writes to SPI1.CLOCK as well, not just to the SRAM clock. During +// PSRAM bring-up and timing calibration this is intentional (SPI1 drives PSRAM +// command sequences); any caller that only wants to touch the SRAM clock must use +// the individual SPI0.SetSRAM_CLK_* calls directly. +// +// Lives in IRAM: calibratePSRAMTiming calls this with flash cache disabled, +// so the code itself can't be fetched from flash-mapped memory at that point. +// +//go:section .iram +func setPSRAMClockDivider(n, h, l uint32) { + esp.SPI0.SetSRAM_CLK_SCLK_EQU_SYSCLK(0) + esp.SPI0.SetSRAM_CLK_SCLKCNT_N(n) + esp.SPI0.SetSRAM_CLK_SCLKCNT_H(h) + esp.SPI0.SetSRAM_CLK_SCLKCNT_L(l) + esp.SPI1.SetCLOCK_CLK_EQU_SYSCLK(0) + esp.SPI1.SetCLOCK_CLKCNT_N(n) + esp.SPI1.SetCLOCK_CLKCNT_H(h) + esp.SPI1.SetCLOCK_CLKCNT_L(l) +} + +// initPSRAM performs the Octal PSRAM bring-up sequence, mirroring +// esp_psram_impl_octal.c's esp_psram_impl_enable (minus DQS/timing +// calibration, see file doc comment): +// 1. CS1 pin select, CS timing, fixed low-speed clock. +// 2. Mode register (MR0) init: fixed read latency. +// 3. Connectivity check via a sync write/read of a reference word. +// 4. Read vendor ID (MR1) and density (MR2), validate and size the MMU window. +// 5. Configure SPI0's cache-facing read/write command phases for Octal DTR. +// 6. Map PSRAM into the MMU, zero-initialize .psram BSS. +func initPSRAM() { + psramStart, _, psramMmuPagesCap := psramWindow() + + // Enable SPI0/SPI1 peripheral clock and clear resets. + esp.SYSTEM.SetPERIP_CLK_EN0_SPI01_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_SPI01_RST(0) + + // Ensure MMU table power is enabled and force-on. + esp.EXTMEM.SetCACHE_MMU_POWER_CTRL_CACHE_MMU_MEM_FORCE_ON(1) + esp.EXTMEM.SetCACHE_MMU_POWER_CTRL_CACHE_MMU_MEM_FORCE_PU(1) + + // CS1 pin (GPIO26): function 0 is the dedicated SPICS1 signal and the + // IO_MUX reset default; function 1 switches the pin to plain GPIO, + // disconnecting it from SPI1 (soc/io_mux_reg.h: FUNC_SPICS1_SPICS1 = 0). + // FUN_DRV(3) sets max drive strength (3 = 40 mA) for the CS1 and + // SPICLK pads; Octal OPI at 80 MHz DTR needs stronger drive than the + // Quad SPI driver's FUN_WPU pull-up approach (the Quad driver operates + // at single-data-rate 40 MHz and the pull-up ensures CS1 isn't + // floating when deasserted). + esp.IO_MUX.SetGPIO26_MCU_SEL(0) + esp.IO_MUX.SetGPIO26_FUN_DRV(3) + esp.SPI0.SetDATE_SPI_SMEM_SPICLK_FUN_DRV(3) + + // GPIO33-37 (Octal D4-D7+DQS) are not configured by the ROM at boot + // (see file doc comment); the efuse.h "GPIO33-37 powered by VDDSPI" + // note is about voltage domain, not pin function. This ROM call does + // the actual per-efuse pad configuration. + romOpiflashPinConfig() + + // CS/hold timing, shared by SPI0 and SPI1 for PSRAM + // (esp_psram_impl_octal.c s_set_psram_cs_timing). + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_SETUP(1) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD(1) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD_TIME(csHoldTime) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_SETUP_TIME(csSetupTime) + esp.SPI0.SetSPI_SMEM_AC_SPI_SMEM_CS_HOLD_DELAY(csHoldDelay) + + // Fixed low-speed clock for SPI0 and SPI1 (no DQS/timing calibration). + setPSRAMClockDivider(spiClkCntN, spiClkCntH, spiClkCntL) + + // Variable-dummy DDR mode on SPI1, no DTR read/write swap + // (esp_psram_impl_octal.c esp_psram_impl_enable). + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(1) + romSpiSetDtrSwapMode(opiSpiNum, false, false) + + // Mode register MR0: fixed latency, read_latency=2, drive_str=0 + // (esp_psram_impl_octal.c: mode_reg.mr0 = {lt:1, read_latency:2, drive_str:0}). + mr0 := readPSRAMModeReg(0x0) + mr0 = (mr0 &^ 0x3f) | (2 << 2) | (1 << 5) + writePSRAMModeReg(0x0, mr0) + + if !checkPSRAMConnected() { + panic("PSRAM init failed: sync read/write mismatch, chip not detected") + } + + mr1 := readPSRAMModeReg(0x1) // vendor id + mr2 := readPSRAMModeReg(0x2) // density / dev id / gb + vendorID := mr1 & 0x1f + if vendorID != opiVendorIDAP && vendorID != opiVendorIDUnilc { + panic("PSRAM init failed: unrecognized vendor id") + } + + // Density encoding (esp_psram_impl_octal.c:372-376): 0x1=4MB, 0x3=8MB, + // 0x5=16MB, in 64KB pages. Chips reporting 0x7 (32MB) or 0x6 (64MB) + // fall through unmatched and are simply capped at psramMmuPagesCap, + // same as any chip bigger than the window: targets/esp32s3.ld only + // reserves a 16M PSRAM window, so there's nothing a bigger case value + // could map to. + density := mr2 & 0x7 + mmuPages := psramMmuPagesCap + switch density { + case 0x1: + if mmuPages > 64 { + mmuPages = 64 // 4MB + } + case 0x3: + if mmuPages > 128 { + mmuPages = 128 // 8MB + } + case 0x5: + if mmuPages > 256 { + mmuPages = 256 // 16MB + } + } + + // Perform DQS / MSPI delay timing calibration to upgrade Octal PSRAM to + // 80 MHz DTR. On failure it reverts to the untuned 40 MHz bring-up + // speed instead of running at an unverified timing; either way, the + // cache-facing register setup below is unaffected by which speed won. + calibratePSRAMTiming() + + // Disable DCache before touching the SPI0 cache-facing registers and + // the MMU table below (see the Cache_Disable_DCache doc comment in + // runtime_esp32_psram.go). + romCacheDisableDCache() + + // Enable SPI0's CS1 output for its cache-triggered SRAM (PSRAM) + // transactions. SPI_MEM_CS1_DIS on SPI0.MISC defaults to 1 (disabled; + // soc/spi_mem_reg.h bitpos:[1]), so without this the chip is never + // selected for SPI0 accesses. Matches ESP-IDF's quad PSRAM driver: + // "ENABLE SPI0 CS1 TO PSRAM (CS0--FLASH; CS1--SRAM)" + // (esp32s2/esp_psram_impl_quad.c:546); the generated SPI0_Type has no + // named CS1_DIS accessor, hence ClearBits. + esp.SPI0.MISC.ClearBits(1 << 1) + + // Configure SPI0's cache-facing read/write command phases for Octal + // DTR PSRAM access (esp_psram_impl_octal.c s_config_psram_spi_phases). + esp.SPI0.SetCACHE_SCTRL_CACHE_SRAM_USR_WCMD(1) + esp.SPI0.SetSRAM_DWR_CMD_CACHE_SRAM_USR_WR_CMD_BITLEN(opiCmdBitLen - 1) + esp.SPI0.SetSRAM_DWR_CMD_CACHE_SRAM_USR_WR_CMD_VALUE(opiSyncWrite) + + esp.SPI0.SetCACHE_SCTRL_CACHE_SRAM_USR_RCMD(1) + esp.SPI0.SetSRAM_DRD_CMD_CACHE_SRAM_USR_RD_CMD_BITLEN(opiCmdBitLen - 1) + esp.SPI0.SetSRAM_DRD_CMD_CACHE_SRAM_USR_RD_CMD_VALUE(opiSyncRead) + + esp.SPI0.SetCACHE_SCTRL_SRAM_ADDR_BITLEN(opiAddrBitLen - 1) + esp.SPI0.SetCACHE_SCTRL_CACHE_USR_SCMD_4BYTE(1) + + esp.SPI0.SetCACHE_SCTRL_USR_RD_SRAM_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_USR_WR_SRAM_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_RDUMMY_CYCLELEN(opiRdDummy - 1) + esp.SPI0.SetSPI_SMEM_DDR_SPI_SMEM_VAR_DUMMY(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_WDUMMY_CYCLELEN(opiWrDummy - 1) + + esp.SPI0.SetSPI_SMEM_DDR_WDAT_SWP(0) + esp.SPI0.SetSPI_SMEM_DDR_RDAT_SWP(0) + esp.SPI0.SetSPI_SMEM_DDR_EN(1) + + esp.SPI0.SetSRAM_CMD_SDUMMY_OUT(1) + esp.SPI0.SetSRAM_CMD_SCMD_OCT(1) + esp.SPI0.SetSRAM_CMD_SADDR_OCT(1) + esp.SPI0.SetSRAM_CMD_SDOUT_OCT(1) + esp.SPI0.SetSRAM_CMD_SDIN_OCT(1) + esp.SPI0.SetCACHE_SCTRL_SRAM_OCT(1) + + // Map PSRAM into the MMU: PSRAM physical page 0 at _psram_start, 64KB + // pages, fixed=0 (physical pages grow linearly with virtual pages). + if romCacheDbusMMUSet(mmuAccessSpiram, uint32(psramStart), 0, 64, uint32(mmuPages), 0) != 0 { + panic("PSRAM init failed: Cache_Dbus_MMU_Set error") + } + + // Re-enable DCache. Cache_Disable_DCache above already invalidated all + // tag memory. + romCacheEnableDCache(0) + + zeroInitPSRAM() +} + +//export rom_spi_flash_disable_cache +func romSpiFlashDisableCache(cpuid uint32, savedState *uint32) + +//export rom_spi_flash_restore_cache +func romSpiFlashRestoreCache(cpuid uint32, savedState uint32) + +//go:section .iram +func calibratePSRAMTiming() (ok bool) { + // Disable both caches so that no flash fetches can be initiated by the + // cache controller during clock switches. + var cacheState uint32 + romSpiFlashDisableCache(0, &cacheState) + + origCoreClkSel := esp.SPI0.GetCORE_CLK_SEL() + + // Disable variable dummy mode on SPI1 during timing calibration (matching ESP-IDF). + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(0) + + // 1. Write reference data pattern (64 bytes) to PSRAM address 0 at low speed (40 MHz). + var refData [16]uint32 + seed := uint32(0xa5ff005a) + for i := range refData { + seed = seed*1664525 + 1013904223 + refData[i] = seed + } + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncWrite, opiCmdBitLen, 0, opiAddrBitLen, opiWrDummy, (*byte)(unsafe.Pointer(&refData[0])), 512, nil, 0, opiCS1Mask, false) + + // 2. Read the original flash clock configuration to scale the divider when the core clock increases. + origFlashClock := esp.SPI0.CLOCK.Get() + var origFlashDiv uint32 = 1 + if (origFlashClock & (1 << 31)) == 0 { + origFlashDiv = ((origFlashClock >> 16) & 0xff) + 1 + } + newFlashDiv := origFlashDiv * 2 + newFlashClock := ((newFlashDiv - 1) << 16) | (((newFlashDiv/2 - 1) & 0xff) << 8) | (newFlashDiv - 1) + + // Apply scaled divider to SPI0 and SPI1 flash clock registers. + esp.SPI0.CLOCK.Set(newFlashClock) + esp.SPI1.CLOCK.Set(newFlashClock) + + // Switch MSPI core clock to 160 MHz. + esp.SPI0.SetCORE_CLK_SEL(2) // 160 MHz core clock + + // Set SPI0/SPI1 SRAM clocks to 80 MHz (divider = 2). + setPSRAMClockDivider(1, 0, 1) + + // Declare candidate delay parameters inside IRAM to avoid RODATA flash access. + psramTuningParams := [14]struct { + dinMode uint8 + dinNum uint8 + extraDummyLen uint8 + }{ + {0, 0, 0}, + {4, 2, 2}, + {2, 1, 2}, + {4, 1, 2}, + {1, 0, 1}, + {4, 0, 2}, // default config index 5 + {0, 0, 1}, + {4, 2, 3}, + {2, 1, 3}, + {4, 1, 3}, + {1, 0, 2}, + {4, 0, 3}, + {0, 0, 2}, + {4, 2, 4}, + } + + romDummyPlus1 := (*uint8)(unsafe.Pointer(&romDummyLenPlusSymbol)) + origRomDummyPlus1 := *romDummyPlus1 + + // 3. Sweep all 14 timing configurations and record successful reads. + var success [14]bool + for i := 0; i < 14; i++ { + p := psramTuningParams[i] + dinModeReg := (uint32(p.dinMode) & 7) * 0x01249249 + dinNumReg := (uint32(p.dinNum) & 3) * 0x00015555 + esp.SPI0.SPI_SMEM_DIN_MODE.Set(dinModeReg) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(dinNumReg) + + if p.extraDummyLen > 0 { + esp.SPI1.TIMING_CALI.Set(2 | ((uint32(p.extraDummyLen) & 7) << 2)) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(2 | ((uint32(p.extraDummyLen) & 7) << 2)) + } else { + esp.SPI1.TIMING_CALI.Set(0) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + } + *romDummyPlus1 = origRomDummyPlus1 + p.extraDummyLen + + // Inline clear of SPI1 FIFO (W0..W15 registers). + esp.SPI1.W0.Set(0) + esp.SPI1.W1.Set(0) + esp.SPI1.W2.Set(0) + esp.SPI1.W3.Set(0) + esp.SPI1.W4.Set(0) + esp.SPI1.W5.Set(0) + esp.SPI1.W6.Set(0) + esp.SPI1.W7.Set(0) + esp.SPI1.W8.Set(0) + esp.SPI1.W9.Set(0) + esp.SPI1.W10.Set(0) + esp.SPI1.W11.Set(0) + esp.SPI1.W12.Set(0) + esp.SPI1.W13.Set(0) + esp.SPI1.W14.Set(0) + esp.SPI1.W15.Set(0) + + var readData [16]uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncRead, opiCmdBitLen, 0, opiAddrBitLen, opiRdDummy, nil, 0, (*byte)(unsafe.Pointer(&readData[0])), 512, opiCS1Mask, false) + + if readData == refData { + success[i] = true + } + } + + // 5. Select best calibration index from passing candidates, in order of + // preference; this covers all 14 measured candidates, so a working but + // non-preferred config is never silently dropped in favor of an + // unverified default. + priority := [14]uint32{4, 10, 6, 12, 0, 5, 1, 2, 3, 7, 8, 9, 11, 13} + var bestIdx uint32 + ok = false + for _, idx := range priority { + if success[idx] { + bestIdx = idx + ok = true + break + } + } + + if ok { + // 6. Apply best calibration parameters to SPI0 (cache) and + // clear SPI1's leftover sweep timing. + best := psramTuningParams[bestIdx] + dinModeReg := (uint32(best.dinMode) & 7) * 0x01249249 + dinNumReg := (uint32(best.dinNum) & 3) * 0x00015555 + esp.SPI0.SPI_SMEM_DIN_MODE.Set(dinModeReg) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(dinNumReg) + + if best.extraDummyLen > 0 { + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(2 | ((uint32(best.extraDummyLen) & 7) << 2)) + } else { + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + } + esp.SPI1.TIMING_CALI.Set(0) + } else { + // No candidate passed the 80 MHz DTR read-back: revert clocks and + // din_mode/din_num/timing-cali to the same untuned 40 MHz state + // used for bring-up (spiClkCntN/H/L), which already passed + // checkPSRAMConnected, instead of running PSRAM at an unverified + // timing. + esp.SPI0.CLOCK.Set(origFlashClock) + esp.SPI1.CLOCK.Set(origFlashClock) + esp.SPI0.SetCORE_CLK_SEL(origCoreClkSel) + + // Restore SPI0 SRAM clock for 40 MHz PSRAM. + setPSRAMClockDivider(spiClkCntN, spiClkCntH, spiClkCntL) + + esp.SPI0.SPI_SMEM_DIN_MODE.Set(0) + esp.SPI0.SPI_SMEM_DIN_NUM.Set(0) + esp.SPI0.SPI_SMEM_TIMING_CALI.Set(0) + esp.SPI1.TIMING_CALI.Set(0) + } + + // Restore variable dummy mode on SPI1. + esp.SPI1.SetDDR_SPI_FMEM_VAR_DUMMY(1) + + // Restore ROM global dummy plus value. + *romDummyPlus1 = origRomDummyPlus1 + + // Re-enable caches. + romSpiFlashRestoreCache(0, cacheState) + return ok +} + +// readPSRAMModeReg reads a 16-bit PSRAM mode register (MR0..MR8) via the +// ROM's Octal DTR command executor, matching esp_psram_impl_octal.c's +// s_get_psram_mode_reg / s_init_psram_mode_reg read calls. +func readPSRAMModeReg(addr uint32) uint32 { + var val uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiRegRead, opiCmdBitLen, addr, opiAddrBitLen, opiRegDummy, nil, 0, (*byte)(unsafe.Pointer(&val)), 16, opiCS1Mask, false) + return val +} + +// writePSRAMModeReg writes a 16-bit PSRAM mode register, matching +// esp_psram_impl_octal.c's s_init_psram_mode_reg write calls. +func writePSRAMModeReg(addr uint32, val uint32) { + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiRegWrite, opiCmdBitLen, addr, opiAddrBitLen, 0, (*byte)(unsafe.Pointer(&val)), 16, nil, 0, opiCS1Mask, false) +} + +// checkPSRAMConnected writes a reference word to PSRAM address 0 and reads +// it back, matching esp_psram_impl_octal.c's s_check_psram_connected. +func checkPSRAMConnected() bool { + refData := uint32(0x5a6b7c8d) + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncWrite, opiCmdBitLen, 0, opiAddrBitLen, opiWrDummy, (*byte)(unsafe.Pointer(&refData)), 32, nil, 0, opiCS1Mask, false) + + var got uint32 + romOpiflashExecCmd(opiSpiNum, opiDtrMode, opiSyncRead, opiCmdBitLen, 0, opiAddrBitLen, opiRdDummy, nil, 0, (*byte)(unsafe.Pointer(&got)), 32, opiCS1Mask, false) + + return got == refData +} diff --git a/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 7c8b208a96..483f505e75 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -74,6 +74,9 @@ func main() { clearbss() } + // Initialize PSRAM if numa_psram_* build tag is enabled. + initPSRAM() + // Initialize main system timer used for time.Now. initTimer() diff --git a/targets/esp32s3-psram-octal.json b/targets/esp32s3-psram-octal.json new file mode 100644 index 0000000000..cc2a55ee6a --- /dev/null +++ b/targets/esp32s3-psram-octal.json @@ -0,0 +1,6 @@ +{ + "inherits": ["esp32s3-generic"], + "build-tags": ["numa_psram_octal"], + "flash-method": "esp32flash", + "serial": "uart" +} diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index 6a08f29983..87d8f64b41 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -6,11 +6,29 @@ * - SRAM1 (416KB): dual-mapped as IRAM 0x40378000-0x403DFFFF and * DRAM 0x3FC88000-0x3FCEFFFF. * - * Flash is memory-mapped via the cache: - * - DROM (read-only data): 0x3C000000, up to 32MB - * - IROM (executable code): 0x42000000, up to 32MB - * The MMU uses 64KB pages, so the bottom 16 bits of the virtual address - * and the flash offset must match. Dummy sections handle this alignment. + * Flash is memory-mapped via the cache, backed by the 512-entry MMU table + * (see esp32s3.S). The table is a SINGLE array shared by the instruction + * bus (IROM window at 0x42000000) and the data bus (DROM window at + * 0x3C000000), indexed by LINEAR address: + * + * entry = (vaddr & 0x1FFFFFF) >> 16 + * + * so 0x42800000 and 0x3C800000 decode to the SAME entry (128). There is no + * ICache/DCache half-split (verified on hardware via the CACHE_MMU_FAULT_* + * registers and ROM's Cache_Dbus_MMU_Set during Octal PSRAM bring-up). + * 512 entries x 64KB = 32M of linear space per window. + * + * Because the two windows share entries, IROM and DROM must occupy disjoint + * linear ranges: .text starts at ORIGIN(IROM) (entry 0) and .rodata starts + * one whole 64KB page past the end of .text (.rodata_dummy pads for this), + * so DROM takes the entries right after IROM. + * + * Virtual pages are NOT tied to flash pages. The image builder + * (builder/esp.go) keeps DROM/IROM out of the ROM-loaded segment table -- + * the ROM bootloader rejects images with a DROM segment over 1MB -- and + * appends them at 64KB-aligned flash offsets after the RAM image, patching + * those offsets into _irom_flash_addr/_drom_flash_addr. The startup code + * (esp32s3.S) programs the MMU entries from them. */ MEMORY @@ -18,8 +36,20 @@ MEMORY DRAM (rw) : ORIGIN = 0x3FC88000, LENGTH = 416K IRAM (x) : ORIGIN = 0x40378000, LENGTH = 416K /* SRAM1 only (SRAM0 used by ICache) */ - DROM (r) : ORIGIN = 0x3C000000, LENGTH = 32M /* Flash data bus (read-only) */ - IROM (rx) : ORIGIN = 0x42000000, LENGTH = 32M /* Flash instruction bus */ + /* DROM and IROM share MMU entries 0-255 by linear address (see above), + * so their combined size must stay below 16M. The DROM region length + * enforces that: .rodata_dummy first burns the IROM pages out of this + * region, so a link that would push .rodata into the PSRAM window at + * 0x3D000000 overflows DROM and fails at link time. + * The DBUS cache window extends to 0x3E000000 + * (SOC_DRAM0_CACHE_ADDRESS_HIGH), so PSRAM lives in its upper half. */ + DROM (r) : ORIGIN = 0x3C000000, LENGTH = 16M /* Flash data bus (read-only); holds IROM padding + .rodata */ + IROM (rx) : ORIGIN = 0x42000000, LENGTH = 16M /* Flash instruction bus; shares entries 0-255 with DROM */ + + /* External PSRAM: upper half of the DBUS cache window, MMU entries + * 256-511 (linear pages of 0x3D000000..0x3DFFFFFF), mapped at runtime + * by runtime_esp32_psram*.go. Disjoint from all flash entries. */ + PSRAM (rw): ORIGIN = 0x3D000000, LENGTH = 16M } /* The entry point. It is set in the image flashed to the chip, so must be @@ -29,23 +59,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 +86,15 @@ SECTIONS _sdata = ABSOLUTE(.); *(.data .data.*) *(.dram*) + + /* Physical flash offsets patched by the ESP image builder. The + * startup code reads them with l32i, so they must be 4-aligned. */ + . = ALIGN (4); + _irom_flash_addr = ABSOLUTE(.); + LONG(0); + _drom_flash_addr = ABSOLUTE(.); + LONG(0); + . = ALIGN (4); _edata = ABSOLUTE(.); } >DRAM @@ -121,30 +143,54 @@ 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 + + /* The MMU mapping loops in esp32s3.S compute a page count as + * ((end - start - 1) >> 16) + 1, which underflows to 65536 pages for an + * empty region and would scribble over the whole MMU window. The image + * builder also requires exactly one segment per region. + */ + ASSERT(_irom_end > _irom_start, "IROM (.text) must not be empty") + ASSERT(_drom_end > _drom_start, "DROM (.rodata) must not be empty") + + .psram (NOLOAD) : ALIGN(4) + { + _spsram = ABSOLUTE(.); + *(.psram .psram.*) + . = ALIGN(4); + _epsram = ABSOLUTE(.); + } > PSRAM + /DISCARD/ : { *(.eh_frame) } } +_psram_start = ORIGIN(PSRAM); +_psram_end = ORIGIN(PSRAM) + LENGTH(PSRAM); + /* For the garbage collector. * _heap_start must be after the DRAM shadow of the IRAM section. * IRAM and DRAM share the same physical SRAM1, with addresses offset by @@ -165,10 +211,14 @@ _heap_start = _iram_end - 0x6F0000 - (__init_end - __init_start); * - 0x3fcef3d4-0x3fcef66c : PHY function table (g_phyFuns, 0x298 bytes) * - 0x3fcef81c-0x3fcef954 : ROM global variables (phy_param_rom … pTxRx) * - 0x3fcefe6d : g_scan_forever + * - 0x3fcefffd : g_rom_spiflash_dummy_len_plus[1], read/adjusted + * by esp_rom_opiflash_exec_cmd() during Octal PSRAM timing calibration * The Go GC stores metadata at [metadataStart, heapEnd) and will corrupt * any blob data that overlaps this range. */ _heap_end = 0x3fceb710; +PROVIDE( g_rom_spiflash_dummy_len_plus = 0x3fcefffd ); + _stack_size = 4K; /* From ESP-IDF: @@ -301,6 +351,16 @@ ets_install_putc1 = 0x400005dc; ets_install_uart_printf = 0x400005e8; ets_install_putc2 = 0x400005f4; PROVIDE( ets_delay_us = 0x40000600 ); +PROVIDE( esp_rom_opiflash_pin_config = 0x40000894 ); +PROVIDE( esp_rom_opiflash_exec_cmd = 0x400008b8 ); +PROVIDE( esp_rom_spi_set_dtr_swap_mode = 0x4000093c ); +PROVIDE( Cache_Disable_ICache = 0x4000186c ); +PROVIDE( Cache_Enable_ICache = 0x40001878 ); +PROVIDE( Cache_Disable_DCache = 0x40001884 ); +PROVIDE( Cache_Enable_DCache = 0x40001890 ); +PROVIDE( Cache_Dbus_MMU_Set = 0x400019b0 ); +PROVIDE( rom_spi_flash_disable_cache = 0x40000c0c ); +PROVIDE( rom_spi_flash_restore_cache = 0x40000c18 ); ets_get_stack_info = 0x4000060c; ets_install_lock = 0x40000618; ets_backup_dma_copy = 0x40000624;