From 2dc3ba37cda7dd82eeb3feaa31a7f77fe2c62ed8 Mon Sep 17 00:00:00 2001 From: Viktor P Date: Fri, 24 Apr 2026 16:34:17 +0300 Subject: [PATCH 01/25] gowin: fix checksum header key typo --- src/gowin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gowin.cpp b/src/gowin.cpp index 0090af6..4c25190 100644 --- a/src/gowin.cpp +++ b/src/gowin.cpp @@ -436,7 +436,7 @@ void Gowin::checkCRC() * is used, try to compare with this value */ try { - std::string hdr = _fs->getHeaderVal("checkSum"); + std::string hdr = _fs->getHeaderVal("CheckSum"); if (!hdr.empty()) { if (ucode == strtol(hdr.c_str(), NULL, 16)) goto success; From 77784f01127b865e98dee3864c2c52f2848ec4a7 Mon Sep 17 00:00:00 2001 From: Till Harbaum Date: Mon, 27 Apr 2026 15:06:21 +0200 Subject: [PATCH 02/25] Efinix: Check for presence of mode header value before checking it --- src/efinix.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/efinix.cpp b/src/efinix.cpp index 899f099..6884a67 100644 --- a/src/efinix.cpp +++ b/src/efinix.cpp @@ -218,10 +218,13 @@ void Efinix::program(unsigned int offset, bool unprotect_flash) throw std::runtime_error("device mismatch: " + device + " != " + target); } } - std::string mode = bit->getHeaderVal("mode"); - if (mode.find("passive") != std::string::npos) { - delete bit; - throw std::runtime_error("passive mode not supported for flash"); + std::map hdr = bit->getHeader(); + if(hdr.find("mode") != hdr.end()) { + std::string mode = bit->getHeaderVal("mode"); + if (mode.find("passive") != std::string::npos) { + delete bit; + throw std::runtime_error("passive mode not supported for flash"); + } } } catch (std::runtime_error& e) { throw; From 52148a457a3f63b4f78670fa2b787ee130f2c163 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 30 Apr 2026 14:04:26 +0200 Subject: [PATCH 03/25] spiOverJtag/xilinx_spiOverJtag.tcl: removed, no more used --- spiOverJtag/xilinx_spiOverJtag.tcl | 82 ------------------------------ 1 file changed, 82 deletions(-) delete mode 100644 spiOverJtag/xilinx_spiOverJtag.tcl diff --git a/spiOverJtag/xilinx_spiOverJtag.tcl b/spiOverJtag/xilinx_spiOverJtag.tcl deleted file mode 100644 index 493feea..0000000 --- a/spiOverJtag/xilinx_spiOverJtag.tcl +++ /dev/null @@ -1,82 +0,0 @@ -set parts [lindex $argv 0] - -set project_name "spiOverJtag" - -set build_path tmp_${parts} -file delete -force $build_path - -# Project creation -set grade [dict create \ - xc7a15tfgg484 -1 \ - xc7a25tcpg238 -1 \ - xc7a25tcsg325 -1 \ - xc7a35tcpg236 -1 \ - xc7a35tcsg324 -1 \ - xc7a35tftg256 -1 \ - xc7a50tcpg236 -2 \ - xc7a50tcsg325 -2 \ - xc7a75tfgg484 -2 \ - xc7a100tfgg484 -2 \ - xc7a200tsbg484 -1 \ - xc7k70tfbg676 -1 \ - xc7k160tffg676 -1 \ - xc7k325tffg676 -1 \ - xc7k325tffg900 -2 \ - xc7s25csga225 -1 \ - xc7s50csga324 -1 \ - ] - -set pkg_name [dict create \ - xc7a25tcpg238 xc7a_cpg238 \ - xc7a25tcsg325 xc7a_csg325 \ - xc7a35tcpg236 xc7a_cpg236 \ - xc7a35tcsg324 xc7a_csg324 \ - xc7a35tftg256 xc7a_ftg256 \ - xc7a50tcpg236 xc7a_cpg236 \ - xc7a50tcsg325 xc7a_csg325 \ - xc7a15tfgg484 xc7a_fgg484 \ - xc7a75tfgg484 xc7a_fgg484 \ - xc7a100tfgg484 xc7a_fgg484 \ - xc7a200tsbg484 xc7a_sbg484 \ - xc7a200tfbg484 xc7a_fbg484 \ - xc7k70tfbg676 xc7k_fbg676 \ - xc7k160tffg676 xc7k_ffg676 \ - xc7k325tffg676 xc7k_ffg676 \ - xc7k325tffg900 xc7k_ffg900 \ - xc7s25csga225 xc7s_csga225 \ - xc7s50csga324 xc7s_csga324 \ - ] - -set curr_grade [dict get $grade $parts] -set curr_pins [dict get $pkg_name $parts] - -create_project $project_name $build_path -part ${parts}${curr_grade} - -add_files -norecurse xilinx_spiOverJtag.vhd -add_files -norecurse -fileset constrs_1 constr_${curr_pins}.xdc - -set_property VERILOG_DEFINE {TOOL_VIVADO} [current_fileset] - -# set the current synth run -current_run -synthesis [get_runs synth_1] -reset_run synth_1 - -set obj [get_runs impl_1] -set_property AUTO_INCREMENTAL_CHECKPOINT 1 [get_runs impl_1] - -set_property "needs_refresh" "1" $obj - -# set the current impl run -current_run -implementation [get_runs impl_1] - -puts "INFO: Project created: $project_name" - -launch_runs synth_1 -jobs 4 -wait_on_run synth_1 -## do implementation -launch_runs impl_1 -jobs 4 -wait_on_run impl_1 -## make bit file -launch_runs impl_1 -jobs 4 -to_step write_bitstream -wait_on_run impl_1 -exit From b456954c2a5bb79c10190433c4c87da3bb420625 Mon Sep 17 00:00:00 2001 From: Cayetano Santos Date: Thu, 30 Apr 2026 14:22:53 +0200 Subject: [PATCH 04/25] cable.cpp: Add digilent_ft4232. --- src/cable.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cable.hpp b/src/cable.hpp index 84c555a..a9b79c9 100644 --- a/src/cable.hpp +++ b/src/cable.hpp @@ -99,6 +99,7 @@ static std::map cable_list = { {"dfu", CABLE_DEF(MODE_DFU, 0, 0 )}, {"digilent", FTDI_SER(0x0403, 0x6010, FTDI_INTF_A, 0xe8, 0xeb, 0x00, 0x60)}, {"digilent_b", FTDI_SER(0x0403, 0x6010, FTDI_INTF_B, 0xe8, 0xeb, 0x00, 0x60)}, + {"digilent_ft4232", FTDI_SER(0x0403, 0x6011, FTDI_INTF_A, 0x08, 0x2B, 0x08, 0x0B)}, {"digilent_hs2", FTDI_SER(0x0403, 0x6014, FTDI_INTF_A, 0xe8, 0xeb, 0x00, 0x60)}, {"digilent_hs3", FTDI_SER(0x0403, 0x6014, FTDI_INTF_A, 0x88, 0x8B, 0x20, 0x30)}, {"digilent_ad", FTDI_SER(0x0403, 0x6014, FTDI_INTF_A, 0x08, 0x0B, 0x80, 0x80)}, From 0e1cfe108bf1ef268613cce713c1ae2fbe9145ec Mon Sep 17 00:00:00 2001 From: Henry Gomersall Date: Thu, 30 Apr 2026 16:16:52 +0100 Subject: [PATCH 05/25] xilinx: fix to Xilinx::program so a reset is not performed in SPI_MODE when _skip_reset is set --- src/xilinx.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xilinx.cpp b/src/xilinx.cpp index 78573bb..133b799 100644 --- a/src/xilinx.cpp +++ b/src/xilinx.cpp @@ -659,7 +659,6 @@ void Xilinx::program(unsigned int offset, bool unprotect_flash) /* Check for BPI flash boards */ if (_is_bpi_board) { program_bpi(bit, offset); - reset(); } else { if (_flash_chips & PRIMARY_FLASH) { select_flash_chip(PRIMARY_FLASH); @@ -669,8 +668,6 @@ void Xilinx::program(unsigned int offset, bool unprotect_flash) select_flash_chip(SECONDARY_FLASH); program_spi(secondary_bit, _secondary_file_extension, offset, unprotect_flash); } - - reset(); } } else { if (_fpga_family == SPARTAN3_FAMILY) @@ -810,6 +807,9 @@ void Xilinx::program_bpi(ConfigBitstreamParser *bit, unsigned int offset) throw std::runtime_error("BPI flash programming failed"); } + /* Reset the board if skip_reset is not set */ + post_flash_access(); + printInfo("BPI flash programming complete"); } From d01f0f1063c8ba0e8f3e925eac0561bf5385c490 Mon Sep 17 00:00:00 2001 From: Denis Bodor Date: Sat, 2 May 2026 12:01:18 +0200 Subject: [PATCH 06/25] fix BPI flash selection (on ypcb003381p1 board) --- src/xilinx.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/xilinx.cpp b/src/xilinx.cpp index 78573bb..0a79730 100644 --- a/src/xilinx.cpp +++ b/src/xilinx.cpp @@ -1277,6 +1277,18 @@ bool Xilinx::dumpFlash(uint32_t base_addr, uint32_t len) bool Xilinx::detect_flash() { + if (_is_bpi_board) { + if (!_bpi_flash) { + if (!load_bpi_bridge()) + return false; + } + if (!_bpi_flash->detect()) { + printError("BPI flash detection failed"); + return false; + } + return post_flash_access(); + } + if (_flash_chips & PRIMARY_FLASH) { select_flash_chip(PRIMARY_FLASH); if (!FlashInterface::detect_flash()) From 631a850d7012466af68432642de2b523949106cf Mon Sep 17 00:00:00 2001 From: Cayetano Santos Date: Thu, 30 Apr 2026 14:23:16 +0200 Subject: [PATCH 07/25] board.hpp: Add sp701. --- doc/boards.yml | 7 +++++++ src/board.hpp | 1 + 2 files changed, 8 insertions(+) diff --git a/doc/boards.yml b/doc/boards.yml index a7185c4..11252bd 100644 --- a/doc/boards.yml +++ b/doc/boards.yml @@ -751,6 +751,13 @@ Memory: OK Flash: NT +- ID: sp701 + Description: Xilinx SP701 + URL: https://www.amd.com/en/products/adaptive-socs-and-fpgas/evaluation-boards/sp701.html + FPGA: Spartan 7 xc7s100-2-fgga676 + Memory: OK + Flash: OK + - ID: spartanEdgeAccelBoard Description: SeeedStudio Spartan Edge Accelerator Board URL: http://wiki.seeedstudio.com/Spartan-Edge-Accelerator-Board diff --git a/src/board.hpp b/src/board.hpp index 57efc97..437e79f 100644 --- a/src/board.hpp +++ b/src/board.hpp @@ -236,6 +236,7 @@ static std::map board_list = { JTAG_BOARD("spartanEdgeAccelBoard", "", "", SPI_FLASH, 0, 0, CABLE_DEFAULT), JTAG_BOARD("spec45", "xc6slx45tfgg484", "", SPI_FLASH, 0, 0, CABLE_DEFAULT), JTAG_BOARD("spec150", "xc6slx150tfgg484", "", SPI_FLASH, 0, 0, CABLE_DEFAULT), + JTAG_BOARD("sp701", "xc7s100fgga676", "digilent_ft4232", SPI_FLASH, 0, 0, CABLE_DEFAULT), JTAG_BOARD("stlv7325", "xc7k325tffg676", "ft4232", SPI_FLASH, 0, 0, CABLE_MHZ(3)), JTAG_BOARD("tangconsole", "", "ft2232", SPI_FLASH, 0, 0, CABLE_DEFAULT), JTAG_BOARD("tangnano", "", "ch552_jtag", SPI_FLASH, 0, 0, CABLE_DEFAULT), From 5d69478d0b97bc1e3099ebccc0d75bf25452b054 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Tue, 5 May 2026 10:56:30 +0200 Subject: [PATCH 08/25] usbBlaster: fix #ifdef ENABLE_USB_BLASTERII location at CTOR level --- src/usbBlaster.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usbBlaster.cpp b/src/usbBlaster.cpp index 245ac53..2adaf06 100644 --- a/src/usbBlaster.cpp +++ b/src/usbBlaster.cpp @@ -49,8 +49,8 @@ UsbBlaster::UsbBlaster(const cable_t &cable, const std::string &firmware_path, #else throw std::runtime_error("usb-blasterI: Not build"); #endif -#ifdef ENABLE_USB_BLASTERII else if (cable.pid == 0x6810 || cable.pid == 0x6010) +#ifdef ENABLE_USB_BLASTERII ll_driver = new UsbBlasterII(firmware_path); #else throw std::runtime_error("usb-blasterII: Not build"); From bd626184cdf52fbf17e8e6c397dcebdb5e6af75d Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Tue, 5 May 2026 14:26:39 +0200 Subject: [PATCH 09/25] CI: added missing hidapi library for macos --- .github/workflows/Test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 6ccf230..1e6c304 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -117,7 +117,7 @@ jobs: - name: '⚙️ Install dependencies' run: | #brew update - brew install --only-dependencies openFPGALoader + brew install --only-dependencies openFPGALoader hidapi - name: '🚧 Build tarball' run: | @@ -214,4 +214,4 @@ jobs: with: token: ${{ github.token }} tag: 'nightly' - files: artifact/**/* \ No newline at end of file + files: artifact/**/* From 6ae42cf38db5e4eaed734fd0fc051404996fda9e Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Tue, 5 May 2026 14:30:27 +0200 Subject: [PATCH 10/25] CI: fix previous commit --- .github/workflows/Test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 1e6c304..5c93959 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -117,7 +117,8 @@ jobs: - name: '⚙️ Install dependencies' run: | #brew update - brew install --only-dependencies openFPGALoader hidapi + brew install --only-dependencies openFPGALoader + brew install hidapi - name: '🚧 Build tarball' run: | From 676f59416a358278fd91a53dff583988d9858c0b Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Tue, 5 May 2026 14:39:24 +0200 Subject: [PATCH 11/25] CI/msys2/PKGBUILD: added missing hidapi dependency --- scripts/msys2/PKGBUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/msys2/PKGBUILD b/scripts/msys2/PKGBUILD index 51376e9..98de171 100644 --- a/scripts/msys2/PKGBUILD +++ b/scripts/msys2/PKGBUILD @@ -9,6 +9,7 @@ url="https://github.com/trabucayre/openFPGALoader" license=('Apache-2.0') depends=( "${MINGW_PACKAGE_PREFIX}-libftdi" + "${MINGW_PACKAGE_PREFIX}-hidapi" "${MINGW_PACKAGE_PREFIX}-zlib" ) makedepends=( From ed6025645f506c8941ab245f142db701b9f9bf2a Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Wed, 6 May 2026 18:12:24 +0200 Subject: [PATCH 12/25] CMakeLists.txt: uses regex to remove potential rcX in LIBFTDI_VERSION_MINOR --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 442327f..2bf20e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,7 @@ if (USE_PKGCONFIG) string(REPLACE "." ";" VERSION_LIST ${LIBFTDI_VERSION}) list(GET VERSION_LIST 0 LIBFTDI_VERSION_MAJOR) list(GET VERSION_LIST 1 LIBFTDI_VERSION_MINOR) + string(REGEX REPLACE "[^0-9].*" "" LIBFTDI_VERSION_MINOR "${LIBFTDI_VERSION_MINOR}") endif() else() set(LIBFTDI_LIBRARY_DIRS "") From 4541b937955888be79fde1b2da0104d260a440c5 Mon Sep 17 00:00:00 2001 From: Craig Haywood Date: Wed, 13 May 2026 16:21:38 +1000 Subject: [PATCH 13/25] board.hpp: Add brs-100-gw1nr9 --- doc/boards.yml | 7 +++++++ doc/vendors/gowin.rst | 5 ++++- src/board.hpp | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/boards.yml b/doc/boards.yml index 11252bd..cab7b33 100644 --- a/doc/boards.yml +++ b/doc/boards.yml @@ -862,6 +862,13 @@ Memory: OK Flash: IF +- ID: brs-100-gw1nr9 + Description: BrisbaneSilicon BRS-100-GW1NR9 + URL: https://brisbanesilicon.com.au/brs-100-gw1nr9/ + FPGA: littleBee GW1NR-9C + Memory: OK + Flash: IF/EF + - ID: trion_t20_bga256_jtag Description: Efinix Trion T20 BGA256 Dev Kit URL: https://www.efinixinc.com/products-devkits-triont20.html diff --git a/doc/vendors/gowin.rst b/doc/vendors/gowin.rst index 103c3e3..4258156 100644 --- a/doc/vendors/gowin.rst +++ b/doc/vendors/gowin.rst @@ -13,6 +13,7 @@ GOWIN GW1N * Sipeed Tang Nano 4K * Honeycomb * RUNBER + * BrisbaneSilicon BRS-100-GW1NR9 ``.fs`` file is the default format generated by *Gowin IDE*, so nothing special must be done to generates this file. @@ -37,12 +38,13 @@ where ``BOARD_NAME`` is: * ``tangnano20k`` * ``tangprimer20k`` * ``runber`` +* ``brs-100-gw1nr9`` Flash ----- .. ATTENTION:: - Only with Trenz TEC0117 and runber. + Only with Trenz TEC0117, runber and brs-100-gw1nr9. with ``-f``, file load: @@ -54,6 +56,7 @@ where ``BOARD_NAME`` is: * ``tec0117`` * ``runber`` +* ``brs-100-gw1nr9`` It's possible to flash external SPI Flash (connected to MSPI) in bscan mode by using ``--external-flash`` instead of ``-f``. diff --git a/src/board.hpp b/src/board.hpp index 437e79f..ef05c6e 100644 --- a/src/board.hpp +++ b/src/board.hpp @@ -250,6 +250,7 @@ static std::map board_list = { JTAG_BOARD("te0712_8", "xc7a200tfbg484", "", SPI_FLASH, 0, 0, CABLE_MHZ(15)), JTAG_BOARD("tec0117", "", "ft2232", SPI_FLASH, 0, 0, CABLE_DEFAULT), JTAG_BOARD("tec0330", "xc7vx330tffg1157", "", SPI_FLASH, 0, 0, CABLE_MHZ(15)), + JTAG_BOARD("brs-100-gw1nr9", "gw1nr-lv9qn88pc7/i6", "ft2232", SPI_FLASH, 0, 0, CABLE_DEFAULT), SPI_BOARD("titanium_ti60_f225","efinix", "titanium", "efinix_spi_ft4232", DBUS4, DBUS5, DBUS7, DBUS3, DBUS0, DBUS1, DBUS2, DBUS6, 0, CABLE_DEFAULT), JTAG_BOARD("titanium_ti60_f225_jtag", "ti60f225","efinix_jtag_ft4232", SPI_FLASH, 0, 0, CABLE_DEFAULT), From ef92800ac3edf637cfec4ae29ff133e110e0c3ba Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Wed, 13 May 2026 16:25:54 +0200 Subject: [PATCH 14/25] fx2_ll: check load_firmware return code. Added some progressbar. Allows caller to set the alternate setting for an interface. Expose windex in write_ctrl/read_ctrl --- src/fx2_ll.cpp | 42 +++++++++++++++++++++++++++++++++--------- src/fx2_ll.hpp | 16 +++++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/fx2_ll.cpp b/src/fx2_ll.cpp index f937040..afd911e 100644 --- a/src/fx2_ll.cpp +++ b/src/fx2_ll.cpp @@ -14,7 +14,7 @@ #include "display.hpp" #include "fx2_ll.hpp" #include "ihexParser.hpp" - +#include "progressBar.hpp" #define FX2_FIRM_LOAD 0xA0 #define FX2_GCR_CPUCS 0xe600 @@ -42,27 +42,36 @@ FX2_ll::FX2_ll(uint16_t uninit_vid, uint16_t uninit_pid, throw std::runtime_error("claim interface failed"); } /* load firmware */ - load_firmware(firmware_path); + if (!load_firmware(firmware_path)) { + libusb_close(dev_handle); + libusb_exit(usb_ctx); + throw std::runtime_error("fx2 firmware load failed"); + } close(); reenum = true; } } /* try to open an already init device - * since fx2 may be not immediately ready + * since fx2 may be not immediatly ready * retry with a delay */ + ProgressBar progress("Waiting reload", 100, 50, false); int timeout = 100; do { dev_handle = libusb_open_device_with_vid_pid(usb_ctx, vid, pid); timeout--; + progress.display(100-timeout); if (!dev_handle) sleep(1); } while (!dev_handle && timeout > 0 && reenum); - if (!dev_handle) + if (!dev_handle) { + progress.fail(); throw std::runtime_error("FX2: fail to open device"); + } + progress.done(); ret = libusb_claim_interface(dev_handle, 0); if (ret) { @@ -72,6 +81,18 @@ FX2_ll::FX2_ll(uint16_t uninit_vid, uint16_t uninit_pid, } } +bool FX2_ll::set_interface_alt_setting(const int if_num, const int alt_setting) +{ + const int ret = libusb_set_interface_alt_setting(dev_handle, if_num, + alt_setting); + if (ret && ret != LIBUSB_ERROR_NOT_FOUND) { + printWarn("unable to select alternate interface 1: " + + std::string(libusb_error_name(ret))); + return false; + } + return true; +} + /* destructor: close current device and * destroy context */ @@ -134,11 +155,11 @@ int FX2_ll::read(uint8_t endpoint, uint8_t *buff, uint16_t len) /* write len data using control */ int FX2_ll::write_ctrl(uint8_t bRequest, uint16_t wValue, - uint8_t *buff, uint16_t len) + uint8_t *buff, uint16_t len, uint16_t wIndex) { int ret = libusb_control_transfer(dev_handle, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, - bRequest, wValue, 0x0000, buff, len, 100); + bRequest, wValue, wIndex, buff, len, 100); if (ret < 0) { printError("Unable to send control request: " + std::string(libusb_error_name(ret))); @@ -150,11 +171,11 @@ int FX2_ll::write_ctrl(uint8_t bRequest, uint16_t wValue, /* read len data using control */ int FX2_ll::read_ctrl(uint8_t bRequest, uint16_t wValue, - uint8_t *buff, uint16_t len) + uint8_t *buff, uint16_t len, uint16_t wIndex) { int ret = libusb_control_transfer(dev_handle, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_IN, - bRequest, wValue, 0x0000, buff, len, 100); + bRequest, wValue, wIndex, buff, len, 100); if (ret < 0) { printError("Unable to read control request: " + std::string(libusb_error_name(ret))); @@ -177,6 +198,7 @@ bool FX2_ll::load_firmware(std::string firmware_path) return false; /* load */ std::vector data = ihex.getDataArray(); + ProgressBar progress("Loading firmware", data.size(), 50, false); for (size_t i = 0; i < data.size(); i++) { IhexParser::data_line_t data_line = data[i]; @@ -186,14 +208,16 @@ bool FX2_ll::load_firmware(std::string firmware_path) while (toSend > 0) { uint16_t xfer_len = (toSend > 64) ? 64: toSend; if (!write_ctrl(FX2_FIRM_LOAD, addr, tx_buff, xfer_len)) { - printError("load firmware failed\n"); + progress.fail(); return false; } toSend -= xfer_len; tx_buff += xfer_len; addr += xfer_len; } + progress.display(i); } + progress.done(); /* unset reset */ if (!reset(0)) diff --git a/src/fx2_ll.hpp b/src/fx2_ll.hpp index 37eb1af..8040530 100644 --- a/src/fx2_ll.hpp +++ b/src/fx2_ll.hpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 /* - * Copyright (c) 2021 Gwenhael Goavec-Merou + * Copyright (c) 2021-2026 Gwenhael Goavec-Merou */ #ifndef SRC_FX2_LL_HPP_ @@ -31,6 +31,14 @@ class FX2_ll { uint16_t vid, uint16_t pid, const std::string &firmware_path); ~FX2_ll(); + /*! + * \brief libusb_set_interface_alt_setting wrapper + * \param[in] if_num: Interface number + * \param[in] alt_setting: alternate setting + * \return true on success + */ + bool set_interface_alt_setting(const int if_num, const int alt_setting); + /*! * \brief bulk write * \param[in] endpoint: endpoint to use @@ -53,20 +61,22 @@ class FX2_ll { * \param[in] wValue * \param[in] buff: buffer to write * \param[in] len: buffer length + * \param[in] wIndex: the index field for the setup packet * \return false when transfer fails, true otherwise */ int write_ctrl(uint8_t bRequest, uint16_t wValue, - uint8_t *buff, uint16_t len); + uint8_t *buff, uint16_t len, uint16_t wIndex=0x0000); /*! * \brief control write * \param[in] bRequest * \param[in] wValue * \param[in] buff: buffer to write * \param[in] len: buffer length + * \param[in] wIndex: the index field for the setup packet * \return false when transfer fails, true otherwise */ int read_ctrl(uint8_t bRequest, uint16_t wValue, - uint8_t *buff, uint16_t len); + uint8_t *buff, uint16_t len, uint16_t wIndex=0x0000); private: /*! From 698c8daaf480de50adc4d7ad3a94b45f93f8bf55 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Wed, 13 May 2026 16:26:31 +0200 Subject: [PATCH 15/25] usbBlaster: added explicit windex when read_ctrl is used --- src/usbBlaster.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usbBlaster.cpp b/src/usbBlaster.cpp index 2adaf06..97eb284 100644 --- a/src/usbBlaster.cpp +++ b/src/usbBlaster.cpp @@ -459,7 +459,7 @@ UsbBlasterII::UsbBlasterII(const std::string &firmware_path) else fpath = firmware_path; fx2 = new FX2_ll(0x09fb, 0x6810, 0x09fb, 0x6010, fpath); - if (!fx2->read_ctrl(0x94, 0, buf, 5)) { + if (!fx2->read_ctrl(0x94, 0, buf, 5, 0x0000)) { throw std::runtime_error("Unable to read firmware version."); } printInfo("USB-Blaster II firmware version: " + std::string((char *)buf)); From db92a5ac24ee78aa1e3f982638b2bde41ea902f3 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Wed, 13 May 2026 16:27:29 +0200 Subject: [PATCH 16/25] xilinxPlatformCableUSB: driver for Xilinx Platform Cable USB probe --- CMakeLists.txt | 31 ++- doc/cable.yml | 7 + src/cable.hpp | 2 + src/jtag.cpp | 12 ++ src/xilinxPlatformCableUSB.cpp | 378 +++++++++++++++++++++++++++++++++ src/xilinxPlatformCableUSB.hpp | 108 ++++++++++ 6 files changed, 531 insertions(+), 7 deletions(-) create mode 100644 src/xilinxPlatformCableUSB.cpp create mode 100644 src/xilinxPlatformCableUSB.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bf20e1..40a4ebd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ option(ENABLE_USB_BLASTERII "enable Altera USB Blaster II support" ${ set(BLASTERII_PATH "" CACHE STRING "usbBlasterII firmware directory") set(ISE_PATH "/opt/Xilinx/14.7" CACHE STRING "ise root directory (default: /opt/Xilinx/14.7)") +set(VIVADO_PATH "" CACHE STRING "Vivado root directory (something like /opt/Xilinx/Vivado/VERSION)") # Libgpiod is only available on Linux OS. if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") @@ -64,6 +65,8 @@ else() set(ENABLE_LIBGPIOD OFF) endif() +option(ENABLE_XILINX_PLATFORM_CABLE_USB "enable Xilinx Platform Cable USB (XPCU) support" ${ENABLE_CABLE_ALL}) + # XVC and RemoteBitbang are not available on Windows OS. if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Windows") option(ENABLE_REMOTEBITBANG "enable remote bitbang driver" ${ENABLE_CABLE_ALL}) @@ -102,6 +105,11 @@ set(USE_LIBFTDI OFF) set(USE_LIBUSB OFF) set(USE_LIBUSB_LL OFF) +# USB BlasterII and Xilinx Platform Cable USB requires FX2 driver +if (ENABLE_XILINX_PLATFORM_CABLE_USB OR ENABLE_USB_BLASTERII) + set(USE_FX2_LL ON) +endif() + # Only adds libftdi as dependency when a cable # need this library. if (ENABLE_FTDI_BASED_CABLE OR ENABLE_USB_BLASTERI OR ENABLE_XILINX_VIRTUAL_CABLE_SERVER) @@ -112,7 +120,7 @@ endif(ENABLE_FTDI_BASED_CABLE OR ENABLE_USB_BLASTERI OR ENABLE_XILINX_VIRTUAL_CA # Only adds libusb as dependency when a cable need this library if (ENABLE_DFU OR ENABLE_ANLOGIC_CABLE OR ENABLE_CH347 OR ENABLE_DIRTYJTAG - OR ENABLE_ESP_USB OR ENABLE_JLINK OR ENABLE_GOWIN_GWU2X OR ENABLE_USB_BLASTERII OR ENABLE_USB_SCAN) + OR ENABLE_ESP_USB OR ENABLE_JLINK OR ENABLE_GOWIN_GWU2X OR USE_FX2_LL OR ENABLE_USB_SCAN) set(USE_LIBUSB ON) endif() @@ -120,11 +128,6 @@ if (ENABLE_USB_SCAN OR ENABLE_GOWIN_GWU2X) set(USE_LIBUSB_LL ON) endif() -# Only enable fx2_ll when cable using it -if (ENABLE_USB_BLASTERII) - set(USE_FX2_LL ON) -endif() - #################################################################################################### # Cross-compilation support #################################################################################################### @@ -179,6 +182,7 @@ include(GNUInstallDirs) add_definitions(-DDATA_DIR=\"${CMAKE_INSTALL_FULL_DATAROOTDIR}\") add_definitions(-DISE_DIR=\"${ISE_PATH}\") +add_definitions(-DVIVADO_DIR=\"${VIVADO_PATH}\") #################################################################################################### # Dependencies check/search @@ -591,6 +595,12 @@ list (APPEND OPENFPGALOADER_HEADERS src/svf_jtag.hpp) add_definitions(-DENABLE_SVF_JTAG) endif() +# Xilinx Platform Cable USB +if (ENABLE_XILINX_PLATFORM_CABLE_USB) + list(APPEND OPENFPGALOADER_SOURCE src/xilinxPlatformCableUSB.cpp) + list(APPEND OPENFPGALOADER_HEADERS src/xilinxPlatformCableUSB.hpp) +endif() + # Xilinx Virtual Cable if (ENABLE_XILINX_VIRTUAL_CABLE_CLIENT) list (APPEND OPENFPGALOADER_SOURCE src/xvc_client.cpp) @@ -773,6 +783,13 @@ else() message("Remote bitbang client support disabled") endif() +if (ENABLE_XILINX_PLATFORM_CABLE_USB) + add_definitions(-DENABLE_XILINX_PLATFORM_CABLE_USB=1) + message("Xilinx Platform Cable USB (XPCU) support enabled") +else() + message("Xilinx Platform Cable USB (XCPU) support disabled") +endif() + if (ENABLE_XILINX_VIRTUAL_CABLE_CLIENT) add_definitions(-DENABLE_XVC_CLIENT=1) set(CMAKE_EXE_LINKER_FLAGS "-pthread ${CMAKE_EXE_LINKER_FLAGS}") @@ -800,6 +817,7 @@ if (ZLIB_FOUND) add_definitions(-DHAS_ZLIB=1) else() message("zlib library not found: can't flash intel/altera devices") + message("or reads compressed bitstreams") endif() if (LINK_CMAKE_THREADS) @@ -814,7 +832,6 @@ if (USE_LIBFTDI) add_definitions(-DFTDI_VERSION=${FTDI_VAL}) endif() - install(TARGETS openFPGALoader DESTINATION bin) #################################################################################################### diff --git a/doc/cable.yml b/doc/cable.yml index f8c8230..81cdd02 100644 --- a/doc/cable.yml +++ b/doc/cable.yml @@ -302,6 +302,13 @@ usb-blasterIII: URL: https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/ug/ug_usb_blstr_ii_cable.pdf +xilinxPlatformCableUsb: + + - Name: Xilinx Platform Cable USB (XPCU) + Description: Xilinx Platform Cable USB (XPCU) from AMD/Xilinx + URL: https://www.amd.com/en/products/adaptive-socs-and-fpgas/board-accessories/hw-usb-ii-g.html + + xvc-client: - Name: Xilinx Virtual Cable diff --git a/src/cable.hpp b/src/cable.hpp index a9b79c9..ab28d74 100644 --- a/src/cable.hpp +++ b/src/cable.hpp @@ -30,6 +30,7 @@ enum communication_type { MODE_CH347, /*! CH347 JTAG mode */ MODE_GWU2X, /*! Gowin GWU2X JTAG mode */ MODE_ESP, /*! esp32c3, esp32s3 */ + MODE_XPCU, /*! Xilinx Platform Cable USB (XPCU) */ }; /*! @@ -137,6 +138,7 @@ static std::map cable_list = { {"usb-blasterII", CABLE_DEF(MODE_USBBLASTER, 0x09Fb, 0x6810 )}, {"usb-blasterII_1", CABLE_DEF(MODE_USBBLASTER, 0x09Fb, 0x6010 )}, {"usb-blasterIII", FTDI_SER(0x09fb, 0x6022, FTDI_INTF_A, 0x08, 0x3B, 0x58, 0xb0)}, + {"xilinxPlatformCableUsb", CABLE_DEF(MODE_XPCU, 0x03fd, 0x0013)}, {"xvc-client", CABLE_DEF(MODE_XVC_CLIENT, 0x0000, 0x0000 )}, #ifdef ENABLE_LIBGPIOD {"libgpiod", CABLE_DEF(MODE_LIBGPIOD_BITBANG, 0, 0x0000 )}, diff --git a/src/jtag.cpp b/src/jtag.cpp index b5ed9a5..d084bd8 100644 --- a/src/jtag.cpp +++ b/src/jtag.cpp @@ -58,6 +58,9 @@ #ifdef ENABLE_XVC_CLIENT #include "xvc_client.hpp" #endif +#ifdef ENABLE_XILINX_PLATFORM_CABLE_USB +#include "xilinxPlatformCableUSB.hpp" +#endif #define DEBUG 0 @@ -206,6 +209,15 @@ Jtag::Jtag(const cable_t &cable, const jtag_pins_conf_t *pin_conf, case MODE_REMOTEBITBANG: _jtag = new RemoteBitbang_client(ip_adr, port, verbose); break; +#endif + case MODE_XPCU: +#ifdef ENABLE_XILINX_PLATFORM_CABLE_USB + _jtag = new XilinxPlatformCableUSB(0x03fd, 0x0013, clkHZ, + firmware_path, verbose); + break; +#else + printError("Jtag: support for Xilinx Platform Cable USB (XPCU) was not enabled at compile time"); + throw std::exception(); #endif default: std::cerr << "Jtag: unknown cable type" << std::endl; diff --git a/src/xilinxPlatformCableUSB.cpp b/src/xilinxPlatformCableUSB.cpp new file mode 100644 index 0000000..4245e90 --- /dev/null +++ b/src/xilinxPlatformCableUSB.cpp @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2022-2026 Gwenhael Goavec-Merou + */ + +#include +#include +#include + +#include +#include +#include + +#include "display.hpp" +#include "fx2_ll.hpp" +#include "xilinxPlatformCableUSB.hpp" + +#define XPCU_BREQUEST 0xB0 +#define XPCU_INITIALIZED_VID 0x03fd +#define XPCU_INITIALIZED_PID 0x0008 +#define XPCU_CMD_DISABLE 0x10 +#define XPCU_CMD_ENABLE 0x18 +#define XPCU_CMD_SET_SPEED 0x28 +#define XPCU_CMD_STATUS 0x38 +#define XPCU_CMD_RETURN_CONSTANT 0x40 +#define XPCU_CMD_GET_VERSION 0x50 +#define XPCU_CMD_GPIO_TRANSFER 0xA6 +#define XPCU_EP_JTAG_OUT 0x02 +#define XPCU_EP_JTAG_IN 0x86 +#define XPCU_STATUS_CONNECTED 0x40 +#define XPCU_SPEED_CLASS_ENABLE 0x10 +#define XPCU_VERSION_CPLD 0x01 +#define XPCU_VERSION_CONST1 0x02 +#define XPCU_VERSION_CONST2 0x03 + +#define TCK_OFFSET 0 +#define TDO_OFFSET 4 +#define TDI_OFFSET 0 +#define TMS_OFFSET 4 + +#define TCK_IDX (1 << TCK_OFFSET) +#define TDO_IDX (1 << TDO_OFFSET) +#define TDI_IDX (1 << TDI_OFFSET) +#define TMS_IDX (1 << TMS_OFFSET) + +XilinxPlatformCableUSB::XilinxPlatformCableUSB(const uint16_t vid, + const uint16_t pid, + uint32_t clkHz, + const std::string &firmware_path, + int8_t verbose): _verbose(verbose), _nb_bit(0), _nb_tdo_bit(0), + _curr_tms(0), _curr_tdi(0), _buffer_size(4096), + _buffer_bit_size((_buffer_size / 2 * 4) - 1) +{ + std::string firmware_file; + /* firmare path must be known: + * 1/ provided by user + * 2/ from Vivado install directory + * 3/ from ISE install directory + */ + if (firmware_path.empty() && strlen(ISE_DIR) == 0 && strlen(VIVADO_DIR) == 0) { + printError("missing FX2 firmware"); + printError("use --probe-firmware with something"); + printError("like /opt/Xilinx/14.7/ISE_DS/ISE/bin/lin64/xusb_xp2.hex for ISE"); + printError("or /opt/Xilinx/Vivado/VERSION/data/xicom/xusb_xp2.hex for Vivado"); + printError("Or use -DISE_DIR=/opt/Xilinx/14.7 / -DVIVADO_DIR=/opt/Xilinx/Vivado/VERSION at build time"); + throw std::runtime_error("xilinxPlatformCableUSB: missing firmware"); + } + + /* Extract firmware according to possibilities */ + if (!firmware_path.empty()) + firmware_file = firmware_path; + else if (strlen(VIVADO_DIR) > 0) + firmware_file = VIVADO_DIR "/data/xicom/xusb_xp2.hex"; + else if (strlen(ISE_DIR) > 0) + firmware_file = ISE_DIR "/ISE_DS/ISE/bin/lin64/xusb_xp2.hex"; + + try { + fx2 = std::make_unique(vid, pid, XPCU_INITIALIZED_VID, + XPCU_INITIALIZED_PID, firmware_file); + } catch (std::exception &e) { + printError(e.what()); + throw std::runtime_error("lowlevel init failed"); + } + + fx2->set_interface_alt_setting(0, 1); + + displayCableVersion(); + + /* Write GPIO bit */ + fx2->write_ctrl(XPCU_BREQUEST, 0x030, nullptr, 0, (1 << 3)); + + if (!enableDevice(true)) + throw std::runtime_error("Unable to enable device"); + + uint8_t buf[1]; + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_STATUS, buf, 1)) + throw std::runtime_error("Unable to read status."); + + char mess[64]; + snprintf(mess, sizeof(mess), "status %02x connected: %s", + buf[0], (buf[0] & XPCU_STATUS_CONNECTED) ? "yes" : "no"); + printInfo(mess); + + _in_buf = std::make_unique(_buffer_size); + + setClkFreq(clkHz); +} + +XilinxPlatformCableUSB::~XilinxPlatformCableUSB() +{ + flush(); + enableDevice(false); +} + +int XilinxPlatformCableUSB::setClkFreq(uint32_t clkHz) +{ + /* speed table: index Hz; bit 4 must always be set in the speed class */ + static constexpr uint32_t speeds[] = {12000000, 6000000, 3000000, 1500000, 750000}; + uint8_t speed = 4; /* default: slowest */ + for (uint8_t i = 0; i < sizeof(speeds) / sizeof(speeds[0]); i++) { + if (speeds[i] <= clkHz) { + speed = i; + break; + } + } + if (!fx2->write_ctrl(XPCU_BREQUEST, XPCU_CMD_SET_SPEED, nullptr, 0, + speed | XPCU_SPEED_CLASS_ENABLE)) { + printError("setClkFreq: failed to set speed"); + return -1; + } + _clkHZ = speeds[speed]; + printInfo("Jtag frequency : requested " + std::to_string(clkHz) + + " Hz -> real " + std::to_string(_clkHZ) + " Hz"); + return _clkHZ; +} + +int XilinxPlatformCableUSB::writeTMS(const uint8_t *tms, uint32_t len, + bool flush_buffer, const uint8_t tdi) +{ + int ret; + + if (len == 0) + return flush_buffer ? flush() : 0; + + _curr_tdi = tdi ? 1 : 0; + + for (uint32_t i = 0; i < len; i++) { + _curr_tms = (tms[i >> 3] >> (i & 0x07)) & 0x01; + if (storeBit(_curr_tdi, _curr_tms, 1, 0)) { + if (write(nullptr, 0) < 0) + return -1; + } + } + + if (flush_buffer) { + ret = flush(); + if (ret < 0) + return ret; + } + + return len; +} + +int XilinxPlatformCableUSB::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, bool end) +{ + if (len == 0) + return 0; + + if (rx && _nb_bit != 0) { + if (write(nullptr, 0) < 0) + return -1; + } + + uint32_t rx_offset = 0; + + for (uint32_t i = 0; i < len; i++) { + bool last_bit = (i == len - 1 && end); + _curr_tdi = tx ? (0x01 & ((tx[i >> 3]) >> (i & 0x07))) : 0; + + if (last_bit) + _curr_tms = 1; + + if (storeBit(_curr_tdi, _curr_tms, 1, rx ? 1 : 0)) { + uint32_t bits = _nb_tdo_bit; + if (write(rx, rx_offset) < 0) + return -1; + rx_offset += bits; + } + } + + if (_nb_bit != 0 && (end || rx)) { + if (write(rx, rx_offset) < 0) + return -1; + } + + return len; +} + +int XilinxPlatformCableUSB::toggleClk([[maybe_unused]] uint8_t tms, + [[maybe_unused]] uint8_t tdi, uint32_t clk_len) +{ + for (uint32_t i = 0; i < clk_len; i++) { + if (storeBit(_curr_tdi, _curr_tms, 1, 0)) { + if (write(nullptr, 0) < 0) + return -1; + } + } + + /* Flush buffer if not empty */ + if (_nb_bit != 0) { + if (flush() < 0) + return -1; + } + + return clk_len; +} + +int XilinxPlatformCableUSB::flush() +{ + return write(nullptr, 0); +} + +/* TMS 1st nibble, TDI 2nd nibble, TDO 3rd nibble, TCK 4th nibble + * byte n byte n+1 + * [7:4 3:0] [7:4 3:0] + * TMS TDI TDO TCK + */ +bool XilinxPlatformCableUSB::storeBit(uint8_t tdi, uint8_t tms, + uint8_t tck, uint8_t tdo) noexcept +{ + const uint32_t buf_pos = (_nb_bit >> 2) << 1; + const uint8_t bit_pos = _nb_bit & 0x03; + + if (bit_pos == 0) + _in_buf[buf_pos] = _in_buf[buf_pos + 1] = 0; + + if (tms) + _in_buf[buf_pos] |= (TMS_IDX << bit_pos); + if (tdi) + _in_buf[buf_pos] |= (TDI_IDX << bit_pos); + if (tdo) + _in_buf[buf_pos + 1] |= (TDO_IDX << bit_pos); + if (tck) + _in_buf[buf_pos + 1] |= (TCK_IDX << bit_pos); + + _nb_bit++; + if (tdo) + _nb_tdo_bit++; + + return _nb_bit >= _buffer_bit_size; +} + +/* Compute how many bytes EP6 will return for nb_bit TDO bits. + * The device uses a shift-register encoding: a 16-bit register that grows + * to 32-bit after 16 bits, then a new register starts every 32 bits. + */ +uint32_t XilinxPlatformCableUSB::rxBufSize(uint32_t nb_bit) noexcept +{ + const uint32_t full_groups = nb_bit / 32; + const uint32_t rem = nb_bit & 31u; + return full_groups * 4 + (rem == 0 ? 0 : (rem > 16 ? 4 : 2)); +} + +int XilinxPlatformCableUSB::write(uint8_t *rx, uint32_t rx_offset) +{ + if (_nb_bit == 0) + return 0; + + /* N ops: N/4 pairs of 2 bytes each (round up to complete pair) */ + uint32_t xfer_tx = (_nb_bit >> 1) & ~0x01u; + xfer_tx += ((_nb_bit & 0x03) != 0) ? 2 : 0; + + /* count is 0-indexed per protocol spec */ + if (!fx2->write_ctrl(XPCU_BREQUEST, XPCU_CMD_GPIO_TRANSFER, nullptr, 0, + _nb_bit - 1)) { + printError("Fails to write GPIO transfer control message"); + return -1; + } + + if (fx2->write(XPCU_EP_JTAG_OUT, _in_buf.get(), xfer_tx) != (int)xfer_tx) + return -1; + + if (rx) { + if (_nb_tdo_bit != _nb_bit) { + printError("Unable to decode mixed TDO/non-TDO transfer"); + return -1; + } + + uint32_t xfer_rx = rxBufSize(_nb_tdo_bit); + std::vector rx_buf(xfer_rx); + if (fx2->read(XPCU_EP_JTAG_IN, rx_buf.data(), xfer_rx) != (int)xfer_rx) + return -1; + + /* Decode shift-register encoded TDO bits into rx. + * Each group of up to 32 bits occupies a 16 or 32-bit little-endian + * shift register: bit_k is at position (reg_size - group + k). + */ + uint32_t buf_off = 0; + uint32_t remaining = _nb_tdo_bit; + uint32_t bit_idx = 0; + while (remaining > 0) { + const uint32_t group = (remaining > 32) ? 32 : remaining; + const uint32_t reg_size = (group > 16) ? 32 : 16; + const uint32_t shift = reg_size - group; + uint32_t reg = 0; + for (uint32_t b = 0; b < reg_size / 8; b++) + reg |= (uint32_t)rx_buf[buf_off + b] << (b * 8); + const uint32_t base = rx_offset + bit_idx; + if ((base & 7u) == 0 && (group & 7u) == 0) { + uint8_t *out = rx + (base >> 3); + for (uint32_t b = 0; b < group / 8; b++) + out[b] = static_cast((reg >> (shift + b * 8)) & 0xFF); + } else { + for (uint32_t k = 0; k < group; k++) { + const uint32_t out_bit = base + k; + if ((reg >> (shift + k)) & 1) + rx[out_bit >> 3] |= (1 << (out_bit & 7)); + else + rx[out_bit >> 3] &= ~(1 << (out_bit & 7)); + } + } + buf_off += reg_size / 8; + bit_idx += group; + remaining -= group; + } + } + + _nb_bit = 0; + _nb_tdo_bit = 0; + return 0; +} + +bool XilinxPlatformCableUSB::enableDevice(bool enable) +{ + if (!fx2->write_ctrl(XPCU_BREQUEST, + (enable ? XPCU_CMD_ENABLE : XPCU_CMD_DISABLE), nullptr, 0)) { + char mess[64]; + snprintf(mess, sizeof(mess), "Unable to %s device", + (enable ? "enable" : "disable")); + printError(mess); + return false; + } + return true; +} + +void XilinxPlatformCableUSB::displayCableVersion() +{ + uint8_t buf[2]; + + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_RETURN_CONSTANT, buf, 2)) + throw std::runtime_error("Unable to read constant."); + const uint16_t const0 = ((uint16_t)buf[0] << 8) | buf[1]; + + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_GET_VERSION, buf, 2)) + throw std::runtime_error("Unable to read firmware version."); + const uint16_t fx2_firmware = ((uint16_t)buf[0] << 8) | buf[1]; + + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_GET_VERSION, buf, 2, + XPCU_VERSION_CPLD)) + throw std::runtime_error("Unable to read CPLD version."); + const uint16_t cpld_firmware = ((uint16_t)buf[0] << 8) | buf[1]; + + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_GET_VERSION, buf, 2, + XPCU_VERSION_CONST1)) + throw std::runtime_error("Unable to read const 1."); + const uint16_t const1 = ((uint16_t)buf[0] << 8) | buf[1]; + + if (!fx2->read_ctrl(XPCU_BREQUEST, XPCU_CMD_GET_VERSION, buf, 2, + XPCU_VERSION_CONST2)) + throw std::runtime_error("Unable to read const 2."); + const uint16_t const2 = ((uint16_t)buf[0] << 8) | buf[1]; + + printf("FX2 version: %04x\n", fx2_firmware); + printf("CPLD version: %04x\n", cpld_firmware); + printf("Const0 version: %04x\n", const0); + printf("Const1 version: %04x\n", const1); + printf("Const2 version: %04x\n", const2); +} diff --git a/src/xilinxPlatformCableUSB.hpp b/src/xilinxPlatformCableUSB.hpp new file mode 100644 index 0000000..0ad44a5 --- /dev/null +++ b/src/xilinxPlatformCableUSB.hpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2022-2026 Gwenhael Goavec-Merou + */ + +#ifndef SRC_XILINXPLATFORMCABLEUSB_HPP_ +#define SRC_XILINXPLATFORMCABLEUSB_HPP_ + +#include +#include +#include + +#include "fx2_ll.hpp" +#include "jtagInterface.hpp" + +#define XPCU_DEFAULT_VID 0x03fd +#define XPCU_DEFAULT_PID 0x0013 + +/*! + * \file xilinxPlatformCableUSB.hpp + * \class XilinxPlatformCableUSB + * \brief concrete class between jtag implementation and Xilinx USB Cable probe + * \author Gwenhael Goavec-Merou + */ +class XilinxPlatformCableUSB : public JtagInterface { + public: + XilinxPlatformCableUSB(const uint16_t vid = XPCU_DEFAULT_VID, + const uint16_t pid = XPCU_DEFAULT_PID, + uint32_t clkHz = 750000, + const std::string &firmware_path = "", + int8_t verbose = 0); + ~XilinxPlatformCableUSB() override; + + int setClkFreq(uint32_t clkHz) override; + + /*! + * \brief drive TMS to move in JTAG state machine + * \param tms serie of TMS state + * \param len number of TMS state + * \param flush_buffer force flushing the buffer + * \param tdi TDI constant value + * \return number of state written + */ + int writeTMS(const uint8_t *tms, uint32_t len, bool flush_buffer, + const uint8_t tdi = 1) override; + + /*! + * \brief send TDI bits (mainly in shift DR/IR state) + * \param tx array of TDI values (used to write) + * \param rx array of TDO values (used when read) + * \param len number of bit to send/receive + * \param end in JTAG state machine last bit and tms are set in same time + * \return number of bit written and/or read + */ + int writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, bool end) override; + + /*! + * \brief toggle clock with static tms and tdi + * \param tms state of tms signal + * \param tdi state of tdi signal + * \param clk_len number of clock cycle + * \return number of clock cycle send + */ + int toggleClk(uint8_t tms, uint8_t tdi, uint32_t clk_len) override; + + /*! + * \brief return internal buffer size (in byte). + * \return _buffer_size + */ + int get_buffer_size() override { return static_cast(_buffer_size); } + + bool isFull() override { return _nb_bit >= _buffer_bit_size; } + + int flush() override; + + /*! + * \brief display fx2 & CPLD firmwares version + */ + void displayCableVersion(); + + /*! + * \brief enable device output + * \param[in] enable: enable or disable device + * \return true when transfer success, false otherwise + */ + bool enableDevice(bool enable); + + private: + /* \brief pack one JTAG bit into the internal buffer + * \return true when buffer is full, false otherwise + */ + bool storeBit(uint8_t tdi, uint8_t tms, uint8_t tck, uint8_t tdo) noexcept; + int write(uint8_t *rx, uint32_t rx_offset = 0); + static uint32_t rxBufSize(uint32_t nb_bit) noexcept; + + int8_t _verbose; + + std::unique_ptr _in_buf; + uint32_t _nb_bit; + uint32_t _nb_tdo_bit; + uint8_t _curr_tms; + uint8_t _curr_tdi; + uint32_t _buffer_size; + uint32_t _buffer_bit_size; + std::unique_ptr fx2; +}; + +#endif // SRC_XILINXPLATFORMCABLEUSB_HPP_ From c7b3613511a5766b7fb10c13348d6d93aa9280bf Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 14 May 2026 14:44:05 +0200 Subject: [PATCH 17/25] doc/cable: alphabetic order --- doc/cable.yml | 145 +++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/doc/cable.yml b/doc/cable.yml index 81cdd02..ee41f2a 100644 --- a/doc/cable.yml +++ b/doc/cable.yml @@ -33,6 +33,13 @@ bus_blaster_b: URL: http://dangerousprototypes.com/docs/Bus_Blaster +ch347: + + - Name: CH347 + Description: CH347 is a USB HS bus converter with UART, I2C, SPI and JTAG interfaces + URL: https://github.com/wuxx/USB-HS-Bridge + + ch347_jtag: - Name: ch347 JTAG adapter @@ -54,33 +61,6 @@ cmsisdap: URL: https://os.mbed.com/docs/mbed-os/v6.11/debug-test/daplink.html -gatemate_pgm: - - - Name: gatemate pgm - Description: Cologne Chip GateMate FPGA Programmer. FT232H-based JTAG/SPI programmer cable - URL: https://colognechip.com/programmable-logic/gatemate/ - - -gatemate_evb_jtag: - - Name: gatemate evb JTAG - Description: Cologne Chip GateMate JTAG programmer - URL: https://colognechip.com/programmable-logic/gatemate/ - - -gatemate_evb_spi: - - - Name: gatemate evb spi - Description: Cologne Chip GateMate SPI programmer - URL: https://colognechip.com/programmable-logic/gatemate/ - - -gwu2x: - - - Name: gwu2x - Description: Gowin GWUX2X - URL: https://www.gowinsemi.com/en/product/detail/55/ - - dfu: - Name: DFU interface @@ -94,7 +74,7 @@ digilent: Description: FT2232 JTAG / UART cable -diglent_b: +digilent_b: - Name: digilent cable Description: digilent FT2232 JTAG / UART cable (interface B) @@ -108,12 +88,14 @@ digilent_hs2: digilent_hs3: + - Name: digilent hs3 Description: JTAG programmer cable from digilent URL: https://digilent.com/shop/jtag-hs3-programming-cable/ dirtyJtag: + - Name: dirty Jtag Description: JTAG probe firmware for STM32F1 URL: https://github.com/jeanthom/DirtyJTAG @@ -121,16 +103,17 @@ dirtyJtag: New version `dirtyjtag2 `__ is also supported -efinix_spi_ft4232: +ecpix5-debug: - - Name: efinix SPI (ft4232) - Description: efinix SPI interface (FTDI4232 interface A) + - Name: ecpix5-debug + Description: LambdaConcept ECPIX5 (45k/85k) UART/JTAG interface + URL: https://shop.lambdaconcept.com/home/46-ecpix-5.html -efinix_jtag_ft4232: +efinix_jtag_ft2232: - - Name: efinix JTAG (ft4232) - Description: efinix JTAG interface (FTDI4232 interface B) + - Name: efinix JTAG (ft2232) + Description: efinix JTAG interface (FTDI2232 interface B) efinix_spi_ft2232: @@ -139,10 +122,16 @@ efinix_spi_ft2232: Description: efinix SPI interface (FTDI2232 interface A) -efinix_jtag_ft2232: +efinix_jtag_ft4232: - - Name: efinix JTAG (ft2232) - Description: efinix JTAG interface (FTDI2232 interface B) + - Name: efinix JTAG (ft4232) + Description: efinix JTAG interface (FTDI4232 interface B) + + +efinix_spi_ft4232: + + - Name: efinix SPI (ft4232) + Description: efinix SPI interface (FTDI4232 interface A) ft2232: @@ -196,23 +185,54 @@ ft4232: Description: quad interface programmer cable. MPSSE capable. URL: https://ftdichip.com/products/ft4232h-56q/ + ft4232hp: - Name: FT4232HP (interface A) Description: quad interface programmer cable. MPSSE capable. High Speed USB Bridge with Type-C/PD3.0 Controller URL: https://ftdichip.com/wp-content/uploads/2023/02/DS_FT4233HP.pdf + ft4232hp_b: - Name: FT4232HP (interface B) Description: quad interface programmer cable. MPSSE capable. High Speed USB Bridge with Type-C/PD3.0 Controller URL: https://ftdichip.com/wp-content/uploads/2023/02/DS_FT4233HP.pdf -ecpix5-debug: - - Name: ecpix5-debug - Description: LambdaConcept ECPIX5 (45k/85k) UART/JTAG interface - URL: https://shop.lambdaconcept.com/home/46-ecpix-5.html +gatemate_pgm: + + - Name: gatemate pgm + Description: Cologne Chip GateMate FPGA Programmer. FT232H-based JTAG/SPI programmer cable + URL: https://colognechip.com/programmable-logic/gatemate/ + + +gatemate_evb_jtag: + + - Name: gatemate evb JTAG + Description: Cologne Chip GateMate JTAG programmer + URL: https://colognechip.com/programmable-logic/gatemate/ + + +gatemate_evb_spi: + + - Name: gatemate evb spi + Description: Cologne Chip GateMate SPI programmer + URL: https://colognechip.com/programmable-logic/gatemate/ + + +gwu2x: + + - Name: gwu2x + Description: Gowin GWUX2X + URL: https://www.gowinsemi.com/en/product/detail/55/ + + +jetson-nano-gpio: + + - Name: Bitbang GPIO + Description: Bitbang GPIO pins on Jetson Nano Linux host. Use /dev/mem to have a faster clock. + URL: https://github.com/jwatte/jetson-gpio-example jlink: @@ -235,6 +255,13 @@ jtag-smt2-nc: URL: https://digilent.com/shop/jtag-smt2-nc-surface-mount-programming-module +libgpiod: + + - Name: Bitbang GPIO + Description: Bitbang GPIO pins on Linux host. + URL: https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/ + + lpc-link2: - Name: lpc-link2 @@ -246,7 +273,8 @@ numato: - Name: numato Description: Embedded cable for Numato Systems Mimas-A7 board - + + orbtrace: - Name: orbtrace interface @@ -261,13 +289,6 @@ papilio: URL: https://papilio.cc/ -steppenprobe: - - - Name: steppenprobe - Description: Open Source Hardware JTAG/SWD/UART/SWO interface board based on FTDI FT2232H - URL: https://github.com/diegoherranz/steppenprobe - - remote-bitgang: - Name: OpenOCD remote bitbang @@ -275,6 +296,13 @@ remote-bitgang: URL: https://github.com/openocd-org/openocd/blob/master/doc/manual/jtag/drivers/remote_bitbang.txt +steppenprobe: + + - Name: steppenprobe + Description: Open Source Hardware JTAG/SWD/UART/SWO interface board based on FTDI FT2232H + URL: https://github.com/diegoherranz/steppenprobe + + tigard: - Name: tigard @@ -321,22 +349,3 @@ xvc-server: - Name: Xilinx Virtual Cable (server side) Description: Xilinx Virtual Cable (XVC) is a TCP/IP-based protocol that acts like a JTAG cable. URL: https://github.com/Xilinx/XilinxVirtualCable - - -libgpiod: - - - Name: Bitbang GPIO - Description: Bitbang GPIO pins on Linux host. - URL: https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/ - -jetson-nano-gpio: - - - Name: Bitbang GPIO - Description: Bitbang GPIO pins on Jetson Nano Linux host. Use /dev/mem to have a faster clock. - URL: https://github.com/jwatte/jetson-gpio-example - -ch347: - - - Name: CH347 - Description: CH347 is a USB HS bus converter with UART, I2C, SPI and JTAG interfaces - URL: https://github.com/wuxx/USB-HS-Bridge From fe21c890a42a01ddd8b07b343ae9130dd81c4642 Mon Sep 17 00:00:00 2001 From: parker-research <166864283+parker-research@users.noreply.github.com> Date: Mon, 4 May 2026 23:30:06 -0600 Subject: [PATCH 18/25] Add --version lowercase flag to comply with convention --- README.md | 5 ++++- src/main.cpp | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc2d2c6..7e48627 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ openFPGALoader -- a program to flash FPGA -v, --verbose Produce verbose output --verbose-level arg verbose level -1: quiet, 0: normal, 1:verbose, 2:debug + --force-terminal-mode force progress bar output as if connected to + a terminal -h, --help Give this help list --verify Verify write operation (SPI Flash only) --xvc Xilinx Virtual Cable Functions @@ -130,7 +132,8 @@ openFPGALoader -- a program to flash FPGA -X, --read-xadc Read XADC (Xilinx FPGA only) --read-register arg Read Status Register(Xilinx FPGA only) --user-flash arg User flash file (Gowin LittleBee FPGA only) - -V, --Version Print program version + -V, --version Print program version + --Version Print program version (Deprecated) Mandatory or optional arguments to long options are also mandatory or optional for any corresponding short options. diff --git a/src/main.cpp b/src/main.cpp index f6bdcb7..9ca2940 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1027,7 +1027,8 @@ int parse_opt(int argc, char **argv, struct arguments *args, cxxopts::value(args->read_register)) ("user-flash", "User flash file (Gowin LittleBee FPGA only)", cxxopts::value(args->user_flash)) - ("V,Version", "Print program version"); + ("V,version", "Print program version") + ("Version", "Print program version (Deprecated)"); options.parse_positional({"bitstream"}); auto result = options.parse(argc, argv); @@ -1055,7 +1056,7 @@ int parse_opt(int argc, char **argv, struct arguments *args, args->verbose = verbose_level; } - if (result.count("Version")) { + if (result.count("Version") || result.count("version")) { std::cout << "openFPGALoader " << VERSION << std::endl; return 1; } From 4000496abc384e941b1aa0ca781962aac2503a7f Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 14 May 2026 17:38:02 +0200 Subject: [PATCH 19/25] 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 20/25] 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; From 956375efbcb2fef3c1d1d2e7131364242fdbc3c4 Mon Sep 17 00:00:00 2001 From: dettus Date: Sat, 16 May 2026 09:54:12 +0200 Subject: [PATCH 21/25] Enabling the compilation on OpenBSD (#651) --- doc/guide/troubleshooting.rst | 34 ++++++++++++++++++++++++++++++++++ src/xvc_server.cpp | 1 + 2 files changed, 35 insertions(+) diff --git a/doc/guide/troubleshooting.rst b/doc/guide/troubleshooting.rst index e2475ad..fa5a33c 100644 --- a/doc/guide/troubleshooting.rst +++ b/doc/guide/troubleshooting.rst @@ -76,3 +76,37 @@ After changing groups or rules, reload udev rules, then unplug/replug the converter and log out/login again. Reference: `install guide (udev rules section) `_. + + +Unable to flash device on OpenBSD: `JTAG init failed with: DirtyJtag: fails to open device` +=========================================================================================== +Certain evaluation boards may show the following error message when running openFPGAloader on OpenBSD: + +.. code:: bash + fail to read data usb bulk read failed + JTAG init failed with: low level FTDI init failed + +This issue is most likely caused by the uftdi module, which has already locked the device. +Disabling the module can be achieved with the following commands: + +.. code:: bash + # doas config -e -f -o /bsd.nouftdi /bsd + OpenBSD 7.8 (GENERIC) #54: Sun Oct 12 12:45:58 MDT 2025 + deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC + Enter 'help' for information + ukc> disable uftdi* + 356 uftdi* disabled + ukc> disable uftdi0 + ukc> disable uftdi1 + ukc> quit + Saving modified kernel. + # reboot + +At the boot prompt, typing in + +.. code:: bash + boot> boot /bsd.nouftdi + +will boot the new kernel with the disabled module. +Afterwards, openFPGALoader will access the development board as a generic USB device. + diff --git a/src/xvc_server.cpp b/src/xvc_server.cpp index f710a79..bd660ee 100644 --- a/src/xvc_server.cpp +++ b/src/xvc_server.cpp @@ -6,6 +6,7 @@ #include "xvc_server.hpp" #include +#include #include #include #include From b3f418c977cac789057ca85633da7b5eb267e5c3 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Tue, 19 May 2026 11:41:37 +0200 Subject: [PATCH 22/25] remoteBitbang_client: fix toggleClk behavior, uses flush instead of ll_write(NULL), fix potential overflows, simplify code --- src/remoteBitbang_client.cpp | 41 +++++++++++++++++------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/remoteBitbang_client.cpp b/src/remoteBitbang_client.cpp index 7c1da31..64aef53 100644 --- a/src/remoteBitbang_client.cpp +++ b/src/remoteBitbang_client.cpp @@ -76,25 +76,27 @@ RemoteBitbang_client::~RemoteBitbang_client() } int RemoteBitbang_client::writeTMS(const uint8_t *tms, uint32_t len, - bool flush_buffer, __attribute__((unused)) const uint8_t tdi) + bool flush_buffer, const uint8_t tdi) { // empty buffer // if asked flush if (len == 0) return ((flush_buffer) ? flush() : 0); + _last_tdi = (tdi) ? TDI_BIT : 0; + uint8_t base_v = '0' + _last_tdi; for (uint32_t pos = 0; pos < len; pos++) { // buffer full -> write - if (_num_bytes == _buffer_size) - ll_write(NULL); + if ((_num_bytes + 2) >= _buffer_size) + flush(); _last_tms = (tms[pos >> 3] & (1 << (pos & 0x07))) ? TMS_BIT : 0; _xfer_buf[_num_bytes++] = base_v + _last_tms; _xfer_buf[_num_bytes++] = base_v + _last_tms + TCK_BIT; } // flush where it's asked or if the buffer is full - if (flush_buffer || _num_bytes == _buffer_size * 8) + if (flush_buffer || _num_bytes == _buffer_size) return flush(); return len; } @@ -107,9 +109,11 @@ int RemoteBitbang_client::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, uint8_t base_v = '0' + _last_tms; for (uint32_t pos = 0; pos < len; pos++) { - if (_num_bytes == _buffer_size) - ll_write(NULL); // NULL because _num_bytes is always 0 when read - _last_tdi = (tx[pos >> 3] & (1 << (pos & 0x07))) ? TDI_BIT : 0; + const uint32_t pos_byte = (pos >> 3); + const uint32_t pos_bit = (1 << (pos & 0x07)); + if ((_num_bytes + 2) >= _buffer_size) + flush(); + _last_tdi = (tx[pos_byte] & pos_bit) ? TDI_BIT : 0; if (end && pos == len - 1) { _last_tms = TMS_BIT; base_v = '0' + _last_tms; @@ -120,9 +124,9 @@ int RemoteBitbang_client::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, uint8_t tdo; ll_write(&tdo); if (tdo == '1') - rx[pos >> 3] |= (1 << (pos & 0x07)); + rx[pos_byte] |= pos_bit; else - rx[pos >> 3] &= ~(1 << (pos & 0x07)); + rx[pos_byte] &= ~pos_bit; } } @@ -130,30 +134,23 @@ int RemoteBitbang_client::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, } // toggle clk with constant TDI and TMS. -int RemoteBitbang_client::toggleClk(uint8_t tms, uint8_t tdi, uint32_t clk_len) +int RemoteBitbang_client::toggleClk([[maybe_unused]] uint8_t tms, + [[maybe_unused]] uint8_t tdi, uint32_t clk_len) { // nothing to do if (clk_len == 0) return 0; - if (_num_bytes != 0) - flush(); - _last_tms = tms; - _last_tdi = tdi; - uint8_t val = (_last_tms | _last_tdi); - - // flush buffer before starting - if (_num_bytes != 0) - flush(); + const uint8_t val = (_last_tms | _last_tdi); for (uint32_t len = 0; len < clk_len; len++) { - if (len == _num_bytes) - ll_write(NULL); + if ((_num_bytes + 2) >= _buffer_size) + flush(); _xfer_buf[_num_bytes++] = '0' + val; _xfer_buf[_num_bytes++] = '0' + (val | TCK_BIT); } - ll_write(NULL); + flush(); return clk_len; } From 7ae2825b5bc36d96ae2ffa0e0d6970bd815a08f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20R=C3=BCger?= Date: Sun, 24 May 2026 08:42:14 +0200 Subject: [PATCH 23/25] feat: add agilex 3 A3CY100 model to fpga list (#669) * feat: add agilex 3 A3CY100 model to fpga list * feat: add agilex 3 devboard to boards --- doc/boards.yml | 27 +++++++++++++++++---------- src/part.hpp | 1 + 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/doc/boards.yml b/doc/boards.yml index cab7b33..5e3d8d0 100644 --- a/doc/boards.yml +++ b/doc/boards.yml @@ -34,6 +34,13 @@ Memory: OK Flash: NT +- ID: axc3000 + Description: Arrow AXC3000 + URL: https://www.arrow.com/de/products/axc3000/trenz-electronic-gmbh.html + FPGA: Altera Agilex 3 A3CY100BM16AE7S + Memory: OK + Flash: NT + - ID: axe5000 Description: A5EC008BM16AE6S FPGA Starter Kit URL: https://www.arrow.com/en/products/axe5000/trenz-electronic-gmbh.html @@ -380,8 +387,8 @@ Memory: OK Flash: OK Constraints: - - ECPIX-5-45F - - ECPIX-5-85F + - ECPIX-5-45F + - ECPIX-5-85F - ID: ecpix5_r03 Description: LambdaConcept ECPIX-5 (FT4232) @@ -390,8 +397,8 @@ Memory: OK Flash: OK Constraints: - - ECPIX-5-45F - - ECPIX-5-85F + - ECPIX-5-45F + - ECPIX-5-85F - ID: fireant Description: Fireant Trion T8 @@ -451,8 +458,8 @@ Memory: NA Flash: OK Constraints: - - iCEBreaker-bitsy-v0 - - iCEBreaker-bitsy-v1 + - iCEBreaker-bitsy-v0 + - iCEBreaker-bitsy-v1 - ID: ice40_generic Description: icestick @@ -918,10 +925,10 @@ Memory: OK Flash: OK Constraints: - - ULX3S-12F - - ULX3S-25F - - ULX3S-45F - - ULX3S-85F + - ULX3S-12F + - ULX3S-25F + - ULX3S-45F + - ULX3S-85F - ID: ulx3s_dfu Description: Radiona ULX3S DFU mode diff --git a/src/part.hpp b/src/part.hpp index 5e6e8dd..1fee941 100644 --- a/src/part.hpp +++ b/src/part.hpp @@ -307,6 +307,7 @@ static std::map fpga_list = { {0x02e050dd, {"altera", "arria 10", "SX660", 10}}, /* Altera Agilex 3 */ + {0x4361b0dd, {"altera", "agilex 3", "A3CY100", 10}}, {0x436db0dd, {"altera", "agilex 3", "A3CZ135", 10}}, /* Altera Agilex 5 */ From 0818a404c95064360c6cda4603578c8e076515d1 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Mon, 25 May 2026 18:42:18 +0200 Subject: [PATCH 24/25] README.md, main: update probe-firmware message --- README.md | 3 ++- src/main.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9e767b..0950423 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,8 @@ openFPGALoader -- a program to flash FPGA non volatile memory (default: 0) --pins arg pin config TDI:TDO:TCK:TMS or MOSI:MISO:SCK:CS[:HOLDN:WPN] - --probe-firmware arg firmware for JTAG probe (usbBlasterII) + --probe-firmware arg firmware for JTAG probe + (usbBlasterII/xilinxPlatformCableUsb) --protect-flash arg protect SPI flash area --quiet Produce quiet output (no progress bar) -r, --reset reset FPGA after operations diff --git a/src/main.cpp b/src/main.cpp index 4c779e6..3998aee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -986,7 +986,7 @@ int parse_opt(int argc, char **argv, struct arguments *args, cxxopts::value(args->offset)) ("pins", "pin config TDI:TDO:TCK:TMS or MOSI:MISO:SCK:CS[:HOLDN:WPN]", cxxopts::value>(pins)) - ("probe-firmware", "firmware for JTAG probe (usbBlasterII)", + ("probe-firmware", "firmware for JTAG probe (usbBlasterII/xilinxPlatformCableUsb)", cxxopts::value(args->probe_firmware)) ("protect-flash", "protect SPI flash area", cxxopts::value(args->protect_flash)) From c6ca4e571b0bb96a310b9ea8a7da38bedc29a882 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Thu, 28 May 2026 17:12:24 +0200 Subject: [PATCH 25/25] CMakeLists.txt: revert fad4a28103d53a30f1946be2ea66664d89c75e6a -> uses pkg_check_modules all the time --- CMakeLists.txt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40a4ebd..45b3d56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,24 +188,17 @@ add_definitions(-DVIVADO_DIR=\"${VIVADO_PATH}\") # Dependencies check/search #################################################################################################### -if (USE_LIBFTDI) - # Try to find the LibFTDI1 with cmake - find_package(LibFTDI1 QUIET) -endif() - if (USE_PKGCONFIG) find_package(PkgConfig REQUIRED) # Backward compat when the libftdi is not found # by using cmake if (USE_LIBFTDI) - if (NOT LIBFTDI_FOUND) - pkg_check_modules(LIBFTDI REQUIRED libftdi1) - string(REPLACE "." ";" VERSION_LIST ${LIBFTDI_VERSION}) - list(GET VERSION_LIST 0 LIBFTDI_VERSION_MAJOR) - list(GET VERSION_LIST 1 LIBFTDI_VERSION_MINOR) - string(REGEX REPLACE "[^0-9].*" "" LIBFTDI_VERSION_MINOR "${LIBFTDI_VERSION_MINOR}") - endif() + pkg_check_modules(LIBFTDI REQUIRED libftdi1) + string(REPLACE "." ";" VERSION_LIST ${LIBFTDI_VERSION}) + list(GET VERSION_LIST 0 LIBFTDI_VERSION_MAJOR) + list(GET VERSION_LIST 1 LIBFTDI_VERSION_MINOR) + string(REGEX REPLACE "[^0-9].*" "" LIBFTDI_VERSION_MINOR "${LIBFTDI_VERSION_MINOR}") else() set(LIBFTDI_LIBRARY_DIRS "") set(LIBFTDI_INCLUDE_DIRS "")