From 53578876d55a4f533945098ac60db41d4e7aba96 Mon Sep 17 00:00:00 2001 From: Gwenhael Goavec-Merou Date: Mon, 20 May 2024 21:10:29 +0200 Subject: [PATCH] added support for Gowin GWU2X USB (JTAG mode) (#434) --- CMakeLists.txt | 11 ++ doc/cable.yml | 7 + src/cable.hpp | 4 + src/gwu2x_jtag.cpp | 456 +++++++++++++++++++++++++++++++++++++++++++++ src/gwu2x_jtag.hpp | 91 +++++++++ src/jtag.cpp | 11 ++ 6 files changed, 580 insertions(+) create mode 100644 src/gwu2x_jtag.cpp create mode 100644 src/gwu2x_jtag.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ad3121b5d..a4e951e45e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ else() option(ENABLE_UDEV "use udev to search JTAG adapter from /dev/xx" ON) endif() option(ENABLE_CMSISDAP "enable cmsis DAP interface (requires hidapi)" ON) +option(ENABLE_GOWIN_GWU2X "enable Gowin GWU2X interface" ON) if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") option(ENABLE_LIBGPIOD "enable libgpiod bitbang driver (requires libgpiod)" ON) option(ENABLE_REMOTEBITBANG "enable remote bitbang driver" ON) @@ -218,6 +219,16 @@ target_link_libraries(openFPGALoader ${LIBFTDI_LIBRARIES} ) +# Gowin GWU2X JTAG interface +if(ENABLE_GOWIN_GWU2X) + target_sources(openFPGALoader PRIVATE src/gwu2x_jtag.cpp) + list (APPEND OPENFPGALOADER_HEADERS src/gwu2x_jtag.hpp) + add_definitions(-DENABLE_GOWIN_GWU2X=1) + message("Gowin GWU2X support enabled") +else() + message("Gowin GWU2X support disabled") +endif() + if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") # winsock provides ntohs target_link_libraries(openFPGALoader ws2_32) diff --git a/doc/cable.yml b/doc/cable.yml index 9f0641cf54..90e54a3770 100644 --- a/doc/cable.yml +++ b/doc/cable.yml @@ -67,6 +67,13 @@ gatemate_evb_spi: 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 diff --git a/src/cable.hpp b/src/cable.hpp index a8019b0bbb..2435a26661 100644 --- a/src/cable.hpp +++ b/src/cable.hpp @@ -28,6 +28,7 @@ enum communication_type { MODE_JETSONNANO_BITBANG, /*! Bitbang gpio pins */ MODE_REMOTEBITBANG, /*! Remote Bitbang mode */ MODE_CH347, /*! CH347 JTAG mode */ + MODE_GWU2X, /*! Gowin GWU2X JTAG mode */ }; /*! @@ -77,6 +78,8 @@ struct cable_t { /* Others cable configuration */ #define CABLE_DEF(_type, _vid, _pid) \ {_type, _vid, _pid, 0, 0, {}} +#define CABLE_DEF_FULL(_type, _vid, _pid, _blv, _bld, _bhv, _bhd) \ + {_type, _vid, _pid, 0, 0, {0, _blv, _bld, _bhv, _bhd, 0, -1}} static std::map cable_list = { // last 4 bytes are ADBUS7-0 value, ADBUS7-0 direction, ACBUS7-0 value, ACBUS7-0 direction @@ -110,6 +113,7 @@ static std::map cable_list = { {"ft4232", FTDI_SER(0x0403, 0x6011, FTDI_INTF_A, 0x08, 0x0B, 0x08, 0x0B)}, {"ft4232hp", FTDI_SER(0x0403, 0x6043, FTDI_INTF_A, 0x08, 0x0B, 0x00, 0x00)}, {"ft4232hp_b", FTDI_SER(0x0403, 0x6043, FTDI_INTF_B, 0x08, 0x0B, 0x00, 0x00)}, + {"gwu2x", CABLE_DEF_FULL(MODE_GWU2X, 0x33AA, 0x0120, 0x02, 0x07, 0x0, 0x0)}, {"ecpix5-debug", FTDI_SER(0x0403, 0x6010, FTDI_INTF_A, 0xF8, 0xFB, 0xFF, 0xFF)}, {"jlink", CABLE_DEF(MODE_JLINK, 0x1366, 0x0105 )}, {"jlink_base", CABLE_DEF(MODE_JLINK, 0x1366, 0x0101 )}, diff --git a/src/gwu2x_jtag.cpp b/src/gwu2x_jtag.cpp new file mode 100644 index 0000000000..13da411882 --- /dev/null +++ b/src/gwu2x_jtag.cpp @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2024 Gwenhael Goavec-Merou + */ + +#include + +#include +#include +#include +#include +#include + +#include "cable.hpp" +#include "display.hpp" +#include "gwu2x_jtag.hpp" +#include "libusb_ll.hpp" + +/* + * TCK -> GPIOL0 + * TMS -> GPIOL1 + * TDI -> GPIOL2 + * TDO -> GPIOL3 + */ +enum { + GWU2X_TMS_LSB_WRO = 0x5B, + GWU2X_TMS_LSB_RDWR = 0x5C, + GWU2X_TCK = 0x9b, + GWU2X_TDI_LSB_BIT_WRO = 0x6B, + GWU2X_TDI_LSB_BIT_RDWR = 0x6C, + GWU2X_TDI_LSB_BYTE_WRO = 0x7B, + GWU2X_TDI_LSB_BYTE_RDWR = 0x7C, + GWU2X_SET_FREQ_FAST = 0xAB, + GWU2X_SET_FREQ_SLOW = 0xAC, + GWU2X_READBACK_BUFFER_FORCED = 0x8B, + GWU2X_READBACK_BUFFER = 0xDB, /* 0x11: LSB, 0xff: MSB */ + GWU2X_GPIO_CONF_LOW = 0x20, /* GPIO0-7 */ + GWU2X_GPIO_CONF_HIGH = 0x21, /* GPIO8-15 */ + GWU2X_GPIO_READ_LOW = 0x22, /* GPIO0-7 */ + GWU2X_GPIO_READ_HIGH = 0x23, /* GPIO8-15 */ + GWU2X_CPOL_SETTING = 0xCB, +}; + +enum { + READBACK_LSB = 0x11, + READBACK_MSB = 0xff, +}; + +GowinGWU2x::GowinGWU2x(cable_t *cable, uint32_t clkHz, int8_t verbose): + libusb_ll(0, 0, verbose), _verbose(verbose > 1), _cable(cable), + _usb_dev(nullptr), _dev(nullptr), _xfer_buf(nullptr), _xfer_pos(0), + _buffer_len(256 + 2 + 1) +{ + const int found = get_devices_list(_cable); + if (found == 0) + throw std::runtime_error("No cable found"); + if (found > 1) + throw std::runtime_error("More than one cable found"); + std::vector dev_list = usb_dev_list(); + + /* here we have only one device present */ + _usb_dev = dev_list[0]; + + int ret = libusb_open(_usb_dev, &_dev); + if (ret < 0) + throw std::runtime_error("Failed to open device"); + + ret = libusb_claim_interface(_dev, 0); + if (ret < 0) { + char mess[256]; + snprintf(mess, 256, "Error claiming interface with error %s", libusb_error_name(ret)); + throw std::runtime_error(mess); + } + + _xfer_buf = new uint8_t[_buffer_len]; // one full TDI packet + readback cmd + + /* cable configuration */ + if (!store_seq(GWU2X_GPIO_CONF_LOW, // gpio0-7 + cable->config.bit_low_dir, // direction + cable->config.bit_low_val)) // value + throw std::runtime_error("Error: low pins configuration failed"); + if (!store_seq(GWU2X_GPIO_CONF_HIGH, // gpio8-15 + cable->config.bit_high_dir, // direction + cable->config.bit_high_val)) // value + throw std::runtime_error("Error: high pins configuration failed"); + if (!xfer(nullptr, 0)) + throw std::runtime_error("Error: pin configuration failed"); + + if (setClkFreq(clkHz) < 0) + throw std::runtime_error("Error: clock frequency configuration failed"); +} + +GowinGWU2x::~GowinGWU2x() +{ + flush(); + delete _xfer_buf; + /* nothing about interface ? */ + libusb_close(_dev); +} + +int GowinGWU2x::writeTMS(const uint8_t *tms, uint32_t len, bool flush_buffer, + const uint8_t tdi) +{ + const uint8_t tdi_bit = (tdi) ? 0x80 : 0x00; + uint8_t tms_buf = tdi_bit; + uint8_t idx = 0; // bit index in tms_buf + + /* As FTDI devices TMS instruction is 3 bytes long and can send up to + * 7bits + * 2nd byte tells number of TMS bits to send (0: 1bit, 6: 7bits) + * 3rd byte is the sequence of TMS values LSB first. Offset 7 is the TDI + * value for all TMS bits + */ + for (uint32_t pos = 0; pos < len; pos++) { + const uint8_t tms_byte = tms[pos >> 3]; + const uint8_t bit_shift = pos & 0x07; + const uint8_t tms_bit = (tms_byte >> bit_shift) & 0x01; + if (tms_bit) + tms_buf |= (1 << idx); + idx += 1; + /* if we have 7bits or if it's the last iteration + * flush the buffer, and restart for the next sequence + */ + if (idx == 7 || pos == len - 1) { + if (!store_seq(GWU2X_TMS_LSB_WRO, idx - 1, tms_buf)) + return -1; + idx = 0; + tms_buf = tdi_bit; + } + } + + if (flush_buffer) { + if (!xfer(nullptr, 0)) { + return -1; + } + } + + return static_cast(len); +} + +/* + * Write / Read data in one to three steps + * a sequence of up to 256 Bytes per packet + * a sequence of up to 8 bits + * the final bit (MSB) using TMS transition when end is true + */ +int GowinGWU2x::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, bool end) +{ + const uint32_t real_len = len - (end ? 1 : 0); // if end: last bit is sent with TMS + const uint32_t byte_len = real_len >> 3; // convert bit len to byte len (floor) + const uint32_t bit_len = real_len & 0x07; // extract remaining bits + uint8_t *tx_ptr = (uint8_t *)tx; + uint8_t *rx_ptr = rx; + + /* if the buffer is not empty, some tms bits are present + * flush to keep maximum size available + */ + if (_xfer_pos != 0) { + if (!xfer(nullptr, 0)) + return -1; + } + + // 1. Byte sequence with up to 256 Bytes + if (byte_len > 0) { + uint32_t xfer_len = 256; + for (uint32_t pos = 0; pos < byte_len; pos += xfer_len) { + if (pos + 256 > byte_len) + xfer_len = byte_len - pos; + _xfer_buf[_xfer_pos++] = (rx) ? GWU2X_TDI_LSB_BYTE_RDWR : GWU2X_TDI_LSB_BYTE_WRO; + _xfer_buf[_xfer_pos++] = xfer_len - 1; + memcpy(&_xfer_buf[_xfer_pos], tx_ptr, xfer_len); + _xfer_pos += xfer_len; + if (rx) + _xfer_buf[_xfer_pos++] = GWU2X_READBACK_BUFFER_FORCED; + if (!xfer((rx) ? rx_ptr : nullptr, xfer_len)) + return -1; + tx_ptr += xfer_len; + if (rx_ptr) + rx_ptr += xfer_len; + } + } + + /* when end with a sequence of bits + * don't do two usb transfers and + * postpone it to end sequence + */ + const bool postponed_read = (bit_len != 0 && end); + + // 2. Remaining bits between 1 and 7. + if (bit_len != 0) { + /* write up to 8 bits + * may be less if end, but not more + * the buffer read must be correctly aligned according to bit_len + */ + if (!store_seq(GWU2X_TDI_LSB_BIT_WRO + (rx != nullptr), + static_cast(bit_len - 1), *tx_ptr, !postponed_read && rx != nullptr)) + return -1; + // When not end, flush buffer + // NOTE: with a more clever global system why not + // postning it for the next call ? + if (!postponed_read) { + /* unlike FTDI bits are filed LSB to MSB ie + * for 2 bits: 0b000000XX + * for 7 bits: 0b0XXXXXXX + * no needs to shift here + */ + if (!xfer((rx) ? rx_ptr : nullptr, 1)) + return -1; + } + } + + // 3. End using TMS instruction and last bit. + if (end) { + uint8_t rx_byte; + /* we are in SHIFTDR or SHIFTIR -> move to next state */ + const uint8_t last_bit = (*tx_ptr >> bit_len) & 0x01; + if (!store_seq(GWU2X_TMS_LSB_WRO + (rx != nullptr), 0, + static_cast(((last_bit) ? 0x80 : 0x00) | 0x01), + rx != nullptr)) + return -1; + if (!xfer((rx)? &rx_byte : nullptr, 1)) + return -1; + if (rx) { + if (postponed_read) { + *rx_ptr = rx_byte; + } else { + const uint8_t tdo_bit = 1 << (bit_len); + if (rx_byte & 0x01) // here only one bit is read (always LSB) + *rx_ptr |= tdo_bit; + else + *rx_ptr &= ~tdo_bit; + } + } + } + return static_cast(len); +} + +int GowinGWU2x::toggleClk(uint8_t tms, uint8_t tdi, uint32_t clk_len) +{ + /* Gowin GWU2X has a specific command to produces + * a sequence sequence to up to 65536 cycles + * 0: 1 clk cycle, 0xffff: 65535 clk cycles + */ + /* No need to check/flush buffer will be done by store_seq */ + uint16_t len = 0; + if (_verbose) + printf("toggleClk : %02x %02x %u\n", tms, tdi, clk_len); + + for (uint32_t length = clk_len; length > 0; length -= len) { + len = ((length > 65536) ? 65536 : length) - 1; + if (!store_seq(GWU2X_TCK, + static_cast((len >> 0) & 0xff), + static_cast((len >> 8) & 0xff))) + return -1; + len += 1; + } + /* flush before return */ + if (_xfer_pos != 0) { + if (!xfer(nullptr, 0)) + return -1; + } + return static_cast(clk_len); +} + +enum { + EP_IN = 0x81, + EP_OUT = 0x02, +}; + +/* when readback we consider it's for the current sequence, not + * for current content + */ +bool GowinGWU2x::store_seq(const uint8_t &opcode, const uint8_t &len, + const uint8_t &data, const bool readback) +{ + if (_verbose) { + char message[256]; + snprintf(message, 256, "store seq %02x %02x %02x %d", + opcode, len, data, readback); + printInfo(message); + } + const size_t xfer_len = 3 + readback; + if (_xfer_pos + xfer_len > _buffer_len) { + if (!xfer(nullptr, 0)) + return false; + } + _xfer_buf[_xfer_pos++] = opcode; + _xfer_buf[_xfer_pos++] = len; + _xfer_buf[_xfer_pos++] = data; + if (readback) + _xfer_buf[_xfer_pos++] = GWU2X_READBACK_BUFFER_FORCED; + return true; +} + +bool GowinGWU2x::xfer(uint8_t *rx, uint16_t rx_len, const uint16_t timeout) +{ + int actual_length; + int ret; + if (_xfer_pos == 0) // nothing to do + return true; + + if (_verbose) { + printInfo("Write " + std::to_string(_xfer_pos) + " Bytes"); + printf("\t"); + for (uint32_t i = 0; i < _xfer_pos; i++) { + char message[6]; + snprintf(message, 6, "0x%02x ", _xfer_buf[i]); + printSuccess(message, false); + } + printf("\n"); + displayCmd(); + } + + ret = libusb_bulk_transfer(_dev, EP_OUT, + _xfer_buf, _xfer_pos, &actual_length, timeout); + if (ret < 0) { + printError("Write failed with error " + std::to_string(ret)); + return false; + } + _xfer_pos = 0; + if (!rx) // message sent, nothing to read -> quit + return true; + uint8_t *rx_ptr = rx; + uint32_t dummy; + ret = libusb_bulk_transfer(_dev, 0x83, (uint8_t*)&dummy, 4, &actual_length, timeout); + if (_verbose) + printf("ret: %d %u %d\n", ret, dummy, actual_length); + do { + ret = libusb_bulk_transfer(_dev, EP_IN, rx_ptr, rx_len, + &actual_length, timeout); + if (ret < 0) { + char message[256]; + snprintf(message, 256, "Failed to read: %d %s\n", ret, + libusb_strerror(static_cast(ret))); + printError(message); + return false; + } + if (_verbose) { + printf("%d %d %d\n", ret, rx_len, actual_length); + for (int ii = 0; ii < rx_len; ii++) + printf("%02x ", rx_ptr[ii]); + printf("\n"); + } + rx_ptr += actual_length; + rx_len -= actual_length; + } while (rx_len > 0); + + return true; +} + +int GowinGWU2x::setClkFreq(uint32_t freqHz) +{ + if (freqHz < 120e3 || freqHz > 30e6) { + printError("clk Frequency must be between 120kHz and 30MHz"); + return -1; + } + const uint16_t div = static_cast(60e6 / freqHz); + const int real_freq = static_cast(60e6 / div); + printInfo("User requested: " + std::to_string(freqHz) + " real frequency is " + + std::to_string(real_freq)); + _xfer_buf[_xfer_pos++] = GWU2X_SET_FREQ_FAST + ((freqHz < 240e3) ? 1 : 0); + _xfer_buf[_xfer_pos++] = static_cast(div - ((freqHz < 240e3) ? 256 : 0)); + if (!xfer(nullptr, 0, 1000)) + return -1; + _clkHZ = real_freq; + return static_cast(_clkHZ); +} + +void GowinGWU2x::displayCmd() +{ + uint32_t len = 0; + while (len < _xfer_pos) { + const uint8_t opcode = _xfer_buf[len++]; + const uint8_t b_len = _xfer_buf[len++]; + uint8_t tdi; + uint8_t tms; + char message[256]; + uint8_t bytes[256]; + switch(opcode) { + case GWU2X_TMS_LSB_WRO: + tdi = (_xfer_buf[len] >> 7) & 0x01; + tms = (_xfer_buf[len++] & 0x7f); + snprintf(message, 256, + "TMS Write Only len %d TDI: %x TMS: %02x", + b_len + 1, tdi, tms); + break; + case GWU2X_TMS_LSB_RDWR: + tdi = (_xfer_buf[len] >> 7) & 0x01; + tms = (_xfer_buf[len++] & 0x7f); + snprintf(message, 256, + "TMS Read/Write len %d TDI: %x TMS: %02x", + b_len + 1, tdi, tms); + break; + case GWU2X_TDI_LSB_BIT_WRO: + tdi = _xfer_buf[len++]; + snprintf(message, 256, + "TDI bit Write Only len %d TDI: %2x", + b_len + 1, tdi); + break; + case GWU2X_TDI_LSB_BIT_RDWR: + tdi = _xfer_buf[len++]; + snprintf(message, 256, + "TDI bit Read/Write len %d TDI: %2x", + b_len + 1, tdi); + break; + case GWU2X_TDI_LSB_BYTE_WRO: + memcpy(bytes, _xfer_buf, b_len + 1); + len += b_len + 1; + snprintf(message, 256, + "TDI Byte Write Only len %d TDI: %2x", + b_len + 1, bytes[0]); + break; + case GWU2X_TDI_LSB_BYTE_RDWR: + memcpy(bytes, _xfer_buf, b_len + 1); + len += b_len + 1; + snprintf(message, 256, + "TDI Byte Read/Write len %d TDI: %2x", + b_len + 1, bytes[0]); + break; + case GWU2X_TCK: + tdi = _xfer_buf[len++]; + snprintf(message, 256, + "toggle Clock len %d", + (b_len | (tdi << 8)) + 1); + break; + + case GWU2X_GPIO_CONF_HIGH: + tms = _xfer_buf[len++]; + snprintf(message, 256, + "GPIO conf high: direction: %x value: %02x", + b_len, tms); + break; + case GWU2X_GPIO_CONF_LOW: + tms = _xfer_buf[len++]; + snprintf(message, 256, + "GPIO conf low: direction: %x value: %02x", + b_len, tms); + break; + case GWU2X_SET_FREQ_FAST: + case GWU2X_SET_FREQ_SLOW: + snprintf(message, 256, + "Set clk freq prescaler: %02x", + b_len); + break; + case GWU2X_READBACK_BUFFER_FORCED: + len--; + snprintf(message, 256, "readback buffer"); + break; + default: + snprintf(message, 256, "Unknown"); + printError(message); + return; + } + printInfo(message); + } +} diff --git a/src/gwu2x_jtag.hpp b/src/gwu2x_jtag.hpp new file mode 100644 index 0000000000..8e1a4834b8 --- /dev/null +++ b/src/gwu2x_jtag.hpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2024 Gwenhael Goavec-Merou + */ + +#ifndef SRC_GWU2X_JTAG_HPP__ +#define SRC_GWU2X_JTAG_HPP__ + +#include + +#include +#include + +#include "cable.hpp" +#include "jtagInterface.hpp" +#include "libusb_ll.hpp" + +class GowinGWU2x: public JtagInterface, private libusb_ll +{ + public: + GowinGWU2x(cable_t *cable, uint32_t clkHz, int8_t verbose); + ~GowinGWU2x(); + + int setClkFreq(uint32_t clkHz) override; + + /*! + * \brief flush TMS internal buffer (ie. transmit to converter) + * \param tdo: pointer for read operation. May be NULL + * \param len: number of bit to send + * \return number of bit send/received + */ + 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 tdi: array of TDI values (used to write) + * \param tdo: 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 + * but only in shift[i|d]r, if end is false tms remain the same. + * \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 clk without touch of TDI/TMS + * \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 internal buffer size + */ + int get_buffer_size() override {return static_cast(_buffer_len);} + + /*! + * \brief return status of internal buffer + * \return true when internal buffer is full + */ + bool isFull() override {return _xfer_pos == _buffer_len;} + + /*! + * \brief force internal flush buffer + * \return 1 if success, 0 if nothing to write, -1 is something wrong + */ + int flush() override { + if (_xfer_pos == 0) + return 0; + return xfer(nullptr, 0) ? 1 : -1; + } + private: + bool xfer(uint8_t *rx, uint16_t rx_len, uint16_t timeout = 1000); + bool store_seq(const uint8_t &opcode, const uint8_t &len, + const uint8_t &data, const bool readback = false); + bool init_device(); + void displayCmd(); // debug purpose: translate sequence to human + // readable sequence + bool _verbose; + cable_t *_cable; + struct libusb_device *_usb_dev; + struct libusb_device_handle *_dev; + uint8_t *_xfer_buf; /* internal buffer */ + uint32_t _xfer_pos; /* number of Bytes already stored in _xfer_buf */ + uint32_t _buffer_len; /* _xfer_buf capacity (Byte) */ +}; + +#endif // SRC_GWU2X_JTAG_HPP__ diff --git a/src/jtag.cpp b/src/jtag.cpp index 5c36f12c08..6c306d792f 100644 --- a/src/jtag.cpp +++ b/src/jtag.cpp @@ -18,6 +18,9 @@ #include "jtag.hpp" #include "ftdiJtagBitbang.hpp" #include "ftdiJtagMPSSE.hpp" +#ifdef ENABLE_GOWIN_GWU2X +#include "gwu2x_jtag.hpp" +#endif #ifdef ENABLE_LIBGPIOD #include "libgpiodJtagBitbang.hpp" #endif @@ -108,6 +111,14 @@ Jtag::Jtag(const cable_t &cable, const jtag_pins_conf_t *pin_conf, case MODE_DIRTYJTAG: _jtag = new DirtyJtag(clkHZ, verbose); break; +#ifdef ENABLE_GOWIN_GWU2X + case MODE_GWU2X: + _jtag = new GowinGWU2x((cable_t *)&cable, clkHZ, verbose); + break; +#else + std::cerr << "Jtag: support for Gowin GWU2X was not enabled at compile time" << std::endl; + throw std::exception(); +#endif case MODE_JLINK: _jtag = new Jlink(clkHZ, verbose, cable.vid, cable.pid); break;