Skip to content

fpga(ax7203): RLC and 5-node ladder through the linear cell -- one board top for both circuits, PASS on the board, negative controls FAIL on their own circuit (Closes #10565) - #10578

Merged
gHashTag merged 3 commits into
masterfrom
claude/analog-m1b-board-10496
Oct 19, 2026

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Part of #10496. Closes #10565 (the board step moves there and stays open until the receipts are attached).

The spec, its tests, its seal and three bitstreams are done, and the board run is in: the committed top prints PASS on every line for both circuits, and each negative control prints FAIL on its own circuit only (see "Board run").

What is in the PR

  • specs/fpga/analog_cell_ax7203.t27 (module FpgaAnalogCell): the linear cell of analog M1: linear state-space cell -- any linear R/C/L circuit, fixed step, folded to x' = Mx + Nu #10487 (specs/analog/linear.t27) on the AX7203, running the series RLC and the 5-node RC ladder, 200 steps each, one receipt line per run, alternating T27 RLC and T27 LAD5.
  • .trinity/seals/fpga_FpgaAnalogCell.json: sealed by t27c, 11/11 tests, 0 vacuous; seal --verify all MATCH.
  • specs/fpga/board_receipt.t27 and its three users (ddr3_head_bandwidth, ddr3_ternary_matvec, tern9m_l0_k_weights): the five-argument beat checksum fold is renamed fold_beat, bodies unchanged, and those four specs plus rc_filter_ax7203 (closure) are resealed. Why: see "Found on the way".
  • No .v, .json, .fasm or .bit is committed, and docs/reports/t27b_expectations.json is not touched.

One top, not two

The issue asks for a board top per circuit. This is one top that runs both. The machine is the same for every circuit (only the ROM of D and N words, the state count, the reported state and the expected values differ), so two tops would be two copies of one on_clock, which tools/dupe_scan.py refuses. CK_FIRST and CK_LAST choose the circuits the board cycles through; a single-circuit build is the same file with them equal.

Every line is a fresh run of 200 steps (the machine restarts after each line), not the same result printed again.

The machine

  • The cell's pack, opposite, round_back, add_sm and fold are used as they are (by use); so are M0's PLL, UART frame and receipt fields (use fpga::rc_filter_ax7203). Names that both specs declare with the same text resolve without a conflict.
  • The multiply is shift-and-add, one bit of the state per clock, straight into the positive or negative accumulator (the cell's pos_after / neg_after, one bit at a time). No DSP48E1 (live operands read wrong on openXC7, BENCHMARKS.md 4.4) and no wide combinational multiplier.
  • delta_of and next_x are split over five clocks (compare, subtract, round, pack, add). The tests prove the split gives next_x's word on edge cases (half an LSB both ways, equal registers, a negative old state, a carry across zero, 2^40 magnitudes).
  • The ROM is an if-chain: t27b verilog refuses ExprIndex, even a constant index into a constant array. A test compares every ROM word with the cell's RLC_D, RLC_NV, LAD5_D, LAD5_NV, and sees a one-LSB change in any of them.
  • Clocks per run (the receipt's C): RLC 56666, ladder 139400. They depend on the bit lengths of the states and are pinned by the tests.

Tests (11, 0 vacuous; t27c test-report, tri test, t27b test --check all agree)

  • The machine, run from reset beside the cell's step, gives the same state after every step, and the same fold, for both circuits (200 steps each), and A, B, S equal run()'s and the pinned values.
  • The serial multiplier equals prod / pos_after / neg_after on ten pairs including MAG * MAG, zero and negative operands.
  • Negative control: one coefficient one LSB off (RLC ROM address 5, ladder address 12). The cell's run on the changed table gives different A, B and S (pinned), the verdict is FAIL, and the machine with the right ROM disagrees with it.
  • A full end-to-end test reads four UART lines out of the machine with baud_div = 1, decodes every field of every line, and checks RLC, LAD5, RLC, LAD5 with L = 0, 1, 2, 3, all PASS.
  • Spec mutant: changing the one RLC ROM literal fails 5 of 11 tests and t27c seal --save refuses it.

Measured

Flow, per bitstream (all in $TMPDIR, nothing committed): t27b verilog then yosys 0.63 with
synth_xilinx -family xc7 -top FpgaAnalogCell -flatten -abc9 -nodsp -nosrl -run :coarse; techmap -map +/cmp2lut.v -map +/cmp2lcu.v -D LUT_WIDTH=6; alumacc; opt; memory -nomap; opt_clean; synth_xilinx (same flags) -run map_memory: (the repo's flow of bootstrap/src/service.rs plus the two mandatory flags), then nextpnr-xilinx --chipdb xc7a200tfbg484-2.bin --freq 100, fasm2frames.py, xc7frames2bit.

Committed top (sealed spec_hash prefix 9980520c7b86):

LUT (yosys) 1197 (LUT2 349, LUT3 108, LUT4 144, LUT5 451, LUT6 145)
FF 1096 (FDRE 1084, FDSE 12)
CARRY4 142 (nextpnr 146)
DSP48E1, BRAM 0, 0
nextpnr SLICE_LUTX 2186 of 269200 (its own count of LUT sites, not yosys cells)
Fmax 106.30 MHz post-route against the 100 MHz constraint (PASS); 66.32 MHz before routing
frames 20230 lines, fasm2frames printed no error
bitstream sha256 8c0753c08c81ecd679df8e77eecff4c3160291a80b1b85db1ccc7cdfcb65913a (9730772 bytes)

The place-and-route is deterministic (two runs of the same netlist gave 106.30 and 66.32 MHz both times) but not stable under small netlist changes. The bitstream header carries the frames file name and a time, so the sha256 of a rebuild differs even for the same netlist (two builds of this one differed in 10 header bytes).

Negative controls (not committed; the same spec with one literal changed; each has the same resources, 20230 frames lines, no fasm2frames error):

ROM change Verilog sha256 prefix nextpnr bitstream sha256
RLC address 5: NEG | 39851 to NEG | 39852 173204925e48 seed 1: 87.84 MHz (FAIL); seed 2: 100.27 MHz (PASS) 26d8d190c780ae35f6d266879381562e698d1b8bcd7a39cb49e70e7d7c4b9c81 (seed 2)
LAD5 address 12: 31709 to 31710 3a67a96b5619 seed 1: 99.32 MHz (FAIL); seed 2: 102.38 MHz (PASS) 29bfa289fb2a87ade703e65ed8cea130f9beb203e4546b09e88061c778876b7b (seed 2)

The negative-control bitstreams are the seed-2 runs because seed 1 did not close 100 MHz; the seed is a nextpnr-xilinx --seed argument, the same knob t27c silicon --pnr-seed exposes.

Simulation of the generated Verilog (iverilog, stub primitives, 8 clocks per bit instead of 868), the lines the machine prints:

T27 RLC A=0000000001136E34 B=0000000080135019 S=84BE2C7F9115D08A C=000000000000DD5A L=0000000000000000 PASS
T27 LAD5 A=00000000002D3886 B=0000000000441EF5 S=29439851A3F5D9EE C=0000000000022088 L=0000000000000001 PASS

and the negative controls print, in the same simulation, exactly the values the tests predict:

RLC mutant   T27 RLC A=0000000001136E1E B=0000000080135011 S=89932D8FBE407911 C=000000000000DD5A ... FAIL   (LAD5 line PASS)
LAD5 mutant  T27 LAD5 A=00000000002D3890 B=0000000000441F01 S=321615C52B5B23EA C=0000000000022088 ... FAIL  (RLC line PASS)

The A, B and S of the right run were also computed by an independent integer model (a scratch Python script, not committed) and agree with the cell's pinned RLC_EXP_S and LAD5_EXP_S.

The bitstreams are in the build session's $TMPDIR (/tmp/claude-501/b/: top_9980520c7b86.bit, neg_rlc_173204925e48_seed2.bit, neg_lad5_3a67a96b5619_seed2.bit), not in the tree.

Board run (2026-10-19, AX7203 on the bench)

Flashed from the orchestrator's terminal with openFPGALoader -c digilent_hs2 --busdev-num 000:004 <bit>, then one reader on /dev/cu.usbserial-130 at 115200 8N1 for 6 s per bitstream (lsof on the port empty before each capture). Every line below is checked against ^T27 (RLC|LAD5) A=<16 hex> B=<16 hex> S=<16 hex> C=<16 hex> L=<16 hex> (PASS|FAIL)$; the only lines that do not match are the first and last of each capture, which the reader cut mid-line.

Bitstream (sha256 prefix) Well-formed lines RLC lines LAD5 lines L counter
committed top 8c0753c08c81 572 286 PASS: A=0000000001136E34 S=84BE2C7F9115D08A 286 PASS: A=00000000002D3886 S=29439851A3F5D9EE 3 .. 574, no gap
RLC control 26d8d190c780 572 286 FAIL: A=0000000001136E1E S=89932D8FBE407911 286 PASS (unchanged values) contiguous
LAD5 control 29bfa289fb2a 573 287 PASS (unchanged values) 286 FAIL: A=00000000002D3890 S=321615C52B5B23EA 4 .. 576, no gap
  • The right top's A, B, S and C on the board equal the spec's pinned RLC_EXP_* / LAD5_EXP_* and the iverilog run above, on every line.
  • Each control's changed values are exactly the ones its test predicts, and the other circuit's line stays PASS with the right values, so the receipt names the circuit whose table is wrong.
  • All three bitstreams ran at 100 MHz with every one of the 1717 well-formed lines carrying the expected values, including the two controls, whose seed-1 placements had missed 100 MHz (the seed-2 ones are used).
  • An earlier, longer capture is not used: a fault in my capture script left extra readers on the port, which split bytes between files. The script now kills the reader process itself and refuses to start when the port is held.

Found on the way

  • A name clash from master. fpga(ax7203): ternary mat-vec over real tern-tc-9m weights in DDR3, UART receipt (Closes #10461) #10475 added specs/fpga/board_receipt.t27 with a five-argument fold, imported by rc_filter_ax7203. This spec imports analog::linear and fpga::rc_filter_ax7203, which both declare a two-argument fold; bootstrap/src/use_resolve.rs deliberately leaves a name that two modules of one closure declare differently unresolved (an explicit use analog::linear::{fold} does not change that), so after merging master the spec failed with use of undeclared identifier 'fold'. board_receipt's header already promised "no name a board spec also defines"; renaming its checksum fold_beat makes that true. After the rename: board_receipt 4/4, rc_filter_ax7203 10/10, tern9m_l0_k_weights 5/5, ddr3_head_bandwidth 12/12, ddr3_ternary_matvec 10/10, this spec 11/11, 0 vacuous; seal --verify all MATCH; tools/dupe_scan.py ok.
  • The first build failed 100 MHz at 79-88 MHz on one wire: the second synchroniser flop of the button sat by the pads and drove the whole restart, 4.9 to 5.9 ns across the die. A third flop did not help (the placer put it by its predecessor too). Setting a request flag (fin, hard) from the synchroniser, so the long wire ends in one LUT and one flop beside the logic, did. The restart then runs from fin alone.
  • t27b: no ExprIndex in the hardware path (not even a constant index into a constant array); the test bench of a top whose clock is a net does not lower (test bench of a module whose clk is a net), so the generated RTL was simulated with a scratch testbench instead of iverilog -DT27B_TESTBENCH.
  • Tooling: fasm2frames.py fails with PermissionError: /dev/stdout when its stdout is redirected in the sandbox; passing the output file as the second argument works. tri test needs the Zig cache directories in the sandbox.

Not claimed

  • Nothing about power, another circuit, or real time (a run takes more clocks than the circuit has time).
  • Fmax is nextpnr's estimate for one placement. The two negative controls (one literal different) placed at 87.84 and 99.32 MHz on the default seed against 106.30 for the committed top, so the margin over 100 MHz is thin; the board run passed on the three placements used, which says nothing about other seeds.

Why the build session did not flash

The build session's sandbox cannot reach USB (openFPGALoader --scan-usb: No USB devices found; stty on the UART: Operation not permitted), so the orchestrator flashed from its terminal tab. The flash and capture steps are in #10565.

🤖 Generated with Claude Code

…ard top for both circuits, bitstreams built, board run pending (Closes #10565)

specs/fpga/analog_cell_ax7203.t27 (module FpgaAnalogCell) runs the series RLC
and the 5-node RC ladder of specs/analog/linear.t27 on the AX7203, one run of
200 steps and one receipt line each, alternating. The machine is a serial
shift-and-add multiply-accumulate over a ROM of the circuit's D and N words;
the cell's pack, opposite, round_back, add_sm and fold are used as they are, by
use. The PLL, UART frame and receipt fields are M0's, by use. 11 tests, 0
vacuous; the tests compare every state of every step with the cell's step.

Fmax 106.30 MHz against 100 MHz (nextpnr-xilinx, xc7a200tfbg484-2). The board
run and the UART captures are the follow-up, #10565: the build session could
not reach USB or the serial port.

Part of #10496.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 18:46:52 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 43
PRs with All Checks Green 7
READY 0
FAILING 43
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 43 + 0 + 0 = 43, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

This was referenced Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 01:08:03 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 45
PRs with All Checks Green 5
READY 4
FAILING 45
PENDING 0
NO CHECKS YET 0

These columns do not partition: 4 + 45 + 0 + 0 = 49, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

…s board top keeps its fold (Closes #10565)

specs/fpga/board_receipt.t27 (#10475) declared a five-argument `fold`. rc_filter_ax7203.t27 imports it,
and rc_filter_ax7203.t27 and specs/analog/linear.t27 both declare a two-argument `fold`. use_resolve.rs
leaves a name that two modules of one import closure declare differently unresolved, so
specs/fpga/analog_cell_ax7203.t27 (which imports linear and rc_filter_ax7203) stopped compiling after
master merged: "use of undeclared identifier 'fold'". board_receipt's own header says it declares no
name a board spec also defines; the rename makes that true.

- board_receipt, ddr3_head_bandwidth, ddr3_ternary_matvec, tern9m_l0_k_weights: every call is the
  five-argument beat checksum; renamed to fold_beat, bodies unchanged.
- Tests: board_receipt 4/4, rc_filter_ax7203 10/10, tern9m_l0_k_weights 5/5, ddr3_head_bandwidth 12/12,
  ddr3_ternary_matvec 10/10, analog_cell_ax7203 11/11, 0 vacuous each (t27c test-report).
- Resealed the four renamed specs, rc_filter_ax7203 (its closure names board_receipt) and
  analog_cell_ax7203 (toolchain); seal --verify all MATCH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag gHashTag changed the title fpga(ax7203): RLC and 5-node ladder through the linear cell -- one board top for both circuits, bitstreams built, board run pending (Closes #10565) fpga(ax7203): RLC and 5-node ladder through the linear cell -- one board top for both circuits, PASS on the board, negative controls FAIL on their own circuit (Closes #10565) Oct 19, 2026
@gHashTag
gHashTag marked this pull request as ready for review October 19, 2026 01:13
@gHashTag
gHashTag enabled auto-merge (squash) October 19, 2026 01:13
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 01:15:02 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 45
PRs with All Checks Green 5
READY 4
FAILING 45
PENDING 0
NO CHECKS YET 0

These columns do not partition: 4 + 45 + 0 + 0 = 49, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

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.

analog M1b board step: flash analog_cell_ax7203 (committed top and two negative controls) and capture the UART receipts

2 participants