Skip to content

xilinx: inferred DSP48E1 ignores its A operand -- INMODE/ALUMODE2/3/OPMODE6 never get their tile constant bits - #159

Open
jasonzeng124 wants to merge 2 commits into
openXC7:mainfrom
jasonzeng124:fix/dsp-const-only-pins
Open

jasonzeng124 wants to merge 2 commits into
openXC7:mainfrom
jasonzeng124:fix/dsp-const-only-pins

Conversation

@jasonzeng124

Copy link
Copy Markdown

Summary

A design whose entire datapath is assign p = a * b; returns wrong products on hardware when the multiply is inferred into a DSP48E1. The netlist is correct, post-synthesis simulation of that netlist passes, and timing is clean — the fault is introduced in the FASM writer, so nothing before the bitstream shows it.

Measured on an EBAZ4205 (xc7z010clg400), reading results back over JTAG:

test before after
two lone 16×16 DSPs, 415 random vectors 1/315 and 315/315 415/415 and 415/415
32×32 through a 3-DSP cascade, 430 vectors 25/430 430/430
a real compiled kernel that uses a multiply 965/2016 516/516

Bit-walk before the fix: sweeping all 16 bits of A with B=1 changed the output not at all — one lane returned 0 for every bit, the other a constant 0x18000000. Sweeping B moved the output but to wrong values. After the fix, all 16 A bits and all 16 B bits are exact on both DSPs.

The failure was deterministic and clock-independent — dropping FCLK0 from 100 MHz to 10 MHz reproduced it bit-for-bit — which is what ruled out timing and pointed at the bitstream.

Cause

Some DSP48E1 site pins have no interconnect path into the site at all. In prjxray they appear in segbits_dsp_{l,r}.db only as DSP_<n>_<PIN>.DSP_GND_* / DSP_<n>_<PIN>.DSP_VCC_*, and in no ppips/pips list, so a tile-local constant bit is the only way to give them a value. That set is:

D0..D24  RSTD  CARRYINSEL2  CED  CEAD  CEINMODE  CEALUMODE
INMODE0..4  ALUMODE2  ALUMODE3  OPMODE6

pack_dsps() converted the first seven groups and left INMODE, ALUMODE2/3 and OPMODE6 out. Those pins stayed on \$PACKER_VCC_NET/\$PACKER_GND_NET and were handed to the router, which has nowhere to take them. Nothing errors, nothing warns, no bit is emitted — and on silicon the pin comes up as the complement of what the netlist asked for.

For a plain inferred multiply the netlist wants INMODE=00000, OPMODE=0000101, ALUMODE=0000. The part instead sees INMODE=11111, OPMODE=1000101, ALUMODE[3:2]=11:

  • INMODE[1]=1 gates the multiplier's A operand to zero (UG479 Table 1-11) — that is the "A does nothing" symptom exactly.
  • OPMODE[6:4]=100 feeds P back into the Z mux instead of zero, a combinational loop through the adder, hence the near-constant output.
  • ALUMODE[3:2]=11 changes the ALU function on top of that.

The second half, in fasm.cc

These tile bits bypass the site's optional input inverter — the ZIS_*_INVERTED bits apply only to the routed pins — so the constant chosen has to be the logical value. write_const_pins() already tried to do that, but it stripped the digits off the pin name before the parameter lookup, asking for IS_INMODE_INVERTED when yosys emits the per-bit IS_INMODE[1]_INVERTED. The lookup always returned false, so every bussed pin got the un-flipped constant.

That is almost certainly the // TODO: these seem to be inverted for unknown reasons that kept INMODE/ALUMODE2/ALUMODE3 commented out of the packer's list in the first place — I can confirm it experimentally: adding the pins without fixing the lookup ties them to VCC and makes the DSP strictly worse (fully inert, constant 0x08000000 on every vector). Both halves are needed.

Commits

  1. AREG_2_ACASCREG_1 is a conjunction, not ACASCREG — prjxray defines the feature as (AREG == 2 && ACASCREG == 1) (fuzzers/100-dsp-mskpat/generate.py), but the Z-form bit was derived from ACASCREG alone, so the AREG=1, ACASCREG=1 configuration synth_xilinx emits when it absorbs an input register wrote no bit and reads back as AREG=2. Found on the way; a latent mis-encoding rather than the cause above — the board result was bit-identical before and after it. Happy to split this out if you would rather review it separately.
  2. The fix above — the packer's pin list plus the fasm.cc lookup.

Testing

Built with the openXC7 toolchain and run on real hardware (EBAZ4205 / xc7z010clg400, Zynq PS driving the PL over AXI, results read back over JTAG). Numbers in the table above. No change in behaviour for designs without a DSP48E1; for DSP designs the only difference in the emitted FASM is eight added DSP_<n>_<PIN>.DSP_GND_* lines per DSP.

jasonzeng124 and others added 2 commits August 20, 2026 11:20
prjxray defines the DSP48E1 feature AREG_2_ACASCREG_1 as
(AREG == 2 && ACASCREG == 1) -- see fuzzers/100-dsp-mskpat/generate.py.
fasm.cc derived its Z-form bit from ACASCREG alone, ignoring AREG, so the
configuration yosys emits whenever synth_xilinx absorbs one level of input
register into the multiplier (AREG=1, ACASCREG=1) wrote no bit at all.
Silicon reads that back as AREG=2: one input pipeline stage more than the
netlist asked for.  Same for BREG/BCASCREG.

Found while bisecting wrong products out of an inferred DSP48E1 on
xc7z010clg400 (EBAZ4205).  This one is a latent mis-encoding rather than
the cause of those wrong products -- the design under test read its result
milliseconds later, so an extra input pipeline stage was invisible to it
and the board result was bit-identical before and after.  The real cause is
the following commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some DSP48E1 site pins have no interconnect path into the site at all.
In prjxray they appear in segbits_dsp_{l,r}.db only as

    DSP_[LR].DSP_<n>_<PIN>.DSP_GND_[LR]
    DSP_[LR].DSP_<n>_<PIN>.DSP_VCC_[LR]

and in no ppips/pips list, so a tile-local constant bit is the ONLY way to
give them a value.  For xc7z010 (and every other 7-series part in the db)
that set is

    D0..D24  RSTD  CARRYINSEL2  CED  CEAD  CEINMODE  CEALUMODE
    INMODE0..4  ALUMODE2  ALUMODE3  OPMODE6

pack_dsps() converted the first seven groups and left the last three out.
Those pins therefore stayed attached to $PACKER_VCC_NET / $PACKER_GND_NET
and were handed to the router, which has nowhere to take them.  That is not
a routing failure -- nothing errors, nothing warns, no bit is emitted, and
on silicon the pin comes up as the complement of what the netlist asked
for.

For a plain inferred `a * b` the netlist wants INMODE=00000, OPMODE=0000101
and ALUMODE=0000.  Without the bits the part sees INMODE=11111,
OPMODE=1000101 and ALUMODE[3:2]=11.  INMODE[1]=1 gates the A operand of the
multiplier to zero (UG479 Table 1-11) and OPMODE[6:4]=100 feeds P back into
the Z mux instead of zero, so the DSP ignores A entirely and returns
near-constant junk -- with a correct netlist, a correct post-synthesis
simulation and clean timing.  Nothing before the bitstream shows it.

The second half of the fix is in fasm.cc.  These bits bypass the site's
optional input inverter -- the ZIS_*_INVERTED bits apply only to the routed
pins -- so the constant chosen has to be the LOGICAL value.
write_const_pins() already tried to do that, but it stripped the digits off
the pin name before looking the parameter up, asking for
IS_INMODE_INVERTED when yosys emits the per-bit IS_INMODE[1]_INVERTED.  The
lookup always returned false, so every bussed pin got the un-flipped
constant.  That is almost certainly the "TODO: these seem to be inverted
for unknown reasons" that kept INMODE and ALUMODE2/3 commented out here in
the first place: adding them without fixing the lookup ties them to VCC and
makes the DSP worse, not better.

Measured on an EBAZ4205 (xc7z010clg400), yosys 0.5x + this nextpnr, reading
results back over JTAG.  A 16x16 multiply whose entire datapath is one `*`:
before, walking all 16 bits of A changed the output not at all (one lane
returned 0 for every bit, another a constant 0x18000000) and walking B gave
wrong values; after, all 16 A bits and all 16 B bits are exact.  Full runs,
before -> after:

    two lone 16x16 DSPs, 415 random vectors :  1/315, 315/315  ->  415/415, 415/415
    32x32 via a 3-DSP cascade, 430 vectors  :         25/430   ->        430/430
    a real compiled kernel using a multiply :        965/2016  ->        516/516

The failure was deterministic and clock-independent: dropping FCLK0 from
100 MHz to 10 MHz reproduced it bit-for-bit, which is what ruled out timing
and pointed at the bitstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hansfbaier

Copy link
Copy Markdown
Collaborator

Can you supply a design to reproduce as a unit test? Gist would be fine.

@jasonzeng124

Copy link
Copy Markdown
Author

https://gist.github.com/jasonzeng124/25677dfa5b51533036b9a8b79ab5f38a here you go, i'm not sure if it really makes sense as a unit test without running it on hardware though.

@hansfbaier hansfbaier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two-axis review (Standards / Spec)

Reviewed a204ae52 vs base 68aeeb39 (three-dot): 2 commits, 2 files (xilinx/fasm.cc, xilinx/pack_dsp_xc7.cc), +72/−8. Spec = the PR body; standards = docs/coding.md (+ .clang-format, not CI-enforced).

Standards

No violations of docs/coding.md (it's a philosophy doc, no rule broken here). Both hunks use the existing helpers exactly as neighbours do; acascreg/bcascreg mirror areg/breg at fasm.cc:5120/5129. Judgement calls:

  • Duplicated Code — the new inversion lookup in write_const_pins repeats the shape of write_bus_zinv (~145 lines above, same function): (int_or_default("IS_"+name+"_INVERTED") >> i) & 0x1 OR'd with the per-bit form. Two copies of "resolve a per-bit inversion param" now live in one file; consider one pin_inverted(name, bit) helper used by both.
  • Dead branch — when idx is empty, pin_basename == pin, so the else re-reads the identical IdString already read into inv; the OR can never change inv for RSTD/CED/CEAD/CEINMODE/CEALUMODE.
  • Speculative Generality — three parameter spellings per pin are accepted, while the new comment itself says yosys only emits the per-bit IS_INMODE[1]_INVERTED form; the IS_INMODE1_INVERTED lookup covers a form the change asserts nobody writes.
  • Primitive Obsession / Data Clumpsstd::string idx carries a bit index as text through all three lookups and is converted back with an unguarded std::stoi(idx); (basename, bit) wants to be a small type.
  • IdString churn (soft, consistent with pre-existing code) — up to 3 constructed names interned per const pin per DSP, most matching no parameter.
  • Shotgun Surgery — one logical change ("these pins get a tile-local constant bit") required edits in both files; the pin set lives in a boost::starts_with cascade (now 9 predicates) instead of one table both sites read.
  • Comment-to-code ratio — 43 of 72 added lines are comment; the pack hunk adds 21 comment lines for 4 code lines. The UG479/prjxray citations are worth keeping; the defect narrative belongs in the commit message.

Spec

(a) Missing/partial: none. All three spec'd changes are present. No automated test, but the spec only claims hardware testing.

(b) Scope creep: commit 1 (ZAREG_2_ACASCREG_1 / ZBREG_2_BCASCREG_1 conjunction) is unrelated to the PR title — the body itself says "latent mis-encoding rather than the cause above" and offers to split it out.

(c) Overclaimed / wrong:

  1. "The only difference in the emitted FASM is eight added DSP_<n>_<PIN>.DSP_GND_* lines per DSP" is false in general. For AREG=1 && ACASCREG=1 (the register-absorption config the PR says synth_xilinx emits), the old !bool_or_default(ACASCREG) emitted no line, while the new !(areg==2 && acascreg==1) emits a ZAREG_2_ACASCREG_1 line (plus ZBREG_2_BCASCREG_1 for B) — up to two extra lines beyond the eight. The "eight" holds only for the plain-multiply tests.
  2. Undisclosed default flip: ACASCREG/BCASCREG default changed 0 → 1 (bool_or_default(..., ACASCREG) vs int_or_default(..., ACASCREG, 1)). With AREG=2 and the parameter absent, old emitted ZAREG (bit 0), new emits nothing (bit 1). Likely benign, but worth a line in the description.

Verified correct (attempted falsification against prjxray-db, all failed):

  • The const-pin set is exactly complete vs zynq7/segbits_dsp_l.db (D0–24, RSTD, CARRYINSEL2, CED, CEAD, CEINMODE, CEALUMODE, INMODE0–4, ALUMODE2/3, OPMODE6); no GND/VCC entry exists outside the packer list.
  • ppips_dsp_l.db has ALUMODE0/1 and OPMODE0–5 but no ALUMODE2/3, OPMODE6 or INMODE0–4 (the only "INMODE" match is RSTINMODE, a different port) — the "no routing path" claim holds.
  • AREG_2_ACASCREG_1 is defined as (AREG == 2 && ACASCREG == 1) in fuzzers/100-dsp-mskpat/generate.py; the db has no standalone ACASCREG bit.
  • areg/breg are in scope at the new write_bit calls; the bitmask shift mirrors write_bus_zinv and idx ≤ 24 keeps it in range.

Bottom line

Standards: 0 hard violations, 7 judgement-call smells — worst is the inversion-resolution logic duplicated across the two fasm.cc lambdas plus the pin-set knowledge split across both files. Spec: 3 findings — worst is the overstated FASM-delta claim, alongside the undisclosed ACASCREG/BCASCREG default flip. The fix itself looks technically sound and is hardware-validated; splitting commit 1 out (as the author offered) would make the history cleaner.

hansfbaier added a commit that referenced this pull request Aug 26, 2026
@hansfbaier

Copy link
Copy Markdown
Collaborator

@jasonzeng124 I tested the reproducer design on Spartan7 and could not reproduce the issue on hardware

https://gist.github.com/jasonzeng124/25677dfa5b51533036b9a8b79ab5f38a here you go, i'm not sure if it really makes sense as a unit test without running it on hardware though.

Hi I created a design for a hardware board, that output the DSP results over serial. Can you adapt and test this on your board:
https://github.com/openXC7/demo-projects/tree/main/dsp-test-arty-s7
?

@jasonzeng124

Copy link
Copy Markdown
Author

Hi, unfortunately I'm not around my board for a while. I do remember it was a zynq-7010 on a ebaz4205 board though.

hansfbaier added a commit that referenced this pull request Aug 29, 2026
#158 (LUT6_2 X_ORIG_PORT) is fixed and merged into main (7cfd1e9), and
toolchain-nix is pinned to 4f78e46 (which pins nextpnr 7cfd1e9), so
lut_shared_pin is green again and back in the regression default suite.

dsp-const-only-pins stays disabled as expected-red until #159 lands.  The
gate checks out toolchain-nix and demo-projects at their default branches
(main), currently 4f78e46 and 72e1ced respectively, so no explicit rev pin
needs bumping here.
loganw234 added a commit to loganw234/cft-fp256 that referenced this pull request Sep 1, 2026
…in flight

The capacity table's load-bearing caveat said the DSP48E1 assumption
was unverified, quoting the 'Clocked DSP48E1s are currently
unsupported' error. Checked against the toolchain on 2026-08-31: that
error was gatecat's 2020 placeholder and the openXC7 fork removed it
on 2023-03-07 with real FASM generation (24113d1); as of 0.9.3 the
full chain infers, places, routes and bitstreams clocked and cascaded
DSP48E1, and the Kintex-7 segbits are in prjxray-db. What remains is
openXC7/nextpnr-xilinx#159 (const-only control pins, wrong products on
Zynq-7010, fix unmerged), an ~1.5x DSP-count penalty from Yosys's
18x18 partial decomposition that flips no verdict, and timing that
ignores registered DSPs. The paragraph now says so, with dates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shenki added a commit to shenki/nextpnr-xilinx that referenced this pull request Sep 9, 2026
A constant sink whose site wire has no uphill pip (the DSP48E1 INMODE,
ALUMODE and OPMODE pins of openXC7#159) is unreachable for any net.  Giving
it a driver LUT only made the re-route fail hard inside the router,
before --allow-const-holdouts was consulted, so the flag never covered
that class.  Send such sinks straight to the report, with their own
reason, and narrow the help text: a driver LUT net the router cannot
route is still a router error.
shenki added a commit to shenki/nextpnr-xilinx that referenced this pull request Sep 10, 2026
A constant sink whose site wire has no uphill pip (the DSP48E1 INMODE,
ALUMODE and OPMODE pins of openXC7#159) is unreachable for any net.  Giving
it a driver LUT only made the re-route fail hard inside the router,
before --allow-const-holdouts was consulted, so the flag never covered
that class.  Send such sinks straight to the report, with their own
reason, and narrow the help text: a driver LUT net the router cannot
route is still a router error.
shenki added a commit to shenki/nextpnr-xilinx that referenced this pull request Sep 10, 2026
A constant sink whose site wire has no uphill pip (the DSP48E1 INMODE,
ALUMODE and OPMODE pins of openXC7#159) is unreachable for any net. Giving it a
driver LUT only made the re-route fail hard inside the router, before
--allow-const-holdouts was consulted, so the flag never covered that
class. Send these sinks to the report and clarify the help text.
hansfbaier pushed a commit that referenced this pull request Sep 11, 2026
A constant sink whose site wire has no uphill pip (the DSP48E1 INMODE,
ALUMODE and OPMODE pins of #159) is unreachable for any net. Giving it a
driver LUT only made the re-route fail hard inside the router, before
--allow-const-holdouts was consulted, so the flag never covered that
class. Send these sinks to the report and clarify the help text.
@hansfbaier

Copy link
Copy Markdown
Collaborator

Independent check of this PR against current main (5fb8f5d2, which now includes #184). Short version: the FASM-level gap you describe is real and reproduces exactly as you say, but on xc7s50 the bitstream difference it produces is behaviourally invisible, and I could not reproduce the hardware failure. I have a board test that decides the AREG commit, plus one question that decides the rest of the PR.

1. The packer/fasm gap is real

Rebased your two commits onto 5fb8f5d2 (only xilinx/fasm.cc needed a real merge; clean otherwise) and diffed the FASM on three families, same design, same chipdb, only the binary differs:

part / family main this PR
xc7s50 spartan7 0/8 const-only pins tied 8/8
xc7a200t artix7 0/8 8/8
xc7z010clg400 zynq7 0/8 8/8

The added features are exactly ALUMODE2, ALUMODE3, INMODE0..4, OPMODE6, all DSP_GND_*. For prjxray cross-checking: in the DSP48 tile the set of pins whose only fuzz-derived features are DSP_GND_*/DSP_VCC_* is 39, of which main's packer ties 31 and omits exactly these 8 — and all 8 are in invertible_pins[DSP48E1], which is why they (and not the other 8) end up on $PACKER_VCC_NET with IS_*[n]_INVERTED=1. So it is an inconsistency inside the packer's own scheme, not a style question. The zero-cost part of your diagnosis — write_const_pins() stripping digits and asking for IS_INMODE_INVERTED where yosys emits IS_INMODE[1]_INVERTED — is also exactly right on main.

2. One correction to the mechanism

they appear in segbits_dsp_{l,r}.db only as DSP_<n>_<PIN>.DSP_GND_* / DSP_VCC_*, and in no ppips/pips list, so a tile-local constant bit is the only way to give them a value … Nothing errors, nothing warns, no bit is emitted

That is true of the fuzz-derived files (no segbit and no ppip for e.g. DSP_FAN7_2->DSP_0_OPMODE6), but prjxray also ships a Vivado-derived tile model — tile_type_DSP_{L,R}.json — and that file does list DSP_FAN7_1->DSP_0_INMODE1, DSP_FAN7_2->DSP_0_OPMODE6, DSP_FAN0_0->DSP_0_ALUMODE2, and DSP_{GND,VCC}_R->… for all eight (is_pass_transistor=1, is_pseudo=0). Current main routes exactly that path and does emit bits — a VCC chain through the INT tile:

INT_R_X9Y90.BYP0.BYP_ALT0 / BYP_ALT0.VCC_WIRE / FAN0.FAN_ALT0 / FAN_ALT0.VCC_WIRE
INT_R_X9Y91.FAN7.FAN_ALT7 / FAN_ALT7.VCC_WIRE   (…)      # 16-17 fabric bits

so "no bit is emitted" isn't accurate for main; rather, the last hop is programmed only in prjxray's Vivado model, and the question is whether the tile constant is sampled before or after the site input inverter. main assumes after-inverter (logical 0 = VCC with the inverter active); your fasm.cc half assumes before-inverter (logical 0 = GND). That is the crux of the PR, it is the part prjxray cannot answer from the fuzz data, and it is the sentence I'd soften.

3. Hardware: no behavioural difference on xc7s50

Arty S7-50, demo-projects/dsp-test-arty-s7 (your gist verbatim), volatile JTAG load, UART read back, A/B/A so a one-off can't be mistaken for an effect — current main, then this PR, then main again:

phase bitstream result
A1 main 9f5a76b4… PASS — 1831/1831 lines satisfy p == a*b
B PR 58b4044a… PASS — 1831/1831
A2 main 9f5a76b4… PASS — 1831/1831

The B and A2 captures are byte-identical. So on this part the plain-multiply case cannot distinguish the two, which is consistent with @hansfbaier's "could not reproduce on Spartan7" — but the design is also insensitive to the bit that changed, see below.

4. Your commit 1 is the half that is distinguishable — and it needs a different design

For the "one 16x16 multiply" case yosys emits AREG=0, so ZAREG_2_ACASCREG_1 is not involved at all; that arm of the PR is untested by it. With registered inputs feeding a registered product — AREG=1, ACASCREG=1, BREG=1, BCASCREG=1, MREG=1, i.e. what synth_xilinx emits when it absorbs an input register — the delta is visible immediately:

> DSP_R_X9Y90.DSP48.DSP_0.ZAREG_2_ACASCREG_1
> DSP_R_X9Y90.DSP48.DSP_0.ZBREG_2_BCASCREG_1

main writes neither, and prjxray's fuzzers/100-dsp-mskpat/generate.py defines the tag as (AREG == 2 && ACASCREG == 1) with add(..., invert=1) → the Z form is the negation, which is what your commit computes. So for that configuration your reading looks right to me. Caveat worth stating in the commit message: the fuzzer only adds AREG_2_ACASCREG_1/BREG_2_BCASCREG_1 when A_INPUT/B_INPUT == CASCADE, so for DIRECT inputs the bit's meaning is inferred from the tag definition rather than fuzz-validated in place.

To settle it on hardware without JTAG readback of the DSP, I built a self-checking design: registered-input multiply (AREG=1/ACASCREG=1) compared every clock against an RTL reference multiply written as a shift-add function (no *, so it can never become a DSP), reported over the same UART as m=%08x n=%08x where m counts mismatches. It is arch-agnostic and takes one command:

  • RTL simulation: m=00000000 over 88439 comparisons (no false positives).
  • Same design with a deliberate one-stage-deeper fault: m=0000fffe — so a CLEAN reading is meaningful.
  • Post-synthesis gate-level simulation of the actual tech-mapped netlist, decoding the tx pin at 115200 8N1: clean m=00000000, fault m=0000fffe — the checker survives synthesis (my first fault model did not: opt_clean folded the pipeline copy away, which is why the post-synth arm exists).
  • Board run: 4 phases, control first (control must report mismatches or the run is void), then main, PR, main.

On xc7s50 that A/B will read: control MISMATCH, main ?, PR ?. Whatever it says, it is one flash and one second of UART per phase, and it is the only hardware evidence I can produce for commit 1 with the board I have (an Arty S7-50; the only JTAG idcode here is 0x362f093).

What I'd ask for

  1. The zynq-7010 datapoint on current main. Your board is the one place the const-pin half can still be decided: main now includes xc7: Drive constant holdouts from local LUTs and fail if unrouted #184, the design is demo-projects/dsp-test-arty-s7 (or your gist), and the only thing that matters is whether p == a*b on the UART. If main is wrong there and this PR is right, the FASM regression plus your board run is a solid merge case. If main is right there too, the const-pin half has no remaining evidence and can be dropped.
  2. A rebase — the branch is based on 68aeeb39; its two commits cherry-pick onto 5fb8f5d2 with one trivial fasm.cc merge, so a rebase should be mechanical (I can push that rebase to a branch if it helps).
  3. Split commit 1, as you offered. It is independent of the contested half and its FASM delta is demonstrable on its own.
  4. If you have the Vivado-side evidence for the polarity question (a bit-diff of a design with a constant 0 on one of these 8 pins, or a readback), that would shortcut all of the above — that assumption is the entire risk in the fasm.cc half.

I have the FASM deltas, the four bitstreams, the simulators' output and the board runner ready; happy to hand any of it over, or to open it as a draft PR against your branch. I have not pushed anything.

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.

2 participants