rhdl_ed25519_scalable_core/machine.rs
1//! The barrel-threaded field machine: the engine that does the arithmetic.
2//!
3//! A [`FieldMachine`] owns one 255-bit multiplier ([`FieldMulPar`](crate::fe17::FieldMulPar), 225 DSPs, or the 150-DSP [`FieldMulKara`](crate::fe17_kara::FieldMulKara) with `ED25519_MULTIPLIER=LEAN150`,
4//! a new product every clock, 23 clocks of latency), one adder
5//! ([`FieldAddSub`]), one table scanner, a program ROM, and `K = 2^KB`
6//! hardware **contexts**: independent jobs (signatures) resident at once.
7//!
8//! # Why barrel threading
9//!
10//! A pipelined multiplier only helps if something always has a multiply
11//! ready. Instead of finding parallelism inside one job, the machine runs `K`
12//! jobs that take turns: in clock `t`, context `t mod K` issues one bundle.
13//! A context's next turn is `K >= 32` clocks later, after every result it
14//! could need is back (the longest write-back is 25 clocks). So:
15//!
16//! - programs need no latency scheduling and the hardware no hazard logic;
17//! - the multiplier is busy every clock the program has a multiply;
18//! - each context's registers live in block RAM, addressed
19//! `register * K + context`, so contexts cost BRAM rather than LUTs.
20//!
21#![doc = concat!("<div style=\"overflow-x:auto\">", include_str!("../docs/images/barrel-machine.svg"), "</div>")]
22//!
23//! # Pipeline
24//!
25//! ```text
26//! cycle t-1 ROM prefetch for the next head context
27//! cycle t S1 head context issues: bank, table-row and digit addresses
28//! cycle t+1 S2 operands selected; scan accumulator updated
29//! cycle t+2 S3 M and A units issue; OUT captured; lookup negated
30//! cycle t+3 S4 bank L (one of two slots) committed
31//! t+2+23 multiplier write-back t+2+5 adder write-back
32//! ```
33//!
34//! # Registers
35//!
36//! Each bank has exactly one writer, so there are no write-port conflicts:
37//! **M** (multiplier results), **A** (adder results), **I** (job inputs from
38//! the loader), **C** (the program's constants, one copy for all contexts),
39//! and **L** (the point selected by the last table lookup, two slots per
40//! context so one lookup can commit while the previous one is still being
41//! read). Banks are replicated once per read port (four operands per
42//! bundle). An operand in bank I with index 96 or more (`CONST_BASE`) reads C.
43//!
44//! Register addresses interleave contexts: register `r` of context `c` is
45//! word `r * K + c` (`r << KB | c`). Multiplier and adder tags carry
46//! `c << 7 | r` so a result returning 23 or 5 cycles later finds its own
47//! word without any bookkeeping.
48//!
49//! # Where the memories live, and why
50//!
51//! The first layout put every bank in block RAM, and on a full chip block
52//! RAM ran out before LUTs or DSPs: a fixed-base machine used 72 BRAM36
53//! (banks M and A 16 each, bank I 16, base-point tables 22, program 2.5),
54//! and the compress machine 289, of which 228 were its input bank. Two
55//! observations fixed most of it:
56//!
57//! - **Banks M, A and I are written at run time and never preloaded**, which
58//! is exactly what UltraRAM can hold; the chip barely used its 960
59//! UltraRAMs. Constants used to sit in bank I as preloaded contents (one
60//! copy per context), which is what kept bank I in block RAM. So
61//! constants moved to their own 32-entry ROM C (a few LUTs, the same for
62//! every context), and the compress stage writes its identity defaults at
63//! reset instead of preloading them.
64//! - **Machines without inputs** (fixed-base, verify) never write bank I, so
65//! it shrinks to the minimum and lives in LUT RAM.
66//!
67//! [`MachineMemory::SPECIALIZED`] (the default) is that layout;
68//! [`MachineMemory::AS_BUILT`] reproduces the one mapped in Vivado on
69//! 2026-10-14. The new layout is projected at about 24 BRAM36 + 32 URAM per
70//! fixed-base machine; it is **not yet mapped**, and UltraRAM timing at
71//! 205 MHz is the thing to check.
72//!
73//! # Instructions
74//!
75//! An [`InstrW`] bundle has three slots, all usable at once: **M** (multiply;
76//! 7-bit destination, two operands of bank + 7-bit index), **A** (add,
77//! subtract, or `OUT`), and **T** (one table-scan step: digit, entry pair,
78//! 9-bit table row, bank-L slot). Bundles are produced by
79//! [`microcode::Builder`](crate::microcode::Builder); [`encode_bundle`] packs
80//! them into ROM words.
81//!
82//! # Constant-address table lookup
83//!
84//! A lookup of a signed digit runs one T step per pair of table entries. Each
85//! step reads both entries at a public row; the context's accumulator keeps
86//! the one whose number equals the digit's magnitude, and the last step
87//! negates it if the digit is negative and commits it to bank L. No secret
88//! value forms an address, an enable or a branch. (A logical constant-time
89//! property, not a power or EM guarantee.) The tables hold `e · 2^offset(i) · B`
90//! for each of the 54 digits, 372 rows in two BRAM images.
91//!
92//! # Backpressure
93//!
94//! An `OUT` bundle issues only when the single output register and the
95//! pipeline hold no other output; otherwise the context retries on its next
96//! turn. This is the machine's only stall.
97//!
98//! # Parameters and cost
99//!
100//! `FieldMachine<KB, K, RA, IA, PB, TB, KT>`: contexts `K = 2^KB`, bank
101//! address widths `RA` (M/A) and `IA` (I), program-counter width `PB`,
102//! base-point table rows `2^TB`, key-table rows `2^KT`.
103//! [`FbMachine`](crate::sign_core::FbMachine) is `<5, 32, 9, 5, 10, 9, 1>`,
104//! [`CpMachine`](crate::sign_core::CpMachine) is `<6, 64, 12, 13, 9, 1, 1>`.
105//! Vivado (as-built layout, routed at 4.878 ns) put a fixed-base machine at
106//! 10,110 LUT, 13,795 FF, 225 DSP and 72 BRAM36, with +0.407 ns of slack.
107
108use rhdl::prelude::*;
109use rhdl_fpga::core::{
110 constant::Constant,
111 dff::DFF,
112 ram::synchronous::{In as RamIn, RamStyle, SyncBRAM, Write as RamWrite},
113};
114
115use super::{
116 fe17::{
117 Fe17, FieldAddIn, FieldAddSub, FieldMulIn, limbs_from_digits, p_minus,
118 digits_from_limbs,
119 },
120 host::{DIGITS, Fp, Niels, TABLE_ROWS, basepoint_table, digit_entries, table_row},
121 microcode::{AOp, Bank, Bundle, Operand, Program},
122};
123
124// Generated by `build.rs` from `ED25519_MULTIPLIER`: `FieldMul`, the
125// multiplier every field machine of this build uses.
126include!(concat!(env!("OUT_DIR"), "/multiplier.rs"));
127// Generated by `build.rs` from `ED25519_URAM_MACHINES`: `URAM_MACHINES`.
128include!(concat!(env!("OUT_DIR"), "/placement.rs"));
129
130/// Bank encodings in [`OperandW::bank`]: bank M (multiplier results).
131pub const BANK_M: u128 = 0;
132/// Bank A (adder results).
133pub const BANK_A: u128 = 1;
134/// Bank I (inputs, indices below [`CONST_BASE`]) and the constant ROM C
135/// (indices [`CONST_BASE`] and up). They share a code so the operand field
136/// stays two bits wide.
137pub const BANK_I: u128 = 2;
138/// Bank L (selected lookup point).
139pub const BANK_L: u128 = 3;
140
141/// Encoded operand: which bank to read and which register in it.
142#[derive(Copy, Clone, Debug, PartialEq, Digital)]
143pub struct OperandW {
144 /// Bank code ([`BANK_M`], [`BANK_A`], [`BANK_I`] or [`BANK_L`]).
145 pub bank: b2,
146 /// Register index within the context (bank I: 64 and up selects
147 /// constant `index - 64`; bank L: `4 * slot + coordinate`).
148 pub index: b7,
149}
150
151/// Encoded VLIW bundle stored in program ROM, one word per program counter.
152///
153/// The three slots are independent; a disabled slot's other fields are
154/// ignored. Operands are read in S1/S2 and the units issue in S3.
155#[derive(Copy, Clone, Debug, PartialEq, Digital)]
156pub struct InstrW {
157 /// Multiply slot enabled.
158 pub m_en: bool,
159 /// Multiply destination register (bank M).
160 pub m_dst: b7,
161 /// First factor (read port 0; never bank L).
162 pub m_a: OperandW,
163 /// Second factor (read port 1; the only operand programs point at bank L).
164 pub m_b: OperandW,
165 /// Add slot enabled.
166 pub a_en: bool,
167 /// 0 add, 1 subtract, 2 output (`OUT` sends `a_a` to the output port
168 /// instead of using the adder).
169 pub a_op: b2,
170 /// Add destination register (bank A); unused by `OUT`.
171 pub a_dst: b7,
172 /// First operand (read port 2), also the value an `OUT` emits.
173 pub a_a: OperandW,
174 /// Second operand (read port 3).
175 pub a_b: OperandW,
176 /// Program output index reported with an `OUT` event.
177 pub out_index: b6,
178 /// Scan slot enabled.
179 pub t_en: bool,
180 /// Digit slot 0..128 (base-point scalar 0..54, key scalar 64..118).
181 pub t_digit: b7,
182 /// Scan the key table instead of the base-point table.
183 pub t_key: bool,
184 /// Entry pair 0..8: this step compares the digit magnitude with
185 /// entries `2 * pair + 1` and `2 * pair + 2`.
186 pub t_pair: b3,
187 /// Table row of the pair (`table_row(digit) + pair`), a public constant.
188 pub t_row: b9,
189 /// Bank-L slot committed by `t_finish`.
190 pub t_lslot: bool,
191 /// Reset the accumulator to the identity first (first step of a lookup).
192 pub t_clear: bool,
193 /// Commit the accumulator, negated if the digit is negative, to bank L
194 /// (last step of a lookup).
195 pub t_finish: bool,
196 /// Last bundle of its program; the context retires after issuing it.
197 pub end: bool,
198}
199
200/// Affine Niels point in radix-`2^17` digits: the form table entries are
201/// stored in, ready for a mixed addition.
202#[derive(Copy, Clone, Debug, PartialEq, Digital)]
203pub struct Niels17 {
204 /// `y + x`.
205 pub ypx: Fe17,
206 /// `y - x`.
207 pub ymx: Fe17,
208 /// `2dxy`.
209 pub xy2d: Fe17,
210}
211
212/// Bits of a key-slot number: 3 (8 public keys) by default, 4 (16) with the
213/// feature `key-slots-16` (uBFT: a replica verifies up to 11 peers). Each slot
214/// owns 512 rows of every verify machine's key table, so 16 slots double the
215/// key-table memory.
216#[cfg(not(feature = "key-slots-16"))]
217pub const KEY_SLOT_BITS: usize = 3;
218/// Bits of a key-slot number (feature `key-slots-16`: 16 slots).
219#[cfg(feature = "key-slots-16")]
220pub const KEY_SLOT_BITS: usize = 4;
221/// Public keys a known-key verifier holds at once.
222pub const KEY_SLOTS: usize = 1 << KEY_SLOT_BITS;
223/// Rows of a verify machine's key table: 512 per slot.
224pub const KEY_TABLE_BITS: usize = 9 + KEY_SLOT_BITS;
225/// A key-slot number.
226pub type KeySlot = Bits<KEY_SLOT_BITS>;
227
228/// Key slot 0, for requests that look up no key. A kernel generic over a
229/// width (`CB`) cannot write `bits(0)` here: rustc would infer that width.
230#[kernel]
231pub fn no_key() -> KeySlot {
232 bits(0)
233}
234
235/// Start request: context `ctx` (idle, inputs loaded) begins at bundle
236/// `entry`.
237///
238/// The request is latched as pending and takes effect on the context's next
239/// turn; the caller may assert it on any cycle.
240#[derive(Copy, Clone, Debug, PartialEq, Digital)]
241pub struct StartReq<const KB: usize>
242where
243 rhdl::bits::W<KB>: BitWidth,
244{
245 /// Request valid this cycle.
246 pub valid: bool,
247 /// Context to start (`KB` bits).
248 pub ctx: Bits<KB>,
249 /// Opaque job tag reported with outputs.
250 pub tag: b16,
251 /// Key-table slot for key lookups (row `key * 512 + row`).
252 pub key: KeySlot,
253 /// First bundle (programs share one ROM; see `microcode::concat`).
254 pub entry: b12,
255}
256
257/// Digits write for the table scanner (sign-magnitude: bit 5 = sign).
258///
259/// Writes all 128 digit slots of one context at once, so a context's scalar
260/// recoding is one memory word read by every T step.
261#[derive(Copy, Clone, Debug, PartialEq, Digital)]
262pub struct DigitsWrite<const KB: usize>
263where
264 rhdl::bits::W<KB>: BitWidth,
265{
266 /// Write valid this cycle.
267 pub valid: bool,
268 /// Context whose digits are replaced.
269 pub ctx: Bits<KB>,
270 /// Sign-magnitude digits, magnitude (0..=16) in bits 4..0: slots 0..54
271 /// for the base-point scalar, 64..118 for the key scalar, zero elsewhere.
272 pub digits: [b6; 128],
273}
274
275/// One key-table coordinate write (the key-loading path): one coordinate
276/// of one entry per cycle, so a full entry pair takes six writes.
277#[derive(Copy, Clone, Debug, PartialEq, Digital)]
278pub struct TableWrite {
279 /// Write valid this cycle.
280 pub valid: bool,
281 /// Row: `key * 512 + table row` (truncated to the `KT`-bit table).
282 pub row: Bits<KEY_TABLE_BITS>,
283 /// Odd image (entries `2*pair + 2`) instead of the even one
284 /// (entries `2*pair + 1`).
285 pub odd: bool,
286 /// Coordinate: 0 `y+x`, 1 `y-x`, 2 `2dxy` (3 writes nothing).
287 pub coord: b2,
288 /// Coordinate value.
289 pub value: Fe17,
290}
291
292/// Bank-I write: one input register of one context, from the loader.
293#[derive(Copy, Clone, Debug, PartialEq, Digital)]
294pub struct InputWrite<const KB: usize>
295where
296 rhdl::bits::W<KB>: BitWidth,
297{
298 /// Write valid this cycle.
299 pub valid: bool,
300 /// Context.
301 pub ctx: Bits<KB>,
302 /// Input register index (below [`CONST_BASE`]).
303 pub index: b7,
304 /// Value.
305 pub value: Fe17,
306}
307
308/// Machine inputs, all sampled every cycle and independent of each other.
309#[derive(Copy, Clone, Debug, PartialEq, Digital)]
310pub struct MachineIn<const KB: usize>
311where
312 rhdl::bits::W<KB>: BitWidth,
313{
314 /// Start a context.
315 pub start: StartReq<KB>,
316 /// Load scan digits.
317 pub digits: DigitsWrite<KB>,
318 /// Load an input register.
319 pub input: InputWrite<KB>,
320 /// Load a key-table entry.
321 pub table: TableWrite,
322 /// The consumer takes [`MachineOut::out`] this cycle. The output
323 /// register is freed for new `OUT` bundles from the next cycle on.
324 pub out_accept: bool,
325}
326
327/// Output-port event from an `OUT` bundle slot.
328///
329/// The machine has one output register, which is why `OUT` bundles are the
330/// only ones that can stall.
331#[derive(Copy, Clone, Debug, PartialEq, Digital)]
332pub struct OutEvent<const KB: usize>
333where
334 rhdl::bits::W<KB>: BitWidth,
335{
336 /// Event valid; stays set until [`MachineIn::out_accept`].
337 pub valid: bool,
338 /// Context that emitted it.
339 pub ctx: Bits<KB>,
340 /// Job tag given at start.
341 pub tag: b16,
342 /// Program output index (`InstrW::out_index`).
343 pub index: b6,
344 /// Emitted value.
345 pub value: Fe17,
346}
347
348/// Machine outputs.
349#[derive(Copy, Clone, Debug, PartialEq, Digital)]
350pub struct MachineOut<const KB: usize>
351where
352 rhdl::bits::W<KB>: BitWidth,
353{
354 /// Output event, held until accepted.
355 pub out: OutEvent<KB>,
356 /// Context retired its last bundle this cycle (one-cycle pulse when that
357 /// bundle reaches S3). All of its earlier `OUT`s have been captured; an
358 /// `OUT` in the last bundle itself appears on `out` one cycle later.
359 pub done: bool,
360 /// Retired context.
361 pub done_ctx: Bits<KB>,
362 /// Retired job tag.
363 pub done_tag: b16,
364}
365
366/// Per-context thread state, held in the rotating ring.
367///
368/// Entries carry no context number: the ring rotates by one each clock, so
369/// `ring[k]` always belongs to context `slot + k (mod K)`.
370#[derive(Copy, Clone, Debug, PartialEq, Digital)]
371#[doc(hidden)]
372pub struct Ring<const PB: usize>
373where
374 rhdl::bits::W<PB>: BitWidth,
375{
376 /// The context is running a program.
377 active: bool,
378 /// Program counter of the context's next bundle.
379 pc: Bits<PB>,
380 /// Job tag of the running program.
381 tag: b16,
382}
383
384/// Control word that travels with a bundle down stages S2 and S3.
385#[derive(Copy, Clone, Debug, PartialEq, Digital)]
386#[doc(hidden)]
387pub struct Stage<const KB: usize, const PB: usize>
388where
389 rhdl::bits::W<KB>: BitWidth,
390 rhdl::bits::W<PB>: BitWidth,
391{
392 /// A bundle issued (false for stalled or idle turns).
393 valid: bool,
394 /// The bundle is its program's last (`InstrW::end`).
395 last: bool,
396 /// Issuing context.
397 ctx: Bits<KB>,
398 /// Job tag of the issuing context.
399 tag: b16,
400}
401
402/// All flip-flop state of a [`FieldMachine`] outside its memories and units.
403#[derive(Copy, Clone, Debug, PartialEq, Digital)]
404#[doc(hidden)]
405pub struct MachineRegs<const KB: usize, const PB: usize, const K: usize>
406where
407 rhdl::bits::W<KB>: BitWidth,
408 rhdl::bits::W<PB>: BitWidth,
409{
410 /// Context whose turn it is this cycle (owner of `ring[0]`); wraps mod K.
411 slot: Bits<KB>,
412 /// Thread state of every context, rotated one place per clock.
413 ring: [Ring<PB>; K],
414 /// A start request is waiting for the context's next turn.
415 pending: [bool; K],
416 /// Job tag of the pending start.
417 pending_tag: [b16; K],
418 /// Key-table slot of each context, latched at start.
419 key: [KeySlot; K],
420 /// Entry bundle of the pending start.
421 pending_entry: [b12; K],
422 /// The single output register.
423 out: OutEvent<KB>,
424 /// S2 control (bundle issued last cycle).
425 s2: Stage<KB, PB>,
426 /// S2 bundle; slot enables are cleared when the bundle did not issue.
427 s2_instr: InstrW,
428 /// S2 scan digit, picked in S1 from the digit word read a cycle ahead.
429 /// (Until 2026-10-17 the 128:1 pick sat in S2 in front of the
430 /// magnitude compare and the 765-bit select: the 8-machine signer's
431 /// worst routed path, -0.581 ns, 87 % route.)
432 s2_digit: b6,
433 /// This cycle's head got its start or its digit word one cycle before
434 /// its turn, after its bundle and digit word were read ahead: a start
435 /// waits a turn.
436 dig_fresh: bool,
437 /// The head's bundle, read from the ROM two slots ahead and registered
438 /// here, so S1 decodes from flip-flops. (Until 2026-10-17 S1 used the ROM
439 /// output directly; with the digit pick in S1 that was the 6-machine
440 /// verifier's worst routed path, -0.370 ns, ROM -> t_digit -> 128:1 pick.)
441 s1_instr: InstrW,
442 /// S3 control.
443 s3: Stage<KB, PB>,
444 /// S3 bundle.
445 s3_instr: InstrW,
446 /// S3 operands, in port order: `m_a`, `m_b`, `a_a`, `a_b`.
447 s3_ops: [Fe17; 4],
448 /// S3 copy of the scan accumulator after this bundle's T step.
449 s3_acc: Niels17,
450 /// S3 sign of the digit the T step scanned.
451 s3_negative: bool,
452 /// S4: commit `s4_l` to bank L this cycle.
453 s4_commit: bool,
454 /// S4: bank-L slot to commit to.
455 s4_lslot: bool,
456 /// S4: context to commit for.
457 s4_ctx: Bits<KB>,
458 /// S4: the selected point, already negated if the digit was negative.
459 s4_l: Niels17,
460 /// The write-only inputs (start, digits, input and table writes),
461 /// registered on arrival and acted on one cycle later, in order. The
462 /// controller's dispatch (a priority pick over machines and contexts)
463 /// and the machine's wide latching (`pending[ctx]` and friends) then
464 /// sit in different cycles. (Until 2026-10-17 they shared one: the
465 /// 8-machine signer's worst routed path, -1.338 ns, 90 % route, from
466 /// `fb_free` to a machine's clock enables.) `out_accept` is a handshake
467 /// and stays combinational.
468 in_start: StartReq<KB>,
469 /// Registered digit write.
470 in_digits: DigitsWrite<KB>,
471 /// Registered input write.
472 in_input: InputWrite<KB>,
473 /// Registered table write.
474 in_table: TableWrite,
475}
476
477/// Constants a program can address (operand index `CONST_BASE + c`): 32,
478/// the size of ROM C, which fills the 7-bit index field above the inputs.
479pub const CONST_SLOTS: usize = 32;
480/// Bank-I operand index of constant 0; lower indices are inputs. 96 leaves
481/// room for the compress program's 96 inputs (32 points x 3 coordinates).
482pub const CONST_BASE: usize = 96;
483
484/// The constant ROM C: [`CONST_SLOTS`] constants as a literal and a
485/// registered read, with the ports of a [`SyncBRAM`] (the write port is
486/// ignored).
487///
488/// This was a `SyncBRAM` with its write disabled, which Vivado 2023.2
489/// mapped to RAM32M16 LUT RAM. In the aux machine (constants 1, d, √−1,
490/// …) synthesis trimmed the bit columns that are zero in every entry,
491/// kept the INIT strings in the original layout, and wired the output
492/// flip-flops packed: the card's netlist read d with 16 wrong bits and
493/// rejected valid keys (DSig, build v3, 2026-10-19). A literal and a mux
494/// leave no RAM to infer.
495#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
496pub struct ConstRom {
497 /// The constants, slot `c` at index `c`.
498 table: Constant<[Fe17; CONST_SLOTS]>,
499 /// The read register (one clock of latency, as a `SyncBRAM`).
500 out: DFF<Fe17>,
501}
502
503impl ConstRom {
504 /// A ROM holding `consts` (slot, value); missing slots read zero.
505 fn new(consts: &[(Bits<5>, Fe17)]) -> Self {
506 let mut table = [Fe17::zero(); CONST_SLOTS];
507 for &(slot, value) in consts {
508 table[slot.raw() as usize] = value;
509 }
510 Self { table: Constant::new(table), out: DFF::new(Fe17::zero()) }
511 }
512}
513
514/// Port types of [`ConstRom`]: those of a 32-word `SyncBRAM`.
515impl SynchronousIO for ConstRom {
516 type I = RamIn<Fe17, 5>;
517 type O = Fe17;
518 type Kernel = const_rom_kernel;
519}
520
521/// Read slot `read_addr` into the register.
522#[kernel]
523pub fn const_rom_kernel(_cr: ClockReset, i: RamIn<Fe17, 5>, q: ConstRomQ) -> (Fe17, ConstRomD) {
524 let mut d = ConstRomD::dont_care();
525 d.table = ();
526 d.out = q.table[i.read_addr];
527 (q.out, d)
528}
529
530/// Register banks M, A and I and the constant ROM C, one copy per read
531/// port. Bank I holds per-context inputs; C holds the program's constants
532/// once for all contexts (32 entries, LUT ROM).
533///
534/// Each memory has one read and one write port; four copies of each bank
535/// with identical writes give the four read ports a bundle needs. Reads have
536/// one clock of latency.
537#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
538pub struct RegBanks<const RA: usize, const IA: usize>
539where
540 rhdl::bits::W<RA>: BitWidth,
541 rhdl::bits::W<IA>: BitWidth,
542{
543 /// Bank M copies, one per read port (`2^RA` words).
544 m: [SyncBRAM<Fe17, RA>; 4],
545 /// Bank A copies, one per read port (`2^RA` words).
546 a: [SyncBRAM<Fe17, RA>; 4],
547 /// Bank I copies, one per read port (`2^IA` words).
548 i: [SyncBRAM<Fe17, IA>; 4],
549 /// Constant ROM C copies, one per read port (32 words).
550 c: [ConstRom; 4],
551}
552
553/// Port bundle of [`RegBanks`].
554#[derive(Copy, Clone, Debug, PartialEq, Digital)]
555pub struct RegBanksIn<const RA: usize, const IA: usize>
556where
557 rhdl::bits::W<RA>: BitWidth,
558 rhdl::bits::W<IA>: BitWidth,
559{
560 /// Bank M ports, indexed by read port.
561 pub m: [RamIn<Fe17, RA>; 4],
562 /// Bank A ports.
563 pub a: [RamIn<Fe17, RA>; 4],
564 /// Bank I ports.
565 pub i: [RamIn<Fe17, IA>; 4],
566 /// Constant ROM ports.
567 pub c: [RamIn<Fe17, 5>; 4],
568}
569
570/// Read data of [`RegBanks`], one cycle after the read address.
571#[derive(Copy, Clone, Debug, PartialEq, Digital)]
572pub struct RegBanksOut {
573 /// Bank M copies.
574 pub m: [Fe17; 4],
575 /// Bank A copies.
576 pub a: [Fe17; 4],
577 /// Bank I copies.
578 pub i: [Fe17; 4],
579 /// Constant ROM copies.
580 pub c: [Fe17; 4],
581}
582
583/// Port types of [`RegBanks`].
584impl<const RA: usize, const IA: usize> SynchronousIO for RegBanks<RA, IA>
585where
586 rhdl::bits::W<RA>: BitWidth,
587 rhdl::bits::W<IA>: BitWidth,
588{
589 type I = RegBanksIn<RA, IA>;
590 type O = RegBanksOut;
591 type Kernel = reg_banks_kernel<RA, IA>;
592}
593
594/// Wiring for [`RegBanks`]: ports pass straight through to the memories.
595#[kernel]
596pub fn reg_banks_kernel<const RA: usize, const IA: usize>(
597 _cr: ClockReset,
598 i: RegBanksIn<RA, IA>,
599 q: RegBanksQ<RA, IA>,
600) -> (RegBanksOut, RegBanksD<RA, IA>)
601where
602 rhdl::bits::W<RA>: BitWidth,
603 rhdl::bits::W<IA>: BitWidth,
604{
605 let mut d = RegBanksD::<RA, IA>::dont_care();
606 d.m = i.m;
607 d.a = i.a;
608 d.i = i.i;
609 d.c = i.c;
610 (
611 RegBanksOut {
612 m: q.m,
613 a: q.a,
614 i: q.i,
615 c: q.c,
616 },
617 d,
618 )
619}
620
621/// Table-scan memories: bank L, the scan accumulator, digits, the base-point
622/// table (`2^TB` rows) and the key table (`2^KT` rows). A machine that never
623/// scans uses `TB = KT = 1`, and one without key lookups `KT = 1`.
624///
625/// The per-context memories (`bank_l0`, `bank_l1`, `acc`, `digits`) have
626/// one word per context, addressed by context number.
627#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
628pub struct ScanMems<const KB: usize, const TB: usize, const KT: usize>
629where
630 rhdl::bits::W<KB>: BitWidth,
631 rhdl::bits::W<TB>: BitWidth,
632 rhdl::bits::W<KT>: BitWidth,
633{
634 /// Bank L, slot 0: one committed lookup result per context.
635 bank_l0: SyncBRAM<Niels17, KB>,
636 /// Bank L, slot 1.
637 bank_l1: SyncBRAM<Niels17, KB>,
638 /// Scan accumulator: the entry selected so far by the current lookup.
639 acc: SyncBRAM<Niels17, KB>,
640 /// 128 sign-magnitude digits per context.
641 digits: SyncBRAM<[b6; 128], KB>,
642 /// Base-point table, even image (entries `2 * pair + 1`); preloaded ROM.
643 table0: SyncBRAM<Niels17, TB>,
644 /// Base-point table, odd image (entries `2 * pair + 2`); preloaded ROM.
645 table1: SyncBRAM<Niels17, TB>,
646 /// Per-key tables written at run time.
647 key: KeyTables<KT>,
648}
649
650/// Port bundle of [`ScanMems`].
651#[derive(Copy, Clone, Debug, PartialEq, Digital)]
652pub struct ScanMemsIn<const KB: usize, const TB: usize, const KT: usize>
653where
654 rhdl::bits::W<KB>: BitWidth,
655 rhdl::bits::W<TB>: BitWidth,
656 rhdl::bits::W<KT>: BitWidth,
657{
658 /// Bank L, slot 0.
659 pub bank_l0: RamIn<Niels17, KB>,
660 /// Bank L, slot 1.
661 pub bank_l1: RamIn<Niels17, KB>,
662 /// Accumulator.
663 pub acc: RamIn<Niels17, KB>,
664 /// Digits.
665 pub digits: RamIn<[b6; 128], KB>,
666 /// Even base-point table entries.
667 pub table0: RamIn<Niels17, TB>,
668 /// Odd base-point table entries.
669 pub table1: RamIn<Niels17, TB>,
670 /// Key tables.
671 pub key: KeyTablesIn<KT>,
672}
673
674/// Read data of [`ScanMems`].
675#[derive(Copy, Clone, Debug, PartialEq, Digital)]
676pub struct ScanMemsOut {
677 /// Bank L, slot 0.
678 pub bank_l0: Niels17,
679 /// Bank L, slot 1.
680 pub bank_l1: Niels17,
681 /// Accumulator.
682 pub acc: Niels17,
683 /// Digits.
684 pub digits: [b6; 128],
685 /// Even base-point table entry.
686 pub table0: Niels17,
687 /// Odd base-point table entry.
688 pub table1: Niels17,
689 /// Key-table entries (even, odd).
690 pub key: [Niels17; 2],
691}
692
693/// Port types of [`ScanMems`].
694impl<const KB: usize, const TB: usize, const KT: usize> SynchronousIO for ScanMems<KB, TB, KT>
695where
696 rhdl::bits::W<KB>: BitWidth,
697 rhdl::bits::W<TB>: BitWidth,
698 rhdl::bits::W<KT>: BitWidth,
699{
700 type I = ScanMemsIn<KB, TB, KT>;
701 type O = ScanMemsOut;
702 type Kernel = scan_mems_kernel<KB, TB, KT>;
703}
704
705/// Wiring for [`ScanMems`]: ports pass straight through to the memories.
706#[kernel]
707pub fn scan_mems_kernel<const KB: usize, const TB: usize, const KT: usize>(
708 _cr: ClockReset,
709 i: ScanMemsIn<KB, TB, KT>,
710 q: ScanMemsQ<KB, TB, KT>,
711) -> (ScanMemsOut, ScanMemsD<KB, TB, KT>)
712where
713 rhdl::bits::W<KB>: BitWidth,
714 rhdl::bits::W<TB>: BitWidth,
715 rhdl::bits::W<KT>: BitWidth,
716{
717 let mut d = ScanMemsD::<KB, TB, KT>::dont_care();
718 d.bank_l0 = i.bank_l0;
719 d.bank_l1 = i.bank_l1;
720 d.acc = i.acc;
721 d.digits = i.digits;
722 d.table0 = i.table0;
723 d.table1 = i.table1;
724 d.key = i.key;
725 (
726 ScanMemsOut {
727 bank_l0: q.bank_l0,
728 bank_l1: q.bank_l1,
729 acc: q.acc,
730 digits: q.digits,
731 table0: q.table0,
732 table1: q.table1,
733 key: q.key,
734 },
735 d,
736 )
737}
738
739/// Key tables: two images (even/odd entries) of three coordinate RAMs each,
740/// written one coordinate at a time.
741///
742/// Splitting by coordinate keeps the write port one [`Fe17`] wide, so the
743/// loader needs no read-modify-write of a full Niels entry.
744#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
745pub struct KeyTables<const KT: usize>
746where
747 rhdl::bits::W<KT>: BitWidth,
748{
749 /// `ram[0..3]`: even image `y+x`, `y-x`, `2dxy`; `ram[3..6]`: odd image.
750 ram: [SyncBRAM<Fe17, KT>; 6],
751}
752
753/// Key tables with the default memory style (left to synthesis).
754impl<const KT: usize> Default for KeyTables<KT>
755where
756 rhdl::bits::W<KT>: BitWidth,
757{
758 fn default() -> Self {
759 KeyTables { ram: std::array::from_fn(|_| SyncBRAM::default()) }
760 }
761}
762
763impl<const KT: usize> KeyTables<KT>
764where
765 rhdl::bits::W<KT>: BitWidth,
766{
767 /// Key tables in UltraRAM (written at run time, never preloaded).
768 pub fn ultra() -> Self {
769 KeyTables { ram: std::array::from_fn(|_| SyncBRAM::default().with_style(RamStyle::Ultra)) }
770 }
771}
772
773/// Port bundle of [`KeyTables`]: one shared read row, one coordinate write.
774#[derive(Copy, Clone, Debug, PartialEq, Digital)]
775pub struct KeyTablesIn<const KT: usize>
776where
777 rhdl::bits::W<KT>: BitWidth,
778{
779 /// Row read by both images.
780 pub read_addr: Bits<KT>,
781 /// One coordinate write.
782 pub write: TableWrite,
783}
784
785/// Port types of [`KeyTables`]: the read returns the (even, odd) entries.
786impl<const KT: usize> SynchronousIO for KeyTables<KT>
787where
788 rhdl::bits::W<KT>: BitWidth,
789{
790 type I = KeyTablesIn<KT>;
791 type O = [Niels17; 2];
792 type Kernel = key_tables_kernel<KT>;
793}
794
795/// Wiring for [`KeyTables`].
796#[kernel]
797pub fn key_tables_kernel<const KT: usize>(
798 _cr: ClockReset,
799 i: KeyTablesIn<KT>,
800 q: KeyTablesQ<KT>,
801) -> ([Niels17; 2], KeyTablesD<KT>)
802where
803 rhdl::bits::W<KT>: BitWidth,
804{
805 let mut d = KeyTablesD::<KT>::dont_care();
806 let w = i.write;
807 // RAM number = image base (0 even, 3 odd) + coordinate; exactly one of
808 // the six RAMs is write-enabled, all share the address and data.
809 let image = if w.odd { b3(3) } else { b3(0) };
810 for k in 0..6 {
811 d.ram[k] = RamIn::<Fe17, KT> {
812 read_addr: i.read_addr,
813 write: RamWrite::<Fe17, KT> {
814 addr: w.row.resize::<KT>(),
815 value: w.value,
816 enable: w.valid && (b3(k as u128) == image + w.coord.resize::<3>()),
817 },
818 };
819 }
820 (
821 [
822 Niels17 { ypx: q.ram[0], ymx: q.ram[1], xy2d: q.ram[2] },
823 Niels17 { ypx: q.ram[3], ymx: q.ram[4], xy2d: q.ram[5] },
824 ],
825 d,
826 )
827}
828
829/// Barrel field machine. `K = 2^KB` contexts; `RA`/`IA` are bank-M/A and
830/// bank-I address widths (`index * K + ctx`); `PB` is the program-counter width.
831/// `TB` and `KT` are the base-point and key table address widths. See the
832/// module docs for the pipeline and bank layout.
833#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
834#[rhdl(dq_no_prefix)]
835pub struct FieldMachine<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
836where
837 rhdl::bits::W<KB>: BitWidth,
838 rhdl::bits::W<RA>: BitWidth,
839 rhdl::bits::W<IA>: BitWidth,
840 rhdl::bits::W<PB>: BitWidth,
841 rhdl::bits::W<TB>: BitWidth,
842 rhdl::bits::W<KT>: BitWidth,
843{
844 /// Thread ring, pipeline registers and the output register.
845 regs: DFF<MachineRegs<KB, PB, K>>,
846 /// Program ROM (`2^PB` bundles, preloaded); read one cycle ahead of issue.
847 rom: SyncBRAM<InstrW, PB>,
848 /// Register banks M, A, I and constant ROM C.
849 banks: RegBanks<RA, IA>,
850 /// Bank L, scan accumulator, digits and lookup tables.
851 scan: ScanMems<KB, TB, KT>,
852 /// The field multiplier, fed from S3: the 225-DSP
853 /// [`FieldMulPar`](crate::fe17::FieldMulPar) or, with `ED25519_MULTIPLIER=LEAN150`, the 150-DSP
854 /// [`FieldMulKara`](crate::fe17_kara::FieldMulKara) (same ports and
855 /// latency).
856 mul: FieldMul,
857 /// The adder/subtractor, fed from S3.
858 add: FieldAddSub,
859}
860
861/// Encode one operand reference. Bank C shares bank I's code with the index
862/// offset by [`CONST_BASE`]. Panics if the encoded index exceeds 7 bits.
863/// `program` is not used.
864fn encode_operand(o: Operand, _program: &Program) -> OperandW {
865 let (bank, index) = match o.bank {
866 Bank::M => (BANK_M, o.index as usize),
867 Bank::A => (BANK_A, o.index as usize),
868 Bank::I => (BANK_I, o.index as usize),
869 Bank::C => (BANK_I, CONST_BASE + o.index as usize),
870 Bank::L => (BANK_L, o.index as usize),
871 };
872 assert!(index < 128, "register index exceeds the 7-bit operand field");
873 OperandW {
874 bank: b2(bank),
875 index: b7(index as u128),
876 }
877}
878
879/// Encode a compiled bundle as a ROM word.
880///
881/// Empty slots are encoded disabled with harmless operands. Panics if the
882/// first multiply factor is in bank L (the instruction set reserves bank L
883/// for the second factor) or an operand index does not fit seven bits.
884pub fn encode_bundle(b: &Bundle, program: &Program) -> InstrW {
885 // Unused operands read input register 0: a valid address in any bank I.
886 let none = OperandW {
887 bank: b2(BANK_I),
888 index: b7(0),
889 };
890 let mut w = InstrW {
891 m_en: false,
892 m_dst: b7(0),
893 m_a: none,
894 m_b: none,
895 a_en: false,
896 a_op: b2(0),
897 a_dst: b7(0),
898 a_a: none,
899 a_b: none,
900 out_index: b6(0),
901 t_en: false,
902 t_digit: b7(0),
903 t_key: false,
904 t_pair: b3(0),
905 t_row: b9(0),
906 t_lslot: false,
907 t_clear: false,
908 t_finish: false,
909 end: false,
910 };
911 if let Some(m) = b.m {
912 w.m_en = true;
913 w.m_dst = b7(m.dst as u128);
914 w.m_a = encode_operand(m.a, program);
915 w.m_b = encode_operand(m.b, program);
916 assert!(m.a.bank != Bank::L);
917 }
918 if let Some(a) = b.a {
919 w.a_en = true;
920 w.a_dst = b7(a.dst as u128);
921 w.a_a = encode_operand(a.a, program);
922 w.a_b = encode_operand(a.b, program);
923 w.a_op = b2(match a.op {
924 AOp::Add => 0,
925 AOp::Sub => 1,
926 AOp::Out(index) => {
927 w.out_index = b6(index as u128);
928 2
929 }
930 });
931 }
932 if let Some(t) = b.t {
933 w.t_en = true;
934 w.t_digit = b7(t.digit as u128);
935 w.t_key = t.key;
936 w.t_pair = b3(t.pair as u128);
937 w.t_row = b9(t.row as u128);
938 w.t_lslot = t.l_slot == 1;
939 w.t_clear = t.clear;
940 w.t_finish = t.finish;
941 }
942 w.end = b.end;
943 w
944}
945
946/// A fixed-base table as two BRAM images (even and odd entries of each
947/// pair), row `table_row(i) + pair`.
948///
949/// Returns `(address, entry)` preload lists for `table0` and `table1`. The
950/// scanner reads both images at one row, so one T step tests two entries.
951pub fn table_images(table: &[Vec<Niels>]) -> (Vec<(usize, Niels17)>, Vec<(usize, Niels17)>) {
952 let mut even = Vec::new();
953 let mut odd = Vec::new();
954 for i in 0..DIGITS {
955 for e in 0..digit_entries(i) {
956 let n = table[i][e];
957 let entry = Niels17 {
958 ypx: Fe17::from_fp(n.ypx),
959 ymx: Fe17::from_fp(n.ymx),
960 xy2d: Fe17::from_fp(n.xy2d),
961 };
962 // Row table_row(i) + pair. `e` is zero-based, so entry number
963 // (magnitude) 2*pair+1 has even e = 2*pair and goes to `even`.
964 let address = table_row(i) + e / 2;
965 if e % 2 == 0 {
966 even.push((address, entry));
967 } else {
968 odd.push((address, entry));
969 }
970 }
971 }
972 // Every digit has an even entry count, so each row has both images.
973 assert_eq!(even.len(), TABLE_ROWS);
974 (even, odd)
975}
976
977/// A per-context scanner memory: LUT RAM when `lean` (the program never
978/// scans), otherwise left to synthesis.
979fn scan_mem<T: Digital, const N: usize>(lean: bool) -> SyncBRAM<T, N>
980where
981 rhdl::bits::W<N>: BitWidth,
982{
983 if lean { SyncBRAM::default().with_style(RamStyle::Distributed) } else { SyncBRAM::default() }
984}
985
986/// One copy of bank M or A: UltraRAM when `ultra`, LUT RAM when `lut`,
987/// otherwise left to synthesis (block RAM).
988fn bank<const RA: usize>(ultra: bool, lut: bool) -> SyncBRAM<Fe17, RA>
989where
990 rhdl::bits::W<RA>: BitWidth,
991{
992 if ultra {
993 SyncBRAM::default().with_style(RamStyle::Ultra)
994 } else if lut {
995 SyncBRAM::default().with_style(RamStyle::Distributed)
996 } else {
997 SyncBRAM::default()
998 }
999}
1000
1001/// Where a field machine keeps its memories.
1002///
1003/// The as-built machines put everything in block RAM, which is what runs
1004/// out first on a full chip (a fixed-base machine used 72 BRAM36: banks M
1005/// and A 16 each, bank I 16, base-point tables 22, program 2.5). The
1006/// specialized layout moves the run-time-written banks to UltraRAM, which
1007/// the design barely uses, and turns a constants-only bank I into LUT ROM.
1008#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1009pub struct MachineMemory {
1010 /// Banks M and A and the key tables in UltraRAM (`ram_style = "ultra"`).
1011 /// Bank I also goes to UltraRAM when the program has inputs and no
1012 /// presets (UltraRAM cannot be preloaded).
1013 pub ultra_banks: bool,
1014 /// Bank I as LUT ROM (`ram_style = "distributed"`) when the program
1015 /// takes no per-context inputs, so the bank only holds constants. Also
1016 /// puts the constant ROM C, and the scanner's per-context memories of
1017 /// programs with no T steps, in LUT RAM.
1018 pub rom_consts: bool,
1019 /// Banks M and A in LUT RAM (`ram_style = "distributed"`) when
1020 /// `ultra_banks` is off: the layout that turns spare LUTs into
1021 /// machines once UltraRAM and block RAM are spent. Key tables stay in
1022 /// UltraRAM.
1023 pub lut_banks: bool,
1024}
1025
1026impl MachineMemory {
1027 /// Every memory left to synthesis (block RAM), as mapped in Vivado on
1028 /// 2026-10-14.
1029 pub const AS_BUILT: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: false, lut_banks: false };
1030 /// UltraRAM banks and LUT-ROM constants (the default for [`FieldMachine::new`]).
1031 pub const SPECIALIZED: MachineMemory = MachineMemory { ultra_banks: true, rom_consts: true, lut_banks: false };
1032 /// The block-RAM fallback for a machine that UltraRAM cannot hold:
1033 /// banks M and A (and key tables) in block RAM, constants still in LUT
1034 /// ROM. What the planner places when a build runs out of UltraRAM.
1035 pub const BRAM_BANKS: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: true, lut_banks: false };
1036 /// The LUT-RAM fallback: banks M and A in LUT RAM, key tables in
1037 /// UltraRAM, constants in LUT ROM.
1038 pub const LUT_BANKS: MachineMemory = MachineMemory { ultra_banks: false, rom_consts: true, lut_banks: true };
1039}
1040
1041/// The layout of machine `k` of a core's replicated machines (fixed-base
1042/// machines in a signer, verify machines in a verifier) in this build:
1043/// [`MachineMemory::SPECIALIZED`] for the first [`URAM_MACHINES`],
1044/// [`MachineMemory::LUT_BANKS`] for the next [`LUT_MACHINES`], and
1045/// [`MachineMemory::BRAM_BANKS`] for the rest.
1046pub fn placement(k: usize) -> MachineMemory {
1047 if k < URAM_MACHINES {
1048 MachineMemory::SPECIALIZED
1049 } else if k - URAM_MACHINES < LUT_MACHINES {
1050 MachineMemory::LUT_BANKS
1051 } else {
1052 MachineMemory::BRAM_BANKS
1053 }
1054}
1055
1056/// Constructors: preload the program ROM, constants and tables, and pick
1057/// memory styles for the program.
1058impl<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
1059 FieldMachine<KB, K, RA, IA, PB, TB, KT>
1060where
1061 rhdl::bits::W<KB>: BitWidth,
1062 rhdl::bits::W<RA>: BitWidth,
1063 rhdl::bits::W<IA>: BitWidth,
1064 rhdl::bits::W<PB>: BitWidth,
1065 rhdl::bits::W<TB>: BitWidth,
1066 rhdl::bits::W<KT>: BitWidth,
1067{
1068 /// Build a machine running `program` (specialized memory layout).
1069 pub fn new(program: &Program) -> Self {
1070 Self::with_presets(program, &[])
1071 }
1072
1073 /// Build a machine running `program` with the given memory layout.
1074 pub fn with_memory(program: &Program, memory: MachineMemory) -> Self {
1075 Self::build(program, &[], memory)
1076 }
1077
1078 /// Build a machine whose bank-I registers `index` start as `value` in
1079 /// every context (used for identity defaults in partial batches).
1080 pub fn with_presets(program: &Program, presets: &[(usize, Fp)]) -> Self {
1081 Self::build(program, presets, MachineMemory::SPECIALIZED)
1082 }
1083
1084 /// Shared constructor. Checks that the parameters fit the program, then
1085 /// builds every memory's preload image and style.
1086 fn build(program: &Program, presets: &[(usize, Fp)], memory: MachineMemory) -> Self {
1087 // K and KB must agree; K >= 32 is what makes hazard logic unnecessary.
1088 assert_eq!(K, 1 << KB);
1089 assert!(K >= 32, "barrel depth must cover the 25-cycle write-back");
1090 // Result tags are `ctx << 7 | dst` in 16 bits.
1091 assert!(KB <= 9, "result tags hold at most 9 context bits");
1092 assert!(program.bundles.len() <= 1 << PB);
1093 assert!(program.regs_m.max(program.regs_a) << KB <= 1 << RA);
1094 assert!(program.regs_i <= CONST_BASE && program.consts.len() <= CONST_SLOTS);
1095 assert!(program.regs_i << KB <= 1 << IA, "bank I too small");
1096 // Program ROM image: bundle `pc` at address `pc`.
1097 let rom: Vec<(Bits<PB>, InstrW)> = program
1098 .bundles
1099 .iter()
1100 .enumerate()
1101 .map(|(pc, b)| (bits(pc as u128), encode_bundle(b, program)))
1102 .collect();
1103 let consts: Vec<(Bits<5>, Fe17)> =
1104 program.consts.iter().enumerate().map(|(c, value)| (bits::<5>(c as u128), Fe17::from_fp(*value))).collect();
1105 // Bank-I preload: the same value at word `index << KB | ctx` for every
1106 // context.
1107 let mut preset_words = Vec::new();
1108 for &(index, value) in presets {
1109 assert!(index < program.regs_i);
1110 for ctx in 0..K {
1111 preset_words.push((bits::<IA>(((index << KB) | ctx) as u128), Fe17::from_fp(value)));
1112 }
1113 }
1114 // Bank I: never written without inputs (LUT RAM, nearly free); run-time
1115 // inputs without presets can live in UltraRAM.
1116 let no_inputs = memory.rom_consts && program.regs_i == 0 && presets.is_empty();
1117 let ultra_inputs = memory.ultra_banks && program.regs_i > 0 && presets.is_empty();
1118 // Machines that never scan the base-point table keep a 2-row stub.
1119 let scans = program.bundles.iter().any(|b| b.t.is_some_and(|t| !t.key));
1120 // Programs with no table steps at all never touch the scanner's
1121 // memories: keep them in LUT RAM instead of block RAM.
1122 let lean_scan = memory.rom_consts && !program.bundles.iter().any(|b| b.t.is_some());
1123 let (even, odd) = if scans {
1124 assert!(TABLE_ROWS <= 1 << TB, "base-point table needs {TABLE_ROWS} rows");
1125 let (e, o) = table_images(basepoint_table());
1126 (
1127 e.into_iter().map(|(a, v)| (bits::<TB>(a as u128), v)).collect::<Vec<_>>(),
1128 o.into_iter().map(|(a, v)| (bits::<TB>(a as u128), v)).collect::<Vec<_>>(),
1129 )
1130 } else {
1131 (Vec::new(), Vec::new())
1132 };
1133 // Reset state: all contexts idle, pipeline empty.
1134 let ring = Ring {
1135 active: false,
1136 pc: bits(0),
1137 tag: b16(0),
1138 };
1139 let stage = Stage {
1140 valid: false,
1141 last: false,
1142 ctx: bits(0),
1143 tag: b16(0),
1144 };
1145 // An all-slots-disabled bundle for the empty S2/S3 stages.
1146 let idle = encode_bundle(&Bundle::default(), program);
1147 let zero = Fe17::zero();
1148 let niels = Niels17 {
1149 ypx: zero,
1150 ymx: zero,
1151 xy2d: zero,
1152 };
1153 Self {
1154 regs: DFF::new(MachineRegs {
1155 slot: bits(0),
1156 ring: [ring; K],
1157 pending: [false; K],
1158 pending_tag: [b16(0); K],
1159 key: [bits(0); K],
1160 pending_entry: [b12(0); K],
1161 out: OutEvent {
1162 valid: false,
1163 ctx: bits(0),
1164 tag: b16(0),
1165 index: b6(0),
1166 value: zero,
1167 },
1168 s2: stage,
1169 s2_instr: idle,
1170 s2_digit: b6(0),
1171 dig_fresh: false,
1172 s1_instr: idle,
1173 s3: stage,
1174 s3_instr: idle,
1175 s3_ops: [zero; 4],
1176 s3_acc: niels,
1177 s3_negative: false,
1178 s4_commit: false,
1179 s4_lslot: false,
1180 s4_ctx: bits(0),
1181 s4_l: niels,
1182 in_start: StartReq { valid: false, ctx: bits(0), tag: b16(0), key: bits(0), entry: b12(0) },
1183 in_digits: DigitsWrite { valid: false, ctx: bits(0), digits: [b6(0); 128] },
1184 in_input: InputWrite { valid: false, ctx: bits(0), index: b7(0), value: zero },
1185 in_table: TableWrite { valid: false, row: bits(0), odd: false, coord: b2(0), value: zero },
1186 }),
1187 rom: SyncBRAM::new(rom),
1188 banks: RegBanks {
1189 m: std::array::from_fn(|_| bank(memory.ultra_banks, memory.lut_banks)),
1190 a: std::array::from_fn(|_| bank(memory.ultra_banks, memory.lut_banks)),
1191 i: std::array::from_fn(|_| {
1192 let bank = SyncBRAM::new(preset_words.clone());
1193 if no_inputs {
1194 bank.with_style(RamStyle::Distributed)
1195 } else if ultra_inputs {
1196 bank.with_style(RamStyle::Ultra)
1197 } else {
1198 bank
1199 }
1200 }),
1201 c: std::array::from_fn(|_| ConstRom::new(&consts)),
1202 },
1203 scan: ScanMems {
1204 bank_l0: scan_mem(lean_scan),
1205 bank_l1: scan_mem(lean_scan),
1206 acc: scan_mem(lean_scan),
1207 digits: scan_mem(lean_scan),
1208 table0: SyncBRAM::new(even),
1209 table1: SyncBRAM::new(odd),
1210 // KT = 1 is a stub table; UltraRAM is only worth it for real ones.
1211 key: if (memory.ultra_banks || memory.lut_banks) && KT > 1 { KeyTables::ultra() } else { KeyTables::default() },
1212 },
1213 mul: FieldMul::default(),
1214 add: FieldAddSub::default(),
1215 }
1216 }
1217}
1218
1219/// Port types of [`FieldMachine`].
1220impl<const KB: usize, const K: usize, const RA: usize, const IA: usize, const PB: usize, const TB: usize, const KT: usize>
1221 SynchronousIO for FieldMachine<KB, K, RA, IA, PB, TB, KT>
1222where
1223 rhdl::bits::W<KB>: BitWidth,
1224 rhdl::bits::W<RA>: BitWidth,
1225 rhdl::bits::W<IA>: BitWidth,
1226 rhdl::bits::W<PB>: BitWidth,
1227 rhdl::bits::W<TB>: BitWidth,
1228 rhdl::bits::W<KT>: BitWidth,
1229{
1230 type I = MachineIn<KB>;
1231 type O = MachineOut<KB>;
1232 type Kernel = field_machine_kernel<KB, K, RA, IA, PB, TB, KT>;
1233}
1234
1235/// The group identity (the point `(0, 1)`) in Niels form: `y + x = 1`,
1236/// `y - x = 1`, `2dxy = 0`. A lookup of digit 0 returns it, and adding it is
1237/// a no-op, so zero digits need no special case in the program.
1238#[kernel]
1239pub fn niels_identity() -> Niels17 {
1240 let mut one = [b17(0); 15];
1241 one[0] = b17(1);
1242 Niels17 {
1243 ypx: Fe17 { d: one },
1244 ymx: Fe17 { d: one },
1245 xy2d: Fe17 { d: [b17(0); 15] },
1246 }
1247}
1248
1249/// `-x mod p` for canonical `x` (zero stays zero).
1250///
1251/// Combinational. [`p_minus`] alone would map 0 to `p`, which is not
1252/// canonical, hence the zero test.
1253#[kernel]
1254pub fn field_neg(x: Fe17) -> Fe17 {
1255 // OR-reduce the digits.
1256 let mut zero = true;
1257 for k in 0..15 {
1258 if x.d[k] != b17(0) {
1259 zero = false;
1260 }
1261 }
1262 if zero {
1263 x
1264 } else {
1265 Fe17 {
1266 d: digits_from_limbs(p_minus(limbs_from_digits(x.d))),
1267 }
1268 }
1269}
1270
1271/// A memory port bundle that reads `addr` and never writes.
1272///
1273/// The write side still needs defined values, so it reuses `addr` and a
1274/// caller-supplied `filler` (usually the memory's current output) rather
1275/// than leaving them as don't-cares.
1276#[kernel]
1277fn read_disabled<T: Digital, const N: usize>(addr: Bits<N>, filler: T) -> RamIn<T, N>
1278where
1279 rhdl::bits::W<N>: BitWidth,
1280{
1281 RamIn::<T, N> {
1282 read_addr: addr,
1283 write: RamWrite::<T, N> {
1284 addr: addr,
1285 value: filler,
1286 enable: false,
1287 },
1288 }
1289}
1290
1291/// State transition for [`FieldMachine`].
1292///
1293/// One call handles every pipeline stage at once, each working on a
1294/// different bundle: S1 for the context whose turn it is, S2 and S3 for the
1295/// bundles issued one and two cycles earlier, S4 for the bank-L commit, and
1296/// the write-back of whatever the units return this cycle. Values read from
1297/// `q` are memory outputs addressed in the previous cycle; values written to
1298/// `d` take effect at the next edge.
1299#[kernel]
1300pub fn field_machine_kernel<
1301 const KB: usize,
1302 const K: usize,
1303 const RA: usize,
1304 const IA: usize,
1305 const PB: usize,
1306 const TB: usize,
1307 const KT: usize,
1308>(
1309 _cr: ClockReset,
1310 i: MachineIn<KB>,
1311 q: Q<KB, K, RA, IA, PB, TB, KT>,
1312) -> (MachineOut<KB>, D<KB, K, RA, IA, PB, TB, KT>)
1313where
1314 rhdl::bits::W<KB>: BitWidth,
1315 rhdl::bits::W<RA>: BitWidth,
1316 rhdl::bits::W<IA>: BitWidth,
1317 rhdl::bits::W<PB>: BitWidth,
1318 rhdl::bits::W<TB>: BitWidth,
1319 rhdl::bits::W<KT>: BitWidth,
1320{
1321 let r = q.regs;
1322 // Next register state; starts as a copy so untouched fields hold.
1323 let mut n = r;
1324 let mut d = D::<KB, K, RA, IA, PB, TB, KT>::dont_care();
1325 // Register the write-only inputs and act on last cycle's (see
1326 // `MachineRegs::in_start`); the output handshake stays direct.
1327 n.in_start = i.start;
1328 n.in_digits = i.digits;
1329 n.in_input = i.input;
1330 n.in_table = i.table;
1331 let i = MachineIn::<KB> { start: r.in_start, digits: r.in_digits, input: r.in_input, table: r.in_table, out_accept: i.out_accept };
1332 // ---- External start requests: latch them; the context starts on its
1333 // next turn.
1334 if i.start.valid {
1335 n.pending[i.start.ctx] = true;
1336 n.pending_tag[i.start.ctx] = i.start.tag;
1337 n.key[i.start.ctx] = i.start.key;
1338 n.pending_entry[i.start.ctx] = i.start.entry;
1339 }
1340
1341 // ---- Output register handshake.
1342 if i.out_accept {
1343 n.out.valid = false;
1344 }
1345 // An OUT already in S2 or S3 will claim the register when it reaches S3,
1346 // so a new OUT may issue only if neither stage holds one. Stalled
1347 // bundles have a_en cleared and do not count.
1348 let out_in_flight = (r.s2_instr.a_en && r.s2_instr.a_op == b2(2))
1349 || (r.s3_instr.a_en && r.s3_instr.a_op == b2(2));
1350 // Uses the registered valid, so an accept frees the register one cycle
1351 // later.
1352 let out_free = !r.out.valid && !out_in_flight;
1353
1354 // ---- S1: the head context issues its prefetched bundle.
1355 let slot = r.slot;
1356 let head = r.ring[0];
1357 // This context's bundle: read two cycles ago, registered last cycle.
1358 let instr = r.s1_instr;
1359 n.s1_instr = q.rom;
1360 // An idle context with a pending start begins now at its entry bundle,
1361 // unless the start came too late for the read-ahead (dig_fresh).
1362 let starting = !head.active && r.pending[slot] && !r.dig_fresh;
1363 let wants = head.active || starting;
1364 let is_out = instr.a_en && instr.a_op == b2(2);
1365 // The only stall: an OUT with the output register busy waits a full turn.
1366 let issue = wants && (!is_out || out_free);
1367 let pc = if head.active { head.pc } else { r.pending_entry[slot].resize::<PB>() };
1368 let tag = if starting { r.pending_tag[slot] } else { head.tag };
1369 if starting && issue {
1370 n.pending[slot] = false;
1371 }
1372 let last = instr.end;
1373 // Rotate: the head moves to the tail with its advanced program counter.
1374 for k in 1..K {
1375 n.ring[k - 1] = r.ring[k];
1376 }
1377 n.ring[K - 1] = if issue {
1378 Ring::<PB> {
1379 // Retiring after the last bundle returns the context to idle.
1380 active: !last,
1381 pc: if last { bits::<PB>(0) } else { pc + bits::<PB>(1) },
1382 tag: tag,
1383 }
1384 } else {
1385 // Not issued: keep the state unchanged and retry next turn.
1386 head
1387 };
1388 // Wraps at K = 2^KB.
1389 n.slot = slot + bits::<KB>(1);
1390 // Prefetch the bundle of the context two turns ahead: the ROM read and
1391 // s1_instr take a cycle each. Its pc is final, since it last issued
1392 // K - 2 >= 1 cycles ago.
1393 let following = r.ring[2];
1394 let next_slot = slot + bits::<KB>(1);
1395 let ahead_slot = slot + bits::<KB>(2);
1396 let ahead_pc = if following.active {
1397 following.pc
1398 } else if i.start.valid && i.start.ctx == ahead_slot {
1399 // A start arriving this cycle is not yet in pending_entry; bypass it.
1400 i.start.entry.resize::<PB>()
1401 } else {
1402 r.pending_entry[ahead_slot].resize::<PB>()
1403 };
1404 d.rom = read_disabled::<InstrW, PB>(ahead_pc, instr);
1405
1406 // S1 register-file addresses: word `index << KB | ctx` in M, A and I,
1407 // and the low six index bits in C. All four are read; S2 picks the bank.
1408 let ctx1 = slot;
1409 let operands = [instr.m_a, instr.m_b, instr.a_a, instr.a_b];
1410 for p in 0..4 {
1411 let index = operands[p].index;
1412 let ra = (index.resize::<RA>() << (KB as u128)) | ctx1.resize::<RA>();
1413 let ia = (index.resize::<IA>() << (KB as u128)) | ctx1.resize::<IA>();
1414 d.banks.m[p].read_addr = ra;
1415 d.banks.a[p].read_addr = ra;
1416 d.banks.i[p].read_addr = ia;
1417 // Constant `c` sits at operand index CONST_BASE + c (the subtraction
1418 // wraps for input indices, whose ROM read is not used).
1419 d.banks.c[p].read_addr = (index - b7(CONST_BASE as u128)).resize::<5>();
1420 }
1421 // Per-context scanner words, and both table images at the bundle's
1422 // public row.
1423 d.scan.bank_l0.read_addr = ctx1;
1424 d.scan.bank_l1.read_addr = ctx1;
1425 d.scan.acc.read_addr = ctx1;
1426 // The digit word is read a cycle ahead, for the next head, so this
1427 // bundle's digit is picked here and S2 sees a 6-bit register. Digits
1428 // are written only together with a start, to an idle context, so the
1429 // word changes under a running context never; a write in the same
1430 // cycle as the read-ahead holds that start back one turn.
1431 d.scan.digits.read_addr = next_slot;
1432 n.s2_digit = q.scan.digits[instr.t_digit];
1433 // A start for the next head arriving now missed its bundle's read-ahead.
1434 n.dig_fresh = (i.digits.valid && i.digits.ctx == next_slot) || (i.start.valid && i.start.ctx == next_slot);
1435 d.scan.table0 = read_disabled::<Niels17, TB>(instr.t_row.resize::<TB>(), q.scan.table0);
1436 d.scan.table1 = read_disabled::<Niels17, TB>(instr.t_row.resize::<TB>(), q.scan.table1);
1437 // Key rows: key * 512 + row, truncated to KT bits.
1438 let key_row = ((r.key[slot].resize::<16>() << 9) | instr.t_row.resize::<16>()).resize::<KT>();
1439 d.scan.key.read_addr = key_row;
1440 n.s2 = Stage::<KB, PB> {
1441 valid: issue,
1442 last: last,
1443 ctx: ctx1,
1444 tag: tag,
1445 };
1446 n.s2_instr = instr;
1447 // A bundle that did not issue becomes a bubble: no unit, write or OUT.
1448 if !issue {
1449 n.s2_instr.m_en = false;
1450 n.s2_instr.a_en = false;
1451 n.s2_instr.t_en = false;
1452 }
1453
1454 // ---- S2: operand selection and the constant-address scan.
1455 // Bank read data addressed in S1 is on q.banks now.
1456 let ins2 = r.s2_instr;
1457 let ops2 = [ins2.m_a, ins2.m_b, ins2.a_a, ins2.a_b];
1458 for p in 0..4 {
1459 let bank = ops2[p].bank;
1460 n.s3_ops[p] = if bank == b2(BANK_M) {
1461 q.banks.m[p]
1462 } else if bank == b2(BANK_A) {
1463 q.banks.a[p]
1464 } else if bank == b2(BANK_I) {
1465 // Indices from CONST_BASE up select the constant ROM.
1466 if ops2[p].index >= b7(CONST_BASE as u128) { q.banks.c[p] } else { q.banks.i[p] }
1467 } else {
1468 // Bank L. Programs only use it as the second multiply factor
1469 // (encode_bundle and the Builder enforce this), but the select
1470 // is generated for every port. Index bit 2 picks the slot and
1471 // bits 1..0 the coordinate (3 also reads 2dxy).
1472 let index = ops2[p].index;
1473 let l = if (index & b7(4)) != b7(0) {
1474 q.scan.bank_l1
1475 } else {
1476 q.scan.bank_l0
1477 };
1478 let coordinate = index & b7(3);
1479 if coordinate == b7(0) {
1480 l.ypx
1481 } else if coordinate == b7(1) {
1482 l.ymx
1483 } else {
1484 l.xy2d
1485 }
1486 };
1487 }
1488 // Constant-address scan step. The digit was selected (in S1) from the
1489 // context's digit word by the public slot number; its value only steers
1490 // data multiplexers, never an address or enable.
1491 let digit = r.s2_digit;
1492 let magnitude = digit & b6(31);
1493 let negative = (digit & b6(32)) != b6(0);
1494 // Entry numbers this step covers: first (even image) and first + 1
1495 // (odd image).
1496 let first = (ins2.t_pair.resize::<6>() << 1) + b6(1);
1497 // The first step of a lookup starts from the identity, so magnitude 0
1498 // selects the identity.
1499 let mut acc = if ins2.t_clear { niels_identity() } else { q.scan.acc };
1500 let (entry0, entry1) = if ins2.t_key {
1501 (q.scan.key[0], q.scan.key[1])
1502 } else {
1503 (q.scan.table0, q.scan.table1)
1504 };
1505 if magnitude == first {
1506 acc = entry0;
1507 }
1508 if magnitude == first + b6(1) {
1509 acc = entry1;
1510 }
1511 // The accumulator is written back every T step, one cycle later (in
1512 // S3, below) from the registered `s3_acc`, so the RAM's write port is
1513 // fed straight from a register: the digit mux, the compare and the
1514 // 765-bit select end at flip-flops, not at the LUT-RAM. The context's
1515 // next step reads it K >= 2 cycles after this step, so the write still
1516 // lands first, and the read in S1 is another context's.
1517 n.s3_acc = acc;
1518 n.s3_negative = negative;
1519 n.s3 = r.s2;
1520 n.s3_instr = ins2;
1521
1522 // ---- S3: issue the arithmetic units, emit outputs, commit bank L.
1523 let ins3 = r.s3_instr;
1524 let ctx3 = r.s3.ctx;
1525 // The scan accumulator write of the step that was in S2 last cycle.
1526 d.scan.acc.write = RamWrite::<Niels17, KB> {
1527 addr: ctx3,
1528 value: r.s3_acc,
1529 enable: ins3.t_en,
1530 };
1531 let ops3 = r.s3_ops;
1532 // Tags: ctx << 7 | destination register. The write-back below unpacks
1533 // them into the bank address, so results need no other tracking.
1534 let mul_tag = (ctx3.resize::<16>() << 7) | ins3.m_dst.resize::<16>();
1535 d.mul = FieldMulIn {
1536 valid: ins3.m_en,
1537 a: ops3[0],
1538 b: ops3[1],
1539 tag: mul_tag,
1540 };
1541 let add_tag = (ctx3.resize::<16>() << 7) | ins3.a_dst.resize::<16>();
1542 // OUT uses the A slot's operand but not the adder.
1543 d.add = FieldAddIn {
1544 valid: ins3.a_en && ins3.a_op != b2(2),
1545 sub: ins3.a_op == b2(1),
1546 a: ops3[2],
1547 b: ops3[3],
1548 tag: add_tag,
1549 };
1550 // Conditional negation gets its own stage; bank L commits at S4.
1551 // Negating a Niels point swaps y+x and y-x and negates 2dxy.
1552 n.s4_commit = ins3.t_en && ins3.t_finish;
1553 n.s4_lslot = ins3.t_lslot;
1554 n.s4_ctx = ctx3;
1555 n.s4_l = if r.s3_negative {
1556 Niels17 {
1557 ypx: r.s3_acc.ymx,
1558 ymx: r.s3_acc.ypx,
1559 xy2d: field_neg(r.s3_acc.xy2d),
1560 }
1561 } else {
1562 r.s3_acc
1563 };
1564 // S4: commit the point registered last cycle into the chosen slot. Two
1565 // slots let a program start the next lookup while it still reads the
1566 // previous result.
1567 d.scan.bank_l0.write = RamWrite::<Niels17, KB> {
1568 addr: r.s4_ctx,
1569 value: r.s4_l,
1570 enable: r.s4_commit && !r.s4_lslot,
1571 };
1572 d.scan.bank_l1.write = RamWrite::<Niels17, KB> {
1573 addr: r.s4_ctx,
1574 value: r.s4_l,
1575 enable: r.s4_commit && r.s4_lslot,
1576 };
1577 // OUT: capture a_a into the output register. S1's out_free check
1578 // guarantees the register is empty.
1579 if ins3.a_en && ins3.a_op == b2(2) {
1580 n.out = OutEvent::<KB> {
1581 valid: true,
1582 ctx: ctx3,
1583 tag: r.s3.tag,
1584 index: ins3.out_index,
1585 value: ops3[2],
1586 };
1587 }
1588
1589 // ---- Write-back: one writer per bank, replicated to every read copy.
1590 // Unit results arriving this cycle: unpack tag (ctx << 7 | dst) into
1591 // word dst << KB | ctx.
1592 let m = q.mul;
1593 let m_addr = ((m.tag & b16(127)).resize::<RA>() << (KB as u128)) | (m.tag >> 7).resize::<RA>();
1594 let a = q.add;
1595 let a_addr = ((a.tag & b16(127)).resize::<RA>() << (KB as u128)) | (a.tag >> 7).resize::<RA>();
1596 // Loader writes to bank I use the same interleaved layout.
1597 let i_addr = (i.input.index.resize::<IA>() << (KB as u128)) | i.input.ctx.resize::<IA>();
1598 for p in 0..4 {
1599 d.banks.m[p].write = RamWrite::<Fe17, RA> {
1600 addr: m_addr,
1601 value: m.r,
1602 enable: m.valid,
1603 };
1604 d.banks.a[p].write = RamWrite::<Fe17, RA> {
1605 addr: a_addr,
1606 value: a.r,
1607 enable: a.valid,
1608 };
1609 d.banks.i[p].write = RamWrite::<Fe17, IA> {
1610 addr: i_addr,
1611 value: i.input.value,
1612 enable: i.input.valid,
1613 };
1614 // The constant ROM is never written.
1615 d.banks.c[p].write = RamWrite::<Fe17, 5> {
1616 addr: b5(0),
1617 value: i.input.value,
1618 enable: false,
1619 };
1620 }
1621 // Loader writes to the digit memory and key tables; the base-point
1622 // tables are ROMs.
1623 d.scan.digits.write = RamWrite::<[b6; 128], KB> {
1624 addr: i.digits.ctx,
1625 value: i.digits.digits,
1626 enable: i.digits.valid,
1627 };
1628 d.scan.table0.write.enable = false;
1629 d.scan.table1.write.enable = false;
1630 d.scan.key.write = i.table;
1631 d.regs = n;
1632 // done pulses when the last bundle of a program is in S3.
1633 let output = MachineOut::<KB> {
1634 out: r.out,
1635 done: r.s3.valid && r.s3.last,
1636 done_ctx: ctx3,
1637 done_tag: r.s3.tag,
1638 };
1639 (output, d)
1640}
1641
1642/// Host-side helper: pack signed digits into the hardware sign-magnitude form
1643/// (base-point scalar in slots 0..54; slots 64.. stay zero). For machines
1644/// without key lookups.
1645pub fn pack_digits(digits: &[i8; 64]) -> [b6; 128] {
1646 pack_two(digits, &[0; 64])
1647}
1648
1649/// Host-side helper: base-point digits in slots 0..54 and key digits in
1650/// slots 64..118.
1651///
1652/// Panics if either scalar has nonzero digits at or above [`DIGITS`]. Each
1653/// slot is `|d|` in bits 4..0 with bit 5 set for negative `d`.
1654pub fn pack_two(base: &[i8; 64], key: &[i8; 64]) -> [b6; 128] {
1655 assert!(base[DIGITS..].iter().all(|&d| d == 0) && key[DIGITS..].iter().all(|&d| d == 0));
1656 std::array::from_fn(|k| {
1657 let d = if k < 64 { base[k] } else { key[k - 64] };
1658 let magnitude = d.unsigned_abs() as u128;
1659 let sign = if d < 0 { 32 } else { 0 };
1660 b6(magnitude | sign)
1661 })
1662}