diff --git a/CMakeLists.txt b/CMakeLists.txt index fb72351f..65ae8b55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.12) project(picotool) +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") @@ -32,13 +36,15 @@ else() add_executable(picotool main.cpp) set(PICOTOOL_VERSION 1.1.0-develop) set(SYSTEM_VERSION "${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_VERSION}") - set(COMPILER_INFO "${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}") + set(COMPILER_INFO "${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}, ${CMAKE_BUILD_TYPE}") target_compile_definitions(picotool PRIVATE PICOTOOL_VERSION="${PICOTOOL_VERSION}" SYSTEM_VERSION="${SYSTEM_VERSION}" COMPILER_INFO="${COMPILER_INFO}" ) target_include_directories(picotool PRIVATE ${LIBUSB_INCLUDE_DIR}) + # todo, this is a bit of an abstraction failure; but don't want to rev the SDK just for this right now + target_include_directories(picotool PRIVATE ${PICO_SDK_PATH}/src/rp2_common/pico_stdio_usb/include) target_link_libraries(picotool pico_binary_info boot_uf2_headers boot_picoboot_headers pico_platform_headers picoboot_connection_cxx ${LIBUSB_LIBRARIES}) # allow `make install` install(TARGETS picotool) diff --git a/README.md b/README.md index 982cadc8..fc358404 100644 --- a/README.md +++ b/README.md @@ -36,25 +36,26 @@ PICOTOOL: Tool for interacting with a RP2040 device in BOOTSEL mode, or with a RP2040 binary SYNOPSYS: - picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] + picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-f] [-F] picotool info [-b] [-p] [-d] [-l] [-a] [-t ] - picotool load [-v] [-r] [-t ] [--bus ] [--address ] - picotool save [-p] [--bus ] [--address ] [-t ] - picotool save -a [--bus ] [--address ] [-t ] - picotool save -r [--bus ] [--address ] [-t ] - picotool verify [--bus ] [--address ] [-t ] [-r ] - picotool reboot [-a] [-u] [--bus ] [--address ] + picotool load [-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 version [-s] picotool help [] COMMANDS: - info Display information from the target device(s) or file. - Without any arguments, this will display basic information for all connected RP2040 devices in - BOOTSEL mode - load Load the program / memory range stored in a file onto the device. - save Save the program / memory stored in flash on the device to a file. - verify Check that the device contents match those in the file. - reboot Reboot the device - help Show general help or help for a specific command + info Display information from the target device(s) or file. + Without any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode + load Load the program / memory range stored in a file onto the device. + save Save the program / memory stored in flash on the device to a file. + verify Check that the device contents match those in the file. + reboot Reboot the device + version Display picotool version + help Show general help or help for a specific command Use "picotool help " for more info ``` @@ -69,15 +70,14 @@ can find (See Binary Info section below). The info command is for reading this i The information can be either read from one or more connected RP2040 devices in BOOTSEL mode, or from a file. This file can be an ELF, a UF2 or a BIN file. -```asciidoc +```text $ picotool help info INFO: Display information from the target device(s) or file. - Without any arguments, this will display basic information for all connected RP2040 devices in USB boot - mode + Without any arguments, this will display basic information for all connected RP2040 devices in BOOTSEL mode SYNOPSYS: - picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] + picotool info [-b] [-p] [-d] [-l] [-a] [--bus ] [--address ] [-f] [-F] picotool info [-b] [-p] [-d] [-l] [-a] [-t ] OPTIONS: @@ -105,7 +105,6 @@ TARGET SELECTION: -t Specify file type (uf2 | elf | bin) explicitly, ignoring file extension ``` - e.g. ```text @@ -247,11 +246,7 @@ Basic information includes - program features - this is a list built from individual strings in the binary, that can be displayed (e.g. we will have one for UART stdio and one for USB stdio) in the SDK - build attributes - this is a similar list of strings, for things pertaining to the binary itself (e.g. Debug Build) -Note it is my intention that things like MicroPython would include features for parts of the language/libraries they include. - -This might not be as a feature string per se, but could be another aggregating list (features/attributes are well known) -but we can add another piece of binary info to name a list attribute that aggregates strings with a particular tag, so you -could trivially add "MicroPython libraries:" etc to the `picotool` output without changing the tool itself. +The binary information is self-describing/extensible, so programs can include information picotool is not aware of (e.g. MicroPython includes a list of in-built libraries) ### Pins @@ -389,11 +384,6 @@ quotes, newlines etc you may have better luck defining via bi_decl in the code. ## Additional binary information/picotool features -### SDK version - -Should add this; git revision in general is hard since the user may not have the SDK checked out from git, so we'll have to stick -a version number in a header - ### Block devices MicroPython and CircuitPython, eventually the SDK and others may support one or more storage devices in flash. We already @@ -433,7 +423,7 @@ enum { ### USB device descriptors Seems like tagging these might be nice (we just need to store the pointer to it assuming - as is often the case - -the descriptor is just a linear chunk of memory) ... I assume there is a tool out there to prettyify such a thing if picotool dumps the descriptor +the descriptor is just a linear chunk of memory) ... I assume there is a tool out there to prettify such a thing if picotool dumps the descriptor in binary. ### Issues diff --git a/main.cpp b/main.cpp index aa7790d5..8212cf5e 100644 --- a/main.cpp +++ b/main.cpp @@ -10,7 +10,7 @@ #include "cli.h" #include "clipp/clipp.h" -#include +#include #include #include #include @@ -24,11 +24,18 @@ #include #include #include +#include + #include "boot/uf2.h" #include "picoboot_connection_cxx.h" #include "pico/binary_info.h" +#include "pico/stdio_usb/reset_interface.h" #include "elf.h" +#if defined(__unix__) || defined(__APPLE__) +#include +#endif + // tsk namespace is polluted on windows #ifdef _MSC_VER #undef min @@ -59,7 +66,6 @@ using std::map; typedef map>> device_map; typedef unsigned int uint; -static libusb_context *ctx; auto memory_names = map{ {memory_type::sram, "RAM"}, @@ -140,8 +146,25 @@ struct range { to = other.clamp(to); } + bool intersects(const range& other) const { + return !(other.from >= to || other.to < from); + } + }; +static void __noreturn fail(int code, string msg) { + throw command_failure(code, std::move(msg)); +} + +static void __noreturn fail(int code, const char *format, ...) { + va_list args; + va_start(args, format); + static char error_msg[512]; + vsnprintf(error_msg, sizeof(error_msg), format, args); + va_end(args); + fail(code, string(error_msg)); +} + // ranges should not overlap template struct range_map { struct mapping { @@ -150,19 +173,19 @@ template struct range_map { const uint32_t max_offset; }; - void check_overlap(uint32_t p) { - auto f = m.lower_bound(p); - if (f != m.end()) { - assert(p >= f->first); - assert(p < f->second.first); - } - } - void insert(const range& r, T t) { if (r.to != r.from) { assert(r.to > r.from); - check_overlap(r.from); - check_overlap(r.to); + // check we don't overlap any existing map entries + auto f = m.lower_bound(r.to); // f is first element that starts after or on r.to + if (f != m.begin()) { + f--; + } + if (f != m.end()) { + // due to edge cases above, f is either the entry before + // or after r, so just check for any overlap + fail(ERROR_FORMAT, "Found overlapping memory ranges 0x%08x->0x%08x and 0x%08x->%08x\n", f->first, f->second.first, r.from, r.to); + } m.insert(std::make_pair(r.from, std::make_pair(r.to, t))); } } @@ -213,11 +236,13 @@ using cli::value; struct cmd { explicit cmd(string name) : _name(std::move(name)) {} - enum device_support { none, one, zero_or_more, one_or_more }; + enum device_support { none, one, zero_or_more }; virtual group get_cli() = 0; virtual string get_doc() const = 0; virtual device_support get_device_support() { return one; } - virtual void execute(device_map& devices) = 0; + virtual bool force_requires_pre_reboot() { return true; } + // return true if the command caused a reboot + virtual bool execute(device_map& devices) = 0; const string& name() { return _name; } private: string _name; @@ -235,7 +260,9 @@ struct _settings { bool offset_set = false; bool range_set = false; bool reboot_usb = false; + bool reboot_app_specified = false; bool force = false; + bool force_no_reboot = false; struct { bool show_basic = false; @@ -248,6 +275,8 @@ struct _settings { struct { bool verify = false; bool execute = false; + bool no_overwrite = false; + bool no_overwrite_force = false; } load; struct { @@ -267,6 +296,10 @@ auto device_selection = .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 !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" +#endif ).min(0).doc_non_optional(true); auto file_types = (option ('t', "--type") & value("type").set(settings.file_type)) @@ -282,7 +315,7 @@ auto file_selection = struct info_command : public cmd { info_command() : cmd("info") {} - void execute(device_map& devices) override; + bool execute(device_map& devices) override; device_support get_device_support() override { if (settings.filename.empty()) return zero_or_more; @@ -313,7 +346,7 @@ struct info_command : public cmd { struct verify_command : public cmd { verify_command() : cmd("verify") {} - void execute(device_map &devices) override; + bool execute(device_map &devices) override; group get_cli() override { return ( @@ -336,7 +369,7 @@ struct verify_command : public cmd { struct save_command : public cmd { save_command() : cmd("save") {} - void execute(device_map &devices) override; + bool execute(device_map &devices) override; group get_cli() override { return ( @@ -362,11 +395,13 @@ struct save_command : public cmd { struct load_command : public cmd { load_command() : cmd("load") {} - void execute(device_map &devices) override; + bool execute(device_map &devices) override; group get_cli() override { return ( ( + option('n', "--no-overwrite").set(settings.load.no_overwrite) % "When writing flash data, do not overwrite an existing program in flash. If picotool cannot determine the size/presence of the program in flash, the command fails" + + option('N', "--no-overwrite-unsafe").set(settings.load.no_overwrite_force) % "When writing flash data, do not overwrite an existing program in flash. If picotool cannot determine the size/presence of the program in flash, the load continues anyway" + option('v', "--verify").set(settings.load.verify) % "Verify the data was written correctly" + option('x', "--execute").set(settings.load.execute) % "Attempt to execute the downloaded file as a program after the load" ).min(0).doc_non_optional(true) % "Post load actions" + @@ -386,7 +421,7 @@ struct load_command : public cmd { struct help_command : public cmd { help_command() : cmd("help") {} - void execute(device_map &devices) override; + bool execute(device_map &devices) override; device_support get_device_support() override { return device_support::none; @@ -405,11 +440,12 @@ struct help_command : public cmd { struct version_command : public cmd { version_command() : cmd("version") {} - void execute(device_map &devices) override { + bool execute(device_map &devices) override { if (settings.version.semantic) std::cout << PICOTOOL_VERSION << "\n"; else - std::cout << "picotool v" << PICOTOOL_VERSION << " (" << SYSTEM_VERSION << ":" << COMPILER_INFO << ")\n"; + std::cout << "picotool v" << PICOTOOL_VERSION << " (" << SYSTEM_VERSION << ", " << COMPILER_INFO << ")\n"; + return false; } device_support get_device_support() override { @@ -428,18 +464,27 @@ struct version_command : public cmd { } version_cmd; struct reboot_command : public cmd { + bool quiet; reboot_command() : cmd("reboot") {} - void execute(device_map &devices) override; + bool execute(device_map &devices) override; group get_cli() override { return ( - option('a', "--application").clear(settings.reboot_usb) % "Reboot back into the application (this is the default)" + + option('a', "--application").set(settings.reboot_app_specified) % "Reboot back into the application (this is the default)" + option('u', "--usb").set(settings.reboot_usb) % "Reboot back into BOOTSEL mode " +#if defined(_WIN32) + + option('f', "--force").set(settings.force) % "Force a device not in BOOTSEL mode but running compatible code to reboot." +#endif ).min(0).doc_non_optional(true) % "Reboot type" + device_selection % "Selecting the device to reboot"; } + bool force_requires_pre_reboot() override { + // no point in rebooting twice + return false; + } + string get_doc() const override { return "Reboot the device"; } @@ -473,6 +518,13 @@ std::basic_string uppercase(const std::basic_string& s) clipp::formatting_ostream fos(std::cout); +static void sleep_ms(int ms) { +#if defined(__unix__) || defined(__APPLE__) + usleep(ms * 1000); +#else + Sleep(ms); +#endif +} using cli::option; using cli::integer; int parse(const int argc, char **argv) { @@ -609,7 +661,8 @@ int parse(const int argc, char **argv) { args.erase(args.begin()); // remove the cmd itself cli::match(settings, selected_cmd->get_cli(), args); } catch (std::exception &e) { - std::cout << "ERROR: " << e.what() << "\n\n"; + fos.wrap_hard(); + fos << "ERROR: " << e.what() << "\n\n"; usage(); return ERROR_ARGS; } @@ -695,10 +748,6 @@ struct memory_access { } }; -static void __noreturn fail(int code, string msg) { - throw command_failure(code, msg); -} - uint32_t bootrom_func_lookup(memory_access& access, uint16_t tag) { auto magic = access.read_int(BOOTROM_MAGIC_ADDR); magic &= 0xffffff; // ignore bootrom version @@ -891,16 +940,6 @@ static void read_and_check_elf32_header(FILE *in, elf32_header& eh_out) { } } -static char error_msg[512]; - -static void __noreturn fail(int code, const char *format, ...) { - va_list args; - va_start(args, format); - vsnprintf(error_msg, sizeof(error_msg), format, args); - va_end(args); - fail(code, string(error_msg)); -} - static void __noreturn fail_read_error() { fail(ERROR_READ_FAILED, "Failed to read input file"); } @@ -963,6 +1002,12 @@ string read_string(memory_access &access, uint32_t addr) { } struct bi_visitor_base { + void visit(memory_access& access, const binary_info_header& hdr) { + for (const auto &a : hdr.bi_addr) { + visit(access, a); + } + } + void visit(memory_access& access, uint32_t addr) { binary_info_core_t bi; access.read_raw(addr, bi); @@ -1364,9 +1409,7 @@ void info_guts(memory_access &raw_access) { named_feature_groups[std::make_pair(group_tag, group_id)] = std::make_pair(label, flags); }); - for (const auto &a : hdr.bi_addr) { - visitor.visit(access, a); - } + visitor.visit(access, hdr); visitor = bi_visitor{}; visitor.id_and_int([&](int tag, uint32_t id, uint32_t value) { @@ -1412,9 +1455,7 @@ void info_guts(memory_access &raw_access) { } }); - for (const auto &a : hdr.bi_addr) { - visitor.visit(access, a); - } + visitor.visit(access, hdr); if (settings.info.show_basic || settings.info.all) { select_group(program_info); @@ -1521,62 +1562,70 @@ void info_guts(memory_access &raw_access) { } } -string missing_device_string() { - char buf[256]; +string missing_device_string(bool wasRetry) { + char b[256]; + if (wasRetry) { + strcpy(b, "Despite the reboot attempt, no "); + } else { + strcpy(b, "No "); + } + char *buf = b + strlen(b); if (settings.address != -1) { if (settings.bus != -1) { - sprintf(buf, "No accessible RP2040 device in BOOTSEL mode was found at bus %d, address %d.", settings.bus, settings.address); + sprintf(buf, "accessible RP2040 device in BOOTSEL mode was found at bus %d, address %d.", settings.bus, settings.address); } else { - sprintf(buf, "No accessible RP2040 devices in BOOTSEL mode were found with address %d.", settings.address); + sprintf(buf, "accessible RP2040 devices in BOOTSEL mode were found with address %d.", settings.address); } } else { if (settings.bus != -1) { - sprintf(buf, "No accessible RP2040 devices in BOOTSEL mode were found found on bus %d.", settings.bus); + sprintf(buf, "accessible RP2040 devices in BOOTSEL mode were found found on bus %d.", settings.bus); } else { - sprintf(buf, "No accessible RP2040 devices in BOOTSEL mode were found."); + sprintf(buf, "accessible RP2040 devices in BOOTSEL mode were found."); } } - return buf; + return b; } -void help_command::execute(device_map &devices) { +bool help_command::execute(device_map &devices) { assert(false); + return false; } -void info_command::execute(device_map &devices) { +bool info_command::execute(device_map &devices) { + fos.first_column(0); fos.hanging_indent(0); if (!settings.filename.empty()) { auto access = get_file_memory_access(); - std::cout << "File " << settings.filename << ":\n\n"; + fos << "File " << settings.filename << ":\n\n"; info_guts(access); - return; + return false; } int size = devices[dr_vidpid_bootrom_ok].size(); if (size) { if (size > 1) { - std::cout << "Multiple RP2040 devices in BOOTSEL mode found:\n"; + fos << "Multiple RP2040 devices in BOOTSEL mode found:\n"; } for (auto handles : devices[dr_vidpid_bootrom_ok]) { + fos.first_column(0); fos.hanging_indent(0); if (size > 1) { auto s = bus_device_string(handles.first); string dashes; std::generate_n(std::back_inserter(dashes), s.length() + 1, [] { return '-'; }); - std::cout << "\n" << s << ":\n" << dashes << "\n"; + fos << "\n" << s << ":\n" << dashes << "\n"; } picoboot::connection connection(handles.second); picoboot_memory_access access(connection); info_guts(access); } } else { - fail(ERROR_NO_DEVICE, missing_device_string()); + fail(ERROR_NO_DEVICE, missing_device_string(false)); } + return false; } -static picoboot::connection get_single_usb_boot_device(device_map& devices, bool exclusive = true) { - if (devices[dr_vidpid_bootrom_ok].size() != 1) { - fail(ERROR_NOT_POSSIBLE, "Command requires a single RP2040 device to be targeted."); - } +static picoboot::connection get_single_bootsel_device_connection(device_map& devices, bool exclusive = true) { + assert(devices[dr_vidpid_bootrom_ok].size() == 1); libusb_device_handle *rc = devices[dr_vidpid_bootrom_ok][0].second; - if (!rc) fail(ERROR_USB, "Unabled to connect to device"); + if (!rc) fail(ERROR_USB, "Unable to connect to device"); return picoboot::connection(rc, exclusive); } @@ -1606,8 +1655,8 @@ struct progress_bar { int width; }; -void save_command::execute(device_map &devices) { - auto con = get_single_usb_boot_device(devices); +bool save_command::execute(device_map &devices) { + auto con = get_single_bootsel_device_connection(devices); picoboot_memory_access raw_access(con); uint32_t end = 0; @@ -1635,9 +1684,7 @@ void save_command::execute(device_map &devices) { return; if (id == BINARY_INFO_ID_RP_BINARY_END) binary_end = value; }); - for (const auto &a : hdr.bi_addr) { - visitor.visit(access, a); - } + visitor.visit(access, hdr); } if (binary_end == 0) { fail(ERROR_NOT_POSSIBLE, @@ -1722,6 +1769,7 @@ void save_command::execute(device_map &devices) { throw; } } + return false; } vector get_colaesced_ranges(file_memory_access &file_access) { @@ -1744,13 +1792,32 @@ vector get_colaesced_ranges(file_memory_access &file_access) { return ranges; } -void load_command::execute(device_map &devices) { +bool load_command::execute(device_map &devices) { if (settings.offset_set && get_file_type() != filetype::bin) { fail(ERROR_ARGS, "Offset only valid for BIN files"); } auto file_access = get_file_memory_access(); - auto con = get_single_usb_boot_device(devices); + auto con = get_single_bootsel_device_connection(devices); picoboot_memory_access raw_access(con); + range flash_binary_range(FLASH_START, FLASH_END); + bool flash_binary_end_unknown = true; + if (settings.load.no_overwrite_force) settings.load.no_overwrite = true; + if (settings.load.no_overwrite) { + binary_info_header hdr; + if (find_binary_info(raw_access, hdr)) { + auto access = remapped_memory_access(raw_access, hdr.reverse_copy_mapping); + auto visitor = bi_visitor{}; + visitor.id_and_int([&](int tag, uint32_t id, uint32_t value) { + if (tag != BINARY_INFO_TAG_RASPBERRY_PI) + return; + if (id == BINARY_INFO_ID_RP_BINARY_END) { + flash_binary_range.to = value; + flash_binary_end_unknown = false; + } + }); + visitor.visit(access, hdr); + } + } auto ranges = get_colaesced_ranges(file_access); for (auto mem_range : ranges) { enum memory_type t1 = get_memory_type(mem_range.from); @@ -1759,6 +1826,16 @@ void load_command::execute(device_map &devices) { fail(ERROR_FORMAT, "File to load contained an invalid memory range 0x%08x-0x%08x", mem_range.from, mem_range.to); } + if (settings.load.no_overwrite && mem_range.intersects(flash_binary_range)) { + if (flash_binary_end_unknown) { + if (!settings.load.no_overwrite_force) { + fail(ERROR_NOT_POSSIBLE, "-n option specified, but the size/presence of an existing flash binary could not be detected; aborting. Consider using the -N option"); + } + } else { + fail(ERROR_NOT_POSSIBLE, "-n option specified, and the loaded data range clashes with the existing flash binary range %08x->%08x", + flash_binary_range.from, flash_binary_range.to); + } + } } for (auto mem_range : ranges) { enum memory_type type = get_memory_type(mem_range.from); @@ -1833,15 +1910,18 @@ void load_command::execute(device_map &devices) { fail(ERROR_FORMAT, "Cannot execute as file does not contain a valid RP2 executable image"); } con.reboot(flash == get_memory_type(start) ? 0 : start, SRAM_END, 500); + std::cout << "\nThe device was rebooted to start the application.\n"; + return true; } + return false; } -void verify_command::execute(device_map &devices) { +bool verify_command::execute(device_map &devices) { if (settings.offset_set && get_file_type() != filetype::bin) { fail(ERROR_ARGS, "Offset only valid for BIN files"); } auto file_access = get_file_memory_access(); - auto con = get_single_usb_boot_device(devices); + auto con = get_single_bootsel_device_connection(devices); picoboot_memory_access raw_access(con); auto ranges = get_colaesced_ranges(file_access); if (settings.range_set) { @@ -1938,36 +2018,94 @@ void verify_command::execute(device_map &devices) { } } } + return false; } -void reboot_command::execute(device_map &devices) { - // not exclusive, because restoring un-exclusive could fail; also if we're rebooting, we don't much - // care what else is happening. - auto con = get_single_usb_boot_device(devices, false); - if (!settings.reboot_usb) { - con.reboot(0, SRAM_END, 500); - std::cout << "The device was rebooted.\n"; +static int reboot_device(libusb_device *device, bool bootsel, uint disable_mask=0) { + // ok, the device isn't in USB boot mode, let's try to reboot via vendor interface + struct libusb_config_descriptor *config; + int ret = libusb_get_active_config_descriptor(device, &config); + if (ret) { + fail(ERROR_USB, "Failed to get descriptor %d\n", ret); + } + libusb_device_handle *dev_handle; + ret = libusb_open(device, &dev_handle); + if (ret) { +#if _MSC_VER + fail(ERROR_USB, "Unable to access device to reboot it; Make sure there is a driver installed via Zadig\n", ret); +#else + fail(ERROR_USB, "Unable to access device to reboot it; Use sudo or setup a udev rule\n", ret); +#endif + } + for (int 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) { + ret = libusb_claim_interface(dev_handle, i); + if (ret) { + fail(ERROR_USB, "Failed to claim interface\n"); + } + if (bootsel) { + ret = libusb_control_transfer(dev_handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + RESET_REQUEST_BOOTSEL, disable_mask, i, nullptr, 0, 2000); + } else { + ret = libusb_control_transfer(dev_handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + RESET_REQUEST_FLASH, 0, i, nullptr, 0, 2000); + } +// if (ret != 0 ) { +// fail(ERROR_UNKNOWN, "Unable to reset the device %d\n", ret); +// } + return 0; +// return ret; + } + } + fail(ERROR_USB, "Unable to locate reset interface on the device"); +} + +bool reboot_command::execute(device_map &devices) { + if (settings.force) { + reboot_device(devices[dr_vidpid_stdio_usb][0].first, settings.reboot_usb); + if (!quiet) { + if (settings.reboot_usb) { + std::cout << "The device was asked to reboot into BOOTSEL mode.\n"; + } else { + std::cout << "The device was asked to reboot into application mode.\n"; + } + } } else { - picoboot_memory_access raw_access(con); - uint program_base = SRAM_START; - std::vector program = { - 0x20002100, // movs r0, #0; movs r1, #0 - 0x47104a00, // ldr r2, [pc, #0]; bx r2 - bootrom_func_lookup(raw_access, rom_table_code('U','B')) - }; + // not exclusive, because restoring un-exclusive could fail; also if we're rebooting, we don't much + // care what else is happening. + auto con = get_single_bootsel_device_connection(devices, false); + if (!settings.reboot_usb) { + con.reboot(0, SRAM_END, 500); + } else { + picoboot_memory_access raw_access(con); + uint program_base = SRAM_START; + std::vector program = { + 0x20002100, // movs r0, #0; movs r1, #0 + 0x47104a00, // ldr r2, [pc, #0]; bx r2 + bootrom_func_lookup(raw_access, rom_table_code('U', 'B')) + }; - raw_access.write_vector(program_base, program); - try { - con.exec(program_base); - } catch (picoboot::connection_error &e) { - if (e.libusb_code == LIBUSB_ERROR_NO_DEVICE) { - // not unreasonable once it reboots - return; + raw_access.write_vector(program_base, program); + try { + con.exec(program_base); + } catch (picoboot::connection_error &e) { + // the reset_usb_boot above has a very short delay, so it frequently causes libusb to return + // fairly unpredictable errors... i think it is best to ignore them, because catching a rare + // case where the reboot command fails, is probably less important than potentially confusing + // the user with spurious error messages + } + } + if (!quiet) { + if (settings.reboot_usb) { + std::cout << "The device was rebooted into BOOTSEL mode.\n"; + } else { + std::cout << "The device was rebooted into application mode.\n"; } - throw e; } - fail(ERROR_NOT_POSSIBLE, "Reboot into USB boot not implemented yet"); } + return true; } #if defined(_WIN32) @@ -1997,6 +2135,8 @@ void cancelled(int) { } int main(int argc, char **argv) { + libusb_context *ctx = nullptr; + int tw=0, th=0; get_terminal_size(tw, th); if (tw) { @@ -2009,6 +2149,9 @@ int main(int argc, char **argv) { return 0; } + // save complicating the grammar + if (settings.force_no_reboot) settings.force = true; + struct libusb_device **devs = nullptr; device_map devices; vector to_close; @@ -2016,58 +2159,143 @@ int main(int argc, char **argv) { try { signal(SIGINT, cancelled); signal(SIGTERM, cancelled); + + if (settings.reboot_usb && settings.reboot_app_specified) { + fail(ERROR_ARGS, "Cannot specify both -u and -a reboot options"); + } + if (selected_cmd->get_device_support() != cmd::none) { if (libusb_init(&ctx)) { fail(ERROR_USB, "Failed to initialise libUSB\n"); } - rc = libusb_get_device_list(ctx, &devs); - if (rc < 0) { - fail(ERROR_USB, "Failed to enumerate USB devices\n"); - } else { - rc = 0; - } - for (libusb_device **dev = devs; *dev; dev++) { - if (settings.bus != -1 && settings.bus != libusb_get_bus_number(*dev)) continue; - if (settings.address != -1 && settings.address != libusb_get_device_address(*dev)) continue; - libusb_device_handle *handle = nullptr; - auto result = picoboot_open_device(*dev, &handle); - if (handle) { - to_close.push_back(handle); + } + + // we only loop a second time if we want to reboot some devices (which may cause device + for (int tries = 0; !rc && tries < 2; tries++) { + if (ctx) { + if (libusb_get_device_list(ctx, &devs) < 0) { + fail(ERROR_USB, "Failed to enumerate USB devices\n"); } - if (result != dr_error) { - devices[result].push_back(std::make_pair(*dev, handle)); + for (libusb_device **dev = devs; *dev; dev++) { + if (settings.bus != -1 && settings.bus != libusb_get_bus_number(*dev)) continue; + if (settings.address != -1 && settings.address != libusb_get_device_address(*dev)) continue; + libusb_device_handle *handle = nullptr; + auto result = picoboot_open_device(*dev, &handle); + if (handle) { + to_close.push_back(handle); + } + if (result != dr_error) { + devices[result].push_back(std::make_pair(*dev, handle)); + } } } - } - - if (!rc) { - switch (selected_cmd->get_device_support()) { + auto supported = selected_cmd->get_device_support(); + switch (supported) { case cmd::device_support::zero_or_more: if (!settings.filename.empty()) break; // fall thru case cmd::device_support::one: - case cmd::device_support::one_or_more: - if (devices[dr_vidpid_bootrom_ok].empty()) { - std::cout << missing_device_string() << "\n"; + if (devices[dr_vidpid_bootrom_ok].empty() && + (!settings.force || devices[dr_vidpid_stdio_usb].empty())) { bool had_note = false; - auto printer = [&](enum picoboot_device_result r, const string& description) { + fos << missing_device_string(tries>0); + if (tries > 0) { + fos << " It is possible the device is not responding, and will have to be manually entered into BOOTSEL mode.\n"; + had_note = true; // suppress "but:" in this case + } + fos << "\n"; + fos.first_column(0); + fos.hanging_indent(4); + auto printer = [&](enum picoboot_device_result r, const string &description) { if (!had_note && !devices[r].empty()) { - std::cout << "\nbut:\n\n"; + fos << "\nbut:\n\n"; had_note = true; } for (auto d : devices[r]) { - std::cout << bus_device_string(d.first) << description << "\n"; + fos << bus_device_string(d.first) << description << "\n"; } }; - printer(dr_vidpid_bootrom_cant_connect, " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect"); - printer(dr_vidpid_picoprobe, " appears to be a RP2040 PicoProbe device not in BOOTSEL mode."); - printer(dr_vidpid_micropython, " appears to be a RP2040 MicroPython device not in BOOTSEL mode."); + printer(dr_vidpid_bootrom_cant_connect, + " appears to be a RP2040 device in BOOTSEL mode, but picotool was unable to connect"); + printer(dr_vidpid_picoprobe, + " appears to be a RP2040 PicoProbe device not in BOOTSEL mode."); + printer(dr_vidpid_micropython, + " appears to be a RP2040 MicroPython device not in BOOTSEL mode."); + if (selected_cmd->force_requires_pre_reboot()) { +#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."); +#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."); +#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."); + } 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)) { + fail(ERROR_NOT_POSSIBLE, "Command requires a single RP2040 device to be targeted."); + } + if (!devices[dr_vidpid_bootrom_ok].empty()) { + settings.force = false; // we have a device, so we're not forcing + } + } else if (supported == cmd::device_support::zero_or_more && settings.force && !devices[dr_vidpid_bootrom_ok].empty()) { + // we have usable devices, so lets use them without force + settings.force = false; } + fos.first_column(0); + fos.hanging_indent(0); + break; default: break; } - if (!rc) selected_cmd->execute(devices); + 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) { + 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; + 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) { + libusb_close(handle); + } + libusb_free_device_list(devs, 1); + devs = nullptr; + to_close.clear(); + devices.clear(); + sleep_ms(1200); + + // we now clear settings.force, because we expect the device to have rebooted and be available. + // we also clear any filters, because the device may have moved, so the only way we can find it + // again is to assume it is the only now visible device. + settings.force = false; + settings.address = -1; + settings.bus = -1; + continue; + } + } + if (!selected_cmd->execute(devices) && tries) { + if (settings.force_no_reboot) { + fos << "\nThe device has been left accessible, but without the drive mounted; use 'picotool reboot' to reboot into regular BOOTSEL mode or application mode.\n"; + } else { + // can only really do this with one device + if (devices[dr_vidpid_bootrom_ok].size() == 1) { + reboot_cmd.quiet = true; + reboot_cmd.execute(devices); + fos << "\nThe device was asked to reboot back into application mode.\n"; + } + } + } + break; + } } } catch (command_failure &e) { std::cout << "ERROR: " << e.what() << "\n"; diff --git a/picoboot_connection/picoboot_connection.c b/picoboot_connection/picoboot_connection.c index 198c5292..d1f2beee 100644 --- a/picoboot_connection/picoboot_connection.c +++ b/picoboot_connection/picoboot_connection.c @@ -24,6 +24,7 @@ static bool verbose; #define PRODUCT_ID_RP2_USBBOOT 0x0003u #define PRODUCT_ID_PICOPROBE 0x0004u #define PRODUCT_ID_MICROPYTHON 0x0005u +#define PRODUCT_ID_STDIO_USB 0x000au uint32_t crc32_for_byte(uint32_t remainder) { const uint32_t POLYNOMIAL = 0x4C11DB7; @@ -72,6 +73,8 @@ enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_d return dr_vidpid_micropython; case PRODUCT_ID_PICOPROBE: return dr_vidpid_picoprobe; + case PRODUCT_ID_STDIO_USB: + return dr_vidpid_stdio_usb; case PRODUCT_ID_RP2_USBBOOT: break; default: diff --git a/picoboot_connection/picoboot_connection.h b/picoboot_connection/picoboot_connection.h index 796b57ca..ebdabfc9 100644 --- a/picoboot_connection/picoboot_connection.h +++ b/picoboot_connection/picoboot_connection.h @@ -9,6 +9,7 @@ // todo we should use fully encapsulate libusb +#include #include #include "boot/picoboot.h" @@ -24,6 +25,7 @@ enum picoboot_device_result { dr_vidpid_picoprobe, dr_vidpid_unknown, dr_error, + dr_vidpid_stdio_usb, }; enum picoboot_device_result picoboot_open_device(libusb_device *device, libusb_device_handle **dev_handle);