diff --git a/go.mod b/go.mod index c0be0ef7..dcdf7f39 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( golang.org/x/tools v0.36.0 ) +require github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 + require ( golang.org/x/mod v0.27.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index 0dee84cd..6dbac84e 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94= github.com/goplus/mod v0.17.1/go.mod h1:iXEszBKqi38BAyQApBPyQeurLHmQN34YMgC2ZNdap50= github.com/qiniu/x v1.15.1 h1:avE+YQaowp8ZExjylOeSM73rUo3MQKBAYVxh4NJ8dY8= github.com/qiniu/x v1.15.1/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= diff --git a/internal/build/build.go b/internal/build/build.go index fee0f3cd..2b2ce4f0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -760,7 +760,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l if orgApp != app { fmt.Printf("cross compile: %#v\n", ctx.crossCompile) - err = firmware.MakeFirmwareImage(orgApp, app, ctx.crossCompile.BinaryFormat) + err = firmware.MakeFirmwareImage(orgApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) check(err) } diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 41458418..1933ed9b 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -31,6 +31,7 @@ type Export struct { ClangBinPath string // Path to clang binary directory BinaryFormat string // Binary format (e.g., "elf", "esp", "uf2") + FormatDetail string // For uf2, it's uf2FamilyID } const ( @@ -462,6 +463,7 @@ func useTarget(targetName string) (export Export, err error) { export.GOARCH = config.GOARCH export.ExtraFiles = config.ExtraFiles export.BinaryFormat = config.BinaryFormat + export.FormatDetail = config.FormatDetail() // Build environment map for template variable expansion envs := buildEnvMap(env.LLGoROOT()) diff --git a/internal/firmware/esp.go b/internal/firmware/esp.go index 7318e066..c678413c 100644 --- a/internal/firmware/esp.go +++ b/internal/firmware/esp.go @@ -1,3 +1,5 @@ +// From tinygo/builder/esp.go + package firmware import ( @@ -11,8 +13,6 @@ import ( "strings" ) -// From tinygo/builder/esp.go - type espImageSegment struct { addr uint32 data []byte diff --git a/internal/firmware/ext.go b/internal/firmware/ext.go index ea5b70ef..3a90ca97 100644 --- a/internal/firmware/ext.go +++ b/internal/firmware/ext.go @@ -7,6 +7,10 @@ import "strings" func BinaryExt(binaryFormat string) string { if strings.HasPrefix(binaryFormat, "esp") { return ".bin" + } else if strings.HasPrefix(binaryFormat, "uf2") { + return ".uf2" + } else if strings.HasPrefix(binaryFormat, "nrf-dfu") { + return ".zip" } return "" } diff --git a/internal/firmware/ext_test.go b/internal/firmware/ext_test.go index 2c594e56..d623326f 100644 --- a/internal/firmware/ext_test.go +++ b/internal/firmware/ext_test.go @@ -11,10 +11,10 @@ func TestBinaryExt(t *testing.T) { {"ESP32", "esp32", ".bin"}, {"ESP8266", "esp8266", ".bin"}, {"ESP32C3", "esp32c3", ".bin"}, - {"UF2", "uf2", ""}, + {"UF2", "uf2", ".uf2"}, {"ELF", "elf", ""}, {"Empty", "", ""}, - {"NRF-DFU", "nrf-dfu", ""}, + {"NRF-DFU", "nrf-dfu", ".zip"}, } for _, tt := range tests { diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index fff89fc2..bbe42e4e 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -5,10 +5,15 @@ import ( "strings" ) -func MakeFirmwareImage(infile, outfile, format string) error { - fmt.Printf("Creating firmware image from %s to %s with format %s\n", infile, outfile, format) +// MakeFirmwareImage creates a firmware image from the given input file. +func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { if strings.HasPrefix(format, "esp") { return makeESPFirmareImage(infile, outfile, format) + } else if format == "uf2" { + uf2Family := fmtDetail + return convertELFFileToUF2File(infile, outfile, uf2Family) + } else if format == "nrf-dfu" { + return makeDFUFirmwareImage(infile, outfile) } return fmt.Errorf("unsupported firmware format: %s", format) } diff --git a/internal/firmware/nrfutil.go b/internal/firmware/nrfutil.go new file mode 100644 index 00000000..f0937c71 --- /dev/null +++ b/internal/firmware/nrfutil.go @@ -0,0 +1,118 @@ +// From tinygo/builder/nrfutil.go + +package firmware + +import ( + "archive/zip" + "bytes" + "encoding/binary" + "encoding/json" + "os" + + "github.com/sigurn/crc16" +) + +// Structure of the manifest.json file. +type jsonManifest struct { + Manifest struct { + Application struct { + BinaryFile string `json:"bin_file"` + DataFile string `json:"dat_file"` + InitPacketData nrfInitPacket `json:"init_packet_data"` + } `json:"application"` + DFUVersion float64 `json:"dfu_version"` // yes, this is a JSON number, not a string + } `json:"manifest"` +} + +// Structure of the init packet. +// Source: +// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/lib/sdk11/components/libraries/bootloader_dfu/dfu_init.h#L47-L57 +type nrfInitPacket struct { + ApplicationVersion uint32 `json:"application_version"` + DeviceRevision uint16 `json:"device_revision"` + DeviceType uint16 `json:"device_type"` + FirmwareCRC16 uint16 `json:"firmware_crc16"` + SoftDeviceRequired []uint16 `json:"softdevice_req"` // this is actually a variable length array +} + +// Create the init packet (the contents of application.dat). +func (p nrfInitPacket) createInitPacket() []byte { + buf := &bytes.Buffer{} + binary.Write(buf, binary.LittleEndian, p.DeviceType) // uint16_t device_type; + binary.Write(buf, binary.LittleEndian, p.DeviceRevision) // uint16_t device_rev; + binary.Write(buf, binary.LittleEndian, p.ApplicationVersion) // uint32_t app_version; + binary.Write(buf, binary.LittleEndian, uint16(len(p.SoftDeviceRequired))) // uint16_t softdevice_len; + binary.Write(buf, binary.LittleEndian, p.SoftDeviceRequired) // uint16_t softdevice[1]; + binary.Write(buf, binary.LittleEndian, p.FirmwareCRC16) + return buf.Bytes() +} + +// Make a Nordic DFU firmware image from an ELF file. +func makeDFUFirmwareImage(infile, outfile string) error { + // Read ELF file as input and convert it to a binary image file. + _, data, err := extractROM(infile) + if err != nil { + return err + } + + // Create the zip file in memory. + // It won't be very large anyway. + buf := &bytes.Buffer{} + w := zip.NewWriter(buf) + + // Write the application binary to the zip file. + binw, err := w.Create("application.bin") + if err != nil { + return err + } + _, err = binw.Write(data) + if err != nil { + return err + } + + // Create the init packet. + initPacket := nrfInitPacket{ + ApplicationVersion: 0xffff_ffff, // appears to be unused by the Adafruit bootloader + DeviceRevision: 0xffff, // DFU_DEVICE_REVISION_EMPTY + DeviceType: 0x0052, // ADAFRUIT_DEVICE_TYPE + FirmwareCRC16: crc16.Checksum(data, crc16.MakeTable(crc16.CRC16_CCITT_FALSE)), + SoftDeviceRequired: []uint16{0xfffe}, // DFU_SOFTDEVICE_ANY + } + + // Write the init packet to the zip file. + datw, err := w.Create("application.dat") + if err != nil { + return err + } + _, err = datw.Write(initPacket.createInitPacket()) + if err != nil { + return err + } + + // Create the JSON manifest. + manifest := &jsonManifest{} + manifest.Manifest.Application.BinaryFile = "application.bin" + manifest.Manifest.Application.DataFile = "application.dat" + manifest.Manifest.Application.InitPacketData = initPacket + manifest.Manifest.DFUVersion = 0.5 + + // Write the JSON manifest to the file. + jsonw, err := w.Create("manifest.json") + if err != nil { + return err + } + enc := json.NewEncoder(jsonw) + enc.SetIndent("", " ") + err = enc.Encode(manifest) + if err != nil { + return err + } + + // Finish the zip file. + err = w.Close() + if err != nil { + return err + } + + return os.WriteFile(outfile, buf.Bytes(), 0o666) +} diff --git a/internal/firmware/objcopy.go b/internal/firmware/objcopy.go new file mode 100644 index 00000000..e163f41f --- /dev/null +++ b/internal/firmware/objcopy.go @@ -0,0 +1,133 @@ +// From tinygo/builder/objcopy.go + +package firmware + +import ( + "debug/elf" + "io" + "os" + "sort" +) + +// maxPadBytes is the maximum allowed bytes to be padded in a rom extraction +// this value is currently defined by Nintendo Switch Page Alignment (4096 bytes) +const maxPadBytes = 4095 + +// objcopyError is an error returned by functions that act like objcopy. +type objcopyError struct { + Op string + Err error +} + +func (e objcopyError) Error() string { + if e.Err == nil { + return e.Op + } + return e.Op + ": " + e.Err.Error() +} + +type progSlice []*elf.Prog + +func (s progSlice) Len() int { return len(s) } +func (s progSlice) Less(i, j int) bool { return s[i].Paddr < s[j].Paddr } +func (s progSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// extractROM extracts a firmware image and the first load address from the +// given ELF file. It tries to emulate the behavior of objcopy. +func extractROM(path string) (uint64, []byte, error) { + f, err := elf.Open(path) + if err != nil { + return 0, nil, objcopyError{"failed to open ELF file to extract text segment", err} + } + defer f.Close() + + // The GNU objcopy command does the following for firmware extraction (from + // the man page): + // > When objcopy generates a raw binary file, it will essentially produce a + // > memory dump of the contents of the input object file. All symbols and + // > relocation information will be discarded. The memory dump will start at + // > the load address of the lowest section copied into the output file. + + // Find the lowest section address. + startAddr := ^uint64(0) + for _, section := range f.Sections { + if section.Type != elf.SHT_PROGBITS || section.Flags&elf.SHF_ALLOC == 0 { + continue + } + if section.Addr < startAddr { + startAddr = section.Addr + } + } + + progs := make(progSlice, 0, 2) + for _, prog := range f.Progs { + if prog.Type != elf.PT_LOAD || prog.Filesz == 0 || prog.Off == 0 { + continue + } + progs = append(progs, prog) + } + if len(progs) == 0 { + return 0, nil, objcopyError{"file does not contain ROM segments: " + path, nil} + } + sort.Sort(progs) + + var rom []byte + for _, prog := range progs { + romEnd := progs[0].Paddr + uint64(len(rom)) + if prog.Paddr > romEnd && prog.Paddr < romEnd+16 { + // Sometimes, the linker seems to insert a bit of padding between + // segments. Simply zero-fill these parts. + rom = append(rom, make([]byte, prog.Paddr-romEnd)...) + } + if prog.Paddr != progs[0].Paddr+uint64(len(rom)) { + diff := prog.Paddr - (progs[0].Paddr + uint64(len(rom))) + if diff > maxPadBytes { + return 0, nil, objcopyError{"ROM segments are non-contiguous: " + path, nil} + } + // Pad the difference + rom = append(rom, make([]byte, diff)...) + } + data, err := io.ReadAll(prog.Open()) + if err != nil { + return 0, nil, objcopyError{"failed to extract segment from ELF file: " + path, err} + } + rom = append(rom, data...) + } + if progs[0].Paddr < startAddr { + // The lowest memory address is before the first section. This means + // that there is some extra data loaded at the start of the image that + // should be discarded. + // Example: ELF files where .text doesn't start at address 0 because + // there is a bootloader at the start. + return startAddr, rom[startAddr-progs[0].Paddr:], nil + } else { + return progs[0].Paddr, rom, nil + } +} + +// objcopy converts an ELF file to a different (simpler) output file format: +// .bin or .hex. It extracts only the .text section. +func objcopy(infile, outfile, binaryFormat string) error { + f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer f.Close() + + // Read the .text segment. + _, data, err := extractROM(infile) + if err != nil { + return err + } + + // Write to the file, in the correct format. + switch binaryFormat { + case "bin": + // The start address is not stored in raw firmware files (therefore you + // should use .hex files in most cases). + _, err := f.Write(data) + return err + default: + panic("unreachable") + } +} diff --git a/internal/firmware/uf2.go b/internal/firmware/uf2.go new file mode 100644 index 00000000..bfc99b3c --- /dev/null +++ b/internal/firmware/uf2.go @@ -0,0 +1,155 @@ +// From tinygo/builder/uf2.go + +package firmware + +// This file converts firmware files from BIN to UF2 format before flashing. +// +// For more information about the UF2 firmware file format, please see: +// https://github.com/Microsoft/uf2 +// +// + +import ( + "bytes" + "encoding/binary" + "os" + "strconv" +) + +// convertELFFileToUF2File converts an ELF file to a UF2 file. +func convertELFFileToUF2File(infile, outfile string, uf2FamilyID string) error { + // Read the .text segment. + targetAddress, data, err := extractROM(infile) + if err != nil { + return err + } + + output, _, err := convertBinToUF2(data, uint32(targetAddress), uf2FamilyID) + if err != nil { + return err + } + return os.WriteFile(outfile, output, 0644) +} + +// convertBinToUF2 converts the binary bytes in input to UF2 formatted data. +func convertBinToUF2(input []byte, targetAddr uint32, uf2FamilyID string) ([]byte, int, error) { + blocks := split(input, 256) + output := make([]byte, 0) + + bl, err := newUF2Block(targetAddr, uf2FamilyID) + if err != nil { + return nil, 0, err + } + bl.SetNumBlocks(len(blocks)) + + for i := 0; i < len(blocks); i++ { + bl.SetBlockNo(i) + bl.SetData(blocks[i]) + + output = append(output, bl.Bytes()...) + bl.IncrementAddress(bl.payloadSize) + } + + return output, len(blocks), nil +} + +const ( + uf2MagicStart0 = 0x0A324655 // "UF2\n" + uf2MagicStart1 = 0x9E5D5157 // Randomly selected + uf2MagicEnd = 0x0AB16F30 // Ditto +) + +// uf2Block is the structure used for each UF2 code block sent to device. +type uf2Block struct { + magicStart0 uint32 + magicStart1 uint32 + flags uint32 + targetAddr uint32 + payloadSize uint32 + blockNo uint32 + numBlocks uint32 + familyID uint32 + data []uint8 + magicEnd uint32 +} + +// newUF2Block returns a new uf2Block struct that has been correctly populated +func newUF2Block(targetAddr uint32, uf2FamilyID string) (*uf2Block, error) { + var flags uint32 + var familyID uint32 + if uf2FamilyID != "" { + flags |= flagFamilyIDPresent + v, err := strconv.ParseUint(uf2FamilyID, 0, 32) + if err != nil { + return nil, err + } + familyID = uint32(v) + } + return &uf2Block{magicStart0: uf2MagicStart0, + magicStart1: uf2MagicStart1, + magicEnd: uf2MagicEnd, + targetAddr: targetAddr, + flags: flags, + familyID: familyID, + payloadSize: 256, + data: make([]byte, 476), + }, nil +} + +const ( + flagFamilyIDPresent = 0x00002000 +) + +// Bytes converts the uf2Block to a slice of bytes that can be written to file. +func (b *uf2Block) Bytes() []byte { + buf := bytes.NewBuffer(make([]byte, 0, 512)) + binary.Write(buf, binary.LittleEndian, b.magicStart0) + binary.Write(buf, binary.LittleEndian, b.magicStart1) + binary.Write(buf, binary.LittleEndian, b.flags) + binary.Write(buf, binary.LittleEndian, b.targetAddr) + binary.Write(buf, binary.LittleEndian, b.payloadSize) + binary.Write(buf, binary.LittleEndian, b.blockNo) + binary.Write(buf, binary.LittleEndian, b.numBlocks) + binary.Write(buf, binary.LittleEndian, b.familyID) + binary.Write(buf, binary.LittleEndian, b.data) + binary.Write(buf, binary.LittleEndian, b.magicEnd) + + return buf.Bytes() +} + +// IncrementAddress moves the target address pointer forward by count bytes. +func (b *uf2Block) IncrementAddress(count uint32) { + b.targetAddr += b.payloadSize +} + +// SetData sets the data to be used for the current block. +func (b *uf2Block) SetData(d []byte) { + b.data = make([]byte, 476) + copy(b.data[:], d) +} + +// SetBlockNo sets the current block number to be used. +func (b *uf2Block) SetBlockNo(bn int) { + b.blockNo = uint32(bn) +} + +// SetNumBlocks sets the total number of blocks for this UF2 file. +func (b *uf2Block) SetNumBlocks(total int) { + b.numBlocks = uint32(total) +} + +// split splits a slice of bytes into a slice of byte slices of a specific size limit. +func split(input []byte, limit int) [][]byte { + var block []byte + output := make([][]byte, 0, len(input)/limit+1) + for len(input) >= limit { + // add all blocks + block, input = input[:limit], input[limit:] + output = append(output, block) + } + if len(input) > 0 { + // add remaining block (that isn't full sized) + output = append(output, input) + } + return output +} diff --git a/internal/targets/config.go b/internal/targets/config.go index e30e8fab..99938e74 100644 --- a/internal/targets/config.go +++ b/internal/targets/config.go @@ -29,6 +29,8 @@ type Config struct { // Binary and firmware configuration BinaryFormat string `json:"binary-format"` + // UF2 configuration + UF2FamilyID string `json:"uf2-family-id"` // Flash and deployment configuration FlashCommand string `json:"flash-command"` @@ -39,9 +41,6 @@ type Config struct { MSDVolumeName []string `json:"msd-volume-name"` MSDFirmwareName string `json:"msd-firmware-name"` - // UF2 configuration - UF2FamilyID string `json:"uf2-family-id"` - // Device-specific configuration RP2040BootPatch bool `json:"rp2040-boot-patch"` @@ -66,6 +65,13 @@ func (c *Config) IsEmpty() bool { return c.Name == "" && c.LLVMTarget == "" && c.GOOS == "" && c.GOARCH == "" } +func (c *Config) FormatDetail() string { + if c.BinaryFormat == "uf2" { + return c.UF2FamilyID + } + return "" +} + // HasInheritance returns true if this config inherits from other configs func (rc *RawConfig) HasInheritance() bool { return len(rc.Inherits) > 0