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" "io/ioutil" "sort" ) type espImageSegment struct { addr uint32 data []byte } // makeESPFirmare 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 makeESPFirmareImage(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 }) // 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. outf := &bytes.Buffer{} // Image header. switch format { case "esp32": // Header format: // https://github.com/espressif/esp-idf/blob/8fbb63c2/components/bootloader_support/include/esp_image_format.h#L58 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 reserved [11]uint8 hash_appended bool }{ magic: 0xE9, segment_count: byte(len(segments)), spi_mode: 0, // irrelevant, replaced by esptool when flashing spi_speed_size: 0, // spi_speed, spi_size: replaced by esptool when flashing entry_addr: uint32(inf.Entry), wp_pin: 0xEE, // disable WP pin 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 format == "esp32" { // SHA256 hash (to protect against image corruption, not for security). hash := sha256.Sum256(outf.Bytes()) outf.Write(hash[:]) } // Write the image to the output file. return ioutil.WriteFile(outfile, outf.Bytes(), 0666) }