Files
deadprogram 0a9b3f91bc 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 <ron@hybridgroup.com>
2026-07-20 14:12:53 +02:00

330 lines
11 KiB
Go

package builder
// This file implements support for writing ESP image files. These image files
// are read by the ROM bootloader so have to be in a particular format.
//
// In the future, it may be necessary to implement support for other image
// formats, such as the ESP8266 image formats (again, used by the ROM bootloader
// to load the firmware).
import (
"bytes"
"crypto/sha256"
"debug/elf"
"encoding/binary"
"fmt"
"os"
"sort"
"strings"
)
type espImageSegment struct {
addr uint32
data []byte
}
// 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.
//
// The following documentation has been used:
// https://github.com/espressif/esptool/wiki/Firmware-Image-Format
// https://github.com/espressif/esp-idf/blob/8fbb63c2a701c22ccf4ce249f43aded73e134a34/components/bootloader_support/include/esp_image_format.h#L58
// https://github.com/espressif/esptool/blob/master/esptool.py
func makeESPFirmwareImage(infile, outfile, format string) error {
inf, err := elf.Open(infile)
if err != nil {
return err
}
defer inf.Close()
// Load all segments to be written to the image. These are actually ELF
// sections, not true ELF segments (similar to how esptool does it).
var segments []*espImageSegment
for _, section := range inf.Sections {
if section.Type != elf.SHT_PROGBITS || section.Size == 0 || section.Flags&elf.SHF_ALLOC == 0 {
continue
}
data, err := section.Data()
if err != nil {
return fmt.Errorf("failed to read section data: %w", err)
}
for len(data)%4 != 0 {
// Align segment to 4 bytes.
data = append(data, 0)
}
if uint64(uint32(section.Addr)) != section.Addr {
return fmt.Errorf("section address too big: 0x%x", section.Addr)
}
segments = append(segments, &espImageSegment{
addr: uint32(section.Addr),
data: data,
})
}
// 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 })
// 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.
outf := &bytes.Buffer{}
// Separate esp32 and esp32-img. The -img suffix indicates we should make an
// image, not just a binary to be flashed at 0x1000 for example.
chip := format
makeImage := false
if strings.HasSuffix(format, "-img") {
makeImage = true
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
// application in the bootloader location.
outf.Write(make([]byte, 4096))
}
// Chip IDs. Source:
// https://github.com/espressif/esp-idf/blob/v4.3/components/bootloader_support/include/esp_app_format.h#L22
chip_id := map[string]uint16{
"esp32": 0x0000,
"esp32c3": 0x0005,
"esp32c6": 0x000d,
"esp32s3": 0x0009,
}[chip]
// SPI flash speed/size byte (byte 3 of header):
// Upper nibble = flash size, lower nibble = flash frequency.
// The espflasher auto-detects and patches the flash size (upper nibble),
// but the frequency (lower nibble) must be correct per chip.
spiSpeedSize := map[string]uint8{
"esp32": 0x1f, // 80MHz=0x0F, 2MB=0x10
"esp32c3": 0x1f, // 80MHz=0x0F, 2MB=0x10
"esp32c6": 0x10, // 80MHz=0x00, 2MB=0x10 (C6 uses different freq encoding)
"esp32s3": 0x1f, // 80MHz=0x0F, 2MB=0x10
}[chip]
// Image header.
switch chip {
case "esp32", "esp32c3", "esp32s3", "esp32c6":
// Header format:
// https://github.com/espressif/esp-idf/blob/v4.3/components/bootloader_support/include/esp_app_format.h#L71
// Note: not adding a SHA256 hash as the binary is modified by
// esptool.py while flashing and therefore the hash won't be valid
// anymore.
binary.Write(outf, binary.LittleEndian, struct {
magic uint8
segment_count uint8
spi_mode uint8
spi_speed_size uint8
entry_addr uint32
wp_pin uint8
spi_pin_drv [3]uint8
chip_id uint16
min_chip_rev uint8
reserved [8]uint8
hash_appended bool
}{
magic: 0xE9,
segment_count: byte(len(segments)),
spi_mode: 2, // ESP_IMAGE_SPI_MODE_DIO
spi_speed_size: spiSpeedSize,
entry_addr: uint32(inf.Entry),
wp_pin: 0xEE, // disable WP pin
chip_id: chip_id,
hash_appended: true, // add a SHA256 hash
})
case "esp8266":
// Header format:
// https://github.com/espressif/esptool/wiki/Firmware-Image-Format
// Basically a truncated version of the ESP32 header.
binary.Write(outf, binary.LittleEndian, struct {
magic uint8
segment_count uint8
spi_mode uint8
spi_speed_size uint8
entry_addr uint32
}{
magic: 0xE9,
segment_count: byte(len(segments)),
spi_mode: 0, // irrelevant, replaced by esptool when flashing
spi_speed_size: 0x20, // spi_speed, spi_size: replaced by esptool when flashing
entry_addr: uint32(inf.Entry),
})
default:
return fmt.Errorf("builder: unknown binary format %#v, expected esp32 or esp8266", format)
}
// Write all segments to the image.
// https://github.com/espressif/esptool/wiki/Firmware-Image-Format#segment
for _, segment := range segments {
binary.Write(outf, binary.LittleEndian, struct {
addr uint32
length uint32
}{
addr: segment.addr,
length: uint32(len(segment.data)),
})
outf.Write(segment.data)
}
// Footer, including checksum.
// The entire image size must be a multiple of 16, so pad the image to one
// byte less than that before writing the checksum.
outf.Write(make([]byte, 15-outf.Len()%16))
outf.WriteByte(checksum)
if chip != "esp8266" {
// SHA256 hash (to protect against image corruption, not for security).
hash := sha256.Sum256(outf.Bytes())
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 {
// Use a default image size of 4MB.
grow := 4096*1024 - outf.Len()
if grow > 0 {
outf.Write(make([]byte, grow))
}
}
// Write the image to the output file.
return os.WriteFile(outfile, outf.Bytes(), 0666)
}