From 4000496abc384e941b1aa0ca781962aac2503a7f Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 14 May 2026 17:38:02 +0200 Subject: [PATCH 1/2] main: ftdi-serial is deprecated. Added --usb-serial-num argument --- src/main.cpp | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9ca2940..bb48463 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -79,7 +79,7 @@ struct arguments { std::string secondary_bit_file; std::string device; std::string cable; - std::string ftdi_serial; + std::string usb_serial_num; int ftdi_channel; int status_pin; uint32_t freq; @@ -264,13 +264,6 @@ int main(int argc, char **argv) cable.config.interface = mapping[args.ftdi_channel]; } - if (!args.ftdi_serial.empty()) { - if (cable.type != MODE_FTDI_SERIAL && cable.type != MODE_FTDI_BITBANG){ - printError("Error: FTDI serial param is for FTDI cables."); - return EXIT_FAILURE; - } - } - if (args.status_pin != -1) { if (cable.type != MODE_FTDI_SERIAL){ printError("Error: FTDI status pin is for FTDI MPSSE cables."); @@ -389,7 +382,7 @@ int main(int argc, char **argv) Jtag *jtag; try { - jtag = new Jtag(cable, &pins_config, args.device, args.ftdi_serial, + jtag = new Jtag(cable, &pins_config, args.device, args.usb_serial_num, args.freq, args.verbose, args.ip_adr, args.port, args.invert_read_edge, args.probe_firmware, args.user_misc_devs); @@ -667,7 +660,7 @@ int run_xvc_server(const struct arguments &args, const cable_t &cable, try { XVC_server *xvc = NULL; xvc = new XVC_server(args.port, cable, pins_config, args.device, - args.ftdi_serial, args.freq, args.verbose, args.ip_adr, + args.usb_serial_num, args.freq, args.verbose, args.ip_adr, args.invert_read_edge, args.probe_firmware); /* create connection */ xvc->open_connection(); @@ -889,6 +882,7 @@ int parse_opt(int argc, char **argv, struct arguments *args, jtag_pins_conf_t *pins_config) { std::string freqo; + std::string ftdi_serial; std::vector pins, bus_dev_num; bool verbose = false, quiet = false; int8_t verbose_level = -2; @@ -927,8 +921,10 @@ int parse_opt(int argc, char **argv, struct arguments *args, ("busdev-num", "select a probe by it bus and device number (bus_num:device_addr)", cxxopts::value>(bus_dev_num)) - ("ftdi-serial", "FTDI chip serial number", - cxxopts::value(args->ftdi_serial)) + ("usb-serial-num", "USB iSerial (FTDI chip serial number)", + cxxopts::value(args->usb_serial_num)) + ("ftdi-serial", "FTDI chip serial number (Deprecated)", + cxxopts::value(ftdi_serial)) ("ftdi-channel", "FTDI chip channel number (channels 0-3 map to A-D)", cxxopts::value(args->ftdi_channel)) @@ -1134,6 +1130,14 @@ int parse_opt(int argc, char **argv, struct arguments *args, } } + if (result.count("ftdi-serial")) { + if (result.count("usb_serial_num")) { + printError("Error: ftdi_serial and usb_serial_num can't be used at the same time."); + return -1; + } + args->usb_serial_num = ftdi_serial; + } + if (result.count("busdev-num")) { if (bus_dev_num.size() != 2) { printError("Error: busdev-num must be xx:yy"); From 9e548983f99f988778abfcc63b04ccfb8c2594e2 Mon Sep 17 00:00:00 2001 From: r4d10n Date: Thu, 14 May 2026 14:22:15 +0530 Subject: [PATCH 2/2] esp_usb_jtag: disambiguate multiple boards via --busdev-num and --usb-serial-num libusb_open_device_with_vid_pid() returns the first VID:PID match, so with two ESP32-S3 cables connected, every invocation programmed the same board. Replace it with a libusb_get_device_list() iteration that also honours cable.bus_addr/device_addr (already exposed as --busdev-num) and --usb-serial-num flag that matches the device iSerialNumber by substring (useful with MAC-derived serials). The -d /dev/ttyACM* path could not help: that's the CDC-ACM interface (iface 0), while JTAG is on the vendor iface 2 reached via libusb; ttyACM numbering and libusb enumeration order are independent. Constraint: must keep existing single-board invocations working without flag changes Confidence: high Scope-risk: narrow Directive: arguments struct uses positional aggregate init at main.cpp:120; new fields must add a matching slot Not-tested: simultaneous two-board programming on real hardware (single-board path verified to build) Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 4 ++- src/esp_usb_jtag.cpp | 69 +++++++++++++++++++++++++++++++++++++------- src/esp_usb_jtag.hpp | 6 +++- src/jtag.cpp | 3 +- src/main.cpp | 14 +++++++-- 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7e48627..b9e767b 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ openFPGALoader -- a program to flash FPGA --cable-index arg probe index (FTDI and cmsisDAP) --busdev-num arg select a probe by it bus and device number (bus_num:device_addr) - --ftdi-serial arg FTDI chip serial number + --usb-serial-num arg USB iSerial (FTDI chip serial number or ESP32 + iSerialNumber substring) + --ftdi-serial arg FTDI chip serial number (Deprecated) --ftdi-channel arg FTDI chip channel number (channels 0-3 map to A-D) -d, --device arg device to use (/dev/ttyUSBx) diff --git a/src/esp_usb_jtag.cpp b/src/esp_usb_jtag.cpp index 1649586..4d4bfe8 100644 --- a/src/esp_usb_jtag.cpp +++ b/src/esp_usb_jtag.cpp @@ -245,13 +245,17 @@ static uint16_t esp_usb_target_chip_id = 0; /* not applicable for FPGA, they hav /* end copy from openocd */ -esp_usb_jtag::esp_usb_jtag(uint32_t clkHZ, int8_t verbose, int vid = ESPUSBJTAG_VID, int pid = ESPUSBJTAG_PID): +esp_usb_jtag::esp_usb_jtag(uint32_t clkHZ, int8_t verbose, + int vid = ESPUSBJTAG_VID, int pid = ESPUSBJTAG_PID, + uint8_t bus_addr = 0, uint8_t dev_addr = 0, + const std::string &serial = ""): _verbose(verbose > 1), dev_handle(NULL), usb_ctx(NULL), _tdi(0), _tms(0), /* Default for emard firmware. */ _esp_usb_jtag_caps(0x2000), _write_ep(0x02), _vid(ESPUSBJTAG_VID), _pid(ESPUSBJTAG_PID) { + libusb_device **devs = NULL; int ret; char mess[256]; @@ -260,18 +264,61 @@ esp_usb_jtag::esp_usb_jtag(uint32_t clkHZ, int8_t verbose, int vid = ESPUSBJTAG_ throw std::exception(); } - dev_handle = libusb_open_device_with_vid_pid(usb_ctx, - _vid, _pid); - if (!dev_handle) { - _esp_usb_jtag_caps = 0x030A; - _write_ep = 0x03; - _pid = 0x1002; - dev_handle = libusb_open_device_with_vid_pid(usb_ctx, - _vid, _pid); + ssize_t cnt = libusb_get_device_list(usb_ctx, &devs); + if (cnt < 0) { + std::cerr << "esp_usb_jtag: libusb_get_device_list failed: " + << libusb_error_name(static_cast(cnt)) << std::endl; + libusb_exit(usb_ctx); + throw std::exception(); } + + for (ssize_t i = 0; i < cnt; i++) { + libusb_device *dev = devs[i]; + struct libusb_device_descriptor desc; + if (libusb_get_device_descriptor(dev, &desc) < 0) + continue; + if (desc.idVendor != vid || desc.idProduct != pid) + continue; + /* bus/device filter (only when both are user-supplied) */ + if (bus_addr != 0 && dev_addr != 0 && + (libusb_get_bus_number(dev) != bus_addr || + libusb_get_device_address(dev) != dev_addr)) + continue; + + /* serial filter */ + if (!serial.empty()) { + libusb_device_handle *probe = NULL; + if (libusb_open(dev, &probe) < 0) + continue; + unsigned char raw[256] = {0}; + int n = 0; + if (desc.iSerialNumber) + n = libusb_get_string_descriptor_ascii(probe, + desc.iSerialNumber, raw, sizeof(raw)); + libusb_close(probe); + if (n <= 0) + continue; + const std::string found(reinterpret_cast(raw), n); + + if (found.find(serial) == std::string::npos) + continue; + } + + if (libusb_open(dev, &dev_handle) == 0) + break; + dev_handle = NULL; + } + libusb_free_device_list(devs, 1); + if (!dev_handle) { - snprintf(mess, 256, "fails to open esp_usb_jtag device"); - printError(mess); + std::cerr << "fails to open esp_usb_jtag device vid:pid 0x" + << std::hex << vid << ":0x" << pid; + if (bus_addr || dev_addr) + std::cerr << " bus:dev " << std::dec << static_cast(bus_addr) + << ":" << static_cast(dev_addr); + if (!serial.empty()) + std::cerr << " serial '" << serial << "'"; + std::cerr << std::endl; libusb_exit(usb_ctx); throw std::exception(); } diff --git a/src/esp_usb_jtag.hpp b/src/esp_usb_jtag.hpp index da0fb8f..65357b2 100644 --- a/src/esp_usb_jtag.hpp +++ b/src/esp_usb_jtag.hpp @@ -12,6 +12,8 @@ #include +#include + #include "jtagInterface.hpp" /*! @@ -23,7 +25,9 @@ class esp_usb_jtag : public JtagInterface { public: - esp_usb_jtag(uint32_t clkHZ, int8_t verbose, int vid, int pid); + esp_usb_jtag(uint32_t clkHZ, int8_t verbose, int vid, int pid, + uint8_t bus_addr, uint8_t dev_addr, + const std::string &serial); virtual ~esp_usb_jtag(); int setClkFreq(uint32_t clkHZ) override; diff --git a/src/jtag.cpp b/src/jtag.cpp index d084bd8..2b99023 100644 --- a/src/jtag.cpp +++ b/src/jtag.cpp @@ -165,7 +165,8 @@ Jtag::Jtag(const cable_t &cable, const jtag_pins_conf_t *pin_conf, break; case MODE_ESP: #ifdef ENABLE_ESP_USB - _jtag = new esp_usb_jtag(clkHZ, verbose, 0x303a, 0x1001); + _jtag = new esp_usb_jtag(clkHZ, verbose, 0x303a, 0x1001, + cable.bus_addr, cable.device_addr, serial); #else std::cerr << "Jtag: support for esp32s3 cable was not enabled at compile time" << std::endl; throw std::exception(); diff --git a/src/main.cpp b/src/main.cpp index bb48463..4c779e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -272,6 +272,14 @@ int main(int argc, char **argv) } #endif + if (!args.usb_serial_num.empty()) { + if (cable.type != MODE_FTDI_SERIAL && cable.type != MODE_FTDI_BITBANG && + cable.type != MODE_ESP){ + printError("Error: usb-serial-num param is for FTDI and esp32s3 cables."); + return EXIT_FAILURE; + } + } + if (args.vid != 0) { printInfo("Cable VID overridden"); cable.vid = args.vid; @@ -921,7 +929,7 @@ int parse_opt(int argc, char **argv, struct arguments *args, ("busdev-num", "select a probe by it bus and device number (bus_num:device_addr)", cxxopts::value>(bus_dev_num)) - ("usb-serial-num", "USB iSerial (FTDI chip serial number)", + ("usb-serial-num", "USB iSerial (FTDI chip serial number or ESP32 iSerialNumber substring)", cxxopts::value(args->usb_serial_num)) ("ftdi-serial", "FTDI chip serial number (Deprecated)", cxxopts::value(ftdi_serial)) @@ -1131,8 +1139,8 @@ int parse_opt(int argc, char **argv, struct arguments *args, } if (result.count("ftdi-serial")) { - if (result.count("usb_serial_num")) { - printError("Error: ftdi_serial and usb_serial_num can't be used at the same time."); + if (result.count("usb-serial-num")) { + printError("Error: ftdi-serial and usb-serial-num can't be used at the same time."); return -1; } args->usb_serial_num = ftdi_serial;