From b2033098dd1bd43e8c231e2de470909097a75026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecs=C3=A9rke?= Date: Wed, 24 May 2023 15:13:22 +0200 Subject: [PATCH 1/2] Add stdio_usb reset interface detection --- picoboot_connection/picoboot_connection.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index d1f2bee..966ff09 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "picoboot_connection.h" @@ -66,6 +67,19 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d } if (!ret) { if (desc.idVendor != VENDOR_ID_RASPBERRY_PI) { + ret = libusb_get_active_config_descriptor(device, &config); + if (ret && verbose) { + output("Failed to read config descriptor\n"); + } + + for (uint8_t i = 0; i < config->bNumInterfaces; ++i) { + if (0xff == config->interface[i].altsetting[0].bInterfaceClass && + RESET_INTERFACE_SUBCLASS == config->interface[i].altsetting[0].bInterfaceSubClass && + RESET_INTERFACE_PROTOCOL == config->interface[i].altsetting[0].bInterfaceProtocol) { + return dr_vidpid_stdio_usb; + } + } + return dr_vidpid_unknown; } switch (desc.idProduct) { From 38ab4ec6f897cd8046f6370de39083235784448b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecs=C3=A9rke?= Date: Sun, 4 Jun 2023 02:31:19 +0200 Subject: [PATCH 2/2] Add CLI option to enable detection of devices with reset interface --- README.md | 63 ++++++++++++++++++----- main.cpp | 29 ++++++++--- picoboot_connection/picoboot_connection.c | 2 +- picoboot_connection/picoboot_connection.h | 1 + 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e33e4ba..f509728 100644 --- a/README.md +++ b/README.md @@ -70,14 +70,14 @@ PICOTOOL: Tool for interacting with a RP2040 device in BOOTSEL mode, or with a RP2040 binary SYNOPSIS: - picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-f] [-F] + picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-i] [-f] [-F] picotool info [-b] [-p] [-d] [-l] [-a] [-t ] - picotool load [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [--bus ] [--address ] [-f] [-F] - picotool save [-p] [--bus ] [--address ] [-f] [-F] [-t ] - picotool save -a [--bus ] [--address ] [-f] [-F] [-t ] - picotool save -r [--bus ] [--address ] [-f] [-F] [-t ] - picotool verify [--bus ] [--address ] [-f] [-F] [-t ] [-r ] [-o ] - picotool reboot [-a] [-u] [--bus ] [--address ] [-f] [-F] + picotool load [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [--bus ] [--address ] [-i] [-f] [-F] + picotool save [-p] [--bus ] [--address ] [-i] [-f] [-F] [-t ] + picotool save -a [--bus ] [--address ] [-i] [-f] [-F] [-t ] + picotool save -r [--bus ] [--address ] [-i] [-f] [-F] [-t ] + picotool verify [--bus ] [--address ] [-i] [-f] [-F] [-t ] [-r ] [-o ] + picotool reboot [-a] [-u] [--bus ] [--address ] [-i] [-f] [-F] picotool version [-s] picotool help [] @@ -111,7 +111,7 @@ INFO: Without any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode SYNOPSIS: - picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-f] [-F] + picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-i] [-f] [-F] picotool info [-b] [-p] [-d] [-l] [-a] [-t ] OPTIONS: @@ -133,6 +133,9 @@ TARGET SELECTION: Filter devices by USB bus number --address Filter devices by USB device address + -i, --detect-reset-interface + Enable detection of devices with a custom USB vendor interface compatible with stdio_usb reset interface. + The stdio_usb reset interface is implemented by Pico SDK in default USB serial setup -f, --force Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be rebooted back to application mode @@ -218,7 +221,7 @@ LOAD: Load the program / memory range stored in a file onto the device. SYNOPSIS: - picotool load [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [--bus ] [--address ] [-f] [-F] + picotool load [-n] [-N] [-u] [-v] [-x] [-t ] [-o ] [--bus ] [--address ] [-i] [-f] [-F] OPTIONS: Post load actions @@ -249,6 +252,9 @@ OPTIONS: Filter devices by USB bus number --address Filter devices by USB device address + -i, --detect-reset-interface + Enable detection of devices with a custom USB vendor interface compatible with stdio_usb reset interface. + The stdio_usb reset interface is implemented by Pico SDK in default USB serial setup -f, --force Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be rebooted back to application mode @@ -275,9 +281,9 @@ SAVE: Save the program / memory stored in flash on the device to a file. SYNOPSIS: - picotool save [-p] [--bus ] [--address ] [-f] [-F] [-t ] - picotool save -a [--bus ] [--address ] [-f] [-F] [-t ] - picotool save -r [--bus ] [--address ] [-f] [-F] [-t ] + picotool save [-p] [--bus ] [--address ] [-i] [-f] [-F] [-t ] + picotool save -a [--bus ] [--address ] [-i] [-f] [-F] [-t ] + picotool save -r [--bus ] [--address ] [-i] [-f] [-F] [-t ] OPTIONS: Selection of data to save @@ -297,6 +303,9 @@ OPTIONS: Filter devices by USB bus number --address Filter devices by USB device address + -i, --detect-reset-interface + Enable detection of devices with a custom USB vendor interface compatible with stdio_usb reset interface. + The stdio_usb reset interface is implemented by Pico SDK in default USB serial setup -f, --force Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be rebooted back to application mode @@ -540,3 +549,33 @@ in binary. If you ctrl+c out of the middle of a long operation, then libusb seems to get a bit confused, which means we aren't able to unlock our lockout of USB MSD writes (we have turned them off so the user doesn't step on their own toes). Simply running `picotool info` again will unlock it properly the next time (or you can reboot the device). + +### Support for custom stdio_usb compatible reset interface + +When you include [`pico_stdio_usb`](https://www.raspberrypi.com/documentation/pico-sdk/runtime.html#pico_stdio_usb) +module and enable default Pico SDK USB stack using +[`stdio_usb_init()`](https://www.raspberrypi.com/documentation/pico-sdk/runtime.html#gab87bcfa3f24e5a3fe92a944f9eecc460), +you also automatically include a custom [USB vendor interface](https://www.usb.org/defined-class-codes). +This interface implements commands to restart the device either to bootloader or to the application. + +However `picotool` does not recognize the device as standard RP2040 device, +when it uses custom USB setup via [TinyUSB](https://docs.tinyusb.org/). +Custom devices implementing stdio_usb compatible +[reset interface](https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_stdio_usb/reset_interface.c) +are supported using picotool `--detect-reset-interface` or `-i` CLI option. + +#### Permissions for devices with custom reset interface - Linux / macOS + +To allow using `picotool` with your custom USB device without sudo, +add following UDEV rule to the end of `/etc/udev/rules.d/99-picotool.rules` file: + +```udev +SUBSYSTEM=="usb", \ + ATTRS{idVendor}=="", \ + ATTRS{idProduct}=="", \ + MODE="660", \ + GROUP="plugdev" +``` + +Replace `` and `` with vendor and product identifier configured in your +custom USB stack respectively. diff --git a/main.cpp b/main.cpp index 1f38c24..7dfe70d 100644 --- a/main.cpp +++ b/main.cpp @@ -264,6 +264,7 @@ struct _settings { bool reboot_app_specified = false; bool force = false; bool force_no_reboot = false; + bool detect_reset_interface = false; struct { bool show_basic = false; @@ -297,7 +298,10 @@ auto device_selection = (option("--bus") & integer("bus").min_value(0).max_value(255).set(settings.bus) .if_missing([] { return "missing bus number"; })) % "Filter devices by USB bus number" + (option("--address") & integer("addr").min_value(1).max_value(127).set(settings.address) - .if_missing([] { return "missing address"; })) % "Filter devices by USB device address" + .if_missing([] { return "missing address"; })) % "Filter devices by USB device address" + + option('i', "--detect-reset-interface").set(settings.detect_reset_interface) + % "Enable detection of devices with a custom USB vendor interface compatible with stdio_usb reset interface." + "The stdio_usb reset interface is implemented by Pico SDK in default USB serial setup." #if !defined(_WIN32) + option('f', "--force").set(settings.force) % "Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be rebooted back to application mode" + option('F', "--force-no-reboot").set(settings.force_no_reboot) % "Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be left connected and accessible to picotool, but without the RPI-RP2 drive mounted" @@ -2110,7 +2114,10 @@ static int reboot_device(libusb_device *device, bool bootsel, uint disable_mask= bool reboot_command::execute(device_map &devices) { if (settings.force) { - reboot_device(devices[dr_vidpid_stdio_usb][0].first, settings.reboot_usb); + auto &to_reboot = devices[dr_vidpid_stdio_usb].empty() + ? devices[dr_reset_interface][0].first + : devices[dr_vidpid_stdio_usb][0].first; + reboot_device(to_reboot, settings.reboot_usb); if (!quiet) { if (settings.reboot_usb) { std::cout << "The device was asked to reboot into BOOTSEL mode.\n"; @@ -2230,7 +2237,7 @@ int main(int argc, char **argv) { if (handle) { to_close.push_back(handle); } - if (result != dr_error) { + if (result != dr_error && (result != dr_reset_interface || settings.detect_reset_interface)) { devices[result].push_back(std::make_pair(*dev, handle)); } } @@ -2242,7 +2249,7 @@ int main(int argc, char **argv) { // fall thru case cmd::device_support::one: if (devices[dr_vidpid_bootrom_ok].empty() && - (!settings.force || devices[dr_vidpid_stdio_usb].empty())) { + (!settings.force || (devices[dr_vidpid_stdio_usb].empty() && devices[dr_reset_interface].empty()))) { bool had_note = false; fos << missing_device_string(tries>0); if (tries > 0) { @@ -2276,19 +2283,25 @@ int main(int argc, char **argv) { #if defined(_WIN32) printer(dr_vidpid_stdio_usb, " appears to be a RP2040 device with a USB serial connection, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u' first."); + printer(dr_reset_interface, + " appears to be a RP2040 device with a USB reset interface, not in BOOTSEL mode. You can force reboot into BOOTSEL mode via 'picotool reboot -f -u -i' first."); #else printer(dr_vidpid_stdio_usb, " appears to be a RP2040 device with a USB serial connection, so consider -f (or -F) to force reboot in order to run the command."); + printer(dr_reset_interface, + " appears to be a RP2040 device with a USB reset interface, so consider -f (or -F) to force reboot in order to run the command."); #endif } else { // special case message for what is actually just reboot (the only command that doesn't require reboot first) printer(dr_vidpid_stdio_usb, " appears to be a RP2040 device with a USB serial connection, so consider -f to force the reboot."); + printer(dr_reset_interface, + " appears to be a RP2040 device with a USB reset interface, so consider -f to force the reboot."); } rc = ERROR_NO_DEVICE; } else if (supported == cmd::device_support::one) { if (devices[dr_vidpid_bootrom_ok].size() > 1 || - (devices[dr_vidpid_bootrom_ok].empty() && devices[dr_vidpid_stdio_usb].size() > 1)) { + (devices[dr_vidpid_bootrom_ok].empty() && (devices[dr_vidpid_stdio_usb].size() + devices[dr_reset_interface].size()) > 1)) { fail(ERROR_NOT_POSSIBLE, "Command requires a single RP2040 device to be targeted."); } if (!devices[dr_vidpid_bootrom_ok].empty()) { @@ -2306,13 +2319,15 @@ int main(int argc, char **argv) { } if (!rc) { if (settings.force && ctx) { // actually ctx should never be null as we are targeting device if force is set, but still - if (devices[dr_vidpid_stdio_usb].size() != 1) { + if ((devices[dr_vidpid_stdio_usb].size() + devices[dr_reset_interface].size()) != 1) { fail(ERROR_NOT_POSSIBLE, "Forced command requires a single rebootable RP2040 device to be targeted."); } if (selected_cmd->force_requires_pre_reboot()) { // we reboot into BOOTSEL mode and disable MSC interface (the 1 here) - auto &to_reboot = devices[dr_vidpid_stdio_usb][0].first; + auto &to_reboot = devices[dr_vidpid_stdio_usb].empty() + ? devices[dr_reset_interface][0].first + : devices[dr_vidpid_stdio_usb][0].first; reboot_device(to_reboot, true, 1); fos << "The device was asked to reboot into BOOTSEL mode so the command can be executed.\n\n"; for (const auto &handle : to_close) { diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index 966ff09..30ce388 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -76,7 +76,7 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d if (0xff == config->interface[i].altsetting[0].bInterfaceClass && RESET_INTERFACE_SUBCLASS == config->interface[i].altsetting[0].bInterfaceSubClass && RESET_INTERFACE_PROTOCOL == config->interface[i].altsetting[0].bInterfaceProtocol) { - return dr_vidpid_stdio_usb; + return dr_reset_interface; } } diff --git a/picoboot_connection/picoboot_connection.h b/picoboot_connection/picoboot_connection.h index ebdabfc..d1e0d3f 100644 --- a/picoboot_connection/picoboot_connection.h +++ b/picoboot_connection/picoboot_connection.h @@ -26,6 +26,7 @@ enum picoboot_device_result { dr_vidpid_unknown, dr_error, dr_vidpid_stdio_usb, + dr_reset_interface, }; enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_device_handle **dev_handle);