Accept USB BOOTSEL reboot on the UART firmware
The regular firmware now answers the EP0 vendor INFO and BOOTSEL reboot requests with the AIO wire protocol, so switch-pico-config reboot bootsel works when the Pico's USB side is on a PC. Wire constants move to usb_management_protocol.h; other operations stall.
This commit is contained in:
parent
6e0676d9df
commit
8db720fb9a
16 changed files with 489 additions and 67 deletions
14
tests/native_stubs/pico/bootrom.h
Normal file
14
tests/native_stubs/pico/bootrom.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
|
||||
uint32_t disable_interface_mask);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -12,6 +13,8 @@ typedef struct {
|
|||
|
||||
absolute_time_t get_absolute_time(void);
|
||||
uint32_t to_ms_since_boot(absolute_time_t time);
|
||||
absolute_time_t make_timeout_time_ms(uint32_t milliseconds);
|
||||
bool time_reached(absolute_time_t time);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ enum {
|
|||
TUSB_DIR_IN = 1,
|
||||
TUSB_DESC_ENDPOINT = 5,
|
||||
TUSB_DESC_STRING = 3,
|
||||
TUSB_REQ_RCPT_DEVICE = 0,
|
||||
TUSB_REQ_TYPE_VENDOR = 2,
|
||||
CONTROL_STAGE_SETUP = 0,
|
||||
CONTROL_STAGE_DATA = 1,
|
||||
CONTROL_STAGE_ACK = 2,
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
|
@ -64,13 +69,23 @@ static inline uint8_t tu_edpt_dir(uint8_t endpoint) {
|
|||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t bmRequestType;
|
||||
union {
|
||||
struct {
|
||||
uint8_t recipient : 5;
|
||||
uint8_t type : 2;
|
||||
uint8_t direction : 1;
|
||||
} bmRequestType_bit;
|
||||
uint8_t bmRequestType;
|
||||
};
|
||||
uint8_t bRequest;
|
||||
uint16_t wValue;
|
||||
uint16_t wIndex;
|
||||
uint16_t wLength;
|
||||
} tusb_control_request_t;
|
||||
|
||||
bool tud_control_xfer(uint8_t rhport, tusb_control_request_t const* request,
|
||||
void* buffer, uint16_t length);
|
||||
|
||||
bool tud_hid_n_ready(uint8_t instance);
|
||||
bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||
const void* report, uint16_t length);
|
||||
|
|
|
|||
78
tests/test_uart_usb_management_native.py
Normal file
78
tests/test_uart_usb_management_native.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""The UART firmware's EP0 management must interoperate with config_manager."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from switch_pico_bridge import config_manager
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def harness(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path_factory.mktemp("uart_usb_management") / "harness"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-I{root / 'tests' / 'native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(root / "src" / "firmware" / "usb" / "uart_usb_management.cpp"),
|
||||
str(root / "src" / "firmware" / "configuration" / "configuration_storage.cpp"),
|
||||
str(root / "tests" / "uart_usb_management_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
return executable
|
||||
|
||||
|
||||
def run(harness: Path, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
[str(harness), *args], check=True, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def test_info_response_parses_as_a_regular_pico(harness: Path) -> None:
|
||||
response = bytes.fromhex(run(harness, "info"))
|
||||
envelope = config_manager.parse_response(response, config_manager.OP_INFO)
|
||||
assert envelope.status == config_manager.STATUS_OK
|
||||
version = tuple(envelope.payload[:3])
|
||||
board, active_mode, capabilities = envelope.payload[3:6]
|
||||
assert version == (0, 2, 0)
|
||||
assert config_manager.BOARD_NAMES[board] == "Pico"
|
||||
assert config_manager.ACTIVE_MODE_NAMES[active_mode] == "Switch"
|
||||
assert capabilities & config_manager.CAPABILITY_INPUT
|
||||
assert capabilities & ~config_manager.CAPABILITY_MASK == 0
|
||||
|
||||
|
||||
def test_bootsel_request_from_config_manager_reboots_after_status_stage(
|
||||
harness: Path,
|
||||
) -> None:
|
||||
request = config_manager.encode_request(config_manager.OP_BOOTSEL_REBOOT, b"")
|
||||
assert run(harness, "bootsel", request.hex()) == "accepted=1 early=0 reboot=1"
|
||||
|
||||
|
||||
def test_corrupted_bootsel_request_is_rejected_without_rebooting(harness: Path) -> None:
|
||||
request = bytearray(config_manager.encode_request(config_manager.OP_BOOTSEL_REBOOT, b""))
|
||||
request[-1] ^= 0x01 # break the payload CRC
|
||||
assert run(harness, "bootsel", bytes(request).hex()) == "accepted=0 early=0 reboot=0"
|
||||
wrong_operation = config_manager.encode_request(config_manager.OP_REBOOT, b"")
|
||||
assert run(harness, "bootsel", wrong_operation.hex()) == "accepted=0 early=0 reboot=0"
|
||||
|
||||
|
||||
def test_unsupported_operations_are_stalled(harness: Path) -> None:
|
||||
assert run(harness, "other", str(config_manager.OP_CONFIGURATION_READ)) == "accepted=0"
|
||||
108
tests/uart_usb_management_test.cpp
Normal file
108
tests/uart_usb_management_test.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Host harness for the UART firmware's EP0 management handler. Drives the
|
||||
// TinyUSB control stages the way usbd does and reports what reached the
|
||||
// (stubbed) ROM. Request/response bytes cross stdin/stdout as hex so the
|
||||
// Python test can build them with config_manager itself.
|
||||
//
|
||||
// uart_usb_management_test info -> prints INFO response hex
|
||||
// uart_usb_management_test bootsel <hex> -> prints "accepted=<0|1> reboot=<0|1>"
|
||||
// uart_usb_management_test other <op> -> prints "accepted=<0|1>" for an IN request
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tusb.h"
|
||||
#include "pico/bootrom.h"
|
||||
#include "pico/time.h"
|
||||
#include "usb/uart_usb_management.h"
|
||||
#include "usb/usb_output_driver.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void* g_xfer_buffer = nullptr;
|
||||
uint16_t g_xfer_length = 0;
|
||||
int g_reboots = 0;
|
||||
uint64_t g_now_ms = 1000;
|
||||
|
||||
std::vector<uint8_t> parse_hex(const char* text) {
|
||||
std::vector<uint8_t> bytes;
|
||||
for (size_t i = 0; text[i] != '\0' && text[i + 1] != '\0'; i += 2) {
|
||||
bytes.push_back(static_cast<uint8_t>(strtoul(std::string(text + i, 2).c_str(), nullptr, 16)));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
tusb_control_request_t make_request(uint8_t direction, uint8_t operation, uint16_t length) {
|
||||
tusb_control_request_t request{};
|
||||
request.bmRequestType = static_cast<uint8_t>((direction << 7) | (TUSB_REQ_TYPE_VENDOR << 5));
|
||||
request.bRequest = operation;
|
||||
request.wValue = 0x5350;
|
||||
request.wIndex = 0x0001;
|
||||
request.wLength = length;
|
||||
return request;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" bool tud_control_xfer(uint8_t, tusb_control_request_t const*, void* buffer, uint16_t length) {
|
||||
g_xfer_buffer = buffer;
|
||||
g_xfer_length = length;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void reset_usb_boot(uint32_t, uint32_t) { ++g_reboots; }
|
||||
|
||||
extern "C" absolute_time_t make_timeout_time_ms(uint32_t milliseconds) {
|
||||
return absolute_time_t{g_now_ms + milliseconds};
|
||||
}
|
||||
|
||||
extern "C" bool time_reached(absolute_time_t time) { return g_now_ms >= time.milliseconds; }
|
||||
|
||||
AdapterUsbMode usb_output_driver_mode() { return AdapterUsbMode::kSwitch; }
|
||||
uint8_t usb_output_driver_capabilities() {
|
||||
return USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE | USB_OUTPUT_CAPABILITY_MOTION;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) return 2;
|
||||
const std::string mode = argv[1];
|
||||
if (mode == "info" || mode == "other") {
|
||||
const uint8_t operation = mode == "info" ? 0x01 : static_cast<uint8_t>(strtoul(argv[2], nullptr, 0));
|
||||
const tusb_control_request_t request = make_request(TUSB_DIR_IN, operation, 837);
|
||||
const bool accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_SETUP, &request);
|
||||
if (mode == "other") {
|
||||
printf("accepted=%d\n", accepted ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
if (!accepted) return 1;
|
||||
uart_usb_management_vendor_control(0, CONTROL_STAGE_ACK, &request);
|
||||
const uint8_t* bytes = static_cast<const uint8_t*>(g_xfer_buffer);
|
||||
for (uint16_t i = 0; i < g_xfer_length; ++i) printf("%02x", bytes[i]);
|
||||
printf("\n");
|
||||
return 0;
|
||||
}
|
||||
if (mode == "bootsel" && argc >= 3) {
|
||||
const std::vector<uint8_t> payload = parse_hex(argv[2]);
|
||||
const tusb_control_request_t request =
|
||||
make_request(TUSB_DIR_OUT, 0x04, static_cast<uint16_t>(payload.size()));
|
||||
bool accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_SETUP, &request);
|
||||
if (accepted) {
|
||||
if (g_xfer_length < payload.size()) return 3;
|
||||
memcpy(g_xfer_buffer, payload.data(), payload.size());
|
||||
accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_DATA, &request) &&
|
||||
uart_usb_management_vendor_control(0, CONTROL_STAGE_ACK, &request);
|
||||
}
|
||||
// The reboot must wait for the status stage, then fire exactly once.
|
||||
uart_usb_management_task();
|
||||
const int early = g_reboots;
|
||||
g_now_ms += 49;
|
||||
uart_usb_management_task();
|
||||
const int before_deadline = g_reboots;
|
||||
g_now_ms += 1;
|
||||
uart_usb_management_task();
|
||||
printf("accepted=%d early=%d reboot=%d\n", accepted ? 1 : 0, early + before_deadline, g_reboots);
|
||||
return 0;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue