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
|
|
@ -573,6 +573,16 @@ else()
|
||||||
target_compile_definitions(switch-pico PRIVATE
|
target_compile_definitions(switch-pico PRIVATE
|
||||||
SWITCH_PICO_HID_INSTANCE_COUNT=${SWITCH_PICO_UART_CONTROLLERS}
|
SWITCH_PICO_HID_INSTANCE_COUNT=${SWITCH_PICO_UART_CONTROLLERS}
|
||||||
)
|
)
|
||||||
|
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||||
|
# INFO + BOOTSEL reboot over EP0 so switch-pico-config can reflash a
|
||||||
|
# UART Pico plugged into a PC. configuration_storage.cpp provides the
|
||||||
|
# protocol CRC; no storage is instantiated.
|
||||||
|
target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_UART_USB_MANAGEMENT=1)
|
||||||
|
target_sources(switch-pico PRIVATE
|
||||||
|
${SWITCH_PICO_SOURCE_DIR}/usb/uart_usb_management.cpp
|
||||||
|
${SWITCH_PICO_SOURCE_DIR}/configuration/configuration_storage.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -2649,24 +2649,36 @@ 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
|
calls the Pico ROM `reset_usb_boot()` entry point. Physical BOOTSEL remains the
|
||||||
fallback if the firmware or USB management path is unavailable.
|
fallback if the firmware or USB management path is unavailable.
|
||||||
|
|
||||||
The regular UART firmware has the same shortcut over its serial link. The
|
The regular UART firmware has the same shortcut, reachable two ways:
|
||||||
host sends the command frame `0xAA 0xFE 0x08 0x01 "BOOTSEL" checksum`; the
|
|
||||||
firmware verifies the checksum and magic, flushes UART TX, and calls
|
- **Over USB** (Pico's USB plugged into the PC, no serial adapter needed): the
|
||||||
`reset_usb_boot()`. Line noise or a misframed report cannot trigger it. The
|
firmware answers the same EP0 vendor `INFO` and `BOOTSEL reboot` requests as
|
||||||
Pico can stay plugged into the Switch or a PC; only the UART adapter needs to
|
the AIO, so `switch-pico-config reboot bootsel` works unchanged. Every other
|
||||||
be connected to the host:
|
management operation is stalled; `status`, profiles and configuration are
|
||||||
|
AIO-only. The Switch never issues vendor requests, so the handler is
|
||||||
|
invisible to the console.
|
||||||
|
- **Over UART** (Pico can stay on the Switch or a PC; only the serial adapter
|
||||||
|
is on the host): the host sends `0xAA 0xFE 0x08 0x01 "BOOTSEL" checksum`;
|
||||||
|
the firmware verifies checksum and magic, flushes UART TX, and calls
|
||||||
|
`reset_usb_boot()`. Line noise or a misframed report cannot trigger it.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# One-off reboot, then flash the published image
|
# USB side on the PC
|
||||||
|
uv run switch-pico-config reboot bootsel
|
||||||
|
picotool load -v -x firmware/switch-pico.uf2
|
||||||
|
|
||||||
|
# UART adapter on the PC
|
||||||
uv run controller-uart-bridge --reboot-bootsel /dev/ttyUSB0
|
uv run controller-uart-bridge --reboot-bootsel /dev/ttyUSB0
|
||||||
picotool load -v -x firmware/switch-pico.uf2
|
picotool load -v -x firmware/switch-pico.uf2
|
||||||
|
|
||||||
# Build, publish, reboot the running Pico and flash in one step
|
# Build, publish, reboot the running Pico over UART and flash in one step
|
||||||
uv run python build.py --uart-port /dev/ttyUSB0
|
uv run python build.py --uart-port /dev/ttyUSB0
|
||||||
```
|
```
|
||||||
|
|
||||||
Firmware from before this command ignores the frame; hold BOOTSEL while
|
Either way the RPI-RP2 loader appears on whichever USB host the Pico's own
|
||||||
replugging once to install a build that has it.
|
USB port is connected to, so flashing needs that port on the PC. Firmware from
|
||||||
|
before these commands ignores both; hold BOOTSEL while replugging once to
|
||||||
|
install a build that has them.
|
||||||
|
|
||||||
Flash alternatives: bootsel + drag-drop or `picotool load`.
|
Flash alternatives: bootsel + drag-drop or `picotool load`.
|
||||||
Flags:
|
Flags:
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -8,6 +8,9 @@
|
||||||
#ifndef SWITCH_PICO_BLUEPAD32
|
#ifndef SWITCH_PICO_BLUEPAD32
|
||||||
#include "hardware/uart.h"
|
#include "hardware/uart.h"
|
||||||
#include "pico/bootrom.h"
|
#include "pico/bootrom.h"
|
||||||
|
#ifdef SWITCH_PICO_UART_USB_MANAGEMENT
|
||||||
|
#include "usb/uart_usb_management.h"
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
#include "adapter/adapter_mode_controller.h"
|
#include "adapter/adapter_mode_controller.h"
|
||||||
#include "input/bluepad32_input_backend.h"
|
#include "input/bluepad32_input_backend.h"
|
||||||
|
|
@ -382,6 +385,9 @@ int main() {
|
||||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||||
(void)usb_output_driver_task(instance);
|
(void)usb_output_driver_task(instance);
|
||||||
}
|
}
|
||||||
|
#ifdef SWITCH_PICO_UART_USB_MANAGEMENT
|
||||||
|
uart_usb_management_task();
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
log_usb_state();
|
log_usb_state();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
src/firmware/usb/uart_usb_management.cpp
Normal file
136
src/firmware/usb/uart_usb_management.cpp
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
#include "usb/uart_usb_management.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "pico/bootrom.h"
|
||||||
|
#include "pico/time.h"
|
||||||
|
|
||||||
|
#include "configuration/configuration_storage.h"
|
||||||
|
#include "usb/usb_management_protocol.h"
|
||||||
|
#include "usb/usb_output_driver.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace UsbConfigurationManagement;
|
||||||
|
|
||||||
|
// Let the status ACK reach the host before the USB controller disappears.
|
||||||
|
constexpr uint32_t kBootselRebootDelayMs = 50;
|
||||||
|
constexpr uint8_t kFirmwareVersion[3] = {0, 2, 0};
|
||||||
|
|
||||||
|
uint8_t g_request[kRequestHeaderSize]{};
|
||||||
|
uint8_t g_response[kResponseHeaderSize + kInfoPayloadSize]{};
|
||||||
|
tusb_control_request_t g_pending_setup{};
|
||||||
|
bool g_out_pending = false;
|
||||||
|
bool g_reboot_requested = false;
|
||||||
|
absolute_time_t g_reboot_deadline{};
|
||||||
|
|
||||||
|
void write_u16(uint8_t* output, uint16_t value) {
|
||||||
|
output[0] = static_cast<uint8_t>(value);
|
||||||
|
output[1] = static_cast<uint8_t>(value >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_u32(uint8_t* output, uint32_t value) {
|
||||||
|
write_u16(output, static_cast<uint16_t>(value));
|
||||||
|
write_u16(output + 2, static_cast<uint16_t>(value >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t read_u16(const uint8_t* input) {
|
||||||
|
return static_cast<uint16_t>(input[0] | (input[1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t read_u32(const uint8_t* input) {
|
||||||
|
return static_cast<uint32_t>(read_u16(input)) |
|
||||||
|
(static_cast<uint32_t>(read_u16(input + 2)) << 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same envelope as usb_configuration_management.cpp encode_response.
|
||||||
|
size_t encode_info() {
|
||||||
|
uint8_t payload[kInfoPayloadSize] = {
|
||||||
|
kFirmwareVersion[0], kFirmwareVersion[1], kFirmwareVersion[2],
|
||||||
|
kBoardPico,
|
||||||
|
static_cast<uint8_t>(usb_output_driver_mode()),
|
||||||
|
usb_output_driver_capabilities(),
|
||||||
|
0, 0, // No configuration storage on this firmware.
|
||||||
|
};
|
||||||
|
memcpy(g_response, "SPMG", 4);
|
||||||
|
g_response[4] = kProtocolVersion;
|
||||||
|
g_response[5] = static_cast<uint8_t>(Operation::kInfo);
|
||||||
|
g_response[6] = static_cast<uint8_t>(Status::kOk);
|
||||||
|
g_response[7] = 0;
|
||||||
|
write_u16(&g_response[8], kInfoPayloadSize);
|
||||||
|
write_u16(&g_response[10], 0);
|
||||||
|
write_u32(&g_response[12], 0);
|
||||||
|
write_u32(&g_response[16], configuration_crc32(payload, sizeof(payload)));
|
||||||
|
memcpy(&g_response[kResponseHeaderSize], payload, sizeof(payload));
|
||||||
|
return sizeof(g_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same header validation as usb_configuration_management.cpp decode_request,
|
||||||
|
// restricted to the empty-payload BOOTSEL request.
|
||||||
|
bool valid_bootsel_request() {
|
||||||
|
return memcmp(g_request, "SPMG", 4) == 0 &&
|
||||||
|
g_request[4] == kProtocolVersion &&
|
||||||
|
g_request[5] == static_cast<uint8_t>(Operation::kBootselReboot) &&
|
||||||
|
g_request[6] == 0 && g_request[7] == 0 &&
|
||||||
|
read_u16(&g_request[8]) == 0 && read_u16(&g_request[10]) == 0 &&
|
||||||
|
read_u32(&g_request[12]) == configuration_crc32(nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool uart_usb_management_vendor_control(uint8_t rhport, uint8_t stage,
|
||||||
|
const tusb_control_request_t* request) {
|
||||||
|
if (request == nullptr ||
|
||||||
|
request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR ||
|
||||||
|
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE ||
|
||||||
|
request->wValue != kRequestValue ||
|
||||||
|
request->wIndex != kRequestIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Operation operation = static_cast<Operation>(request->bRequest);
|
||||||
|
const bool input = request->bmRequestType_bit.direction == TUSB_DIR_IN;
|
||||||
|
|
||||||
|
if (stage == CONTROL_STAGE_SETUP) {
|
||||||
|
g_out_pending = false;
|
||||||
|
if (input) {
|
||||||
|
if (operation != Operation::kInfo) return false;
|
||||||
|
return tud_control_xfer(rhport, request, g_response,
|
||||||
|
static_cast<uint16_t>(encode_info()));
|
||||||
|
}
|
||||||
|
if (operation != Operation::kBootselReboot ||
|
||||||
|
request->wLength != kRequestHeaderSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_pending_setup = *request;
|
||||||
|
g_out_pending = tud_control_xfer(rhport, request, g_request,
|
||||||
|
request->wLength);
|
||||||
|
return g_out_pending;
|
||||||
|
}
|
||||||
|
if (stage == CONTROL_STAGE_DATA) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stage != CONTROL_STAGE_ACK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (input) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!g_out_pending ||
|
||||||
|
memcmp(request, &g_pending_setup, sizeof(*request)) != 0 ||
|
||||||
|
!valid_bootsel_request()) {
|
||||||
|
g_out_pending = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_out_pending = false;
|
||||||
|
if (!g_reboot_requested) {
|
||||||
|
g_reboot_requested = true;
|
||||||
|
g_reboot_deadline = make_timeout_time_ms(kBootselRebootDelayMs);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_usb_management_task() {
|
||||||
|
if (g_reboot_requested && time_reached(g_reboot_deadline)) {
|
||||||
|
reset_usb_boot(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/firmware/usb/uart_usb_management.h
Normal file
16
src/firmware/usb/uart_usb_management.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "tusb.h"
|
||||||
|
|
||||||
|
// Minimal EP0 vendor-request management for the regular UART firmware. It
|
||||||
|
// answers INFO and accepts BOOTSEL reboot using the same wire protocol as the
|
||||||
|
// AIO firmware, so `switch-pico-config reboot bootsel` works when the Pico's
|
||||||
|
// USB side is plugged into a PC. Every other operation is stalled.
|
||||||
|
bool uart_usb_management_vendor_control(uint8_t rhport, uint8_t stage,
|
||||||
|
const tusb_control_request_t* request);
|
||||||
|
|
||||||
|
// Main-loop hook: performs a requested BOOTSEL reboot once the USB status
|
||||||
|
// stage has had time to complete.
|
||||||
|
void uart_usb_management_task();
|
||||||
|
|
@ -8,18 +8,14 @@
|
||||||
#include "input/bluepad32_input_backend.h"
|
#include "input/bluepad32_input_backend.h"
|
||||||
#include "configuration/configuration_service.h"
|
#include "configuration/configuration_service.h"
|
||||||
#include "profile/profile_service.h"
|
#include "profile/profile_service.h"
|
||||||
|
#include "usb/usb_management_protocol.h"
|
||||||
|
|
||||||
namespace UsbConfigurationManagement {
|
namespace UsbConfigurationManagement {
|
||||||
|
|
||||||
constexpr uint16_t kRequestValue = 0x5350;
|
|
||||||
constexpr uint16_t kRequestIndex = 0x0001;
|
|
||||||
constexpr uint8_t kProtocolVersion = 1;
|
|
||||||
// INFO-only active mode; never persisted as an AdapterRequestedMode.
|
// INFO-only active mode; never persisted as an AdapterRequestedMode.
|
||||||
constexpr uint8_t kNativeHubActiveMode = 5;
|
constexpr uint8_t kNativeHubActiveMode = 5;
|
||||||
constexpr size_t kRequestHeaderSize = 16;
|
|
||||||
constexpr uint16_t kSwitch2WakeSchemaVersion = 1;
|
constexpr uint16_t kSwitch2WakeSchemaVersion = 1;
|
||||||
constexpr size_t kSwitch2WakePayloadSize = 20;
|
constexpr size_t kSwitch2WakePayloadSize = 20;
|
||||||
constexpr size_t kResponseHeaderSize = 20;
|
|
||||||
constexpr size_t kPairingRecordSize = 8;
|
constexpr size_t kPairingRecordSize = 8;
|
||||||
constexpr size_t kPairingPayloadHeaderSize = 4;
|
constexpr size_t kPairingPayloadHeaderSize = 4;
|
||||||
constexpr size_t kMaximumRequestSize = 80;
|
constexpr size_t kMaximumRequestSize = 80;
|
||||||
|
|
@ -51,56 +47,6 @@ constexpr size_t kMaximumChunkSize =
|
||||||
static_assert(kMaximumResponseSize == 837,
|
static_assert(kMaximumResponseSize == 837,
|
||||||
"profile list no longer fits the EP0 response buffer");
|
"profile list no longer fits the EP0 response buffer");
|
||||||
|
|
||||||
enum class Operation : uint8_t {
|
|
||||||
kInfo = 0x01,
|
|
||||||
kModeSet = 0x02,
|
|
||||||
kReboot = 0x03,
|
|
||||||
kBootselReboot = 0x04,
|
|
||||||
kSwitch2Wake = 0x05,
|
|
||||||
kConfigurationRead = 0x10,
|
|
||||||
kConfigurationBegin = 0x11,
|
|
||||||
kConfigurationChunk = 0x12,
|
|
||||||
kConfigurationCommit = 0x13,
|
|
||||||
kConfigurationReset = 0x14,
|
|
||||||
kTransactionStatus = 0x15,
|
|
||||||
kPairingRead = 0x20,
|
|
||||||
kPairingRefresh = 0x21,
|
|
||||||
kPairingClear = 0x22,
|
|
||||||
kRuntimeDiagnostics = 0x23,
|
|
||||||
kProfileList = 0x30,
|
|
||||||
kProfileSelect = 0x31,
|
|
||||||
kProfileRead = 0x32,
|
|
||||||
kProfileBegin = 0x33,
|
|
||||||
kProfileChunk = 0x34,
|
|
||||||
kProfileCommit = 0x35,
|
|
||||||
kProfileReset = 0x36,
|
|
||||||
kProfileActivate = 0x37,
|
|
||||||
kProfileTransactionStatus = 0x38,
|
|
||||||
kProfilePlaytest = 0x39,
|
|
||||||
kProfileMetadataRead = 0x3a,
|
|
||||||
kProfileMetadataSet = 0x3b,
|
|
||||||
kProfileIdentify = 0x3c,
|
|
||||||
kWiiOrientation = 0x3d,
|
|
||||||
kHapticsExperiment = 0x40,
|
|
||||||
kHapticsTransportProbe = 0x41,
|
|
||||||
kMacroCapture = 0x42,
|
|
||||||
kNativeSwitchRumble = 0x43,
|
|
||||||
kSwitch2MouseCapture = 0x44,
|
|
||||||
kWiiIrGyro = 0x45,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class Status : uint8_t {
|
|
||||||
kOk = 0,
|
|
||||||
kPending = 1,
|
|
||||||
kMalformed = 2,
|
|
||||||
kUnsupportedSchema = 3,
|
|
||||||
kTooLarge = 4,
|
|
||||||
kOutOfOrder = 5,
|
|
||||||
kBadCrc = 6,
|
|
||||||
kBusy = 7,
|
|
||||||
kStorageError = 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DecodedRequest {
|
struct DecodedRequest {
|
||||||
Operation operation = Operation::kInfo;
|
Operation operation = Operation::kInfo;
|
||||||
const uint8_t* payload = nullptr;
|
const uint8_t* payload = nullptr;
|
||||||
|
|
|
||||||
72
src/firmware/usb/usb_management_protocol.h
Normal file
72
src/firmware/usb/usb_management_protocol.h
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// EP0 vendor-request wire protocol shared by every firmware variant and
|
||||||
|
// switch_pico_bridge.config_manager. Keep this header free of adapter,
|
||||||
|
// Bluetooth or storage dependencies so the UART firmware can speak the
|
||||||
|
// subset it supports.
|
||||||
|
namespace UsbConfigurationManagement {
|
||||||
|
|
||||||
|
constexpr uint16_t kRequestValue = 0x5350;
|
||||||
|
constexpr uint16_t kRequestIndex = 0x0001;
|
||||||
|
constexpr uint8_t kProtocolVersion = 1;
|
||||||
|
constexpr size_t kRequestHeaderSize = 16;
|
||||||
|
constexpr size_t kResponseHeaderSize = 20;
|
||||||
|
constexpr size_t kInfoPayloadSize = 8;
|
||||||
|
// Board byte of the INFO payload.
|
||||||
|
constexpr uint8_t kBoardPico = 1;
|
||||||
|
constexpr uint8_t kBoardPico2W = 2;
|
||||||
|
|
||||||
|
enum class Operation : uint8_t {
|
||||||
|
kInfo = 0x01,
|
||||||
|
kModeSet = 0x02,
|
||||||
|
kReboot = 0x03,
|
||||||
|
kBootselReboot = 0x04,
|
||||||
|
kSwitch2Wake = 0x05,
|
||||||
|
kConfigurationRead = 0x10,
|
||||||
|
kConfigurationBegin = 0x11,
|
||||||
|
kConfigurationChunk = 0x12,
|
||||||
|
kConfigurationCommit = 0x13,
|
||||||
|
kConfigurationReset = 0x14,
|
||||||
|
kTransactionStatus = 0x15,
|
||||||
|
kPairingRead = 0x20,
|
||||||
|
kPairingRefresh = 0x21,
|
||||||
|
kPairingClear = 0x22,
|
||||||
|
kRuntimeDiagnostics = 0x23,
|
||||||
|
kProfileList = 0x30,
|
||||||
|
kProfileSelect = 0x31,
|
||||||
|
kProfileRead = 0x32,
|
||||||
|
kProfileBegin = 0x33,
|
||||||
|
kProfileChunk = 0x34,
|
||||||
|
kProfileCommit = 0x35,
|
||||||
|
kProfileReset = 0x36,
|
||||||
|
kProfileActivate = 0x37,
|
||||||
|
kProfileTransactionStatus = 0x38,
|
||||||
|
kProfilePlaytest = 0x39,
|
||||||
|
kProfileMetadataRead = 0x3a,
|
||||||
|
kProfileMetadataSet = 0x3b,
|
||||||
|
kProfileIdentify = 0x3c,
|
||||||
|
kWiiOrientation = 0x3d,
|
||||||
|
kHapticsExperiment = 0x40,
|
||||||
|
kHapticsTransportProbe = 0x41,
|
||||||
|
kMacroCapture = 0x42,
|
||||||
|
kNativeSwitchRumble = 0x43,
|
||||||
|
kSwitch2MouseCapture = 0x44,
|
||||||
|
kWiiIrGyro = 0x45,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Status : uint8_t {
|
||||||
|
kOk = 0,
|
||||||
|
kPending = 1,
|
||||||
|
kMalformed = 2,
|
||||||
|
kUnsupportedSchema = 3,
|
||||||
|
kTooLarge = 4,
|
||||||
|
kOutOfOrder = 5,
|
||||||
|
kBadCrc = 6,
|
||||||
|
kBusy = 7,
|
||||||
|
kStorageError = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace UsbConfigurationManagement
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
#include "usb/usb_configuration_management.h"
|
#include "usb/usb_configuration_management.h"
|
||||||
|
#elif defined(SWITCH_PICO_UART_USB_MANAGEMENT)
|
||||||
|
#include "usb/uart_usb_management.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||||
|
|
@ -390,6 +392,8 @@ extern "C" bool tud_vendor_control_xfer_cb(
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
return usb_configuration_management_vendor_control(rhport, stage,
|
return usb_configuration_management_vendor_control(rhport, stage,
|
||||||
request);
|
request);
|
||||||
|
#elif defined(SWITCH_PICO_UART_USB_MANAGEMENT)
|
||||||
|
return uart_usb_management_vendor_control(rhport, stage, request);
|
||||||
#else
|
#else
|
||||||
(void)rhport;
|
(void)rhport;
|
||||||
(void)stage;
|
(void)stage;
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,8 @@ CAPABILITY_INPUT = 1 << 0
|
||||||
CAPABILITY_RUMBLE = 1 << 1
|
CAPABILITY_RUMBLE = 1 << 1
|
||||||
CAPABILITY_MOTION = 1 << 2
|
CAPABILITY_MOTION = 1 << 2
|
||||||
CAPABILITY_MASK = CAPABILITY_INPUT | CAPABILITY_RUMBLE | CAPABILITY_MOTION
|
CAPABILITY_MASK = CAPABILITY_INPUT | CAPABILITY_RUMBLE | CAPABILITY_MOTION
|
||||||
|
# USB management info byte 3.
|
||||||
|
BOARD_NAMES = {1: "Pico", 2: "Pico 2 W"}
|
||||||
PAIRING_RECORD_SIZE = 8
|
PAIRING_RECORD_SIZE = 8
|
||||||
PAIRING_RECORD_CAPACITY = 16
|
PAIRING_RECORD_CAPACITY = 16
|
||||||
TRANSPORT_UNKNOWN = 0
|
TRANSPORT_UNKNOWN = 0
|
||||||
|
|
@ -5489,7 +5491,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||||
configuration = read_configuration(device)
|
configuration = read_configuration(device)
|
||||||
version = ".".join(str(part) for part in info.firmware_version)
|
version = ".".join(str(part) for part in info.firmware_version)
|
||||||
print(f"Firmware: {version}")
|
print(f"Firmware: {version}")
|
||||||
print(f"Board: Pico 2 W ({info.board})")
|
print(f"Board: {BOARD_NAMES.get(info.board, 'unknown')} ({info.board})")
|
||||||
print(
|
print(
|
||||||
"Requested USB mode: "
|
"Requested USB mode: "
|
||||||
f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}"
|
f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}"
|
||||||
|
|
|
||||||
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
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
@ -12,6 +13,8 @@ typedef struct {
|
||||||
|
|
||||||
absolute_time_t get_absolute_time(void);
|
absolute_time_t get_absolute_time(void);
|
||||||
uint32_t to_ms_since_boot(absolute_time_t time);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ enum {
|
||||||
TUSB_DIR_IN = 1,
|
TUSB_DIR_IN = 1,
|
||||||
TUSB_DESC_ENDPOINT = 5,
|
TUSB_DESC_ENDPOINT = 5,
|
||||||
TUSB_DESC_STRING = 3,
|
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)
|
#pragma pack(push, 1)
|
||||||
|
|
@ -64,13 +69,23 @@ static inline uint8_t tu_edpt_dir(uint8_t endpoint) {
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
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;
|
uint8_t bRequest;
|
||||||
uint16_t wValue;
|
uint16_t wValue;
|
||||||
uint16_t wIndex;
|
uint16_t wIndex;
|
||||||
uint16_t wLength;
|
uint16_t wLength;
|
||||||
} tusb_control_request_t;
|
} 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_ready(uint8_t instance);
|
||||||
bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||||
const void* report, uint16_t length);
|
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