Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RP2350 support #4459

Draft
wants to merge 15 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@ test.exe
test.gba
test.hex
test.nro
test.uf2
test.wasm
wasm.wasm

*.uf2
*.elf
48 changes: 47 additions & 1 deletion builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"

"github.com/gofrs/flock"
"github.com/soypat/tinyboot/boot/picobin"
"github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/compiler"
"github.com/tinygo-org/tinygo/goenv"
Expand Down Expand Up @@ -804,6 +805,12 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
return fmt.Errorf("could not modify stack sizes: %w", err)
}
}

// Apply patches of bootloader in the order they appear.
if len(config.Target.BootPatches) > 0 {
err = applyPatches(result.Executable, config.Target.BootPatches)
}

if config.RP2040BootPatch() {
// Patch the second stage bootloader CRC into the .boot2 section
err = patchRP2040BootCRC(result.Executable)
Expand Down Expand Up @@ -1422,6 +1429,23 @@ func printStacks(calculatedStacks []string, stackSizes map[string]functionStackS
}
}

func applyPatches(executable string, bootPatches []string) (err error) {
for _, patch := range bootPatches {
switch patch {
case "rp2040":
err = patchRP2040BootCRC(executable)
case "rp2350":
err = patchRP2350BootIMAGE_DEF(executable)
default:
err = errors.New("undefined boot patch name")
}
if err != nil {
return fmt.Errorf("apply boot patch %q: %w", patch, err)
}
}
return nil
}

// RP2040 second stage bootloader CRC32 calculation
//
// Spec: https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf
Expand All @@ -1433,7 +1457,7 @@ func patchRP2040BootCRC(executable string) error {
}

if len(bytes) != 256 {
return fmt.Errorf("rp2040 .boot2 section must be exactly 256 bytes")
return fmt.Errorf("rp2040 .boot2 section must be exactly 256 bytes, got %d", len(bytes))
}

// From the 'official' RP2040 checksum script:
Expand All @@ -1460,6 +1484,21 @@ func patchRP2040BootCRC(executable string) error {
return replaceElfSection(executable, ".boot2", bytes)
}

// RP2350 block patching.
func patchRP2350BootIMAGE_DEF(executable string) error {
boot2, _, err := getElfSectionData(executable, ".boot2")
if err != nil {
return err
}
item0 := picobin.MakeImageDef(picobin.ImageTypeExecutable, picobin.ExeSecSecure, picobin.ExeCPUARM, picobin.ExeChipRP2350, false)
newBoot := make([]byte, 256)
newBoot, _, err = picobin.AppendBlockFromItems(newBoot[:0], []picobin.Item{item0.Item}, boot2, 0)
off := len(newBoot)
newBoot, _, err = picobin.AppendFinalBlock(newBoot, -off)
// Update the .boot2 section to included the CRC
return replaceElfSection(executable, ".boot2", newBoot)
}

// lock may acquire a lock at the specified path.
// It returns a function to release the lock.
// If flock is not supported, it does nothing.
Expand All @@ -1472,3 +1511,10 @@ func lock(path string) func() {

return func() { flock.Close() }
}

func b2u8(b bool) uint8 {
if b {
return 1
}
return 0
}
85 changes: 25 additions & 60 deletions builder/objcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package builder

import (
"debug/elf"
"io"
"fmt"
"os"
"sort"

"github.com/marcinbor85/gohex"
"github.com/soypat/tinyboot/build/elfutil"
)

// maxPadBytes is the maximum allowed bytes to be padded in a rom extraction
Expand All @@ -26,18 +26,12 @@ func (e objcopyError) Error() string {
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}
return 0, nil, objcopyError{Op: "failed to open ELF file to extract text segment", Err: err}
}
defer f.Close()

Expand All @@ -47,62 +41,33 @@ func extractROM(path string) (uint64, []byte, error) {
// > 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
}
start, end, err := elfutil.ROMAddr(f)
if err != nil {
return 0, nil, objcopyError{Op: "failed to calculate ELF ROM addresses", Err: err}
}

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)
err = elfutil.EnsureROMContiguous(f, start, end, maxPadBytes)
if err != nil {
return 0, nil, objcopyError{Op: "checking if ELF ROM contiguous", Err: err}
}
if len(progs) == 0 {
return 0, nil, objcopyError{"file does not contain ROM segments: " + path, nil}
const (
_ = 1 << (iota * 10)
kB
MB
GB
)
const maxSize = 1 * GB
if end-start > maxSize {
return 0, nil, objcopyError{Op: fmt.Sprintf("obj size exceeds max %d/%d, bad ELF address calculation?", end-start, maxSize)}
}
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
ROM := make([]byte, end-start)
n, err := elfutil.ReadROMAt(f, ROM, start)
if err != nil {
return 0, nil, objcopyError{Op: "reading ELF ROM", Err: err}
} else if n != len(ROM) {
return 0, nil, objcopyError{Op: "short ELF ROM read"}
}
return start, ROM, nil
}

// objcopy converts an ELF file to a different (simpler) output file format:
Expand Down
148 changes: 18 additions & 130 deletions builder/uf2.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,146 +8,34 @@ package builder
//

import (
"bytes"
"encoding/binary"
"math"
"os"
"strconv"

"github.com/soypat/tinyboot/build/uf2"
)

// 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
uf2Formatter := uf2.Formatter{ChunkSize: 256}
if uf2FamilyID != "" {
err := uf2Formatter.SetFamilyID(uf2FamilyID)
if err != nil {
return err
}
}

output, _, err := convertBinToUF2(data, uint32(targetAddress), uf2FamilyID)
start, ROM, err := extractROM(infile)
if err != nil {
return err
} else if start > math.MaxUint32 {
return objcopyError{Op: "ELF start ROM address overflows uint32"}
}
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)
// Write raw ROM contents in UF2 format to outfile.
expectBlocks := len(ROM)/int(uf2Formatter.ChunkSize) + 1
uf2data := make([]byte, expectBlocks*uf2.BlockSize)
uf2data, _, err = uf2Formatter.AppendTo(uf2data[:0], ROM, uint32(start))
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 objcopyError{Op: "writing UF2 blocks", Err: err}
}
return output
return os.WriteFile(outfile, uf2data, 0644)
}
1 change: 1 addition & 0 deletions compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type TargetSpec struct {
LinkerScript string `json:"linkerscript,omitempty"`
ExtraFiles []string `json:"extra-files,omitempty"`
RP2040BootPatch *bool `json:"rp2040-boot-patch,omitempty"` // Patch RP2040 2nd stage bootloader checksum
BootPatches []string `json:"boot-patches,omitempty"` // Bootloader patches to be applied in the order they appear.
Emulator string `json:"emulator,omitempty"`
FlashCommand string `json:"flash-command,omitempty"`
GDB []string `json:"gdb,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/soypat/tinyboot v0.0.0-20240922191516-a0ab175013d3 // indirect
github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/text v0.16.0 // indirect
)
Loading
Loading