Add UART BOOTSEL reboot command to the regular firmware
The bridge sends 0xAA 0xFE 0x08 0x01 'BOOTSEL' ck; the firmware verifies checksum and magic and calls reset_usb_boot(). Exposed as controller-uart-bridge --reboot-bootsel PORT and build.py --uart-port PORT, which waits for the ROM loader before running picotool.
This commit is contained in:
parent
b6a017eb06
commit
6e0676d9df
8 changed files with 161 additions and 1 deletions
19
README.md
19
README.md
|
|
@ -2649,6 +2649,25 @@ The firmware acknowledges the endpoint-zero request, waits 50 ms, and then
|
|||
calls the Pico ROM `reset_usb_boot()` entry point. Physical BOOTSEL remains the
|
||||
fallback if the firmware or USB management path is unavailable.
|
||||
|
||||
The regular UART firmware has the same shortcut over its serial link. The
|
||||
host sends the command frame `0xAA 0xFE 0x08 0x01 "BOOTSEL" checksum`; the
|
||||
firmware verifies the checksum and magic, flushes UART TX, and calls
|
||||
`reset_usb_boot()`. Line noise or a misframed report cannot trigger it. The
|
||||
Pico can stay plugged into the Switch or a PC; only the UART adapter needs to
|
||||
be connected to the host:
|
||||
|
||||
```sh
|
||||
# One-off reboot, then flash the published image
|
||||
uv run controller-uart-bridge --reboot-bootsel /dev/ttyUSB0
|
||||
picotool load -v -x firmware/switch-pico.uf2
|
||||
|
||||
# Build, publish, reboot the running Pico and flash in one step
|
||||
uv run python build.py --uart-port /dev/ttyUSB0
|
||||
```
|
||||
|
||||
Firmware from before this command ignores the frame; hold BOOTSEL while
|
||||
replugging once to install a build that has it.
|
||||
|
||||
Flash alternatives: bootsel + drag-drop or `picotool load`.
|
||||
Flags:
|
||||
- `SWITCH_PICO_LOG`: enable/disable UART logging on the Pico.
|
||||
|
|
|
|||
50
build.py
50
build.py
|
|
@ -8,6 +8,7 @@ import re
|
|||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
|
|
@ -309,9 +310,19 @@ def parse_args():
|
|||
metavar="RRGGBB",
|
||||
help="Set every emulated controller slot to the provided hex color.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--uart-port",
|
||||
metavar="PORT",
|
||||
help=(
|
||||
"Serial port of a running regular (UART) Pico. Sends the BOOTSEL reboot "
|
||||
"command before flashing so the button does not need to be held."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.wake_capture and (args.random_grip_color or args.grip_color):
|
||||
parser.error("wake capture firmware does not use grip-color options")
|
||||
if args.uart_port and (args.aio or args.wake_capture or args.wake_only):
|
||||
parser.error("--uart-port only applies to the regular UART firmware")
|
||||
if args.wake_only:
|
||||
if args.random_grip_color or args.grip_color is not None:
|
||||
parser.error("wake-only firmware does not use grip-color options")
|
||||
|
|
@ -522,7 +533,41 @@ def build_wake_only():
|
|||
return elf_path
|
||||
|
||||
|
||||
def flash(elf_path, allow_elf_override):
|
||||
BOOTSEL_ENUMERATION_TIMEOUT_S = 10.0
|
||||
|
||||
|
||||
def reboot_uart_pico_to_bootsel(port):
|
||||
"""Ask the running UART firmware to reboot into BOOTSEL and wait for the loader."""
|
||||
from switch_pico_bridge.switch_pico_uart import PicoUART
|
||||
|
||||
try:
|
||||
uart = PicoUART(port)
|
||||
except Exception as exc:
|
||||
sys.stderr.write(f"Error: Cannot open UART {port}: {exc}\n")
|
||||
sys.exit(1)
|
||||
try:
|
||||
uart.reboot_bootsel()
|
||||
finally:
|
||||
uart.close()
|
||||
print(f"Sent BOOTSEL reboot to {port}; waiting for the ROM loader to enumerate...")
|
||||
picotool = resolve_picotool()
|
||||
deadline = time.monotonic() + BOOTSEL_ENUMERATION_TIMEOUT_S
|
||||
while True:
|
||||
result = subprocess.run(
|
||||
[str(picotool), "info"], cwd=SCRIPT_DIR, capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
if time.monotonic() >= deadline:
|
||||
sys.stderr.write(
|
||||
"Error: No BOOTSEL device appeared after the reboot command. "
|
||||
"Check the UART wiring/port or hold BOOTSEL while replugging.\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def flash(elf_path, allow_elf_override, uart_port=None):
|
||||
picotool = resolve_picotool()
|
||||
if not elf_path.exists():
|
||||
if allow_elf_override:
|
||||
|
|
@ -532,6 +577,8 @@ def flash(elf_path, allow_elf_override):
|
|||
else:
|
||||
sys.stderr.write(f"Error: Cannot find ELF at {elf_path}.\n")
|
||||
sys.exit(1)
|
||||
if uart_port:
|
||||
reboot_uart_pico_to_bootsel(uart_port)
|
||||
run_cmd([str(picotool), "load", str(elf_path), "-fx"])
|
||||
|
||||
|
||||
|
|
@ -599,6 +646,7 @@ def main():
|
|||
flash(
|
||||
elf_path,
|
||||
allow_elf_override=not args.aio,
|
||||
uart_port=args.uart_port,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "bsp/board.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "tusb.h"
|
||||
|
|
@ -6,6 +7,7 @@
|
|||
#include "usb/usb_output_driver.h"
|
||||
#ifndef SWITCH_PICO_BLUEPAD32
|
||||
#include "hardware/uart.h"
|
||||
#include "pico/bootrom.h"
|
||||
#else
|
||||
#include "adapter/adapter_mode_controller.h"
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
|
|
@ -34,6 +36,9 @@
|
|||
#define UART_RX_PIN 5
|
||||
#define UART_RUMBLE_HEADER 0xBB
|
||||
#define UART_RUMBLE_TYPE_SLOT 0x03
|
||||
// Host -> Pico command frame: 0xAA 0xFE payload_len payload... checksum.
|
||||
#define UART_COMMAND_VERSION 0xFE
|
||||
#define UART_COMMAND_REBOOT_BOOTSEL 0x01
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
|
|
@ -106,6 +111,29 @@ static void on_rumble_from_usb(uint8_t instance,
|
|||
}
|
||||
|
||||
#ifndef SWITCH_PICO_BLUEPAD32
|
||||
// Command frames share the report framing. The only command reboots into the
|
||||
// ROM BOOTSEL loader; it must carry the "BOOTSEL" magic so line noise or a
|
||||
// mis-framed report can never trigger it.
|
||||
static void handle_uart_command(const uint8_t* frame, uint8_t length) {
|
||||
static const uint8_t kBootselMagic[7] = {'B', 'O', 'O', 'T', 'S', 'E', 'L'};
|
||||
uint8_t sum = 0;
|
||||
for (uint8_t i = 0; i + 1 < length; ++i) {
|
||||
sum = static_cast<uint8_t>(sum + frame[i]);
|
||||
}
|
||||
if (sum != frame[length - 1]) {
|
||||
return;
|
||||
}
|
||||
const uint8_t payload_len = frame[2];
|
||||
const uint8_t* payload = frame + 3;
|
||||
if (payload_len == 1 + sizeof(kBootselMagic) &&
|
||||
payload[0] == UART_COMMAND_REBOOT_BOOTSEL &&
|
||||
memcmp(payload + 1, kBootselMagic, sizeof(kBootselMagic)) == 0) {
|
||||
LOG_PRINTF("[UART] reboot to BOOTSEL requested\n");
|
||||
uart_tx_wait_blocking(UART_ID);
|
||||
reset_usb_boot(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Consume UART bytes and forward complete frames to the Switch Pro driver.
|
||||
static bool poll_uart_frames() {
|
||||
static uint8_t buffer[64];
|
||||
|
|
@ -150,6 +178,12 @@ static bool poll_uart_frames() {
|
|||
}
|
||||
|
||||
if (expected_len > 0 && index >= expected_len) {
|
||||
if (buffer[1] == UART_COMMAND_VERSION) {
|
||||
handle_uart_command(buffer, expected_len);
|
||||
index = 0;
|
||||
expected_len = 0;
|
||||
continue;
|
||||
}
|
||||
ControllerState parsed{};
|
||||
uint8_t slot = 0;
|
||||
if (switch_pro_apply_uart_packet(buffer, expected_len, parsed, slot)) {
|
||||
|
|
|
|||
|
|
@ -721,6 +721,15 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|||
action="store_true",
|
||||
help="List serial ports, descriptions, and manufacturers, then exit. Uses port filters and --all-ports.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reboot-bootsel",
|
||||
metavar="PORT",
|
||||
help=(
|
||||
"Send the BOOTSEL reboot command to the Pico on PORT and exit, so it can be "
|
||||
"flashed with picotool without holding the button. Requires UART firmware "
|
||||
"with command support."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frequency",
|
||||
type=float,
|
||||
|
|
@ -1147,6 +1156,23 @@ def list_serial_ports(console: Console, args: argparse.Namespace) -> None:
|
|||
console.print(table)
|
||||
|
||||
|
||||
def reboot_pico_bootsel(port: str, baud: int, console: Console) -> None:
|
||||
"""Send the BOOTSEL reboot command over UART; the port vanishes on success."""
|
||||
try:
|
||||
uart = PicoUART(port, baud)
|
||||
except Exception as exc:
|
||||
console.print(f"[red]Failed to open UART {port}: {exc}[/red]")
|
||||
raise SystemExit(1) from exc
|
||||
try:
|
||||
uart.reboot_bootsel()
|
||||
finally:
|
||||
uart.close()
|
||||
console.print(
|
||||
f"[green]Sent BOOTSEL reboot to {port}; wait for the RPI-RP2 device, then "
|
||||
"run picotool load -v -x <image>.[/green]"
|
||||
)
|
||||
|
||||
|
||||
def resolve_mapping_slots(
|
||||
mappings: List[Tuple[int, str, Optional[int]]],
|
||||
slots_per_port: int,
|
||||
|
|
@ -1866,6 +1892,9 @@ def main() -> None:
|
|||
if args.list_ports:
|
||||
list_serial_ports(console, args)
|
||||
return
|
||||
if args.reboot_bootsel:
|
||||
reboot_pico_bootsel(args.reboot_bootsel, args.baud, console)
|
||||
return
|
||||
config = build_bridge_config(console, args)
|
||||
initialize_sdl(parser)
|
||||
contexts: Dict[int, ControllerContext] = {}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ from serial.tools import list_ports, list_ports_common
|
|||
|
||||
UART_HEADER = 0xAA
|
||||
UART_PROTOCOL_VERSION = 0x03
|
||||
# Command frames reuse the report framing with this version byte.
|
||||
UART_COMMAND_VERSION = 0xFE
|
||||
UART_COMMAND_REBOOT_BOOTSEL = 0x01
|
||||
UART_BOOTSEL_MAGIC = b"BOOTSEL"
|
||||
RUMBLE_HEADER = 0xBB
|
||||
# Legacy 5-byte frame (no slot) from firmware before multi-controller support.
|
||||
RUMBLE_TYPE_DECODED = 0x02
|
||||
|
|
@ -301,6 +305,22 @@ class PicoUART:
|
|||
"""Send a controller report to one of the Pico's controller slots."""
|
||||
self.serial.write(report.to_bytes(slot))
|
||||
|
||||
@staticmethod
|
||||
def reboot_bootsel_frame() -> bytes:
|
||||
"""Command frame that makes the UART firmware reboot into ROM BOOTSEL."""
|
||||
payload = bytes([UART_COMMAND_REBOOT_BOOTSEL]) + UART_BOOTSEL_MAGIC
|
||||
frame = bytes([UART_HEADER, UART_COMMAND_VERSION, len(payload)]) + payload
|
||||
return frame + bytes([compute_checksum(frame)])
|
||||
|
||||
def reboot_bootsel(self) -> None:
|
||||
"""Ask the Pico to reboot into BOOTSEL so picotool can flash it.
|
||||
|
||||
The serial device disappears once the Pico reboots; callers should
|
||||
close this object afterwards and wait for the BOOTSEL USB device.
|
||||
"""
|
||||
self.serial.write(self.reboot_bootsel_frame())
|
||||
self.serial.flush()
|
||||
|
||||
def read_rumble(self) -> Optional[Tuple[int, float, float]]:
|
||||
"""
|
||||
Extract one decoded rumble frame as (slot, low, high) with magnitudes
|
||||
|
|
|
|||
|
|
@ -217,3 +217,13 @@ def test_rumble_frame_with_out_of_range_slot_is_skipped():
|
|||
uart, _ = make_uart(make_rumble_frame(1, 2, slot=UART_SLOT_COUNT) + make_rumble_frame(3, 4))
|
||||
|
||||
assert uart.read_rumble() == pytest.approx((0, 3 / 255.0, 4 / 255.0))
|
||||
|
||||
|
||||
def test_reboot_bootsel_frame_matches_firmware_contract():
|
||||
"""0xAA 0xFE len(8) cmd(1) 'BOOTSEL' checksum: 12 bytes, the parser's minimum frame."""
|
||||
frame = PicoUART.reboot_bootsel_frame()
|
||||
assert frame[:3] == bytes([UART_HEADER, 0xFE, 8])
|
||||
assert frame[3] == 0x01
|
||||
assert frame[4:11] == b"BOOTSEL"
|
||||
assert len(frame) == 12
|
||||
assert frame[-1] == compute_checksum(frame[:-1])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue