Xilinx Platform Cable USB: technical reproduction and debug record¶
Date: 2026-07-15
Host used for the measurements: Windows, WinUSB, Docker Desktop
Repository: openFPGALoader
Target used for the successful test: Spartan-6 XC6SLX45T-FGG484
1. Scope and present result¶
This document reconstructs the complete debug procedure used to make Xilinx Platform Cable USB devices usable from a native Windows openFPGALoader binary built in Docker. It deliberately distinguishes two electrically similar but firmware-incompatible cable families:
- the embedded/clone path, cold PID
03fd:000d; - genuine Platform Cable USB II/DLC10, cold PID
03fd:0013.
The 000d path is verified end to end. It loads two FX2 images, detects the
XC6SLX45T, and programs a 1.48 MB bitstream in approximately 4.1--4.6 seconds.
The 0013 path is verified through both FX2 stages. The first-stage loader was
recovered from the installed Xilinx kernel driver and the cable reaches FX2
version 0x0961. On the tested cable, however, the internal programmable logic
then reports 0xFFFE. Xilinx iMPACT normally replays xusb_xp2.fmwr through an
internal JTAG chain at this point. That last .fmwr interpreter is not yet
implemented in openFPGALoader, so a cold genuine DLC10 is not yet an end-to-end
standalone result. This is an explicit boundary of the current implementation,
not a target-board failure.
The state diagram is available as editable
diagrams/xpcu-firmware-state-machine.mmd
and rendered
diagrams/xpcu-firmware-state-machine.svg.
2. Hardware and software architecture¶
The cable has three distinct layers which must not be confused:
- Windows USB driver binding. WinUSB only gives libusb access to a USB identity. It does not load cable firmware.
- Cypress FX2 firmware. This RAM-resident 8051 program implements USB control requests and bulk endpoints. It disappears after complete power removal.
- Cable programmable logic. Embedded cables expose a CPLD implementation; Platform Cable USB II contains a Spartan-3A-class internal FPGA/PROM path. It generates the fast JTAG waveforms for the external target connector.
The target FPGA is a fourth, independent JTAG TAP. A successful FX2 version
read proves only layers 1 and 2. connected: yes proves that target voltage is
sensed; neither result proves valid target TDO.
3. USB identities and firmware matrix¶
| VID:PID | openFPGALoader cable name | Meaning | Correct first image |
|---|---|---|---|
03fd:000d |
xilinxPlatformCableUsb_alt |
Embedded cable cold loader | xusb_emb.hex |
03fd:0013 |
xilinxPlatformCableUsb |
Platform Cable USB II cold loader | xusb_xp2_loader.hex |
03fd:0008 |
xilinxPlatformCableUsb_initialized |
An FX2 image is running | Do not upload a boot image blindly |
The PID 0008 identity alone does not identify which operational firmware is
running. Read request 0x50 first.
| File | HEX version record | Role | Tested cable |
|---|---|---|---|
xusb_emb.hex |
0x0404 |
initial FX2 image | cold PID 000d |
xusb_xlp.hex |
0x0517 |
accelerated second-stage FX2 image | embedded/clone path |
xusb_xp2_loader.hex |
0x08FD (decimal 2301) |
initial FX2 image extracted from xusb_xp2.sys |
cold PID 0013 |
xusb_xp2.hex |
0x0961 (decimal 2401) |
operational second-stage FX2 image | genuine DLC10 |
xusb_xp2.fmwr |
proprietary microprogram | internal FPGA/PROM overlay/update | genuine DLC10 |
Version byte order¶
The Intel HEX record at FX2 address 0x19B9 stores the printable four-digit
firmware value directly. For example:
:0219B9000517... -> 0x0517
:0219B9000961... -> 0x0961
The FX2 GET_VERSION control response returns the low byte first, so host code
must decode buf[1] << 8 | buf[0]. Earlier debug output swapped the bytes and
called 0x0517 "1705". That display was wrong; it did not describe a different
firmware file.
4. Reproducible Windows driver deployment¶
Build the host-driver packages from the external project:
Set-Location externals\xilinx-usb-driver
.\docker-build.ps1
Extract the Windows ZIP, start an elevated PowerShell in its Windows package, and run:
.\install.ps1
.\detect.ps1
install.ps1 calls the Docker-built wdi-simple.exe once for each PID:
0013, 000d, and 0008. A success message for an absent PID can mean that
the package was staged, not that a live device changed driver. After firmware
causes PID 0008 to appear, rerun the elevated installer while 0008 is
physically present.
Verify with:
Get-PnpDevice -PresentOnly | Where-Object InstanceId -Like 'USB\VID_03FD*'
Device Manager should list the live identity under Universal Serial Bus devices using WinUSB. Xilinx iMPACT normally needs the vendor driver instead; WinUSB intentionally replaces that binding.
5. Docker cross-build and firmware packaging¶
From the repository root:
.\deploy\scripts\verify-docker-linux-engine.ps1
docker compose -f docker-compose.cross-windows.yml build windows-cross
docker compose -f docker-compose.cross-windows.yml run --rm windows-cross
The cross-build uses an Alpine Linux image. Docker Desktop must use its WSL2
Linux-container engine; Windows-container mode cannot pull alpine:edge.
The executable is installed at:
dist/docker-windows/install/bin/openFPGALoader.exe
Firmware must be copied in deploy/scripts/docker-cross-windows.sh, while the
container is running. The Compose /src bind mount does not exist during a
Dockerfile RUN layer, so this does not work in alpine.Dockerfile:
RUN cp /src/deploy/ise_programmer_bins/*.hex ...
The runtime script instead copies deploy/ise_programmer_bins/*.hex to:
dist/docker-windows/install/share/openFPGALoader/
Always invoke the built executable by absolute path. Otherwise an older binary
on PATH can make a fixed source tree appear broken:
$ofl = (Resolve-Path '.\dist\docker-windows\install\bin\openFPGALoader.exe').Path
& $ofl --scan-usb
6. Recovering the DLC10 loader from the installed Xilinx driver¶
ISE 14.7 was found at E:\Xilinx\14.7, not at a guessed root directory. The
relevant files are:
E:\Xilinx\14.7\ISE_DS\common\bin\nt64\xusb_xp2.sys
E:\Xilinx\14.7\ISE_DS\ISE\data\xusb_xp2.hex
E:\Xilinx\14.7\ISE_DS\ISE\data\xusb_xp2.fmwr
E:\Xilinx\14.7\ISE_DS\ISE\data\xusbcpld.fmwr
Only the operational 0x0961 HEX is installed as a normal file. Loading it
directly into cold PID 0013 fails because the missing 2301 loader is
embedded in the Windows .sys driver.
Run the repository extractor:
powershell.exe -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\extract-xusb-loader-from-sys.ps1 `
-DriverPath 'E:\Xilinx\14.7\ISE_DS\common\bin\nt64\xusb_xp2.sys' `
-OutputPath '.\deploy\ise_programmer_bins\xusb_xp2_loader.hex'
The explicit process-scoped execution-policy override is needed on hosts which disable direct PowerShell script execution. It does not change the machine-wide policy.
The extractor performs these steps:
- validates the DOS
MZand PE signatures; - locates the PE
.datasection from the section table; - skips the first
0x100bytes of.dataglobals; - reads records encoded as
uint16_le length,uint16_le address, one zero type byte,lengthdata bytes, and one zero padding byte; - rejects scanner artifacts whose FX2 address range overlaps an accepted record;
- requires at least 500 records, an address-zero reset vector, a USB
descriptor at
0x0090containing03fd:0008, and a two-byte version record at0x19B9; - writes checksummed Intel HEX and an EOF record.
The tested driver yields 595 firmware records plus the HEX EOF line and version
0x08FD.
7. Cold-start procedures¶
7.1 Embedded cable, PID 000d -- verified fast path¶
Fully remove USB and target power, reconnect, then run:
& $ofl --scan-usb
& $ofl -c xilinxPlatformCableUsb_alt --detect -v
The expected state sequence is:
- open
03fd:000d; - upload
xusb_emb.hex(0x0404) into FX2 RAM; - release FX2 reset and wait for
03fd:0008; - reopen and reload
xusb_xlp.hex(0x0517); - wait for
03fd:0008again; - discover bulk endpoints
OUT=0x02,IN=0x86; - select the external chain and prime accelerated transfer;
- detect the target TAP.
The accelerator priming sequence reproduced from the reference driver is:
disable -> speed class 0x11 -> enable -> A6 transfer count 2
-> bulk OUT 00 00 -> speed class 0x12 -> restore requested speed
The tested XLP firmware requires the zero-based transfer value
number_of_bits - 1. Sending N makes common 32-bit scans a multiple-of-four
XPCU CPLD failure case and stalls bulk IN. A USER/SPI readback problem must not
be diagnosed by changing this count after the accelerator is working.
Program SRAM with the same cable name and the desired .bit file:
& $ofl -c xilinxPlatformCableUsb_alt --freq 6000000 .\path\design.bit -v
The verified XC6SLX45T IDCODE is 0x04028093 after the normal version-nibble
mask. The 1.48 MB test image completed in about 4.1--4.6 seconds.
7.2 Genuine Platform Cable USB II, PID 0013 -- current boundary¶
After a full power cycle:
& $ofl --scan-usb
& $ofl -c xilinxPlatformCableUsb --detect -v
The expected implemented portion is:
0013 -> xusb_xp2_loader.hex 0x08FD -> 0008
-> xusb_xp2.hex 0x0961 -> 0008
The observed diagnostic after those successful reloads was:
FX2 version: 0961
CPLD version: fffe
Const0 version: 03b5
Const1 version: 0004
Const2 version: 200d
status 00 connected: no
TDO stuck at 0
0xFFFE is the decisive value: the cable-internal programmable logic has not
been configured. The external Spartan-6 cannot be reached through that state.
Do not debug the SP605 ribbon or its IDCODE until the internal overlay loads.
Binary strings in libImpactComm.dll show the vendor sequence uses
playxsvf, with fpga, cpld, and cpld2 modes. The 734,080-byte
xusb_xp2.fmwr begins with version/comment metadata and a proprietary Xilinx
microprogram; it is not a plain FX2 HEX or a directly usable standard XSVF
file. USBPcap is installed at C:\Program Files\USBPcap\USBPcapCMD.exe; a
capture made while the official driver and iMPACT initialize a cold cable is
the next reliable way to recover the exact replay protocol.
7.3 Capturing the remaining official DLC10 initialization¶
This operation intentionally requires a manual driver transition. Do not run the WinUSB and Xilinx stacks against the cable simultaneously.
- Rebind
03fd:0013and03fd:0008to the official ISE 14.7 Xilinx driver. - Remove power from the cable so the next session starts at PID
0013. - In an elevated terminal, identify the USBPcap root-hub interface containing
the cable, for example
\\.\USBPcap1. - Start a full-length capture before opening iMPACT:
& 'C:\Program Files\USBPcap\USBPcapCMD.exe' `
-d '\\.\USBPcap1' -A --inject-descriptors `
-o '.\xpcu-xp2-official-init.pcap'
- Open iMPACT and perform exactly one cable connect/detect. Wait until it can see the external chain, then stop USBPcap with Ctrl+C.
- In Wireshark use display filter
usb.idVendor == 0x03fd. Preserve the enumeration, control transfers, and both bulk endpoints; do not filter the capture at acquisition time. - Record the last
GET_VERSIONresponse before the.fmwrtraffic, all0x52route-selection requests, every0xA6transfer length, bulk OUT payload, bulk IN verification data, delays, and the first version response after completion. - Close iMPACT, rebind all three PIDs to WinUSB with
install.ps1, and fully power-cycle before testing a replay implementation.
The capture contains copyrighted Xilinx firmware bytes. Keep it as a local reverse-engineering artifact and implement the transport/parser independently; do not add the capture or extracted proprietary payload to a public commit.
8. XPCU USB protocol facts established during debugging¶
The operational device uses vendor request bRequest=0xB0. Important command
values are:
wValue |
Purpose |
|---|---|
0x10 |
disable |
0x18 |
enable |
0x28 |
set speed; class is carried in wIndex |
0x30 |
synchronous GPIO write |
0x38 |
status/TDO read |
0x40 |
return fixed cable constants |
0x50 |
return FX2/internal-logic version |
0x52 |
select internal/external GPIO/JTAG routing |
0xA6 |
prepare accelerated bulk JTAG transfer |
Endpoint discovery should select bulk OUT 0x02 and bulk IN 0x86 from the
active interface descriptors rather than assuming them without verification.
On Windows, a newly enumerated device can report configuration zero. The
libusb open path therefore reads the active configuration, sets configuration
1 when necessary, claims interface zero, and retries after
LIBUSB_ERROR_NOT_FOUND. Handles must be closed even when interface release
fails because firmware reload already disconnected the device.
9. Why the slow fallback produced misleading results¶
FX2 firmware 0x0404 stalls endpoint zero when the accelerated 0xA6 path is
used in the tested WinUSB and libusbK configurations. Continuing on the same
handle after that stall produced random-looking values such as 0x0C23EA80
and 0xB8B1E8C0. They were transport corruption, not FPGA IDCODEs. Perform a
complete power cycle after this failure.
The reliable fallback toggles TCK using synchronous control transfers: two USB writes per JTAG bit, plus reads when TDO is captured. The printed 6 MHz value is the requested cable clock, not effective host throughput. Reaching only 9% of a bitstream after 5--10 minutes was therefore expected and implied a total time approaching an hour.
The fallback also exposed two decoding details:
- command
0x38returns the sample following the last shifted bit, so the0x0404TDO stream must be compensated by one bit; - after a valid device, the tested cable returned trailing word
0x0A001093instead of the usual all-ones terminator. It is ignored only as a trailing artifact after at least one valid TAP has been accepted.
Before correction, the one-bit-shifted value was 0xA2014049. Restoring the
bit alignment yields the valid Spartan-6 family IDCODE 0x04028093 after
version masking.
10. Diagnostic environment variables¶
| Variable | Meaning |
|---|---|
OPENFPGALOADER_XUSB_FIRMWARE |
override the initial boot HEX |
OPENFPGALOADER_XUSB_XLP_FIRMWARE |
override embedded-cable second stage |
OPENFPGALOADER_XUSB_XP2_FIRMWARE |
override DLC10 second stage |
OPENFPGALOADER_XPCU_XLP_UPGRADE=1 |
force XLP reload |
OPENFPGALOADER_XPCU_XP2_UPGRADE=1 |
force XP2 reload |
OPENFPGALOADER_XPCU_SKIP_FIRMWARE_UPGRADE=1 |
skip any second-stage reload |
OPENFPGALOADER_XPCU_SKIP_XLP_UPGRADE=1 |
skip only XLP reload |
OPENFPGALOADER_XPCU_SKIP_XP2_UPGRADE=1 |
skip only XP2 reload |
OPENFPGALOADER_XPCU_CONTROL_BITBANG=1 |
force the slow control path |
OPENFPGALOADER_XPCU_ACCELERATED=1 |
developer override for accelerated mode |
OPENFPGALOADER_FX2_VERBOSE_USB_ERRORS=1 |
print detailed libusb failures |
OPENFPGALOADER_XPCU_CTRL_RETRIES |
control-request retry count |
OPENFPGALOADER_XPCU_CTRL_RETRY_DELAY_MS |
delay between control retries |
OPENFPGALOADER_XPCU_OUT_EP / ..._IN_EP |
developer endpoint override |
OPENFPGALOADER_XPCU_TDO_MASK |
developer-only TDO mask override |
OPENFPGALOADER_XPCU_TRACE_A6=1 |
print raw accelerated bulk-IN data for alignment debugging |
Unset experiments before a normal test:
Get-ChildItem Env:OPENFPGALOADER_XPCU* | Remove-Item
Get-ChildItem Env:OPENFPGALOADER_XUSB* | Remove-Item
11. Failure signatures and decisions¶
| Symptom | Interpretation | Correct action |
|---|---|---|
--scan-usb shows nothing |
live PID lacks WinUSB or another process owns it | inspect Device Manager; rerun elevated installer for the live PID |
remains 000d or 0013 after complete power removal |
normal cold state | select matching cable name and boot HEX |
| reload reaches 100% then returns to boot PID | wrong/incompatible HEX stage | restore the proper boot/upgrade pair |
LIBUSB_ERROR_NOT_FOUND on claim |
device has no active configuration | use build containing configuration-1 activation/retry |
| changing invalid IDCODE after A6 timeout | endpoint zero is poisoned | remove all cable/target power; do not trust that scan |
stable 0x0A001093 after valid Spartan-6 word |
XPCU control-mode trailing artifact | use corrected decoder/build |
0x0961, internal version 0xFFFE, status 00 |
DLC10 internal FPGA overlay absent | replay xusb_xp2.fmwr; target JTAG is not yet reachable |
connected: yes, no TAP |
target voltage exists but JTAG data is invalid | then check power, ground, ribbon, jumpers, TDO and clock |
12. Integrity values for this exact reproduction¶
| File | Bytes | SHA-256 |
|---|---|---|
xusb_emb.hex |
21,708 | C0A7A1F0110D110A2CE2A577EC1EBAF528FD066FEAC54F6FFC9816BE22FAC9D4 |
xusb_xlp.hex |
26,216 | 03116AB70D74E68204E03F72AD8E45E3A22428CBE612B28DDEBCEC620954270A |
xusb_xp2.hex |
24,830 | F551E3C71EFEE0556B465DC9975D528115F9849EA143F51B5C44D6F3F4F4A9CF |
xusb_xp2_loader.hex |
23,836 | B6E3FAA81C34FAD863D2DDA261E62BBBCA6FAEA4BD6ECF2FF40572BFE71EC0FE |
ISE xusb_xp2.sys |
19,840 | 02A762D7BB32CCAA97460ED143AAC00286D961302D97B964C467654F42B1EE6A |
ISE xusb_xp2.fmwr |
734,080 | 91B15AAAA52A00E55B633F74D833D0C23061D021EEF4325592C5BA3126E6A75A |
ISE xusbcpld.fmwr |
90,076 | FC1556D3BFE7486E3841F2E1B6DC62400D779ED853EB8D2662BC8943B3B0CFB4 |
The locally installed and repository xusb_xp2.hex hashes are identical.
13. Minimal regression checklist¶
For each cable family, start from a complete USB and target power removal.
$ofl = (Resolve-Path '.\dist\docker-windows\install\bin\openFPGALoader.exe').Path
& $ofl --scan-usb
& $ofl -c xilinxPlatformCableUsb_alt --detect -v --freq 6000000
& $ofl -c xilinxPlatformCableUsb_initialized --detect -v --freq 6000000
Record all of these in a test log:
- PID before and after each reload;
- selected firmware path and parsed version;
- endpoint pair;
- FX2, internal logic, and constant versions;
- status byte and sensed connection state;
- requested and real JTAG frequency;
- whether control or accelerated transfer was selected;
- every IDCODE and process exit code;
- bitstream size, elapsed time, and DONE result.
Do not combine results taken before and after a failed 0xA6 request without a
power cycle. That was the main source of false conclusions in the original
session.
14. External W25Q64 flash investigation¶
The 5042-9033 target uses an XC6SLX45T-FGG484 and identifies its attached
configuration memory in iMPACT as a Winbond W25Q64BV/CV, data width 4.
Do not use test_fw/5042-9033/pab0_top.bit with --bridge. Its BIT header is:
pab0_top.ncd;HW_TIMEOUT=FALSE;UserID=0xFFFFFFFF
6slx45tfgg484
It is a normal application image and contains no openFPGALoader SPI-over-JTAG
protocol. Unknown key HW_TIMEOUT is only an unrecognised BIT-header field; it
is not the flash failure. With that file loaded, the absent USER interface is
reported as legacy SOJ version: 1.000000 and JEDEC detection returns zero.
Omit --bridge so --fpga-part xc6slx45tfgg484 selects the packaged
spiOverJtag_xc6slx45tfgg484.bit.gz automatically:
& $ofl -c xilinxPlatformCableUsb_alt `
--fpga-part xc6slx45tfgg484 --external-flash `
-f .\test_fw\5042-9033\FW-5042-9033-T30.mcs
The packaged bridge is dated 2024-03-28 and predates the 2025 v2 bridge core. It is legitimately reported as SOJ v1 because it has no USER4 version endpoint. Raw A6 tracing nevertheless proved that its USER1 flash response is also zero:
XPCU A6 RX: bytes=4 data=93800244 # valid FPGA IDCODE stream
XPCU A6 RX: bytes=6 data=000000000000 # USER1 / JEDEC response
Reducing the requested JTAG frequency to 100 kHz (750 kHz was the cable's
minimum realised frequency) did not change the zero response. Current bridge
source uses an updated v2 protocol and opposite Spartan-6 SPI clock phase, so
the next controlled experiment is to rebuild the bridge with ISE 14.7 and test
USER4 02.00 plus JEDEC ID before erasing flash.
The local ISE installation can run iMPACT but has no user/WebPACK synthesis
license configured. Both Edalize/xtclsh and direct XST stopped before creating
a synthesis report. The temporary libPortabilityNOSH.dll compatibility test
was reverted and the original installed DLL was hash-verified and restored.
Configure a valid Spartan-6 WebPACK license with
E:\Xilinx\14.7\ISE_DS\common\bin\nt64\xlcm.exe before rebuilding:
Set-Location .\spiOverJtag
cmd.exe /d /c "call E:\Xilinx\14.7\ISE_DS\settings64.bat && python build.py xc6slx45tfgg484 spi"
Only after --detect -f prints a nonzero JEDEC ID should the MCS write be
allowed to proceed.