From ebab7dd8bc8a2785053a16cb07c723024c836854 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 10 Jul 2026 19:15:16 +0200 Subject: [PATCH 1/2] builder: improvements needed for esp32 XIP to allow for correctly flashing large programs This fixes the builder for ESP32 (original) by separating the RAM segments loadable by the ROM bootloader from flash-mapped segments (DROM/IROM) which require MMU setup by startup code. These changes are needed for to allow for ESP32 to correctly flashing large programs which as a result require XIP support. Signed-off-by: deadprogram --- builder/esp.go | 141 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 132 insertions(+), 9 deletions(-) diff --git a/builder/esp.go b/builder/esp.go index a480b4d1ea..739035c089 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -65,15 +65,6 @@ func makeESPFirmwareImage(infile, outfile, format string) error { // Sort the segments by address. This is what esptool does too. sort.SliceStable(segments, func(i, j int) bool { return segments[i].addr < segments[j].addr }) - // Calculate checksum over the segment data. This is used in the image - // footer. - checksum := uint8(0xef) - for _, segment := range segments { - for _, b := range segment.data { - checksum ^= b - } - } - // Write first to an in-memory buffer, primarily so that we can easily // calculate a hash over the entire image. // An added benefit is that we don't need to check for errors all the time. @@ -88,6 +79,86 @@ 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. + var flashSegments []*espImageSegment + if chip == "esp32" { + var ramSegments []*espImageSegment + for _, seg := range segments { + if (seg.addr >= 0x3F400000 && seg.addr < 0x3F800000) || // DROM + (seg.addr >= 0x400D0000 && seg.addr < 0x40400000) { // IROM + flashSegments = append(flashSegments, seg) + } else { + ramSegments = append(ramSegments, seg) + } + } + segments = ramSegments + } + + // 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 + } + ramImageSize += 24 // image header (8) + trailer fields (16) + for _, seg := range segments { + ramImageSize += 8 + len(seg.data) // segment header + data (4-aligned) + } + 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 + } + } + if dromSymAddr == 0 { + return fmt.Errorf("ESP32: _drom_flash_addr symbol not found") + } + 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 + } + } + if !patched { + return fmt.Errorf("ESP32: _drom_flash_addr (0x%x) not in any RAM segment", dromSymAddr) + } + } + + // Calculate checksum over the segment data. This is used in the image + // footer. + checksum := uint8(0xef) + for _, segment := range segments { + for _, b := range segment.data { + checksum ^= b + } + } + if makeImage { // The bootloader starts at 0x1000, or 4096. // TinyGo doesn't use a separate bootloader and runs the entire @@ -191,6 +262,58 @@ func makeESPFirmwareImage(infile, outfile, format string) error { outf.Write(hash[:]) } + // 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 { + const flashBase = esp32FlashBase + const pageSize = esp32PageSize + dromFlashAddr := esp32DromFlashAddr + + // Separate DROM and IROM segments. + var dromSegs, iromSegs []*espImageSegment + for _, seg := range flashSegments { + if seg.addr >= 0x3F400000 && seg.addr < 0x3F800000 { + dromSegs = append(dromSegs, seg) + } else { + iromSegs = append(iromSegs, seg) + } + } + + // Write DROM segments at the computed page-aligned flash offset. + dromSize := 0 + if len(dromSegs) > 0 { + targetImageOffset := int(dromFlashAddr - flashBase) + if outf.Len() > targetImageOffset { + return fmt.Errorf("ESP32: RAM segments too large (%d bytes), overlap DROM at flash 0x%x", outf.Len(), dromFlashAddr) + } + outf.Write(make([]byte, targetImageOffset-outf.Len())) + for _, seg := range dromSegs { + outf.Write(seg.data) + dromSize += len(seg.data) + } + } + + // Write IROM segments immediately after DROM, at the next page boundary. + // IROM flash addr = dromFlashAddr + ceil(dromSize/pageSize)*pageSize + // (must match the computation in the startup assembly). + if len(iromSegs) > 0 { + dromPages := (dromSize + pageSize - 1) / pageSize + if dromPages == 0 { + dromPages = 1 + } + iromFlashAddr := dromFlashAddr + uint32(dromPages)*pageSize + targetImageOffset := int(iromFlashAddr - flashBase) + if outf.Len() > targetImageOffset { + return fmt.Errorf("ESP32: DROM too large, overlaps IROM at flash 0x%x", iromFlashAddr) + } + outf.Write(make([]byte, targetImageOffset-outf.Len())) + for _, seg := range iromSegs { + outf.Write(seg.data) + } + } + } + // QEMU (or more precisely, qemu-system-xtensa from Espressif) expects the // image to be a certain size. if makeImage { From 9db0ed13e3fd75add6fe93150f98a682adbfcfcf Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 14 Jul 2026 00:34:08 +0200 Subject: [PATCH 2/2] main: update to espflasher 0.7.1 and add flash erase progress tracker Signed-off-by: deadprogram --- go.mod | 7 +++---- go.sum | 14 ++++++-------- main.go | 22 +++++++++++----------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index a8193fd4a4..af92ae8173 100644 --- a/go.mod +++ b/go.mod @@ -15,14 +15,14 @@ require ( github.com/mgechev/revive v1.3.9 github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 github.com/tetratelabs/wazero v1.9.0 - go.bug.st/serial v1.6.4 + go.bug.st/serial v1.8.0 go.bytecodealliance.org v0.6.2 go.bytecodealliance.org/cm v0.2.2 golang.org/x/net v0.56.0 - golang.org/x/sys v0.46.0 + golang.org/x/sys v0.47.0 golang.org/x/tools v0.47.0 gopkg.in/yaml.v2 v2.4.0 - tinygo.org/x/espflasher v0.6.1 + tinygo.org/x/espflasher v0.7.1 tinygo.org/x/go-llvm v0.0.0-20260707200325-ddd595b68360 ) @@ -30,7 +30,6 @@ require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/creack/goselect v0.1.2 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fatih/structtag v1.2.0 // indirect diff --git a/go.sum b/go.sum index 55320e4a22..f606061bef 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= -github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -89,8 +87,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= -go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= -go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +go.bug.st/serial v1.8.0 h1:ZtnmN8aYXtPlTghwSvDWPHKBHL9TM6oFDa+KpSn4SQE= +go.bug.st/serial v1.8.0/go.mod h1:d0MmS16Qt9b1m06yoYRNUXhRRTJV5Qg2S5EKqQtnayQ= go.bytecodealliance.org v0.6.2 h1:Jy4u5DVmSkXgsnwojBhJ+AD/YsJsR3VzVnxF0xRCqTQ= go.bytecodealliance.org v0.6.2/go.mod h1:gqjTJm0y9NSksG4py/lSjIQ/SNuIlOQ+hCIEPQwtJgA= go.bytecodealliance.org/cm v0.2.2 h1:M9iHS6qs884mbQbIjtLX1OifgyPG9DuMs2iwz8G4WQA= @@ -107,8 +105,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= @@ -120,7 +118,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/espflasher v0.6.1 h1:9jfyAP9jGjxF63FQUY2Bml9TFb5fCZYxVgbgR2IjUGs= -tinygo.org/x/espflasher v0.6.1/go.mod h1:tr5u08HoE67WD5zxJesCiiVF/R1b6Akz3yXwh5zah8U= +tinygo.org/x/espflasher v0.7.1 h1:oJ+tt58QltP+c5+iHGRdCNNnVaXZ9yYXyWK/F747BVY= +tinygo.org/x/espflasher v0.7.1/go.mod h1:a3l/4jOSMf9vhaPtEmGbCwtrC5oiZiNKx1i8jUkyyRI= tinygo.org/x/go-llvm v0.0.0-20260707200325-ddd595b68360 h1:BOGCjKGwPn2q54p/CNyfGzUDmqyi8YBSm3KlDheaTEU= tinygo.org/x/go-llvm v0.0.0-20260707200325-ddd595b68360/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/main.go b/main.go index 62f01e3e3a..6751b2669c 100644 --- a/main.go +++ b/main.go @@ -1122,6 +1122,15 @@ const ( jtagReset = "jtag" ) +var progressFunc = func(current, total int) { + pct := float64(current) / float64(total) * 100 + bar := int(pct / 2) + fmt.Printf("\r[%-50s] %6.1f%%", strings.Repeat("#", bar)+strings.Repeat(".", 50-bar), pct) + if current >= total { + fmt.Println() + } +} + func flashBinUsingEsp32(port, resetMode, tmppath string, options *compileopts.Options) error { opts := espflasher.DefaultOptions() opts.Compress = true @@ -1152,21 +1161,12 @@ func flashBinUsingEsp32(port, resetMode, tmppath string, options *compileopts.Op return err } - if err := flasher.EraseFlash(); err != nil { + if err := flasher.EraseFlash(progressFunc); err != nil { return fmt.Errorf("erase failed: %v", err) } - progress := func(current, total int) { - pct := float64(current) / float64(total) * 100 - bar := int(pct / 2) - fmt.Printf("\r[%-50s] %6.1f%%", strings.Repeat("#", bar)+strings.Repeat(".", 50-bar), pct) - if current >= total { - fmt.Println() - } - } - // Flash with progress reporting - err = flasher.FlashImage(data, offset, progress) + err = flasher.FlashImage(data, offset, progressFunc) if err != nil { return err }