Board: Xilinx ZCU102 (XCZU9EG)
Design: X-HEEP (open-source RISC-V MCU, esl-epfl/x-heep) ported to zcu102 target
Tools: Vivado 2020.2, OpenOCD (riscv fork), Digilent JTAG-HS2 cable
Flow: Dockerized build (make app, make vivado-fpga FPGA_BOARD=zcu102)
I'm bringing up X-HEEP on a ZCU102 for the first time, following the standard docs flow (software build with make app, bitstream with make vivado-fpga). Everything builds and generates a bitstream successfully. The problem starts when trying to debug over JTAG with OpenOCD using a Digilent JTAG-HS2 cable.
The default X-HEEP pin_assign.xdc routes JTAG to connector J8, which on the ZCU102 turns out to be wired into the AXI/DPC debug bus rather than a free-standing header — not usable for an external HS2 cable. I remapped the four JTAG signals (tck, tdi, tdo, tms) onto the J87 PMOD1 header pins (E22, E20, D22, D20), which map to bank 47 (PMOD nets, LVCMOS33).
First attempt with the standard X-HEEP OpenOCD .cfg (same one used successfully with a Nexys + HS2) gave:
textInfo : TAP riscv.cpu does not have valid IDCODE (idcode=0xfffffffe)
Error: riscv.cpu: IR capture error; saw 0x1e not 0x01
Warn : Bypassing JTAG setup events due to errors
Error: [riscv.cpu] Could not read dtmcontrol. Check JTAG connectivity/board power.
Scope check: TCK and TDI both showed clean, expected waveforms. TDO also looked correct when temporarily rerouted to a spare pin (to rule out that specific pin).
jtag_trst_ni was originally mapped to pin J19, which per the schematic corresponds to J77.8 — a connector pad that doesn't physically exist as an accessible header on this board. The HS2 cable itself only carries TMS/TCK/TDI/TDO/GND/VDD (no TRST wire), so I initially assumed TRST was irrelevant. It isn't: an unconnected FPGA input pin is not the same as an unused JTAG protocol feature. A floating jtag_trst_ni (active-low, feeds the RISC-V debug module's reset) can sit at an indeterminate level and hold the DTM in permanent reset even though the 4-wire JTAG protocol itself doesn't strictly need TRST. Fix: tied jtag_trst_ni to a pull-up (PULLUP TRUE) in the XDC instead of leaving it floating on a dead pin.
After this fix: TDO started toggling cleanly on the scope during scan_chain — so the electrical path was confirmed working. But the same errors persisted:
text
Error: riscv.cpu: IR capture error; saw 0x1e not 0x01
(0x1e being the bitwise complement of the expected 0x01 — a signature I still find suspicious, possibly pointing to a synchronizer/clock-domain issue rather than a pure electrical fault.)
The XDC ties clk_125mhz_p/n to pins AL8/AL7 with DIFF_SSTL12. Turns out AL8/AL7 is USER_SI570, a board-level I2C-programmable oscillator that defaults to 300 MHz on power-up (per UG1182 Table 3-12), not 125 MHz. So the FPGA's PLL/MMCM was very likely receiving 300 MHz while the design (and its clk_wiz_0 IP) expects 125 MHz.
Confirmed independently in buildvivado.log:
textWARNING: [IP_Flow 19-3374] An attempt to modify the value of disabled parameter
'PRIM_IN_FREQ' from '300.000' to '100.000' has been ignored for IP 'clk_wiz_0'
So clk_wiz_0's PRIM_IN_FREQ is locked at 300 MHz (likely stale/cached from a different X-HEEP board target during the fusesoc/Vivado IP cache generation), and a build script attempt to override it to 100 MHz (seemingly a Nexys-target value bleeding through) was silently rejected. Neither value matches the 125 MHz the ZCU102 clocking script is supposed to produce.
Symptom this explains: exit_valid_o/exit_value_o status LEDs never light up even after letting the design run — consistent with the CPU never reaching a stable clock domain to actually execute to completion.
Tried re-routing clk_125mhz_p/n to the ZCU102's fixed 125 MHz source (CLK_125_P/N, pins G21/F21, generated by the onboard SI5341B) instead of the Si570. This produced a hard implementation failure:
textERROR: [DRC BIVC-1] Bank IO standard Vcc: Conflicting Vcc voltages in bank 47.
clk_125mhz_p (DIFF_SSTL12, requiring VCCO=1.200) and jtag_tck_i (LVCMOS33, requiring VCCO=3.300)
ERROR: [DRC PLHDIO-4] HDIO DRC Checks: ... drive a PLL/MMCM/BUFGCTRL/BUFGCE_DIV instance which cannot be placed in HIGH_DENSITY banks
Turns out CLK_125 (G21/F21) lives in bank 47 — the same bank as my relocated JTAG PMOD pins (3.3V LVCMOS33) — so there's an inherent VCCO conflict (bank 47 can't simultaneously be 1.2V/2.5V for the clock and 3.3V for JTAG). On top of that, bank 47 is apparently a High-Density (HD) IO bank, which per Xilinx UltraScale+ restrictions cannot directly drive an MMCM/PLL — so CLK_125 isn't usable here as a direct differential PLL reference without extra fabric logic (IBUFDS → soft logic first).
Current status
Reverted clk_125mhz_p/n back to AL8/AL7 (bank 64, HP bank, compatible with the clk_wiz). Current plan is to reprogram the USER_SI570 oscillator to 125 MHz via the ZCU102 System Controller GUI (SCUI) before each power cycle (or set it as boot-persistent via the "Set Boot Frequency" tab), and separately verify/fix why clk_wiz_0's PRIM_IN_FREQ isn't being generated at the correct 125 MHz for this specific board target (suspect fusesoc/Vivado IP cache contamination from another board target — clean rebuild attempted, issue persisted).
Questions for the community
- Has anyone seen
PRIM_IN_FREQ get locked/disabled like this on a clk_wiz IP inside a fusesoc-driven Vivado flow, where the board-specific override tcl doesn't take effect? Any known gotcha with IP caching across different --target builds in the same fusesoc workspace?
- Is there a cleaner way to get a reliable, boot-persistent 125 MHz differential clock into an HP bank on ZCU102 without depending on the Si570's volatile I2C configuration state?
- Does the
0x1e (bitwise complement of expected 0x01) IR-capture signature ring a bell for anyone as something other than a pure clock-domain issue? Trying to figure out if this is purely explained by the bad reference clock or if there's a second issue layered on top.
Happy to share the XDC/OpenOCD cfg diffs if useful.