Following a reboot: RP2350 BOOTSEL¶
A device that updates in place can leave the job to the chip's boot ROM: the app reboots into the ROM bootloader,
the host writes the new image, the ROM boots it. On the RP2350 (and RP2040) that is BOOTSEL mode, where the ROM
offers a UF2 drive and PICOBOOT, the USB interface picotool speaks. Nothing of WireWeaver runs there, so the host
has to find the same unit again after each reboot, talk PICOBOOT to it, and find it once more when the new firmware
is up. This is CLT-21 in the features, and the "update stage 1" of the rePort.
app (WireWeaver) boot ROM (BOOTSEL) app (WireWeaver), new image
serial = chip id ──► serial = chip id ──► serial = chip id
BootControl::reboot PICOBOOT: erase, write,
(Updater) verify, REBOOT2
What the firmware does¶
- Report the chip id as the USB serial number, in the boot ROM's format: the 64-bit id (
GET_INFOSYS chip info, or OTP), 16 hex digits, high word first.wire_weaver_client::follow::rp2350_serial(chip_id)makes the same string on the host. Matching ignores case, as all device filters do. - Implement
ww_dfu::UpdateNodeandBootControl(the lean app of the ww_dfu scenarios), withRole::App,Profile::InPlace,Offers { boot_control: true, .. }.reboot(Updater)replies first, then calls the ROM'srebootinto BOOTSEL (rom_reboot(REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL, ..)) a few tens of milliseconds later, so the reply leaves. - Keep PICOBOOT enabled in BOOTSEL (don't pass
BOOTSEL_FLAG_DISABLE_PICOBOOT_INTERFACE, don't disable it in OTP).
What the host does¶
follow::rp2_update() and the wire_weaver_client::ww_picoboot re-export need the picoboot Cargo feature of
wire_weaver_client (off by default, pulls in ww_picoboot and its nusb-backed PICOBOOT transport): most host apps
that only talk WireWeaver over USB never flash an RP2040 / RP2350 boot ROM, so they shouldn't pay for it.
follow::rp2_update_with() and follow::Follow/Bus/Found stay available without it — that is what
tests/follow_reboot runs against a simulated device.
use wire_weaver_client::follow;
use wire_weaver_client::ww_picoboot::{Image, image::family};
let serial = device_info.serials[0].clone(); // as listed, before the reboot
app.boot().reboot(BootTarget::Updater).call().await?;
drop(app);
let image = Image::from_uf2(&std::fs::read("app.uf2")?, &[family::RP2350_ARM_S])?;
let back = follow::rp2_update(&serial, &image, &mut ()).await?; // DeviceInfo of the app that came back
let app = App::config(|c| c.usb().serial_eq(serial)).connect().await?;
rp2_update waits for the BOOTSEL device with the serial number (10 s), opens its PICOBOOT interface, writes and
verifies the image, sends REBOOT2 as picotool load -x does (FLASH_UPDATE at the image's base, 500 ms delay) and
waits for a WireWeaver device with the same serial number (10 s). A timeout says what was there instead, e.g. the app
still running because it ignored the reboot request.
The pieces work on their own too:
follow::Followwaits on anyfollow::Bus(UsbBuslists real USB devices):gone()until no device has the serial number (needed when the device reboots into the same kind of firmware; it has to stay off the bus longer thanFollow::poll, 50 ms, which USB re-enumeration always does),back()with a predicate onfollow::Found(WireWeaver,RpBootsel,Other),back_in_bootsel(),back_in_wire_weaver().follow::rp2_update_with()takes the bus and the PICOBOOT opener as arguments: that is how the tests run it on a simulated device.ww_picobootis the PICOBOOT host on its own:usb::open(Some(serial))gives aPicobootwithflash()(erase the touched 4 KiB sectors, write 256-byte pages, read back, with a progress callback),read,write,flash_erase,chip_id,reboot2.Imagecomes from a UF2 file (blocks of the given families; picotool's RP2350-E10 dummy block is skipped), an ELF file (PT_LOADsegments by their flash physical address, straight fromcargo build, noobjcopy), or a raw binary at an address. No libusb: nusb, pure Rust. It lives in its own repo, picoboot_host (crate namepicoboot_host, re-exported here asww_picoboot), with no WireWeaver dependency of its own.
From the command line¶
ww dfu rp list # boot ROMs in BOOTSEL mode with their serial numbers
ww dfu rp info -s E661 # chip id over PICOBOOT
ww dfu rp flash app.uf2 -s E661 # erase, write, verify, reboot into the new image
ww dfu rp flash app.uf2 -s E661640843522C2F --wait 10 --follow
# after the app was asked to reboot(Updater): wait for the
# boot ROM, flash, wait for the app to come back
ww dfu rp reboot -s E661 [--bootsel]
-s / --serial selects the device as for every other command (flag, WW_SERIAL, ww.toml), a part of the serial
number is enough when only one device matches; --wait and --follow need the full one. An ELF file (detected by
its magic bytes, so target/.../release/app works directly) is taken by its flash physical address; otherwise a
raw binary (any extension other than .uf2) is written at --addr, 0x10000000 by default. On Linux the boot ROM (2e8a:000f) needs a udev
rule for access without root, as picotool does.
The app is asked to reboot into BOOTSEL with ww dfu reboot, which calls
ww_dfu::BootControl on any device that implements it, whatever its API and version (a global trait path, API-20 and
CLT-10):
ww dfu reboot -s E661640843522C2F # app -> BOOTSEL
ww dfu rp flash app.uf2 -s E661640843522C2F --wait 10 --follow
STM32 system bootloader (ROM DFU)¶
The same sequence for an STM32 whose updater is the ROM bootloader (CLT-34): the app replies to reboot(Updater),
sets whatever the chip needs to enter the system memory bootloader (on the H72x a backup-register magic checked first
thing after reset, since the bootloader can't be entered with the peripherals dirty), and resets. The bootloader
enumerates as 0483:df11 ("STM32 BOOTLOADER") on PA11/PA12 (USB FS, HSI48 with CRS, no crystal needed; AN2606 §59) and
speaks DFU with the DfuSe extension (AN3156). It doesn't work under RDP level 1.
- Report the serial number the bootloader reports, so the host can follow the unit. The ROM forms it from the
chip's unique id;
follow::stm32_serial([uid0, uid1, uid2])makes the string ST's USB library uses (UID0 + UID2 as 8 hex digits, the top 16 bits of UID1 as 4) and has not been compared with a real H72x yet:ww dfu stm32 listshows the real one. - Implement
reboot(Updater)as above.
use wire_weaver_client::follow;
use wire_weaver_client::ww_stm32_dfu::Image;
let image = Image::from_elf(&std::fs::read("bootloader.elf")?)?; // or Image::from_bin(0x0800_0000, bytes)?
app.boot().reboot(BootTarget::Updater).call().await?;
drop(app);
// waits for 0483:df11 with the serial, erases the sectors the image touches, writes, verifies, jumps to the start
// of flash, waits for the firmware to come back
let back = follow::stm32_dfu_update(&serial, &image, None, &mut ()).await?;
stm32_dfu_update() and the ww_stm32_dfu re-export need the stm32_dfu Cargo feature of wire_weaver_client;
stm32_dfu_update_with() retries the open of the DFU interface for Timeouts::open (3 s), since the bootloader can be listed before the host's USB stack lets it be opened; it takes the bus and the DFU opener as arguments and needs neither. ww_stm32_dfu is the DfuSe
host on its own (stm32_dfu_host, no WireWeaver dependency): usb::open(Some(serial)) gives a Dfuse with
flash() (erase, write, read back, progress callback), erase_sector, read, write, leave, and the parsed
layout(); the sector sizes come from the device's own layout string, so one tool fits all STM32 families.
ww dfu stm32 list # bootloaders in DFU mode with their serial numbers
ww dfu stm32 info -s 3A0B # the alt settings and their memory layout
ww dfu stm32 flash bootloader.elf -s 3A0B # erase, write, verify, jump to the start of flash
ww dfu stm32 flash app.bin --addr 0x08060000 --leave-addr 0x08000000 -s 3A0B
ww dfu stm32 flash bootloader.elf -s 3A0B1C2D4E5F --wait 10 --follow
# after the app was asked to reboot(Updater)
ww dfu stm32 read 0x08060000 0x80 -s 3A0B # hexdump; -o FILE writes the raw bytes
ww dfu stm32 erase 0x08060000 -s 3A0B # the sector containing the address
ww dfu stm32 erase --sectors 3..=5 -s 3A0B # sectors by index (A..B excludes B, A..=B includes it)
--no-leave stays in DFU, --no-verify skips the read-back. On Linux the bootloader needs a udev rule for access
without root.
ww_stm32_dfu::sim::SimStm32Dfu simulates the bootloader at the level of the control requests (DFU state machine
with busy polling and error states, DfuSe set address and erase, NOR flash that rejects programming without an erase
and unaligned writes); tests/follow_reboot runs the whole sequence on it: v1 → reboot(Updater) → 0483:df11 under
the same serial → v2 written → jump → v2 runs. What only hardware shows: the exact serial string, the transfer size
and erase times of the H72x, the behaviour under the backup-register entry.
Tests without hardware¶
ww_picoboot::sim::SimRp2350 (feature sim) is a boot ROM at the level of PICOBOOT's USB transfers: command
packets, data phases, zero-length status packets, stalls on refused commands, IF_RESET / IF_CMD_STATUS, flash
that only clears bits when programmed. tests/follow_reboot puts it under a simulated RP2350 device whose app (the
dfu_lean_app_api server on the in-process simulated USB) boots from that flash and reports the image version, and
runs the whole sequence: v1 → reboot(Updater) → BOOTSEL under the same serial → v2 written over PICOBOOT → v2 runs.
What only hardware shows: real enumeration times, the ROM's exact serial number string, REBOOT2 behaviour with
partition tables, Windows and macOS drivers.