Skip to content

fpga(ax7203): the SAR converter of the golden deck on the AX7203 -- 31 conversions, UART receipts equal to the spec's codes, a one-literal negative control (Closes #10910) - #10919

Merged
gHashTag merged 4 commits into
masterfrom
claude/analog-sar-board-10910
Oct 19, 2026

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Closes #10910. Part of #10524 (epic #10486). Board run done 2026-10-19 15:08 (below): 31 of 31 PASS on the board, the negative control fails exactly where predicted.

Board run, AX7203, 2026-10-19 (the fixed v2 bitstreams)

Flashed from the orchestrator's terminal: openFPGALoader -c digilent_hs2 --busdev-num 000:004 <bit>, then one reader on /dev/cu.usbserial-130, 115200 8N1, 3 s per bitstream, port free before each capture. Each line is checked against ^T27 SAR K=<2 hex> C=<3 hex> E=<3 hex> N=<3 hex> S=<16 hex> L=<4 hex> (PASS|FAIL)$ and its payload (K, C, E, N, S, verdict) against expected_*_lines.txt.

Bitstream (sha256 prefix) Well-formed lines Payload equal to expected Distinct K Verdicts
top 652c6a3daeeb 550 550 of 550 31 of 31 550 PASS
control 4990720f67f0 (one more DAC unit on the LSB plate) 550 550 of 550 31 of 31 282 FAIL, 268 PASS: exactly the 16 predicted conversions FAIL (K = 03 06 09 0A 0B 0C 0E 10 11 13 14 15 16 17 1B 1D) and the other 15 PASS
  • The T27 SAR prefix is intact on every line (the first build's 7-byte garbage prefix is gone).
  • After the first lines the L counter runs without a gap through each capture. The only lines that do not match the grammar are at the very start (leftover bytes of the previous bitstream in the USB-UART buffer and noise while the FPGA reconfigures) and the cut last line.
  • 30 of the 31 codes equal ngspice's (N); K = 19 (conversion 25) has C = E = 731 and N = 732, the one miss the spec states.

Nothing about speed or power is claimed; the blocks are behavioural with the two fitted numbers of M4e.

Update 2026-10-19: the board run found a defect, fixed in b76beea and 4b53ed6 (a comment only; the Verilog and so the bitstreams are unchanged) (this section replaces what the older ones below say about the bitstreams)

The orchestrator flashed both bitstreams of the first push and captured them for 6 s at 115200: the payload after K= (K, C, E, N, S, verdict) was right on every line, all 31 K PASS for the top, the control FAILed on exactly the 16 predicted conversions. But every line after the first began with the same seven bytes 95 ba 02 9a 05 49 81 instead of T27 SAR (8 bytes with the space), so the line did not match the grammar.

The cause, in the generated machine. The transmitter set the restart request fin at the shift that STARTS the LF's stop bit: if (last and tx_left == 1) { fin = true; } in the spec, if ((last && (tx_left == 8'd1))) fin <= 1'b1; at line 237 of the first build's Verilog (sha256 bc6a6299dd71...). After fin the machine runs the next conversion (1 + 75 clocks), the verdict, and loads the next line's first frame: about 80 clocks, 0.09 of a bit time at 868 clocks a bit. So the next line's first start bit cut the LF's stop bit to a tenth of its time. A terminal reads that LF with a framing error, hunts for the next falling edge, and takes the fourth data bit of the T (0x54: data bits 0 0 1 0 1 0 1 0, low first) for a start bit: its first byte is then 95 (data bits 4 to 7 and the stop bit of the T, then the start bit and data bits 0 and 1 of the 2: 1 0 1 0 1 0 0 1, low first), and it stays misaligned through the contiguous frames of T27 SAR until the K. M1b has tens of thousands of clocks between its lines and the 8-clocks-per-bit simulation of the first push had 10 bit times of compute between lines (80 / 8), so neither showed it: the ratio of compute clocks to bit time is the thing.

Reproduced at the real bit period before fixing. The Verilog of the first push, simulated at 868 clocks a bit (no patch of baud_div) and read by a receiver that samples the middle of each bit, gives line 0 right and then, after every line, \x95\xba\x02\x9a\x05I\x81 followed by K=01 C=349 ... : the same bytes the board showed, with a framing error at every LF (12 in 227 bytes).

The fix (specs/fpga/sar_adc_ax7203.t27, one new register gap, constant GAP_BITS = 2). fin is set only after the last frame's stop bit has run its whole bit time and the transmitter has been idle for GAP_BITS more bit times: in the transmitter, else if (last) takes the place of loading a next frame, counts gap up on the baud ticks and sets fin when it reaches GAP_BITS. It is M1b's own handshake (fin restarts the machine), later. Nothing else changes: the machine, the ROMs, the receipt's fields, the pins.

Proof at the real bit period.

  • A new test (13 in all, 28 asserts, 0 vacuous): the machine at the real divider 868 and a receiver written in the spec that samples the middle of each bit, with framing-error accounting, reads line 0 whole and the first 12 bytes of line 1 (T27 SAR K=01), with 0 framing errors, and the line was high for at least the LF's stop bit and GAP_BITS more bit times (3 x 868 clocks, under 200 more) before line 1's first start bit. With the first push's handshake restored in a scratch copy the test fails (4 framing errors) and the other 12 pass.
  • The generated Verilog of the fixed top, simulated in iverilog 13 at baud_div 868 for 19 million clocks (34.7 lines), read by two receivers that sample the middle of each bit (a scratch testbench, not committed): receiver A, listening from the start, reads 34 complete lines, K = 00 .. 1E and the wrap to K = 00, L = 0000 .. 0021, 0 framing errors, every line well-formed and character for character equal to the expected one (the table below with L counting on), 2181 bytes. Receiver B starts listening in the middle of line 0 (clock 123457, an arbitrary capture start): its first, cut line is garbage with 2 framing errors, then it resynchronises at the first gap and reads lines 1 .. 33 equal to the expected ones. The control (below), 8 million clocks: 14 complete lines, equal to the expected control lines (FAIL on K = 03 06 09 0A 0B 0C, PASS on the others seen), 0 framing errors for the receiver that listens from the start.

New bitstreams (the first push's two are superseded and deleted; the payload of every line is unchanged, so the expected lines below still stand, with the prefix now intact):

fixed top fixed control
bitstream /tmp/claude-501/sarboard/sar_adc_top_v2.bit /tmp/claude-501/sarboard/sar_adc_neg_bit1_2units_v2.bit
sha256 652c6a3daeebe897d5a5dfb89112d62a29b13582359ba7b497b21249abb32399 (9730778 bytes) 4990720f67f0bf0f3a7e942418ed98be28fe4c87cd9d354797372d2c09a4c02c (9730778 bytes)
Verilog sha256 d8e31fdcc479e1f99df471b3fa5f68fca5ba3ac514301ef5b20199532116cfc2 66c1c155d5a39b3aae84bbdd3e631845934614f56f2d65630887d896ef30fc3c
LUT (yosys) 908 (LUT1 2, LUT2 337, LUT3 72, LUT4 168, LUT5 234, LUT6 95; plus 80 INV, 4 MUXF7, 2 MUXF8) 933 (LUT1 1, LUT2 341, LUT3 86, LUT4 167, LUT5 222, LUT6 106; plus 80 INV, 7 MUXF7, 1 MUXF8)
FF 538 (FDRE 507, FDSE 31) 538
CARRY4 137 (nextpnr 141) 140 (nextpnr 144)
DSP48E1, BRAM 0, 0 0, 0
nextpnr SLICE_LUTX 1972 of 269200 2025 of 269200
Fmax 108.27 MHz post-route against the 100 MHz constraint (PASS); 93.51 before routing 111.30 MHz (PASS); 87.62 before routing
frames 20230 lines, no fasm2frames error 20230 lines

Same flow, default seed, as below. The fix cost 8 flip-flops (gap); yosys counts 908 LUTs against 916 before, nextpnr 1972 LUT sites against 1959; the top now places at 108.27 MHz (the first push: 124.60), an 8 % margin over the constraint. The control is the fixed spec with the same one literal changed (if (j == 0) { return 16083; } to 32166); its Verilog differs from the fixed top's in that one line, and it fails 5 of the 13 tests. Expected receipt lines, FAIL and PASS conversions of the control: unchanged, see below.

Board steps (replaces the ones below): the same, with the _v2 files

openFPGALoader -c digilent_hs2 --busdev-num 000:004 /tmp/claude-501/sarboard/sar_adc_top_v2.bit
openFPGALoader -c digilent_hs2 --busdev-num 000:004 /tmp/claude-501/sarboard/sar_adc_neg_bit1_2units_v2.bit

The lines are now followed by 3 bit times of high (the stop bit and two idle bits: 0.35 ms) before the next one; a capture that starts in the middle of a line may still cut its first line, as before.


What is in the PR

  • specs/fpga/sar_adc_ax7203.t27 (module FpgaSarAdc, 13 tests, 0 vacuous) and its seal .trinity/seals/fpga_FpgaSarAdc.json (sealed by t27c, seal --verify all MATCH, spec_hash prefix 54c6f2c7e86c). The board top of the M4e converter (specs/analog/sar_adc.t27): it runs the 31 conversions of the spec's fitted behavioural converter on the 31 pinned held samples (V(VDP), V(VDN) of our own ngspice + PSP103 run of the golden deck) and prints one UART line per conversion.
  • No .v, .json, .fasm or .bit is committed. Nothing in specs/analog/ is edited and no analog_AnalogSar* seal is touched. No other spec is touched, no docs/reports/t27b_expectations.json row.
  • Everything reused is reused by use, no body is copied: from analog::linear (add_sm, pack, is_neg, round_back, fold, q_sm, NEG, MAG), analog::sar_cdac (UNITS, VCM, event_k, plate_step), analog::sar_adc (neg_word, decide, convert_sar, model_k, FIT_CODE, NG_CODE, S_VP, S_VN, CP_FIT, OFF_FIT_V) and fpga::rc_filter_ax7203 with fpga::board_receipt (the PLL, the UART frame, hex_digit, pass_fail_char, the verdict and state constants). python3 -I tools/dupe_scan.py: no new duplicate body; tri types ratchet: RATCHET: CLEAN.

The machine

One step a clock at 100 MHz, the same for every conversion, no loop that depends on data:

phase what
P_SMP xp, xn from the sample ROMs (rom_vp, rom_vn), the two pinned codes from theirs
P_NEG, P_DIFF, P_DEC xnn = neg_word(xn); td = add_sm(xp, xnn); dd = add_sm(td, OFF). This is the spec's decide split over three clocks so that no clock chains more than one add_sm (a test proves the split equals decide, on all 31 samples both ways round and on a 10 x 10 grid of edge words)
P_BIT b = dd > 0, the code takes the bit; the 11th decision stops here, the others load the plate's ratio K from the DAC ROM (rom_k, 10 words)
P_MUL K x VCM as two shifted adds, VCM = 2^23 + 2^22 (0.75 V). K and VCM are both constants, so no live operand is multiplied: no DSP48 (live operands read wrong on openXC7, BENCHMARKS.md 4.4) and no serial multiplier
P_RND, P_APP round_back of the product; xp and xn take the step with opposite signs (pack(rnd, b) and pack(rnd, !b))

A conversion is 75 clocks from P_SMP to the verdict (1 + 10 x 7 + 4, pinned for all 31 samples). Net1 of the spec's DAC never reaches the comparator, so the board does not carry it; a test runs both nodes of both halves of the spec beside the machine, with M4a's own event_k, and compares Vout of each half and the code so far before every one of the eleven decisions, for all 31 conversions.

The restart is M1b's: the button and LOCKED pass M0's two registers, the request flags fin and hard set from them restart the machine, a finished line sets fin alone and the next conversion starts. After conversion 30 the board goes back to conversion 0, so the 31 lines repeat (L keeps counting).

The ROMs are if-chains (t27b verilog refuses array indexing) and a test compares every word with the spec's own tables: q_sm(S_VP[k]), q_sm(S_VN[k]), FIT_CODE[k], NG_CODE[k], model_k(UNITS, 1.0, CP_FIT)[2 j + 1], q_sm(OFF_FIT_V).

The receipt

115200 baud 8N1 on N15, /dev/cu.usbserial-130, one line per conversion (63 characters, CR LF, 5.5 ms a line, 31 lines a round = 0.17 s):

T27 SAR K=<2 hex> C=<3 hex> E=<3 hex> N=<3 hex> S=<16 hex> L=<4 hex> PASS|FAIL

K the conversion (hex 00 .. 1E), C the board's 11-bit code, E the spec's fitted code for it (FIT_CODE), N the code of the ngspice + PSP103 run (NG_CODE), S the order-sensitive fold of the board's codes of the round so far (K = 0 .. this one), L the lines since the button. PASS when C equals E, else FAIL; N is printed and not judged, 30 of the 31 E equal N (K=19, conversion 25: 1841 against 1842, the miss the spec reports at 0.133 mV from a threshold). LEDs (active low) as M0: B13 a line is being sent, C13 FAIL, D14 PASS, D15 heartbeat; the button T6 resets.

Measured (local flow, nothing committed) -- of the FIRST push; the fixed numbers are in the update above

t27b verilog then yosys 0.63 with M1b's script (synth_xilinx -family xc7 -top FpgaSarAdc -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:), then nextpnr-xilinx --chipdb xc7a200tfbg484-2.bin --json .. --fasm .. --freq 100 (default seed), fasm2frames.py --db-root prjxray-db/artix7 --part xc7a200tfbg484-2, xc7frames2bit.

committed top negative control
LUT (yosys) 916 (LUT2 342, LUT3 85, LUT4 182, LUT5 214, LUT6 93; plus 79 INV) 935 (LUT2 341, LUT3 81, LUT4 182, LUT5 249, LUT6 82)
FF 530 (FDRE 499, FDSE 31) 530
CARRY4 135 (nextpnr 139) 138 (nextpnr 143)
DSP48E1, BRAM 0, 0 0, 0
nextpnr SLICE_LUTX 1959 of 269200 2018 of 269200
Fmax 124.60 MHz post-route against the 100 MHz constraint (PASS); 96.90 MHz before routing 116.16 MHz (PASS); 92.17 before routing
frames 20230 lines, fasm2frames printed no error (only its antlr fast-parser import warning) 20230 lines, same
Verilog sha256 bc6a6299dd71ad4e4c2890dd85a00d8b81c8393b9cf33231a5ad6788f5250404 33fd87077b130ae0f145a805dc63439bfe3b658ee6716ec8eab44cba1fbfb9db
bitstream sha256 94dc11837bd637b349bda53d1ee2444eed8d58b1a2c3a247055e590824846346 (9730778 bytes) 66cd7a60834c9f8d6186c63e879fcfda0341fd564f7dc91d5ab3c58cbca3acb8 (9730778 bytes)

The first push's bitstreams (sar_adc_top.bit, sar_adc_neg_bit1_2units.bit) are superseded and deleted; the _v2 files are the current ones. The bitstream header carries a file name and a time, so a rebuild of the same netlist has another sha256 (an earlier build of this very top gave 5c3809c4... with the same frames count). Place-and-route is deterministic for one netlist and not stable under small netlist changes; both builds used nextpnr's default seed and closed 100 MHz in nextpnr's report. The top was placed at 124.60 MHz, a margin of 24 % over the constraint: more than M1b's 106.30.

Simulation of the generated Verilog of the first push (iverilog 13, stub primitives, 8 clocks per bit instead of 868, a scratch testbench that is not committed; this is the simulation that could not show the defect, see the update above): both builds print 64 lines, and the first 63 equal, character for character, the lines an independent integer model of the converter (a scratch Python mirror, not committed, which also reproduces the spec's 31 FIT codes and its 30 of 31 matches) predicts, including S and L and the wrap from K=1E to K=00 with S restarting.

Expected receipt lines (the committed top)

Every complete line must match ^T27 SAR K=[0-9A-F]{2} C=[0-9A-F]{3} E=[0-9A-F]{3} N=[0-9A-F]{3} S=[0-9A-F]{16} L=[0-9A-F]{4} (PASS|FAIL)$. A capture starts at an arbitrary place, so the first and last line of it may be cut. For each K the fields C, E, N, S and the verdict are fixed (S folds from the round's K=00, so it does not depend on where the capture started); L is the line number since the button and counts on without a gap. The first round after the button is:

T27 SAR K=00 C=3EE E=3EE N=3EE S=00000000000003EE L=0000 PASS
T27 SAR K=01 C=349 E=349 N=349 S=0000000000000495 L=0001 PASS
T27 SAR K=02 C=2AE E=2AE N=2AE S=0000000000000B84 L=0002 PASS
T27 SAR K=03 C=220 E=220 N=220 S=0000000000001528 L=0003 PASS
T27 SAR K=04 C=1A5 E=1A5 N=1A5 S=0000000000002BF5 L=0004 PASS
T27 SAR K=05 C=141 E=141 N=141 S=00000000000056AB L=0005 PASS
T27 SAR K=06 C=0F8 E=0F8 N=0F8 S=000000000000ADAE L=0006 PASS
T27 SAR K=07 C=0CD E=0CD N=0CD S=0000000000015B91 L=0007 PASS
T27 SAR K=08 C=0C2 E=0C2 N=0C2 S=000000000002B7E0 L=0008 PASS
T27 SAR K=09 C=0D7 E=0D7 N=0D7 S=0000000000056F17 L=0009 PASS
T27 SAR K=0A C=10B E=10B N=10B S=00000000000ADF25 L=000A PASS
T27 SAR K=0B C=15C E=15C N=15C S=000000000015BF16 L=000B PASS
T27 SAR K=0C C=1C8 E=1C8 N=1C8 S=00000000002B7FE4 L=000C PASS
T27 SAR K=0D C=249 E=249 N=249 S=000000000056FD81 L=000D PASS
T27 SAR K=0E C=2DC E=2DC N=2DC S=0000000000ADF9DE L=000E PASS
T27 SAR K=0F C=379 E=379 N=379 S=00000000015BF0C5 L=000F PASS
T27 SAR K=10 C=41C E=41C N=41C S=0000000002B7E596 L=0010 PASS
T27 SAR K=11 C=4BF E=4BF N=4BF S=00000000056FCF93 L=0011 PASS
T27 SAR K=12 C=55A E=55A N=55A S=000000000ADF9A7C L=0012 PASS
T27 SAR K=13 C=5E8 E=5E8 N=5E8 S=0000000015BF3110 L=0013 PASS
T27 SAR K=14 C=663 E=663 N=663 S=000000002B7E6443 L=0014 PASS
T27 SAR K=15 C=6C7 E=6C7 N=6C7 S=0000000056FCCE41 L=0015 PASS
T27 SAR K=16 C=710 E=710 N=710 S=00000000ADF99B92 L=0016 PASS
T27 SAR K=17 C=73B E=73B N=73B S=000000015BF3301F L=0017 PASS
T27 SAR K=18 C=746 E=746 N=746 S=00000002B7E66778 L=0018 PASS
T27 SAR K=19 C=731 E=731 N=732 S=000000056FCCC9C1 L=0019 PASS
T27 SAR K=1A C=6FD E=6FD N=6FD S=0000000ADF99957F L=001A PASS
T27 SAR K=1B C=6AC E=6AC N=6AC S=00000015BF332C52 L=001B PASS
T27 SAR K=1C C=641 E=641 N=641 S=0000002B7E665EE5 L=001C PASS
T27 SAR K=1D C=5BF E=5BF N=5BF S=00000056FCCCB875 L=001D PASS
T27 SAR K=1E C=52D E=52D N=52D S=000000ADF99975C7 L=001E PASS

All 31 PASS; after K=1E the board goes round again with K=00 (L=001F), S restarting from 0x3EE.

The negative control

One literal of rom_k changed in a scratch copy of the spec (if (j == 0) { return 16083; } to 32166, plate 0 = Bit_1, the Vout ratio of 2 units instead of 1: one more DAC unit on the LSB plate); the generated Verilog differs from the top's in that one line and nothing else. The spec's own converter on that table gives the pinned FAULT_CODE and 16 of the 31 conversions differ from E (FAULT_MASK), so the control prints FAIL on K = 03 06 09 0A 0B 0C 0E 10 11 13 14 15 16 17 1B 1D (decimal 3, 6, 9, 10, 11, 12, 14, 16, 17, 19, 20, 21, 22, 23, 27, 29) and PASS on the other 15 (K = 00 01 02 04 05 07 08 0D 0F 12 18 19 1A 1C 1E). Its S field differs from the top's from K=03 on. The changed spec fails 5 of its 13 tests, and t27c seal --save would refuse it. The first round of the control:

the 31 expected control lines
T27 SAR K=00 C=3EE E=3EE N=3EE S=00000000000003EE L=0000 PASS
T27 SAR K=01 C=349 E=349 N=349 S=0000000000000495 L=0001 PASS
T27 SAR K=02 C=2AE E=2AE N=2AE S=0000000000000B84 L=0002 PASS
T27 SAR K=03 C=221 E=220 N=220 S=0000000000001529 L=0003 FAIL
T27 SAR K=04 C=1A5 E=1A5 N=1A5 S=0000000000002BF7 L=0004 PASS
T27 SAR K=05 C=141 E=141 N=141 S=00000000000056AF L=0005 PASS
T27 SAR K=06 C=0F9 E=0F8 N=0F8 S=000000000000ADA7 L=0006 FAIL
T27 SAR K=07 C=0CD E=0CD N=0CD S=0000000000015B83 L=0007 PASS
T27 SAR K=08 C=0C2 E=0C2 N=0C2 S=000000000002B7C4 L=0008 PASS
T27 SAR K=09 C=0D6 E=0D7 N=0D7 S=0000000000056F5E L=0009 FAIL
T27 SAR K=0A C=10A E=10B N=10B S=00000000000ADFB6 L=000A FAIL
T27 SAR K=0B C=15D E=15C N=15C S=000000000015BE31 L=000B FAIL
T27 SAR K=0C C=1C9 E=1C8 N=1C8 S=00000000002B7DAB L=000C FAIL
T27 SAR K=0D C=249 E=249 N=249 S=000000000056F91F L=000D PASS
T27 SAR K=0E C=2DD E=2DC N=2DC S=0000000000ADF0E3 L=000E FAIL
T27 SAR K=0F C=379 E=379 N=379 S=00000000015BE2BF L=000F PASS
T27 SAR K=10 C=41D E=41C N=41C S=0000000002B7C163 L=0010 FAIL
T27 SAR K=11 C=4BE E=4BF N=4BF S=00000000056F8678 L=0011 FAIL
T27 SAR K=12 C=55A E=55A N=55A S=000000000ADF09AA L=0012 PASS
T27 SAR K=13 C=5E9 E=5E8 N=5E8 S=0000000015BE16BD L=0013 FAIL
T27 SAR K=14 C=662 E=663 N=663 S=000000002B7C2B18 L=0014 FAIL
T27 SAR K=15 C=6C6 E=6C7 N=6C7 S=0000000056F850F6 L=0015 FAIL
T27 SAR K=16 C=711 E=710 N=710 S=00000000ADF0A6FD L=0016 FAIL
T27 SAR K=17 C=73A E=73B N=73B S=000000015BE14AC0 L=0017 FAIL
T27 SAR K=18 C=746 E=746 N=746 S=00000002B7C292C6 L=0018 PASS
T27 SAR K=19 C=731 E=731 N=732 S=000000056F8522BD L=0019 PASS
T27 SAR K=1A C=6FD E=6FD N=6FD S=0000000ADF0A4387 L=001A PASS
T27 SAR K=1B C=6AD E=6AC N=6AC S=00000015BE1481A3 L=001B FAIL
T27 SAR K=1C C=641 E=641 N=641 S=0000002B7C290507 L=001C PASS
T27 SAR K=1D C=5BE E=5BF N=5BF S=00000056F8520FB0 L=001D FAIL
T27 SAR K=1E C=52D E=52D N=52D S=000000ADF0A41A4D L=001E PASS

The tests also assert, for the spec's own two controls (the bridge of 2 units, and Bit_8 with 3 units, each with its whole table re-derived), that every one of the 31 conversions would print FAIL.

Tests (13, 0 vacuous; t27c test-report, tri test and t27b test --check agree, 3276 asserts under --check)

  1. the ROMs are the spec's tables word for word (141 asserts), and a one-LSB change in any is seen
  2. a plate step is the spec's DAC event for every plate (prod(K, VCM) = two shifted adds; round_back of it = event_k's Vout from rest, both signs)
  3. the three-clock decision equals decide (170)
  4. the machine gives the spec's code for all 31 conversions, the same Vout of both halves before every decision, and the verdict PASS (249)
  5. a conversion takes 75 clocks for every sample (94)
  6. one more unit on the LSB plate fails exactly the predicted 16 (mask and codes pinned), and the machine with the right ROM disagrees with that table's converter on exactly those
  7. the spec's own two controls fail all 31
  8. the restart: lock and button, the first conversion, the button again
  9. the pins show the state one clock later
  10. the 8 segments of the line are 63 characters, the fields sit at the top of the shift register, the round's fold is pinned (0xADF99975C7)
  11. 63 UART lines out of the machine (two rounds and the wrap, baud_div = 1), every field decoded and compared with the spec: K, C, E, N, S, L, PASS
  12. a wrong code prints FAIL (E changed by one before the verdict)
  13. at the real divider 868 the line ends with the LF's whole stop bit and two idle bit times: a mid-bit receiver reads line 0 and the start of line 1 with no framing error (28 asserts)

Board steps for the orchestrator (#10910)

openFPGALoader -c digilent_hs2 --busdev-num 000:004 /tmp/claude-501/sarboard/sar_adc_top_v2.bit
# capture /dev/cu.usbserial-130 at 115200 8N1 for about 1 s (5 rounds), one reader on the port
openFPGALoader -c digilent_hs2 --busdev-num 000:004 /tmp/claude-501/sarboard/sar_adc_neg_bit1_2units_v2.bit
# the same capture

Pass: 31 of 31 distinct K lines PASS for the top with C, E, N, S as in the table above; the control FAILs on the 16 predicted K and PASSes on the other 15, with its changed C values.

Not claimed

  • Nothing about the circuit beyond the spec: the comparator and the top-plate load are two numbers fitted on these same 31 conversions (OFF_FIT_V, CP_FIT), so "30 of 31 equal ngspice's" describes this run and predicts nothing; the switch is the run's, not modelled; the SAR logic is a sequence, not gates (M4d).
  • The board holds ROMs of the held samples; it does not sample anything. Nothing about the PSP103 transistors, power, noise, mismatch or another corner.
  • No speed claim. A conversion is 75 clocks (0.75 us) but a line takes 5.5 ms on the UART, and the reference took 65 minutes of ngspice; the two are not the same thing (may_claim_speedup in specs/analog/bench.t27 stays no).
  • Fmax is nextpnr's estimate for one placement; the fixed control placed at 111.30 MHz and the fixed top at 108.27 MHz. The seed was not varied.

Found on the way

  • A 8-clocks-per-bit simulation hides a line-end defect that the board shows. What a UART testbench must keep is the ratio of compute clocks to bit time, not only the line's content. The new spec test and the iverilog runs use the real divider and a receiver that counts framing errors.

  • t27b: a module-level var must not share a name with a local of any function a test pulls in through use. StmtLocal(shadows module var) on acc: analog::linear has f64 helpers with a local acc, and my first draft of the machine had a register acc. Renamed pacc. The error points at a line of the resolved file, not of the spec.

  • The use of three analog specs and M0 resolves every name the spec references: t27c gen prints no UNRESOLVED line, and t27b test and t27b verilog accept the file. The names that two modules of the closure declare (fold, round_back) agree on their text, as in M1b.

Why this session did not flash

The sandbox has no USB; flashing and the UART capture are the orchestrator's steps (above), as in #10565. The first run on the board is what found the defect fixed above.

🤖 Generated with Claude Code

… conversions, one UART receipt each, a one-literal negative control (Closes #10910)

specs/fpga/sar_adc_ax7203.t27 (module FpgaSarAdc) is the board top of the M4e
converter (specs/analog/sar_adc.t27): a machine that walks the spec's MSB-first
sequence on the fabric (neg, diff, decide, ratio, K x VCM as two shifted adds,
round, plate step), with the 31 held samples, the DAC's ten Vout ratios and the
two pinned codes of each conversion as if-chain ROMs. It prints one line per
conversion, "T27 SAR K=.. C=.. E=.. N=.. S=.. L=.. PASS|FAIL", PASS when the
board's code equals the spec's fitted code (30 of the 31 are also ngspice's).

Twelve tests, 0 vacuous: the ROMs equal the spec's tables word for word; a
plate step is the spec's DAC event for every plate; the three-clock decision is
decide(); the machine gives the spec's code for all 31 conversions and the same
Vout voltage after every one of the eleven decisions; a conversion is 75 clocks;
one more DAC unit on the LSB plate (one rom_k literal) fails exactly 16 of the
31 conversions, the spec's own bridge and Bit_8 controls fail all 31; the
restart, the pins and 63 UART lines (two rounds and the wrap) decode.

Nothing in specs/analog/ or its seals is touched. Bitstreams are built locally
and not committed.

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

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 07:00:04 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 44
PRs with All Checks Green 6
READY 2
FAILING 44
PENDING 0
NO CHECKS YET 0

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

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=88b8cb73ceda != 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).

…o-bit-time gap; the board lost "T27 SAR " at the start of every line after the first (Closes #10910)

Found on the board: every line after the first began with the same seven bytes
95 BA 02 9A 05 49 81 instead of "T27 SAR ". The cause: the transmitter set the
restart request `fin` at the shift that starts the LF's stop bit (tx_left == 1),
and the next conversion plus the next line's first start bit follow about 80
clocks later, 0.09 of a bit time at 868 clocks a bit. The LF's stop bit was cut
to a tenth of its time, a terminal read a framing error, hunted for the next
falling edge and took bit 4 of the "T" for a start bit. M1b has tens of
thousands of clocks between lines, so it never showed; the 8-clocks-per-bit
simulation had 10 bit times of compute between lines, so it did not show either.

The fix: `fin` is set only after the last frame's stop bit has run its whole bit
time and the line has been idle GAP_BITS (2) more bit times (one new register,
`gap`, reset with the restart). Nothing else changes in the machine, the ROMs or
the receipt.

One new test (13 in all, 0 vacuous): the machine at the real divider 868 with a
receiver that samples the middle of each bit reads line 0 and the first 12 bytes
of line 1 with no framing error, the line was high for the stop bit and two more
bit times before line 1's first start bit; with the old handshake it fails (4
framing errors). The simulation of the generated Verilog at 868 clocks a bit
reads 34 lines, K = 00 .. 1E and the wrap, all equal to the expected ones.

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

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 08:00:22 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 42
PRs with All Checks Green 8
READY 1
FAILING 42
PENDING 0
NO CHECKS YET 0

These columns do not partition: 1 + 42 + 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)=88b8cb73ceda != 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).

…h data bit of the "T" for a start bit (comment only, Closes #10910)

Comment-only change in the spec header: the generated Verilog is byte for byte the
one the _v2 bitstreams were built from (sha256 d8e31fdcc479...), and the seal's
generated-code hashes are unchanged; only spec_hash moves.

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

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 08:24:47 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 44
PRs with All Checks Green 6
READY 1
FAILING 44
PENDING 0
NO CHECKS YET 0

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

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=88b8cb73ceda != 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).

@gHashTag
gHashTag merged commit 30a1062 into master Oct 19, 2026
36 of 41 checks passed
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 M4e-board: the SAR converter on the AX7203 -- 31 golden conversions, UART receipts bit-exact with the spec

2 participants