From 50c19cc25f9c408b41f689c455e2ce0cd30b2798 Mon Sep 17 00:00:00 2001 From: Douglas Reis Date: Wed, 15 Apr 2026 13:53:08 +0100 Subject: [PATCH 1/2] [ftdi] Add function to detach ftdi_sio driver This function was written with the help of claude code. Signed-off-by: Douglas Reis --- lib/ftdi/libusb.cc | 61 ++++++++++++++++++++++++++++++++++++++++++++++ lib/ftdi/libusb.hh | 19 +++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 lib/ftdi/libusb.cc create mode 100644 lib/ftdi/libusb.hh diff --git a/lib/ftdi/libusb.cc b/lib/ftdi/libusb.cc new file mode 100644 index 0000000..440ac7c --- /dev/null +++ b/lib/ftdi/libusb.cc @@ -0,0 +1,61 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "libusb.hh" + +#include +#include + +#include + +namespace ftdi { + +enum { kFtdiVid = 0x0403 }; + +// Use libusb to temporarily detach ftdi_sio from every interface of +// every FTDI USB device. The kernel driver is re-attached automatically when +// the D2XX / libft4222 handle is closed. +void detach_ftdi_sio(uint16_t pid) { + libusb_context* ctx = nullptr; + if (libusb_init(&ctx) != LIBUSB_SUCCESS) { + std::cerr << "libusb_init failed\n"; + return; + } + + libusb_device** devs = nullptr; + ssize_t cnt = libusb_get_device_list(ctx, &devs); + if (cnt < 0) { + libusb_exit(ctx); + return; + } + + for (ssize_t i = 0; i < cnt; i++) { + libusb_device_descriptor desc{}; + if (libusb_get_device_descriptor(devs[i], &desc) != LIBUSB_SUCCESS) continue; + if (desc.idVendor != kFtdiVid || desc.idProduct != pid) continue; + + libusb_device_handle* handle = nullptr; + if (libusb_open(devs[i], &handle) != LIBUSB_SUCCESS) continue; + + libusb_config_descriptor* config = nullptr; + if (libusb_get_active_config_descriptor(devs[i], &config) == LIBUSB_SUCCESS) { + for (uint8_t j = 0; j < config->bNumInterfaces; j++) { + if (libusb_kernel_driver_active(handle, j) != 1) continue; + int ret = libusb_detach_kernel_driver(handle, j); + if (ret != LIBUSB_SUCCESS) { + std::cerr << "Failed to detach kernel driver on interface " << (int)j << ": " + << libusb_error_name(ret) << "\n"; + } + } + libusb_free_config_descriptor(config); + } + + libusb_close(handle); + } + + libusb_free_device_list(devs, 1); + libusb_exit(ctx); +} + +} // namespace ftdi diff --git a/lib/ftdi/libusb.hh b/lib/ftdi/libusb.hh new file mode 100644 index 0000000..1f0760c --- /dev/null +++ b/lib/ftdi/libusb.hh @@ -0,0 +1,19 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +namespace ftdi { + +// Detach ftdi_sio (or any kernel driver) from all FTDI USB interfaces +// using libusb. This allows the D2XX library to claim the device afterwards. +// Requires write access to the USB device files in the udev rules. +void detach_ftdi_sio(uint16_t pid); + +} // namespace ftdi From bdae0e2266f3f478fe2a8956e6060e403664d4a8 Mon Sep 17 00:00:00 2001 From: Douglas Reis Date: Wed, 15 Apr 2026 12:26:46 +0100 Subject: [PATCH 2/2] Automatically detach ftdi_sio driver In linux the kernel will automatically bind the ftdi device to the ftdi_sio driver, this prevents the lib D2XX to discover the ftdi. Signed-off-by: Douglas Reis --- app/main.cc | 45 +++++++++++++++++++++++++++-------------- flake.nix | 2 +- lib/ftdi/CMakeLists.txt | 5 ++++- lib/ftdi/ftdi.hh | 8 ++++++-- nix/ftditool.nix | 4 ++++ 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/app/main.cc b/app/main.cc index e0ff356..6d9b5f1 100644 --- a/app/main.cc +++ b/app/main.cc @@ -21,8 +21,8 @@ using Action = std::function; -static std::vector scan() { - auto result = ftdi::Discovery::scan(); +static std::vector scan(uint16_t pid = 0x6010) { + auto result = ftdi::Discovery::scan(pid); if (!result) { std::cerr << "Error: Failed to communicate with FTDI driver." << std::endl; exit(0); @@ -61,11 +61,11 @@ new_flash_command(const std::string& name, const std::string& desc) { } static std::optional -handle_flash_command(std::unique_ptr& cmd) { +handle_flash_command(std::unique_ptr& cmd, uint16_t pid) { auto ftdi = cmd->get("--ftdi"); auto traces = cmd->get("--traces"); auto clock = cmd->get("--clock"); - auto devices = scan(); + auto devices = scan(pid); auto filtered = ftdi::DeviceInfo::filter_by_description(devices, ftdi); if (filtered.empty()) { std::print("No {} ftdi found.\n", ftdi); @@ -81,9 +81,9 @@ handle_flash_command(std::unique_ptr& cmd) { } static std::optional -handle_gpio_command(std::unique_ptr& cmd) { +handle_gpio_command(std::unique_ptr& cmd, uint16_t pid) { auto ftdi_filter = cmd->get("--ftdi"); - auto devices = scan(); + auto devices = scan(pid); auto filtered = ftdi::DeviceInfo::filter_by_description(devices, ftdi_filter); if (filtered.empty()) { std::print("No {} ftdi found.\n", ftdi_filter); @@ -100,13 +100,20 @@ int main(int argc, char* argv[]) { std::map commands; argparse::ArgumentParser program("ftditool"); + program.add_argument("--pid") + .default_value(std::uint16_t{0x6010}) + .help( + "The usb product id, in case the ftdi_sio driver needs to be detached. Defaults to " + "FT2232H") + .scan<'x', std::uint16_t>(); argparse::ArgumentParser list_devices_cmd("list-devices"); list_devices_cmd.add_description("List FTDI devices."); program.add_subparser(list_devices_cmd); commands["list-devices"] = [&]() -> int { + auto pid = program.get("--pid"); auto idx = 0; - for (auto device : scan()) { + for (auto device : scan(pid)) { std::print("{}: {}\n", idx++, device); } return 0; @@ -115,7 +122,8 @@ int main(int argc, char* argv[]) { auto jedec_cmd = new_flash_command("jedec", "Read the JEDEC identifier."); program.add_subparser(*jedec_cmd); commands["jedec"] = [&]() -> int { - auto spih = handle_flash_command(jedec_cmd); + auto pid = program.get("--pid"); + auto spih = handle_flash_command(jedec_cmd, pid); commands::ReadJedec(flash::Generic(*spih)).run(); spih->close(); return 0; @@ -124,7 +132,8 @@ int main(int argc, char* argv[]) { auto sfdp_cmd = new_flash_command("sfdp", "Read serial flash description."); program.add_subparser(*sfdp_cmd); commands["sfdp"] = [&]() -> int { - auto spih = handle_flash_command(sfdp_cmd); + auto pid = program.get("--pid"); + auto spih = handle_flash_command(sfdp_cmd, pid); commands::ReadSfdp(flash::Generic(*spih)).run(); spih->close(); return 0; @@ -134,8 +143,9 @@ int main(int argc, char* argv[]) { read_page_cmd->add_argument("--addr").help("The page address").scan<'x', std::size_t>(); program.add_subparser(*read_page_cmd); commands["read-page"] = [&]() -> int { + auto pid = program.get("--pid"); auto addr = read_page_cmd->get("--addr"); - auto spih = handle_flash_command(read_page_cmd); + auto spih = handle_flash_command(read_page_cmd, pid); commands::ReadPage(flash::Generic(*spih), addr).run(); spih->close(); return 0; @@ -150,9 +160,10 @@ int main(int argc, char* argv[]) { test_page_cmd->add_argument("--quad").help("Use qSPI").default_value(false).implicit_value(true); program.add_subparser(*test_page_cmd); commands["test-page"] = [&]() -> int { + auto pid = program.get("--pid"); auto addr = test_page_cmd->get("--addr"); auto quad = test_page_cmd->get("--quad"); - auto spih = handle_flash_command(test_page_cmd); + auto spih = handle_flash_command(test_page_cmd, pid); commands::TestPage(flash::Generic(*spih), addr, quad).run(); spih->close(); @@ -169,10 +180,11 @@ int main(int argc, char* argv[]) { load_file_cmd->add_argument("--quad").help("Use qSPI").default_value(false).implicit_value(true); program.add_subparser(*load_file_cmd); commands["load-file"] = [&]() -> int { + auto pid = program.get("--pid"); auto filename = load_file_cmd->get("filename"); auto addr = load_file_cmd->get("--addr"); auto quad = load_file_cmd->get("--quad"); - auto spih = handle_flash_command(load_file_cmd); + auto spih = handle_flash_command(load_file_cmd, pid); commands::LoadFile(flash::Generic(*spih), filename, addr, false, quad).run(); spih->close(); @@ -192,10 +204,11 @@ int main(int argc, char* argv[]) { .implicit_value(true); program.add_subparser(*verify_file_cmd); commands["verify-file"] = [&]() -> int { + auto pid = program.get("--pid"); auto filename = verify_file_cmd->get("filename"); auto addr = verify_file_cmd->get("--addr"); auto quad = verify_file_cmd->get("--quad"); - auto spih = handle_flash_command(verify_file_cmd); + auto spih = handle_flash_command(verify_file_cmd, pid); commands::VerifyFile(flash::Generic(*spih), filename, addr, quad).run(); spih->close(); @@ -212,10 +225,11 @@ int main(int argc, char* argv[]) { bootstrap_cmd->add_argument("--quad").help("Use qSPI").default_value(false).implicit_value(true); program.add_subparser(*bootstrap_cmd); commands["bootstrap"] = [&]() -> int { + auto pid = program.get("--pid"); auto filename = bootstrap_cmd->get("filename"); auto addr = bootstrap_cmd->get("--addr"); auto quad = bootstrap_cmd->get("--quad"); - auto spih = handle_flash_command(bootstrap_cmd); + auto spih = handle_flash_command(bootstrap_cmd, pid); commands::LoadFile(flash::Generic(*spih), filename, addr, true, quad).run(); spih->close(); @@ -227,9 +241,10 @@ int main(int argc, char* argv[]) { gpio_write_cmd->add_argument("value").help("Value to write (0 or 1)").required().scan<'d', int>(); program.add_subparser(*gpio_write_cmd); commands["gpio-write"] = [&]() -> int { + auto pid = program.get("--pid"); auto pin = static_cast(gpio_write_cmd->get("pin")); auto value = gpio_write_cmd->get("value") != 0; - auto gpio = handle_gpio_command(gpio_write_cmd); + auto gpio = handle_gpio_command(gpio_write_cmd, pid); commands::GpioWrite(*gpio, pin, value).run(); gpio->close(); return 0; diff --git a/flake.nix b/flake.nix index bc2f4b5..ac840b4 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,7 @@ ft4222 libmpsse ]; - buildInputs = [ft4222 libmpsse ftd2xx]; + buildInputs = with pkgs; [ft4222 libmpsse ftd2xx libusb1 systemd]; shellHook = '' # Setting LD_LIBRARY_PATH for libftd2xx export LD_LIBRARY_PATH="${ftd2xx}/lib:$LD_LIBRARY_PATH" diff --git a/lib/ftdi/CMakeLists.txt b/lib/ftdi/CMakeLists.txt index a674556..489ea12 100644 --- a/lib/ftdi/CMakeLists.txt +++ b/lib/ftdi/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(ftdipp PRIVATE spi_host.cc gpio.cc + libusb.cc PUBLIC FILE_SET HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} @@ -16,6 +17,7 @@ target_sources(ftdipp spi_host.hh gpio.hh log.hh + libusb.hh ) target_include_directories(ftdipp @@ -25,7 +27,8 @@ target_include_directories(ftdipp ) target_link_libraries(ftdipp - PUBLIC magic_enum::magic_enum embeddedpp mpsse ft4222 + PUBLIC magic_enum::magic_enum embeddedpp mpsse ft4222 + PRIVATE usb-1.0 udev ) target_compile_options(ftdipp PRIVATE "-fno-exceptions") diff --git a/lib/ftdi/ftdi.hh b/lib/ftdi/ftdi.hh index 38357c7..65929d2 100644 --- a/lib/ftdi/ftdi.hh +++ b/lib/ftdi/ftdi.hh @@ -5,11 +5,11 @@ #pragma once #include "ftd2xx.h" -#include #include #include #include #include "device_info.hh" +#include "libusb.hh" namespace ftdi { @@ -19,7 +19,11 @@ class Discovery { * @brief Scans for all connected FTDI devices. * @return A vector of devices if successful, or std::nullopt if the driver call failed. */ - static std::optional> scan() { + + static std::optional> scan(uint16_t pid) { + // Detach ftdi_sio (if present) so D2XX can enumerate the device. + detach_ftdi_sio(pid); + DWORD num_devs = 0; // 1. Get the number of devices diff --git a/nix/ftditool.nix b/nix/ftditool.nix index 16410ca..3fdcf4a 100644 --- a/nix/ftditool.nix +++ b/nix/ftditool.nix @@ -12,6 +12,8 @@ argparse, magic-enum, picosha2, + libusb1, + systemd, makeWrapper, }: stdenv.mkDerivation { @@ -33,6 +35,8 @@ stdenv.mkDerivation { magic-enum picosha2 ftd2xx + libusb1 + systemd ]; cmakeFlags = [