Add single-drag combined UF2 (bootloader + manifest + app), verified on hardware

This commit is contained in:
Kevin Thomas committed 2026-10-13 14:04:22 -04:00
1 parent cf6f1f2c33
commit 2c8ee0a70e
7 files changed
+185 -24

No files matched your search

Binary file not shown.
+3 -2
View File
@@ -81,8 +81,9 @@ pico_enable_stdio_usb(0x0026a_app 0)
pico_set_linker_script(0x0026a_app
${CMAKE_CURRENT_LIST_DIR}/app/memmap_app.ld)
# The application starts at 0x10020000, so a UF2 (which BOOTSEL always writes to
# 0x10000000) would be wrong. Emit the raw artifacts we flash with OpenOCD.
# The application starts at 0x10020000. A standalone app UF2 would flash only
# this one region, so the lab ships a single combined UF2 instead (built by
# tools/make_full_uf2.py). Emit the raw artifacts we sign and flash with OpenOCD.
pico_add_dis_output(0x0026a_app)
pico_add_hex_output(0x0026a_app)
pico_add_bin_output(0x0026a_app)
+17 -8
View File
@@ -24,13 +24,14 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2
## Artifacts
| File | Description |
| ---------------------------- | ---------------------------------------- |
| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) |
| `0x0026a_bootloader.uf2` | Bootloader for the BOOTSEL drag-and-drop |
| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) |
| `manifest.bin` | Manifest (flash at `0x1001F000`) |
| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook |
| File | Description |
| ---------------------------- | ------------------------------------------------- |
| `0x0026a_full.uf2` | **All three images** in one BOOTSEL drag |
| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) |
| `0x0026a_bootloader.uf2` | Bootloader-only UF2 for the BOOTSEL drag-and-drop |
| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) |
| `manifest.bin` | Manifest (flash at `0x1001F000`) |
| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook |
## Build
@@ -38,12 +39,20 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2
cmake -B build -DPICO_SDK_PATH=~/.pico-sdk/sdk/2.2.0
cmake --build build
python3 tools/sign_firmware.py build/0x0026a_app.bin --out manifest.bin --key "IronNetBootKey!!" --app-base 0x10020000
PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool python3 tools/make_full_uf2.py
```
This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer writes `manifest.bin`.
This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer
writes `manifest.bin`; `make_full_uf2.py` packs all three into
`0x0026a_full.uf2`.
## Flash
Easiest - copy `0x0026a_full.uf2` to the `RP2350` BOOTSEL drive; one drag writes
the whole chain (bootloader + manifest + app).
Or with OpenOCD:
```sh
openocd -f interface/picoprobe.cfg -f target/rp2350.cfg -c \
"program 0x0026a_bootloader.bin 0x10000000 verify; \
+46 -13
View File
@@ -519,15 +519,31 @@ $ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-nm -n build/0x0026a_app.elf
...
```
### 6.9 Why the Application Has No .uf2
### 6.9 One UF2 for the Whole Chain
A UF2 file writes to the address encoded in each of its blocks, and the SDK's
UF2 tooling always targets `0x10000000`. If a user holds BOOTSEL and drags in
`0x0026a_app.uf2`, the app would be written at the wrong base and the
bootloader would jump into garbage (or nothing). So the app is deliberately
built **without** a UF2 and with explicit `.bin`/`.hex`/`.dis`/`.map` outputs
instead. The **bootloader** still ships a UF2 (it belongs at `0x10000000`), and
the manifest + app are written to their own addresses with OpenOCD.
No single image is the whole story. The device needs the bootloader at
`0x10000000`, the manifest at `0x1001F000`, and the app at `0x10020000`. A
standalone `0x0026a_app.uf2` would flash only the app region, leaving the other
two to be written separately, so the app is deliberately built with explicit
`.bin`/`.hex`/`.dis`/`.map` outputs instead of its own UF2.
To bring a bare board up in one move the lab ships a single combined UF2,
`0x0026a_full.uf2`, that carries all three images. It is built by laying the
images out flat - offset 0 mapped to `0x10000000`, the reserved gaps filled
with `0xFF` - and handing the result to picotool as one image:
```
$ PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool \
python3 tools/make_full_uf2.py
```
picotool numbers the blocks, tags them with the `rp2350-arm-s` family, and
injects the RP2350-E10 "absolute block" (family `0xE48BFF57` at `0x10FFFF00`)
that tells the boot ROM to write every block to its **absolute** address rather
than into a partition. A single BOOTSEL drag then restores the whole chain.
Part 11 uses it. Note the contrast with the naive file layout: a UF2 is not
restricted to `0x10000000`, but a multi-region UF2 is only correct once the
blocks and the E10 absolute block are laid out the way picotool produces them.
---
@@ -974,11 +990,27 @@ the manifest, then the app. If you flash the app before the manifest, the
bootloader happily boots it anyway (the manifest is already in flash from a
previous lab run) - order only matters on a bare board.
### The whole chain in one drag (recommended)
On a bare board skip straight to one file: hold BOOTSEL, connect the Pico 2,
and copy `0x0026a_full.uf2` to the `RP2350` drive. That single UF2 writes all
three images at once - no OpenOCD, no second drag. Section 6.9 explains how it
is built; the hardware-verified behavior is:
- BOOTSEL accepts it and unmounts the drive;
- the boot ROM writes bootloader, manifest, and app to their absolute
addresses;
- on reset the bootloader verifies the manifest and jumps to the app.
The steps below are the three-image equivalent, which is what you use while
iterating on a single image or when you prefer OpenOCD over the button.
### A. The bootloader: two options
**Option 1 - BOOTSEL (no extra tools):** hold the BOOTSEL button, connect the
Pico 2, copy `0x0026a_bootloader.uf2` to the drive. BOOTSEL always writes at
`0x10000000`, which is correct for the bootloader.
Pico 2, copy `0x0026a_bootloader.uf2` to the drive, which lands the bootloader
at `0x10000000` (or drag `0x0026a_full.uf2` to write the whole chain in one
shot).
**Option 2 - OpenOCD:**
@@ -1107,9 +1139,10 @@ the bootloader refuses.
### Step 13: Confirm it still boots after restoring the good image
Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset. The
banner returns. The bootloader is doing real enforcement, not just complaining
once.
Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset - or just
drag `0x0026a_full.uf2` onto the BOOTSEL drive again to restore the whole
chain in one move. The banner returns. The bootloader is doing real
enforcement, not just complaining once.
Stop and think about the security statement this demonstrated:
Binary file not shown.
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Build a single BOOTSEL-draggable UF2 holding the whole secure-boot chain.
The 0x0026a chain is three separate images at three flash addresses:
bootloader 0x10000000 (owns the system, verifies the next image)
manifest 0x1001F000 (header + AES-128-CMAC tag)
app 0x10020000 (the signed application)
The SDK emits a UF2 only for the bootloader, so a bare board would need three
separate flashes. This tool packs all three into ONE UF2 so a single BOOTSEL
drag-and-drop brings the whole device up.
How it works:
1. Lay the three images out in a flat buffer whose offset 0 maps to
0x10000000, filling the reserved gap between them with 0xFF (erased
flash).
2. Hand that flat image to picotool as a normal image at 0x10000000.
3. picotool numbers the blocks, tags them with the rp2350-arm-s family and
injects the RP2350-E10 absolute block that makes the boot ROM write each
block to its absolute address.
The result is exactly the shape the boot ROM accepts over the BOOTSEL mass
storage interface, and it has been verified on hardware: the ROM accepts the
drag, the bootloader verifies the manifest, and the app runs.
"""
import argparse
import hashlib
import os
import struct
import subprocess
import sys
import tempfile
BOOTLOADER_BASE = 0x10000000
MANIFEST_BASE = 0x1001F000
APP_BASE = 0x10020000
FAMILY = "rp2350-arm-s"
PLATFORM = "rp2350"
def build_flat(bootloader, manifest, app):
"""Lay the three images out flat, offset 0 mapped to 0x10000000."""
if APP_BASE < MANIFEST_BASE < BOOTLOADER_BASE:
raise SystemExit("address map is not ordered")
size = (APP_BASE - BOOTLOADER_BASE) + len(app)
image = bytearray(b"\xFF" * size)
image[0:len(bootloader)] = bootloader
m_off = MANIFEST_BASE - BOOTLOADER_BASE
image[m_off:m_off + len(manifest)] = manifest
a_off = APP_BASE - BOOTLOADER_BASE
image[a_off:a_off + len(app)] = app
return bytes(image)
def convert(flat, out, picotool):
"""Run picotool to turn the flat image into an absolute-address UF2."""
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp:
tmp.write(flat)
flat_path = tmp.name
try:
cmd = [
picotool, "uf2", "convert", flat_path, out,
"-o", hex(BOOTLOADER_BASE),
"--family", FAMILY,
"--platform", PLATFORM,
"--abs-block",
]
subprocess.run(cmd, check=True)
finally:
os.unlink(flat_path)
def verify(out):
"""Report the block count and the families present in the UF2."""
data = open(out, "rb").read()
blocks = [data[i:i + 512] for i in range(0, len(data), 512)]
families = set()
payload = 0
for blk in blocks:
if len(blk) != 512 or struct.unpack_from("<I", blk, 0)[0] != 0x0A324655:
continue
families.add(struct.unpack_from("<I", blk, 28)[0])
if not (struct.unpack_from("<I", blk, 8)[0] & 0x8000):
payload += 1
digest = hashlib.sha256(data).hexdigest()
print("wrote %s" % out)
print(" blocks : %d (%d payload)" % (len(blocks), payload))
print(" families : %s" % ", ".join(hex(f) for f in sorted(families)))
print(" sha256 : %s" % digest)
def main():
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--dir", default=here, help="folder holding the images")
ap.add_argument("--bootloader", default="0x0026a_bootloader.bin")
ap.add_argument("--manifest", default="manifest.bin")
ap.add_argument("--app", default="0x0026a_app.bin")
ap.add_argument("--out", default="0x0026a_full.uf2")
ap.add_argument("--picotool", default=os.environ.get("PICOTOOL", "picotool"),
help="picotool binary (env PICOTOOL)")
args = ap.parse_args()
bootloader = open(os.path.join(args.dir, args.bootloader), "rb").read()
manifest = open(os.path.join(args.dir, args.manifest), "rb").read()
app = open(os.path.join(args.dir, args.app), "rb").read()
flat = build_flat(bootloader, manifest, app)
out = os.path.join(args.dir, args.out)
convert(flat, out, args.picotool)
verify(out)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -581,7 +581,7 @@ Structures and Functions in Embedded Systems: Debugging and Hacking w/ IR Remote
### 0x0026a Bootloader BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.bin)
### 0x0026a Bootloader UF2 [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.uf2)
### 0x0026a Full Secure Boot UF2 (bootloader + manifest + app) [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_full.uf2)
### 0x0026a Signed Application BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_app.bin)