Skip to content

feat: add QMTech XC7A100T (FGG676) board support#663

Open
gHashTag wants to merge 23 commits into
trabucayre:masterfrom
gHashTag:feat/qmtech-xc7a100t-board
Open

feat: add QMTech XC7A100T (FGG676) board support#663
gHashTag wants to merge 23 commits into
trabucayre:masterfrom
gHashTag:feat/qmtech-xc7a100t-board

Conversation

@gHashTag
Copy link
Copy Markdown

Summary

Adds support for the QMTech XC7A100T core board (FGG676 package, MT25QL128 SPI flash) and fixes the root cause that prevented xc7a100tfgg676-targeted flash programming from working at all.

The bug

spiOverJtag/spiOverJtag_xc7a100tfgg676.bit.gz was a symlink to spiOverJtag_xc7a100t.bit.gz, which is built with the csg324 pinout. csg324 and fgg676 are different physical packages with different SPI flash routing, so on a real FGG676 board the proxy bitstream drove SPI on the wrong pins. Users hit this as a JEDEC ID readback of FF FF FE and high-Z MISO instead of the expected 20 BA 18 for MT25QL128.

The root cause is in spiOverJtag/build.py: after building the bitstream for the first package in packages[family][part] (here, csg324), it ln -s'd all other package variants to that same .bit.gz. That's fine when the SPI pinout is identical across packages of one device size — but for xc7a100t it isn't.

Changes

  • spiOverJtag/constr_xc7a_fgg676.xdc — switch to the dedicated SPI configuration pins on XC7A*-FGG676 (per UG470/UG475):
    • CSnC8, MOSIB19, MISOA18, WPnB18, HOLDnA19
    • CCLK is driven internally through STARTUPE2 (unchanged).
  • spiOverJtag/build.py — accept the full device+package form (e.g. xc7a100tfgg676) and emit a package-specific bitstream. Only generate per-package symlinks when building the bare-device target, and replace pre-existing symlinks so a package-specific build wins.
  • spiOverJtag/Makefile — register xc7a100tfgg676 as its own target.
  • spiOverJtag/README.md — document package-specific bitstream builds.
  • spiOverJtag/spiOverJtag_xc7a100tfgg676.bit.gz — removed (was a stale symlink). Regenerate locally with Vivado:
    cd spiOverJtag && make spiOverJtag_xc7a100tfgg676.bit.gz
  • src/board.hpp — new qmtechArtix7_100T board entry (xc7a100tfgg676, SPI flash).
  • doc/boards.yml — list qmtechArtix7_100T with Memory: OK / Flash: OK.

Note on the regenerated bitstream

The repo previously shipped spiOverJtag_xc7a100tfgg676.bit.gz as a (broken) symlink. This PR removes that file rather than committing a new one, because Vivado is required to regenerate it and the maintainer's build environment is the canonical place for it. Reviewers / users can build it with the command above.

Test plan

  • python3 -c 'import ast, pathlib; ast.parse(pathlib.Path("spiOverJtag/build.py").read_text())' passes (syntax check).
  • src/board.hpp compiles in the openFPGALoader build (verified locally with g++ -c -std=c++11 -I src).
  • Rebuild proxy bitstream: cd spiOverJtag && make spiOverJtag_xc7a100tfgg676.bit.gz — confirms FGG676 pinout is applied (set_property PACKAGE_PIN C8 ... csn, etc.) instead of falling back to csg324.
  • On a QMTech XC7A100T FGG676 board with an MT25QL128 flash, openFPGALoader -b qmtechArtix7_100T --detect reports JEDEC ID 20 BA 18 and --write-flash / --read-flash round-trips successfully.

🤖 Generated with Claude Code

The existing spiOverJtag bitstream for xc7a100tfgg676 was a symlink to the
xc7a100t (csg324) build. csg324 and fgg676 packages use different SPI flash
pin mappings, so on a QMTech XC7A100T FGG676 core board the proxy bitstream
drove SPI on the wrong pins, making the flash unreadable (JEDEC ID returned
as 0xFFFFFE).

Changes:
- spiOverJtag/constr_xc7a_fgg676.xdc: switch to the XC7A*-FGG676 dedicated
  SPI configuration pins per UG470/UG475 (CSn=C8, MOSI=B19, MISO=A18,
  WPn=B18, HOLDn=A19). CCLK is driven internally via STARTUPE2.
- spiOverJtag/build.py: accept the full "device+package" form
  (e.g. xc7a100tfgg676) and emit a package-specific bitstream; only create
  per-package symlinks when building the bare device target, and replace
  pre-existing symlinks so package-specific builds win.
- spiOverJtag/Makefile: register xc7a100tfgg676 as its own build target.
- spiOverJtag/README.md: document package-specific bitstream builds.
- spiOverJtag/spiOverJtag_xc7a100tfgg676.bit.gz: removed (stale symlink to
  the csg324 build). Regenerate locally with Vivado:
    cd spiOverJtag && make spiOverJtag_xc7a100tfgg676.bit.gz
- src/board.hpp: add qmtechArtix7_100T board (xc7a100tfgg676, SPI flash).
- doc/boards.yml: list qmtechArtix7_100T with OK/OK status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gHashTag pushed a commit to gHashTag/t27 that referenced this pull request May 12, 2026
Refs #592 trabucayre/openFPGALoader#663

- fpga/bscan_spi_qmtech/bscan_spi_qmtech.v  - plain Verilog port of the
  openocd xilinx_bscan_spi.py Migen module (BSCANE2 USER1 + STARTUPE2 +
  marker/length/data shift state machine).
- fpga/bscan_spi_qmtech/bscan_spi_qmtech.xdc - FGG676 dedicated SPI pin
  LOCs (C8/B19/A18 = FCS_B/MOSI/DIN), LVCMOS33, SPI_BUSWIDTH=1.
- fpga/bscan_spi_qmtech/Makefile - standalone openXC7 driver
  (yosys + nextpnr-himbaechel + fasm2frames + xc7frames2bit).
- cli/tri/src/fpga.rs - new tri fpga build-proxy [--install] subcommand
  that drives the same pipeline through std::process, no shell or Python.
- docs/fpga/SPI_FLASH_DEBUG.md - new "Solution" section with the openXC7
  build flow and a pointer to the Vivado-based PR #663 fallback.
- docs/NOW.md - entry for 2026-05-12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@trabucayre
Copy link
Copy Markdown
Owner

The board integration LGTM.
It's interesting for the spiOverJtag bitstream: during modification to have only one bitstream per size, I have tried differents combo and I have observed at the end the SPI interface (die point of view) is always correctly connected to pins (package point of view). Maybe in some specifics case, like this one, this is not true.

gHashTag pushed a commit to gHashTag/t27 that referenced this pull request May 12, 2026
…ing blocked

bbaexport + bbasm + nextpnr-xilinx user-pin route all succeed on
xc7a100tfgg676-1 (Fmax 254 MHz). The real proxy bitstream requires
LOC C8/B19/A18 onto FCS_B/DQ0/DQ1 (dedicated configuration pins via
STARTUPE2). openXC7 pack_clocking_xc7.cc aborts in dict::at() at
prepare_clocking after placing cs_n on OPAD_X0Y10 (GTP_CHANNEL). This
matches trabucayre/openFPGALoader#663 — the spiOverJtag FGG676 build
is currently Vivado-only across the open-source ecosystem.

Docker-Vivado (ce0f7ae build-proxy-docker) remains the SSOT for
fpga/tools/bscan_spi_xc7a100t.bit until openXC7 grows STARTUPE2
support.

Closes #592

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude Agent and others added 10 commits May 12, 2026 09:35
Adds .github/workflows/build-fgg676.yml that attempts to build the
package-specific spiOverJtag_xc7a100tfgg676.bit.gz inside a public
Vivado Docker image. The job:

  - Frees disk space on the ubuntu-latest runner
  - Pulls fpgatools/vivado:latest (best-effort public image)
  - Sources settings64.sh, installs edalize, runs
    'python3 build.py xc7a100tfgg676 spi'
  - Verifies the resulting bitstream contains the string
    '7a100tfgg676' (not '7a100tftg256') and reports sha256
  - Uploads spiOverJtag-fgg676 artifact with .bit, .bit.gz and
    Vivado logs from tmp_xc7a100tfgg676/

This complements upstream PR trabucayre#663 which fixed build.py to honor an
explicit device+package argument so package-specific bitstreams are
no longer aliased to the first-package build (ftg256).
The fpgatools/vivado:latest image launches as a non-root user, so
apt-get update fails with EACCES and pip3 is not preinstalled. Run
the container with --user 0:0 and install python3/python3-pip via
apt before pip-installing edalize. Also chown/chmod the build
outputs so they are readable by the runner user after the container
exits.
The fpgatools/vivado:latest container ships Ubuntu 18.04 with system
Python 3.6. Newer edalize 0.6.x pulls Jinja2 3.x which uses
importlib.metadata APIs that don't work cleanly under 3.6, leading to
'A loader was not found for the package' AssertionError from
PackageLoader('edalize', 'templates'). Pin edalize==0.4.0 and
Jinja2<3 / MarkupSafe<2.1 to match what the build was originally
written against.
edalize 0.4.0 already declares 'Jinja2>=3' in install_requires, which
clashes with Python 3.6 / setuptools 39 in the Ubuntu 18.04 Vivado
container. edalize 0.2.4 has no formal install_requires, so a manual
Jinja2 2.11.3 + MarkupSafe < 2.1 pin works.
edalize 0.2.x predates the get_edatool API exposed by spiOverJtag/build.py,
so 'from edalize.edatool import get_edatool' fails with ImportError.
edalize 0.3.0 is the earliest release that exposes get_edatool *and*
declares Jinja2 >= 2.11.3 (without forcing >= 3), so pin to that combo.
Drop the 'pip install --upgrade pip<21' step — when run as 'python3 -m pip'
it succeeds but apparently confuses the shell state (Run build step
silently exits after Successfully installed pip-20.3.4). Use the
stock pip 9.0.1 directly with --disable-pip-version-check. Also
verify that 'from edalize.edatool import get_edatool' works before
invoking build.py.
The ca-certificates trigger inside fpgatools/vivado:latest can return
non-zero (likely because of how Xilinx ships its own Java cacerts
hook). Under 'set -e' that silently terminates our entire bash block
before python3 build.py ever runs — the Run-build step appears to
succeed but exits in ~10 s with no useful log lines past 'Running
hooks in /etc/ca-certificates/update.d'. Drop 'set -e' and gate
apt-get with '|| true', then explicitly verify python3 is on PATH.
The comment 'A loader was not found for the package' contained an
apostrophe which terminated the surrounding 'bash -c'...' single-quoted
string. Bash then exited cleanly after the python3 --version line and
the rest of the build script (pip install + python3 build.py) was
silently dropped. That's why Run-build kept finishing in ~11 s with
no error — there *was* no command to run.
Replace the (unreliable) public Vivado Docker image strategy with a real
silent install of AMD Vivado ML Standard on the runner. Free ~45 GiB with
easimon/maximize-build-space, fetch the AMD Unified Installer via a
presigned URL from the XILINX_INSTALLER_URL secret, run xsetup
AuthTokenGen with XILINX_USER / XILINX_PASS to authorize the web
installer to pull device data, and install only the Artix-7 family to
keep the footprint inside the disk budget.

Builds spiOverJtag_xc7a100tfgg676.bit via python3 build.py and verifies
the embedded device string is 7a100tfgg676 (not the wrong ftg256).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The heredoc body at column 0 collapsed the surrounding 'run: |'
literal block, causing GitHub Actions to reject the workflow with
'workflow file issue' (0 jobs scheduled). Replace with a block of
printf statements at the correct indent so YAML parses cleanly.

Validated locally with python yaml.safe_load.

Updates t27#592
@trabucayre
Copy link
Copy Markdown
Owner

The modifications in spiOverJtag directory, boards.yml and boards.hpp make sense. I'm not convince by the CI integration: the bitstream is built and tested by the contributor so this part is not relevant.
Thanks

@trabucayre
Copy link
Copy Markdown
Owner

Could you open a new PR for the specific case of the QMTech / bitstream?

gHashTag added 12 commits May 12, 2026 16:01
Bypass the AMD presigned-URL/CAPTCHA flow by staging the unmodified
Vivado Web Installer .bin (sha256 a78dbe89…) as a release asset on
this fork (tag: vivado-installer-2025.2). The CI job now downloads it
with 'gh release download' using the automatic GITHUB_TOKEN — no
human-in-the-loop, no XILINX_INSTALLER_URL secret needed.

The runner still authenticates to xilinx.com at install time to fetch
the Artix-7 device payload (~10 GiB), but that download happens at
gigabit on a GitHub-hosted runner instead of 40 KB/sec via QEMU on the
Mac, so the previously-fatal bandwidth blocker is gone.

Updates t27#592
AMD's xsetup -b AuthTokenGen requires an interactive TTY because the
Java installer reads the password via /dev/tty. GitHub-hosted runners
run in non-interactive mode (FATAL Could not get a console!!!).

Replace the runtime AuthTokenGen with a pre-generated
wi_authentication_key shipped as base64 repo secret
WI_AUTHENTICATION_KEY_B64. Lifetime ~7 days; regenerate locally with
xsetup -b AuthTokenGen and rotate the secret.

Drops dependency on XILINX_USER and XILINX_PASS for the install step
(still used by Vivado at runtime via the token, just not via stdin).

Updates t27#592
xsetup for Vivado ML Standard 2025.2 rejects WebTalkTerms in the
--agree list and prints the EULA help text, exiting with code 1.
Only XilinxEULA and 3rdPartyEULA are valid here. The WebTalk consent
is already disabled via InstallOptions in the config file.

Updates t27#592
Download, extract, install, and tmp directories were all on the root
partition which only has ~145G total and was at 100% used (357M free)
right after the 363MB installer download. easimon/maximize-build-space
provisions ~104G of dedicated space at ${{ github.workspace }} but the
workflow never used it.

Move INSTALLER_DIR, XSETUP_EXTRACT_DIR, XILINX_INSTALL_DIR and TMPDIR
under ${{ github.workspace }}. Drop sudo for the install dir since it
is now owned by runner. Print df -h before and after install for
diagnostics.

Updates t27#592
The hand-written Modules= line listed 'System Generator for DSP'
which the 2025.2 installer no longer accepts, causing xsetup to
exit with 'value specified ... is not valid' before installing.

Generate a reference config from the installer itself
(xsetup -b ConfigGen), then patch it with awk:
  - Destination -> LVM build-mount install dir
  - Modules     -> Artix-7:1, every other entry :0
  - InstallOptions -> every option :0
  - Create*Shortcuts/FileAssociation -> 0

This is resilient to AMD renaming or removing modules between
Vivado releases. No shell scripts or python (constitution rule).

Updates t27#592
xsetup -b ConfigGen is interactive (asks for edition choice on stdin)
and on a non-TTY shell it spammed 'Not a valid choice' indefinitely,
producing >700 MB of logs in 6 minutes and killing the runner.

Revert to a hand-written install_config.txt but remove 'System Generator
for DSP', which the 2025.2 installer no longer recognizes. All other
module names match the canonical schema.

Updates t27#592
Every hand-written install_config.txt is doomed to drift: the 2025.2
installer no longer accepts 'System Generator for DSP' (run 25748594744
failed on 'Vivado Simulator' too).

Per UG973 (Running the Installer, 2025.2), invoking xsetup with
--edition / --product / --location uses the installer's own default
module selection, which is by construction self-consistent with its
schema. The 'Vivado ML Standard' default install for Vivado is ~30 GB,
well under our 103 GB build-mount.

The install_config step still runs and writes the file for diagnostic
purposes, but Install Vivado no longer consumes it.

Updates t27#592
Run 25748987884 hung 19+ minutes on a single apt-get install batch.
The likely culprits are (a) an unattended-upgrades dpkg lock held by
the runner image, and (b) the legacy libtinfo5/libncurses5 packages
which no longer exist on the ubuntu-24.04 runner.

Changes:
  - Wait up to 25s for the dpkg lock to free up before proceeding.
  - Wrap apt-get update and the strict install in `timeout`, so a
    network hang fails the step in minutes instead of forever.
  - Drop libtinfo5/libncurses5 entirely (gone in ubuntu-24.04).
  - Install GUI libs one-by-one with their own per-pkg timeout, so
    a single missing package skips instead of taking down the batch.

Updates t27#592
Run 25750098370 hit apt-get update timeout (180s) against a flaky
mirror and aborted the whole step. The github-runner image ships an
up-to-date package cache, so we can safely continue with cached lists
when update is slow.

Updates t27#592
Run 25750432794 finally reached Install Vivado, accepted the EULAs,
and failed with:
  ERROR - At least one device must be selected.
  To avoid this error, add a valid Modules entry...
  Modules=Zynq UltraScale+ MPSoC:1,Engineering Sample Devices:0,DocNav:1

So --edition/--product alone don't pick any device families. Write a
minimal install_config that lists ONLY Artix-7:1 (the architecture
QMTech XC7A100T uses) and DocNav:0 (cited by xsetup as a valid module
name). Omit every other module entry entirely so we can't trip on
renamed/removed names like 'System Generator for DSP' (2025.1) or
'Vivado Simulator' (2025.2).

Updates t27#592
The previous constr_xc7a_fgg676.xdc tried to use dedicated configuration
bank pins (C8/B19/A18/B18/A19) which Vivado rejected on XC7A100T-FGG676:
B19/A19 fall in GTP bank terminals and B18 is not a valid package pin.

Per QMTECH_XC7A75T_100T_200T-CORE-BOARD schematic
(ChinaQMTECH/QMTECH_XC7A75T-100T-200T_Core_Board), the on-board
N25Q064A SPI flash is wired through Bank 14 user IO dual-purpose pins
(D00..D03, FCS_B). These match the existing constr_xc7a_fbg676.xdc
because FBG676/FGG676 share ball-out for XC7A75T/100T/200T.

  PACKAGE_PIN P18 -> csn         (FCS_B / IO_L6P_T0_FCS_B_14)
  PACKAGE_PIN R14 -> sdi_dq0     (D00 / IO_L1P_T0_D00_MOSI_14)
  PACKAGE_PIN R15 -> sdo_dq1     (D01 / IO_L1N_T0_D01_DIN_14)
  PACKAGE_PIN P14 -> wpn_dq2     (D02 / IO_L2P_T0_D02_14)
  PACKAGE_PIN N14 -> hldn_dq3    (D03 / IO_L2N_T0_D03_14)

CCLK is driven internally via STARTUPE2 in xilinx_spiOverJtag.v.

Updates t27#592
Without JTAGCLK the startup sequencer never reaches EOS=1 when the
bitstream is loaded over JTAG (DLC10/Platform Cable). Symptoms:
INIT_COMPLETE=1, MMCM_LOCK=1, CRC_ERROR=0, ID_ERROR=0 but DONE=0
and EOS=0 forever (STAT=0x4000190C on XC7A100T).

UG470 §6.3 Table 6-3: STARTUPCLK selects the clock for the 8-step
startup FSM. Default is CFGCLK = CCLK from STARTUPE2, which is only
driven during SelectMAP / SPI configuration, not during JTAG load.
JTAGCLK reuses TCK so the sequencer can complete under JTAG.

Affects only XC7A*-FGG676 (QMTech XC7A100T core board).
gHashTag pushed a commit to gHashTag/t27 that referenced this pull request May 14, 2026
Refs #592 trabucayre/openFPGALoader#663

- fpga/bscan_spi_qmtech/bscan_spi_qmtech.v  - plain Verilog port of the
  openocd xilinx_bscan_spi.py Migen module (BSCANE2 USER1 + STARTUPE2 +
  marker/length/data shift state machine).
- fpga/bscan_spi_qmtech/bscan_spi_qmtech.xdc - FGG676 dedicated SPI pin
  LOCs (C8/B19/A18 = FCS_B/MOSI/DIN), LVCMOS33, SPI_BUSWIDTH=1.
- fpga/bscan_spi_qmtech/Makefile - standalone openXC7 driver
  (yosys + nextpnr-himbaechel + fasm2frames + xc7frames2bit).
- cli/tri/src/fpga.rs - new tri fpga build-proxy [--install] subcommand
  that drives the same pipeline through std::process, no shell or Python.
- docs/fpga/SPI_FLASH_DEBUG.md - new "Solution" section with the openXC7
  build flow and a pointer to the Vivado-based PR #663 fallback.
- docs/NOW.md - entry for 2026-05-12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gHashTag pushed a commit to gHashTag/t27 that referenced this pull request May 14, 2026
…ing blocked

bbaexport + bbasm + nextpnr-xilinx user-pin route all succeed on
xc7a100tfgg676-1 (Fmax 254 MHz). The real proxy bitstream requires
LOC C8/B19/A18 onto FCS_B/DQ0/DQ1 (dedicated configuration pins via
STARTUPE2). openXC7 pack_clocking_xc7.cc aborts in dict::at() at
prepare_clocking after placing cs_n on OPAD_X0Y10 (GTP_CHANNEL). This
matches trabucayre/openFPGALoader#663 — the spiOverJtag FGG676 build
is currently Vivado-only across the open-source ecosystem.

Docker-Vivado (ce0f7ae build-proxy-docker) remains the SSOT for
fpga/tools/bscan_spi_xc7a100t.bit until openXC7 grows STARTUPE2
support.

Closes #592

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants