Add managed reboot into USB BOOTSEL
This commit is contained in:
parent
bd5d4b3177
commit
4626b13bd8
13 changed files with 153 additions and 0 deletions
|
|
@ -534,6 +534,16 @@ Evaluate a newer tagged Bluepad32 only when it closes a specific coverage gap. L
|
|||
|
||||
The ROM UF2 path remains the trusted update mechanism.
|
||||
|
||||
The management foundation is now available:
|
||||
|
||||
```text
|
||||
switch-pico-config reboot bootsel
|
||||
```
|
||||
|
||||
The endpoint-zero request is acknowledged before a guarded 50 ms delayed call
|
||||
to the Pico ROM `reset_usb_boot()` entry point. Physical BOOTSEL remains the
|
||||
recovery path when management USB is unavailable.
|
||||
|
||||
Add:
|
||||
|
||||
```text
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -92,6 +92,7 @@ Connect the Pico 2 W to the PC while the AIO firmware is running normally; do no
|
|||
```sh
|
||||
uv run switch-pico-config status
|
||||
uv run switch-pico-config diagnostics
|
||||
uv run switch-pico-config reboot bootsel
|
||||
uv run switch-pico-config config show
|
||||
uv run switch-pico-config config set --pairing-window-seconds 90
|
||||
uv run switch-pico-config config reset --yes
|
||||
|
|
@ -390,6 +391,17 @@ If you already have a built (or use the pre-built one in `firmware/`) `.uf2`, yo
|
|||
|
||||
Tip: if you don’t see `RPI-RP2`, try a different USB cable (some are charge-only) or a different USB port/hub.
|
||||
|
||||
When the AIO firmware is already running on a PC, enter ROM BOOTSEL without
|
||||
touching the board:
|
||||
|
||||
```sh
|
||||
uv run switch-pico-config reboot bootsel
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Flash alternatives: bootsel + drag-drop or `picotool load`.
|
||||
Flags:
|
||||
- `SWITCH_PICO_LOG`: enable/disable UART logging on the Pico.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "controller_profile.h"
|
||||
#include "controller_profile_runtime.h"
|
||||
#include "hardware/watchdog.h"
|
||||
#include "pico/bootrom.h"
|
||||
#include "tusb.h"
|
||||
#include "usb_output_driver.h"
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ constexpr uint32_t kInternalTransactionValueMask =
|
|||
~kInternalTransactionBit;
|
||||
constexpr uint32_t kFeedbackPhaseMs = 75;
|
||||
constexpr uint32_t kFeedbackGuardMs = 75;
|
||||
constexpr uint32_t kBootselRebootDelayMs = 50;
|
||||
static_assert(
|
||||
ADAPTER_MODE_CHORD_BUTTON_MASK ==
|
||||
static_cast<uint16_t>(
|
||||
|
|
@ -69,6 +71,9 @@ ModeOperation g_operation{};
|
|||
uint32_t g_reboot_transaction_id = 0;
|
||||
bool g_reboot_scheduled = false;
|
||||
bool g_correlated_reboot_scheduled = false;
|
||||
bool g_bootsel_reboot_requested = false;
|
||||
bool g_bootsel_reboot_delay_started = false;
|
||||
uint32_t g_bootsel_reboot_deadline_ms = 0;
|
||||
AdapterRequestedMode g_requested_mode = AdapterRequestedMode::kAuto;
|
||||
uint32_t g_next_internal_transaction_value = 1;
|
||||
bool g_recovery_requested = false;
|
||||
|
|
@ -298,6 +303,9 @@ void adapter_mode_controller_initialize_usb() {
|
|||
g_recovery_failed = false;
|
||||
g_recovery_clear_pairings_token = 0;
|
||||
g_correlated_reboot_scheduled = false;
|
||||
g_bootsel_reboot_requested = false;
|
||||
g_bootsel_reboot_delay_started = false;
|
||||
g_bootsel_reboot_deadline_ms = 0;
|
||||
|
||||
configuration_service_initialize_pre_usb();
|
||||
ConfigurationServiceSnapshot snapshot{};
|
||||
|
|
@ -383,6 +391,18 @@ void adapter_mode_controller_process_input(
|
|||
}
|
||||
|
||||
void adapter_mode_controller_task(uint32_t now_ms) {
|
||||
if (g_bootsel_reboot_requested) {
|
||||
if (!g_bootsel_reboot_delay_started) {
|
||||
g_bootsel_reboot_delay_started = true;
|
||||
g_bootsel_reboot_deadline_ms =
|
||||
now_ms + kBootselRebootDelayMs;
|
||||
} else if (deadline_reached(
|
||||
now_ms, g_bootsel_reboot_deadline_ms)) {
|
||||
g_bootsel_reboot_requested = false;
|
||||
reset_usb_boot(0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (g_reboot_scheduled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -449,3 +469,13 @@ bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) {
|
|||
reboot_now();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool adapter_reboot_to_bootsel() {
|
||||
if (g_reboot_scheduled) {
|
||||
return g_bootsel_reboot_requested;
|
||||
}
|
||||
g_reboot_scheduled = true;
|
||||
g_bootsel_reboot_requested = true;
|
||||
controller_profile_runtime_reset();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,3 +5,7 @@
|
|||
// Correlated normal reboot entry point for management opcode 0x03. Only a
|
||||
// successful host mode transaction can reset profile runtime and reboot.
|
||||
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id);
|
||||
|
||||
// Schedules a direct ROM USB BOOTSEL reboot after the current endpoint-zero
|
||||
// control transfer has completed.
|
||||
bool adapter_reboot_to_bootsel();
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -38,6 +38,7 @@ HOST_TRANSACTION_ID_MASK = 0x7FFFFFFF
|
|||
OP_INFO = 0x01
|
||||
OP_MODE_SET = 0x02
|
||||
OP_REBOOT = 0x03
|
||||
OP_BOOTSEL_REBOOT = 0x04
|
||||
OP_CONFIGURATION_READ = 0x10
|
||||
OP_CONFIGURATION_BEGIN = 0x11
|
||||
OP_CONFIGURATION_CHUNK = 0x12
|
||||
|
|
@ -1512,6 +1513,9 @@ def request_reboot(device: UsbDevice, transaction_id: int) -> None:
|
|||
)
|
||||
_control_out(device, OP_REBOOT, struct.pack("<I", transaction_id))
|
||||
|
||||
def request_bootsel_reboot(device: UsbDevice) -> None:
|
||||
_control_out(device, OP_BOOTSEL_REBOOT)
|
||||
|
||||
|
||||
def _mode_is_active(requested_mode: int, active_mode: int) -> bool:
|
||||
if requested_mode == REQUESTED_MODE_AUTO:
|
||||
|
|
@ -2169,6 +2173,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
commands.add_parser(
|
||||
"diagnostics", help="show live Bluetooth and rumble pipeline counters"
|
||||
)
|
||||
reboot = commands.add_parser(
|
||||
"reboot", help="reboot into a firmware or ROM target"
|
||||
)
|
||||
reboot.add_argument("target", choices=("bootsel",))
|
||||
mode = commands.add_parser("mode", help="select the persistent USB mode")
|
||||
mode.add_argument("mode", choices=SELECTABLE_MODE_NAMES)
|
||||
|
||||
|
|
@ -2319,6 +2327,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
"Rumble-pending slots: "
|
||||
f"{diagnostics.rumble_pending_slots}"
|
||||
)
|
||||
elif args.command == "reboot":
|
||||
request_bootsel_reboot(device)
|
||||
print("Rebooting into USB BOOTSEL mode.")
|
||||
elif args.command == "mode":
|
||||
requested_mode = REQUESTED_MODE_NAMES.index(args.mode)
|
||||
_, changed = configure_mode(device, requested_mode, args.timeout)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ enum class Call : uint8_t {
|
|||
kClearPairings,
|
||||
kPairingSnapshot,
|
||||
kWatchdogReboot,
|
||||
kBootselReboot,
|
||||
};
|
||||
|
||||
std::vector<Call> calls;
|
||||
|
|
@ -61,6 +62,9 @@ ControllerProfileConfirmationPolicy feedback_policy =
|
|||
int reboot_count = 0;
|
||||
int runtime_reset_count = 0;
|
||||
int clear_pairings_count = 0;
|
||||
int bootsel_reboot_count = 0;
|
||||
uint32_t bootsel_gpio_mask = UINT32_MAX;
|
||||
uint32_t bootsel_disable_mask = UINT32_MAX;
|
||||
bool recovery_reserved = false;
|
||||
bool abandoned_host_receive = false;
|
||||
bool abandoned_host_receive_canceled = false;
|
||||
|
|
@ -101,6 +105,9 @@ void reset_harness(AdapterRequestedMode requested_mode =
|
|||
reboot_count = 0;
|
||||
runtime_reset_count = 0;
|
||||
clear_pairings_count = 0;
|
||||
bootsel_reboot_count = 0;
|
||||
bootsel_gpio_mask = UINT32_MAX;
|
||||
bootsel_disable_mask = UINT32_MAX;
|
||||
recovery_reserved = false;
|
||||
abandoned_host_receive = false;
|
||||
abandoned_host_receive_canceled = false;
|
||||
|
|
@ -553,6 +560,29 @@ void test_auto_xinput_disconnect_reboots_to_probe() {
|
|||
"Auto Switch probe rebooted after USB unmount");
|
||||
}
|
||||
|
||||
void test_bootsel_reboot_is_delayed_and_idempotent() {
|
||||
reset_harness(AdapterRequestedMode::kAuto);
|
||||
require(adapter_reboot_to_bootsel() &&
|
||||
adapter_reboot_to_bootsel() &&
|
||||
runtime_reset_count == 1 &&
|
||||
bootsel_reboot_count == 0 && reboot_count == 0,
|
||||
"BOOTSEL request was not accepted exactly once");
|
||||
|
||||
adapter_mode_controller_task(UINT32_MAX - 25u);
|
||||
adapter_mode_controller_task(23);
|
||||
require(bootsel_reboot_count == 0,
|
||||
"BOOTSEL reboot occurred before the control-transfer guard");
|
||||
adapter_mode_controller_task(24);
|
||||
adapter_mode_controller_task(25);
|
||||
require(bootsel_reboot_count == 1 &&
|
||||
bootsel_gpio_mask == 0 &&
|
||||
bootsel_disable_mask == 0 &&
|
||||
reboot_count == 0 &&
|
||||
!adapter_reboot_for_mode_transaction(91),
|
||||
"BOOTSEL reboot was not delayed, one-shot, or isolated from "
|
||||
"the correlated watchdog path");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void configuration_service_initialize_pre_usb() {
|
||||
|
|
@ -669,6 +699,14 @@ void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
|
|||
++reboot_count;
|
||||
}
|
||||
|
||||
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
|
||||
uint32_t disable_interface_mask) {
|
||||
calls.push_back(Call::kBootselReboot);
|
||||
++bootsel_reboot_count;
|
||||
bootsel_gpio_mask = usb_activity_gpio_pin_mask;
|
||||
bootsel_disable_mask = disable_interface_mask;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_pre_tusb_ordering_and_configured_selection();
|
||||
test_mode_availability_has_one_stable_value();
|
||||
|
|
@ -681,5 +719,6 @@ int main() {
|
|||
test_recovery_auto_wins_host_mode_interleaving();
|
||||
test_failed_recovery_never_reboots();
|
||||
test_auto_xinput_disconnect_reboots_to_probe();
|
||||
test_bootsel_reboot_is_delayed_and_idempotent();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
6
tests/mode_native_stubs/pico/bootrom.h
Normal file
6
tests/mode_native_stubs/pico/bootrom.h
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
|
||||
uint32_t disable_interface_mask);
|
||||
|
|
@ -58,6 +58,7 @@ class FakeDevice:
|
|||
self.mode_pending_reads = 0
|
||||
self.fail_mode_status: int | None = None
|
||||
self.reboot_transaction_ids: list[int] = []
|
||||
self.bootsel_reboot_requested = False
|
||||
self.records = [
|
||||
(
|
||||
config_manager.TRANSPORT_CLASSIC,
|
||||
|
|
@ -392,6 +393,9 @@ class FakeDevice:
|
|||
assert reboot_transaction_id == self.transaction_id
|
||||
assert self.transaction_status == config_manager.STATUS_OK
|
||||
self.reboot_transaction_ids.append(reboot_transaction_id)
|
||||
elif request == config_manager.OP_BOOTSEL_REBOOT:
|
||||
assert payload == b""
|
||||
self.bootsel_reboot_requested = True
|
||||
elif request == config_manager.OP_PAIRING_REFRESH:
|
||||
self.pairing_generation += 1
|
||||
elif request == config_manager.OP_PAIRING_CLEAR:
|
||||
|
|
@ -660,6 +664,15 @@ def test_mode_envelopes_and_host_side_validation(
|
|||
config_manager.OP_REBOOT, reboot_payload
|
||||
)
|
||||
|
||||
config_manager.request_bootsel_reboot(device)
|
||||
operation, payload, encoded = device.out_requests[-1]
|
||||
assert operation == config_manager.OP_BOOTSEL_REBOOT
|
||||
assert payload == b""
|
||||
assert encoded == config_manager.encode_request(
|
||||
config_manager.OP_BOOTSEL_REBOOT
|
||||
)
|
||||
assert device.bootsel_reboot_requested
|
||||
|
||||
for transaction_id in (0, 0x80000000, True):
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.request_reboot(device, transaction_id)
|
||||
|
|
@ -1687,6 +1700,20 @@ def test_mode_cli_changes_then_noops(
|
|||
assert reenumerated.out_requests == []
|
||||
|
||||
|
||||
def test_bootsel_reboot_cli(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: (device,)
|
||||
)
|
||||
|
||||
assert config_manager.main(["reboot", "bootsel"]) == 0
|
||||
assert capsys.readouterr().out == "Rebooting into USB BOOTSEL mode.\n"
|
||||
assert device.bootsel_reboot_requested
|
||||
|
||||
|
||||
def test_mode_parser_accepts_all_implemented_modes() -> None:
|
||||
for mode in config_manager.REQUESTED_MODE_NAMES:
|
||||
args = config_manager.build_parser().parse_args(["mode", mode])
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ uint32_t mode_set_call_count = 0;
|
|||
uint32_t correlated_reboot_transaction_id = 0;
|
||||
uint32_t reboot_transaction_id = 0;
|
||||
uint32_t reboot_call_count = 0;
|
||||
bool bootsel_reboot_requested = false;
|
||||
bool refresh_requested = false;
|
||||
bool clear_requested = false;
|
||||
Bluepad32BackendDiagnostics current_diagnostics{};
|
||||
|
|
@ -221,6 +222,9 @@ void test_vendor_requests() {
|
|||
perform_out(Operation::kPairingRefresh, {});
|
||||
require(refresh_requested,
|
||||
"pairing refresh was not dispatched");
|
||||
perform_out(Operation::kBootselReboot, {});
|
||||
require(bootsel_reboot_requested,
|
||||
"BOOTSEL reboot request was not dispatched");
|
||||
perform_out(Operation::kPairingClear, {});
|
||||
require(clear_requested, "pairing clear was not dispatched");
|
||||
|
||||
|
|
@ -756,6 +760,11 @@ void bluepad32_input_backend_diagnostics(
|
|||
*out = current_diagnostics;
|
||||
}
|
||||
|
||||
bool adapter_reboot_to_bootsel() {
|
||||
bootsel_reboot_requested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool adapter_host_probe_vendor_control(
|
||||
uint8_t, uint8_t, const tusb_control_request_t*) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ bool valid_out_size(Operation operation, size_t size) {
|
|||
return size == kRequestHeaderSize + 5;
|
||||
case Operation::kReboot:
|
||||
return size == kRequestHeaderSize + 4;
|
||||
case Operation::kBootselReboot:
|
||||
return size == kRequestHeaderSize;
|
||||
case Operation::kConfigurationBegin:
|
||||
return size == kRequestHeaderSize + 12;
|
||||
case Operation::kProfileBegin:
|
||||
|
|
@ -385,6 +387,8 @@ bool process_out_request() {
|
|||
}
|
||||
return adapter_reboot_for_mode_transaction(transaction_id);
|
||||
}
|
||||
case Operation::kBootselReboot:
|
||||
return adapter_reboot_to_bootsel();
|
||||
case Operation::kConfigurationBegin:
|
||||
configuration_service_begin(
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ enum class Operation : uint8_t {
|
|||
kInfo = 0x01,
|
||||
kModeSet = 0x02,
|
||||
kReboot = 0x03,
|
||||
kBootselReboot = 0x04,
|
||||
kConfigurationRead = 0x10,
|
||||
kConfigurationBegin = 0x11,
|
||||
kConfigurationChunk = 0x12,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue