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