Skip to content

xilinx: emit the BUFR divide the design asks for, not always BYPASS - #151

Merged
hansfbaier merged 1 commit into
openXC7:mainfrom
gHashTag:fix/bufr-divide
Aug 18, 2026
Merged

xilinx: emit the BUFR divide the design asks for, not always BYPASS#151
hansfbaier merged 1 commit into
openXC7:mainfrom
gHashTag:fix/bufr-divide

Conversation

@gHashTag

Copy link
Copy Markdown

Part of #149 — the smallest of its three gaps, and the only one that needs no new database rows.

The defect

BUFR configuration is emitted by pp_config, a table built once at startup, so
it cannot see cell parameters. It hardcodes, for every BUFR in every design:

pp_config[{ ctx->id("HCLK_IOI3"), ... }] = {
    "BUFR_Y" + yy + ".IN_USE",
    "BUFR_Y" + yy + ".BUFR_DIVIDE.BYPASS"     // <- always
};

prjxray-db has documented the whole ladder since it was fuzzed — BYPASS and
D1..D8 for all four slots:

HCLK_IOI3.BUFR_Y0.BUFR_DIVIDE.BYPASS !32_22 !33_18 !33_19 !33_20 !33_21
HCLK_IOI3.BUFR_Y0.BUFR_DIVIDE.D1     32_22 33_18 !33_19 !33_20 !33_21
...
HCLK_IOI3.BUFR_Y0.BUFR_DIVIDE.D8     32_22 33_18 33_19 33_20 33_21

So the divider is reachable in silicon and in the database, and was unreachable
only through this flow.

A design written with BUFR_DIVIDE("5") produced a bitstream bit-identical to
one written with BYPASS
— no error, no warning, a silently undivided clock.
That is the failure mode this project keeps finding: a parameter accepted,
ignored, and reported as success.

The change

populate_bufr_divide() records what each bound BUFR_BUFR asks for, keyed by
(tile, slot_y) — the same shape as the existing bufgctrl_bound_slots. In
write_pip, a BUFR_Y<y>.BUFR_DIVIDE.BYPASS feature is replaced by the bound
cell's actual setting.

Strictly additive. When no BUFR is bound at that slot, or when the cell does
ask for BYPASS, the emitted bits are unchanged — every design that built
before builds identically. Only a design that explicitly asks for a division
sees a different bitstream, and today that design is silently mis-built.

An unsupported BUFR_DIVIDE value is now a log_error naming the cell.
Emitting an undocumented feature instead fails later in fasm2frames with a
FasmLookupError naming a bit — the same information with the cell removed.

Verification status, stated honestly

  • Compiles: to be confirmed by CI on this PR. My local check runs the
    amd64 image under QEMU and did not finish inside a sane timeout.
  • Bit-level: NOT yet verified. Proving BUFR_DIVIDE("5") produces
    D5 = 32_22 33_18 33_19 !33_20 !33_21 needs a design whose router actually
    crosses HCLK_IOI_RCLK_OUT<i> -> HCLK_IOI_RCLK_BEFORE_DIV<i>, i.e. a BUFR
    driving a regional clock into ILOGIC/OLOGIC. That is the same test design the
    BUFIO half of BUFIO is not packed, the BUFR divide ladder is unreachable, and the HCLK_L enables are never emitted #149 needs, so I would rather build it once and use it for both
    than hand-wave it here.
  • Silicon: not attempted. @hansfbaier's golden-bitstream method from xilinx: configure SDP BRAM opposite-side port widths #150 is
    the right instrument — a one-cell Vivado design with BUFR_DIVIDE("5"), decoded,
    would settle the encoding independently of the database.

I am raising it now rather than holding it until that design exists, because the
change is small and the current behaviour is silent. Happy to hold the merge
until the bit-level check lands if you prefer that ordering.

What this does not do

The other two gaps in #149 are untouched: nothing packs BUFIO (it exists only
as the constid BUFIO_BUFIO), and nothing emits the HCLK_L
ENABLE_BUFFER.HCLK_CK_BUFRCLK* features. Those need database rows that do not
exist yet — @cavearr's 047b campaign is the prerequisite, and the ordering is
agreed in that thread.

🤖 Generated with Claude Code

A placed BUFR emits no configuration at all today, and the divider is
unreachable. Both follow from the same cause: BUFR configuration is
carried by pp_config, the pseudo-pip table, keyed on

    HCLK_IOI_RCLK_BEFORE_DIV<i> -> HCLK_IOI_RCLK_OUT<i>

which is the route taken when a BUFR site is traversed as ROUTING. When
a BUFR is an actual placed cell the router enters and leaves the site,
no pip is crossed, and the table never fires.

Measured, not inferred. A probe instantiating BUFR -> ODDR places the
buffer (nextpnr reports 'BUFR: 1/20') and the HCLK_IOI3 tile receives
its eight routing pips:

    HCLK_IOI3_X113Y78.HCLK_IOI_RCLK_BEFORE_DIV3.HCLK_IOI_RCLK0
    HCLK_IOI3_X113Y78.HCLK_IOI_RCLK2RCLK3.HCLK_IOI_RCLK_OUT3
    HCLK_IOI3_X113Y78.HCLK_IOI_BUFR3_CE.HCLK_RCLK_DIV_CE3
    ...

with no BUFR_Y*.IN_USE and no BUFR_Y*.BUFR_DIVIDE.* anywhere. The buffer
is placed, wired, clocked and left unconfigured.

The divider compounds it: pp_config hardcodes BUFR_DIVIDE.BYPASS, so
even on the pass-through path a design asking for BUFR_DIVIDE("5")
produced a bitstream bit-identical to one asking for BYPASS.
prjxray-db has documented BYPASS and D1..D8 for all four slots since it
was fuzzed, so the ladder was reachable in silicon and in the database,
and unreachable only through this flow.

write_bufr() emits from the cell, where the parameters are, and is
dispatched from the site loop next to DSP/GT/PCIE. The pp_config entries
are untouched: they cover the pass-through case, which is disjoint.

An unsupported BUFR_DIVIDE is a log_error naming the cell rather than a
FasmLookupError naming a bit.

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

Copy link
Copy Markdown
Author

Probe result — the fix works, and it confirms the larger defect rather than the one I opened this PR for.

Run: https://github.com/gHashTag/trinity-fpga/actions/runs/32037150788
Sources: research/bufr-probe/ — two files differing only in BUFR_DIVIDE, each built with stock 05aaa06 and with this branch.

design toolchain BUFR feature emitted
bufr_divide stock none
bufr_divide patched BUFR_Y1.IN_USE, BUFR_Y1.BUFR_DIVIDE.D5
bufr_bypass stock none
bufr_bypass patched BUFR_Y1.IN_USE, BUFR_Y1.BUFR_DIVIDE.BYPASS

Complete FASM delta for the divided design, stock → patched:

+ HCLK_IOI3_X113Y78.BUFR_Y1.IN_USE
+ HCLK_IOI3_X113Y78.BUFR_Y1.BUFR_DIVIDE.D5

Two lines added, none removed.

Four assertions, all green:

  • stock emits no BUFR_Y* feature at all — not IN_USE, not a divide — for a
    BUFR that nextpnr reports as placed (BUFR: 1/20), wired, and clocking an ODDR
  • patched emits IN_USE and the requested divide
  • the BYPASS design still gets BYPASS, so the parameter is honoured rather than
    hardcoded in the other direction
  • the patched FASM removes nothing stock emitted

Slot Y1 is worth one line of its own: the route runs through RCLK index 3, and
pp_config maps slot→wire through {2, 3, 0, 1}, so Y1 is the right slot for
index 3. That is the same permutation @cavearr reported for the BUFIO site→CLK
mapping (X0Y0→CLK2, X0Y1→CLK3, X0Y2→CLK0, X0Y3→CLK1), which is a small
independent check that the site-y derivation here is correct.

Two corrections to my own work, for the record

The first version of this PR was wrong, and this probe is what showed it. It
kept the emission in pp_config and substituted the divide at write_pip time.
That path is never taken by a design with a real BUFR, so it would have changed
nothing — while appearing to fix the reported symptom.

The probe's own assertions were wrong too. They tested the original
hypothesis, that stock emits BUFR_DIVIDE.BYPASS where the design asks for 5.
Stock emits nothing, so the run reported "claim not reproduced" at the moment the
fix was in fact working. The assertions now state the measured defect. Changing a
test so that it passes deserves saying out loud: the hypothesis it encoded was
refuted by this same probe, and the replacement asserts the evidence. Both
versions are in that repository's history.

Still not verified

Silicon. The probe is pinned to xc7a35tcsg324-1 with prjxray's own arty-a7 pins,
so it can serve directly as the subject of a @hansfbaier-style one-cell Vivado
golden — and on hardware the output pin toggles at BUFR_O/2, so BYPASS and
D5 are 50 MHz against 10 MHz, a five-to-one ratio that needs no calibration.
Happy to hold this until that lands if you would rather have the golden first.

@gHashTag
gHashTag changed the base branch from stable-backports to main August 18, 2026 02:57
@hansfbaier

Copy link
Copy Markdown
Collaborator

@gHashTag Should we merge this before a release? I want to do a bugfix release now.

@hansfbaier
hansfbaier merged commit 68aeeb3 into openXC7:main Aug 18, 2026
3 checks passed
@hansfbaier

Copy link
Copy Markdown
Collaborator

I am merging this now. @gHashTag Please supply a new PR if any issues arise.

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