Compare commits

...

7 Commits

Author SHA1 Message Date
0fa7f48ed4 build(vivado): add sd_bram to create_project.tcl, fix run instructions
mlkem_top instantiates sd_bram (ek/dk byte buffers) but create_project.tcl
was missing it -- elaboration would fail. Add sync_rtl/storage/sd_bram.v to
match xsim_run.tcl exactly. Also clarify the run flow: runtime=all means
launch_simulation runs to the TB $finish; a manual 'run all' afterwards trips
the 120ms watchdog and prints a spurious timeout.

Verified: vivado -mode batch builds the project (0 errors) and the simulation
prints 'K=2 CASE 0 PASS: ek==pk, dk==sk'.
2026-06-28 17:14:14 +08:00
f4493966ac test(top): stream raw xsim output to terminal (tee to log)
The fast top runner was hiding xsim stdout in /tmp and only echoing PASS/FAIL,
which buried the TB $display diagnostics needed when a case fails. Now xsim
output is tee'd to both terminal and log, with the PASS/FAIL summary appended.
2026-06-28 16:52:27 +08:00
74d8f021c9 test(top): add fast single-K / single-case runner to run_tb.sh
./run_tb.sh top [K] [CASE] now elaborates only the requested K and runs only
the requested CASE(s), parsing PASS/FAIL + file-not-found per run. Avoids the
full 3-snapshot, 11-case sweep when iterating. 'top' with no extra args and
all other modules keep their original full-tcl behaviour.
2026-06-28 16:46:33 +08:00
75c350c1e4 refactor(kg): registered read-ahead for ST_M accumulate (bank_se/bank_t)
Third per-consumer step of stage 2b, the read-modify-write one. m_acc_src is
now a registered read (m_acc_rd) whose address leads by pm_vo (m_oidx+1 when
consuming) so the value lands exactly on the pm_vo cycle. Same-index RMW on
bank_t is read-old: the running-t_hat source was written by the previous term
and is long settled; read addr (m_oidx+1) leads write addr (m_oidx).
11/11 KAT PASS incl. K=3/4 j>0 paths, byte-exact.
2026-06-28 16:37:07 +08:00
9c08273c5f refactor(kg): registered read-ahead for ST_M load (bank_a + bank_se)
Second per-consumer step of stage 2b. m_ld becomes a read-ahead pointer; both
poly_mul load reads (bank_a A_hat, bank_se s_hat) are registered into
pm_a_rd/pm_b_rd and fed one cycle later (pm_valid delayed 1 cyc), same pattern
as ST_N. 11/11 KAT PASS, byte-exact.
2026-06-28 16:27:00 +08:00
45e07c28e8 refactor(kg): registered read-ahead for ST_N (bank_se NTT load)
First per-consumer step of stage 2b. n_ridx becomes a read-ahead pointer
leading the consume index by 1; bank_se read is registered into n_rd_data and
fed to ntt_core one cycle later (n_valid delayed 1 cyc). Cores hold ready_o
high through LOAD so a fixed 1-cycle skew suffices. Matches sd_bram registered
timing for this read port. 11/11 KAT PASS, byte-exact.
2026-06-28 16:17:30 +08:00
4d3adc6b57 refactor(kg): split polymem into 3 banks {a, se, t} (async, stage 2a)
Replace the single async-read polymem[0:28*256-1] with 3 polynomial-indexed
banks (bank_a A_hat / bank_se s_hat||e_hat / bank_t t_hat), addressed by
abs_slot - base_slot. Still async-read here -- a pure refactor that validates
bank sizing and base-relative addressing with zero timing change before
stage 2b converts them to registered sd_bram + read-ahead pipelines.

11/11 KAT PASS, byte-exact, 0 file-not-found.
2026-06-28 15:55:26 +08:00
4 changed files with 240 additions and 41 deletions

View File

@@ -0,0 +1,47 @@
# Phase 2: polymem -> BRAM banks
## Goal
Replace the single multi-port async-read `reg [11:0] polymem [0:28*256-1]` with
registered-read `sd_bram` banks that infer real BRAM (ASIC: compiled SRAM).
## Bank split (all sd_bram, W=12, 1R+1W registered)
- bank_a : A_hat[i][j], slots 0..K^2-1 -> D=4096 (KMAX^2*256), A=12
- bank_se: s_hat[i] then e_hat[i] -> D=2048 (2*KMAX*256), A=11
relative slot = abs_slot - slot_s_rt (s: 0..K-1, e: K..2K-1)
- bank_t : t_hat[i], slots slot_t_rt.. -> D=1024 (KMAX*256), A=10
relative slot = abs_slot - slot_t_rt
## Port budget (verified disjoint)
- poly_mul LOAD vs ACCUMULATE never overlap; ST_N/ST_M/ST_E are disjoint
top-states. => each bank needs only 1R+1W. sd_bram fits.
- bank_t ST_M-acc reads t_hat[idx] (j>0 source) and writes t_hat[idx] result
same cycle: 1R+1W, read-old/write-new. Prefetch keeps read AHEAD of write
(monotonic idx), so no same-pass RAW hazard.
## Read sites (all need 1-cycle read-ahead vs async today)
- ntt_in (ST_N load) <- bank_se[n_slot]
- e_c0,e_c1 (ST_E) <- bank_se (dk) or bank_t (ek); SERIALIZE the
two reads across the 3-byte/pair window
- pm_a_in (ST_M load) <- bank_a[m_aslot]
- pm_b_in (ST_M load) <- bank_se[m_j]
- m_acc_src (ST_M acc) <- bank_se[K+m_i] (j==0) or bank_t[m_i] (j>0)
- dbg_coeff_o (not on KAT path) <- route by slot range (compile-only)
## Write sites
- ST_A: bank_a[a_slot] <= snt_coeff
- ST_C: bank_se[c_slot-slot_s] <= cbd_modq
- ST_N: bank_se[n_slot] <= ntt_coeff (write-back, same slot)
- ST_M-acc: bank_t[m_i] <= m_accq
## Read-ahead strategy
Cores hold ready_o high through entire LOAD (no mid-stream backpressure) =>
fixed 1-cycle skew. Pattern: advance a read-address pointer 1 cycle ahead of
the consumer index; delay the consumer's valid by 1 cycle ("prime the pipe").
For ST_M-acc (irregular pm_vo cadence 0,1,1): 2-entry prefetch skid buffer,
read pointer runs monotonically ahead of write pointer.
## Checkpoints (KAT-gated: 11 cases, byte-exact, 0 file-not-found)
- 2a: split polymem -> 4 ASYNC-read banks (pure refactor, zero timing change).
Validates bank sizing + base-relative addressing + debug mux. COMMIT.
- 2b: convert banks to registered sd_bram + add read-ahead pipelines to every
consumer FSM (ST_N, ST_E, ST_M load, ST_M acc). COMMIT.

View File

@@ -2,7 +2,7 @@
# 顶层 mlkem_top 及其全部叶子算子与参数化 KAT testbench。
#
# 与已验证的 XSIM 流程sync_rtl/top/TB/xsim_run.tcl保持一致
# - 加载 mlkem_top 实际依赖的 14 个源文件
# - 加载 mlkem_top 实际依赖的全部 RTL 源文件(含 sd_bram 存储)
# - 顶层仿真模块 = tb_mlkem_kg_katK_xsim
# - 运行时安全等级由 generic KP2/3/4选择用例号由 +CASE 选择
#
@@ -30,7 +30,7 @@ create_project -force ${PROJECT_NAME} ${PROJECT_DIR}/vivado_prj
set_property target_simulator XSim [current_project]
# ===================================================================
# RTL 源文件 —— 与 xsim_run.tcl 完全一致的 14 个文件
# RTL 源文件 —— 与 xsim_run.tcl 完全一致
# ===================================================================
# ── SHA3 / Keccak ──
@@ -53,6 +53,9 @@ read_verilog -sv ${PROJECT_DIR}/sync_rtl/poly_mul/basecase_mul.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/poly_mul/poly_mul_zeta_rom.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/poly_mul/poly_mul_sync.v
# ── 存储ek/dk 字节缓冲registered-read BRAM──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/storage/sd_bram.v
# ── 顶层 KeyGen 集成 ──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/top/mlkem_top.v
@@ -107,7 +110,13 @@ set_property -name {xsim.simulate.xsim.more_options} \
puts "========================================"
puts " Project created: ${PROJECT_DIR}/vivado_prj/${PROJECT_NAME}.xpr"
puts " 仿: ML-KEM K=${SIM_KP}, KAT CASE=${SIM_CASE}"
puts " 仿: launch_simulation; run all"
puts ""
puts " 仿runtime=all TB \$finish"
puts " - GUI: Run Simulation Tcl Console: launch_simulation"
puts " - batch: vivado -mode batch -source create_project.tcl "
puts " : open_project vivado_prj/mlkem.xpr; launch_simulation"
puts " launch_simulation run all 仿 \$finish"
puts " run TB 120ms timeout"
puts " : K=${SIM_KP} CASE ${SIM_CASE} PASS: ek==pk, dk==sk"
puts ""
puts " K=4 SIM_KP/SIM_CASE "

View File

@@ -1,10 +1,18 @@
#!/bin/bash
# run_tb.sh - Run Vivado XSIM testbench for a module
#
# Usage: ./run_tb.sh <module>
# ./run_tb.sh mod_add
# Usage: ./run_tb.sh <module> [K] [CASE]
# ./run_tb.sh mod_add # run the module's full xsim_run.tcl
# ./run_tb.sh top # ML-KEM KeyGen: all K, all cases
# ./run_tb.sh top 2 # only K=2 (ML-KEM-512), all its cases
# ./run_tb.sh top 2 3 # only K=2, only CASE=3
# ./run_tb.sh top 4 0 # only K=4 (ML-KEM-1024), CASE=0
# ./run_tb.sh --list
#
# For the 'top' module, K (2/3/4) and CASE select a single KAT run so you can
# iterate quickly instead of elaborating 3 snapshots and running all 11 cases.
# Other modules ignore the extra args and run their xsim_run.tcl verbatim.
#
# Prerequisites:
# Vivado 2019.2 at /opt/Xilinx/Vivado/2019.2/
@@ -31,6 +39,8 @@ if [ -z "$1" ]; then
fi
MODULE="$1"
SEL_K="$2" # optional: 2/3/4 (top module only)
SEL_CASE="$3" # optional: KAT case index (top module only)
TB_DIR="$SCRIPT_DIR/sync_rtl/$MODULE/TB"
TCL_FILE="$TB_DIR/xsim_run.tcl"
@@ -80,4 +90,48 @@ execute_tcl() {
rm -f "$tmp_script"
}
# Fast single-K / single-case path for the 'top' (ML-KEM KeyGen) module.
# Compiles the same RTL as the tcl, but elaborates ONLY the requested K and
# runs ONLY the requested CASE(s). Avoids the 3-snapshot + 11-run full sweep.
run_top_selected() {
local tcl_file="$1" k="$2" csel="$3"
set +e # we do our own error handling / result parsing here
rm -rf xsim.dir .Xil
# Compile every xvlog line from the tcl, verbatim.
while read -r cmd; do
eval "$cmd" || { echo "COMPILE FAILED: $cmd"; return 1; }
done < <(grep -E '^xvlog ' "$tcl_file")
# Cases per K: K=2 has 0..4, K=3/4 have 0..2.
local cases
if [ "$k" = "2" ]; then cases="0 1 2 3 4"; else cases="0 1 2"; fi
if [ -n "$csel" ]; then cases="$csel"; fi
echo " xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps"
xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps \
|| { echo "ELAB FAILED for K=$k"; return 1; }
local fail=0
for c in $cases; do
local log="/tmp/run_tb_top_k${k}_c${c}.log"
echo " xsim mlkem_kg_k$k -R -testplusarg CASE=$c"
echo "========================================" | tee "$log"
xsim "mlkem_kg_k$k" -R -testplusarg "CASE=$c" 2>&1 | tee -a "$log"
echo "========================================" | tee -a "$log"
local pf nf
pf=$(grep -oE 'PASS|FAIL' "$log" | tail -1)
nf=$(grep -c 'cannot be opened' "$log")
echo " K=$k CASE=$c -> ${pf:-NORESULT} (file-not-found=$nf, log: $log)"
{ [ "$pf" = "PASS" ] && [ "$nf" -eq 0 ]; } || fail=1
done
return $fail
}
if [ "$MODULE" = "top" ] && [ -n "$SEL_K" ]; then
run_top_selected "$TCL_FILE" "$SEL_K" "$SEL_CASE"
exit $?
fi
execute_tcl "$TCL_FILE"

View File

@@ -82,11 +82,40 @@ module mlkem_top #(
localparam NUM_SLOTS = KMAX*KMAX + 3*KMAX;
localparam SAW = 5; // slot-address width (>=clog2(28))
reg [11:0] polymem [0:NUM_SLOTS*256-1];
// ================================================================
// Coefficient storage as 3 banks (Phase 2). Was one async-read reg
// array polymem[0:28*256-1]; split by polynomial so each becomes a
// registered-read sd_bram (infers BRAM / ASIC SRAM) in the final step.
// bank_a : A_hat[i][j] slots 0..K^2-1 -> D=KMAX*KMAX*256=4096
// bank_se: s_hat||e_hat slots slot_s.. -> D=2*KMAX*256=2048 (rel 0..2K-1)
// bank_t : t_hat[i] slots slot_t.. -> D=KMAX*256=1024 (rel 0..K-1)
// Addresses are base-relative: bank index = abs_slot - base_slot.
// Stage 2b (in progress): per-consumer registered read-ahead. Banks are
// still async reg arrays here; converted consumers read via their own
// 1-cycle pipeline reg (== sd_bram timing). Once every consumer of a
// bank is registered, the array is replaced by an sd_bram with its read
// port muxed across the (phase-disjoint) consumers.
// ================================================================
localparam PA_AW = 12; // bank_a addr width (4096)
localparam PSE_AW = 11; // bank_se addr width (2048)
localparam PT_AW = 10; // bank_t addr width (1024)
reg [11:0] bank_a [0:(1<<PA_AW)-1];
reg [11:0] bank_se [0:(1<<PSE_AW)-1];
reg [11:0] bank_t [0:(1<<PT_AW)-1];
// Debug readback (registered for timing)
reg [11:0] dbg_coeff_r;
always @(posedge clk) dbg_coeff_r <= polymem[dbg_slot_i*256 + dbg_idx_i];
// Route debug read by absolute slot range (not on the KAT correctness
// path TB reads ek/dk BRAMs only). A: 0..K^2-1; s/e: slot_s..slot_t-1;
// t: slot_t.. Index within bank is (abs_slot-base)*256 + idx.
wire [13:0] dbg_a_addr = dbg_slot_i*256 + dbg_idx_i;
wire [13:0] dbg_se_addr = (dbg_slot_i - slot_s_rt)*256 + dbg_idx_i;
wire [13:0] dbg_t_addr = (dbg_slot_i - slot_t_rt)*256 + dbg_idx_i;
always @(posedge clk) begin
if (dbg_slot_i >= slot_t_rt) dbg_coeff_r <= bank_t [dbg_t_addr[PT_AW-1:0]];
else if (dbg_slot_i >= slot_s_rt) dbg_coeff_r <= bank_se[dbg_se_addr[PSE_AW-1:0]];
else dbg_coeff_r <= bank_a [dbg_a_addr[PA_AW-1:0]];
end
assign dbg_coeff_o = dbg_coeff_r;
// ek and dk_pke byte memories sized for KMAX.
@@ -377,17 +406,22 @@ module mlkem_top #(
// ---- ntt_core: forward NTT (mode=0, no scaling) of s[i],e[i] in place ----
// N-stage bookkeeping: process slots S0,S1,E0,E1 (= SLOT_S0 + n_slot).
reg [4:0] n_slot; // 0..2K (process s_hat[0..K-1] then e_hat[0..K-1])
reg [8:0] n_ridx; // load read index 0..256
reg [8:0] n_ridx; // load read-AHEAD pointer 0..256 (leads consume by 1)
reg [7:0] n_widx; // output write index 0..255
reg n_valid; // feeding coeffs to ntt_core
reg n_valid; // feeding coeffs to ntt_core (delayed 1 cyc vs n_ridx)
reg n_loading; // 1 while presenting load addresses to bank_se
reg n_pending; // waiting for ntt_core IDLE to start next slot
reg [11:0] n_rd_data; // registered bank_se read (== sd_bram timing)
wire [SAW-1:0] n_slot_addr = slot_s_rt + n_slot; // s_hat then e_hat contiguous
wire ntt_ready;
wire [11:0] ntt_coeff;
wire ntt_vo;
wire ntt_done;
wire [11:0] ntt_in = polymem[n_slot_addr*256 + n_ridx[7:0]];
// bank_se read addr for the NTT load (relative slot = n_slot); registered
// into n_rd_data, which feeds ntt_core 1 cycle later.
wire [13:0] ntt_rd_full = n_slot*256 + n_ridx[7:0];
wire [11:0] ntt_in = n_rd_data;
ntt_core u_ntt (
.clk(clk), .rst_n(rst_n),
@@ -423,9 +457,12 @@ module mlkem_top #(
wire e_is_dk = (e_poly >= {1'b0, k_r});
wire [4:0] e_pidx = e_is_dk ? (e_poly - {1'b0, k_r}) : e_poly; // index within target
wire [SAW-1:0] e_slot = e_is_dk ? (slot_s_rt + e_pidx) : (slot_t_rt + e_pidx);
// two coeffs of the current pair
wire [11:0] e_c0 = polymem[e_slot*256 + {e_pair, 1'b0}];
wire [11:0] e_c1 = polymem[e_slot*256 + {e_pair, 1'b1}];
// two coeffs of the current pair: ek half reads t_hat (bank_t), dk half
// reads s_hat (bank_se). Relative index within the bank = e_pidx.
wire [13:0] e_rd0_full = e_pidx*256 + {e_pair, 1'b0};
wire [13:0] e_rd1_full = e_pidx*256 + {e_pair, 1'b1};
wire [11:0] e_c0 = e_is_dk ? bank_se[e_rd0_full[PSE_AW-1:0]] : bank_t[e_rd0_full[PT_AW-1:0]];
wire [11:0] e_c1 = e_is_dk ? bank_se[e_rd1_full[PSE_AW-1:0]] : bank_t[e_rd1_full[PT_AW-1:0]];
// 3 packed bytes
wire [7:0] e_b0 = e_c0[7:0];
wire [7:0] e_b1 = {e_c1[3:0], e_c0[11:8]};
@@ -445,8 +482,15 @@ module mlkem_top #(
wire pm_ready;
wire [11:0] pm_coeff;
wire pm_vo;
wire [11:0] pm_a_in = polymem[m_aslot*256 + m_ld[7:0]];
wire [11:0] pm_b_in = polymem[m_sslot*256 + m_ld[7:0]];
// pm_a: A_hat[i][j] in bank_a (abs slot m_aslot). pm_b: s_hat[j] in
// bank_se (relative slot = m_sslot - slot_s_rt = m_j). m_ld is a read-ahead
// pointer; both bank reads are registered into pm_a_rd/pm_b_rd and fed to
// poly_mul one cycle later (pm_valid delayed 1 cyc), like ST_N.
wire [13:0] pm_a_full = m_aslot*256 + m_ld[7:0];
wire [13:0] pm_b_full = m_j*256 + m_ld[7:0];
reg [11:0] pm_a_rd, pm_b_rd; // registered bank reads (sd_bram timing)
wire [11:0] pm_a_in = pm_a_rd;
wire [11:0] pm_b_in = pm_b_rd;
poly_mul_sync u_pmul (
.clk(clk), .rst_n(rst_n),
@@ -459,9 +503,23 @@ module mlkem_top #(
.ready_i(1'b1)
);
// accumulator source: e_hat[i] for first term (j==0), else running t_hat[i]
wire [11:0] m_acc_src = (m_j == 2'd0) ? polymem[m_eslot*256 + m_oidx]
: polymem[m_tslot*256 + m_oidx];
// accumulator source: e_hat[i] for first term (j==0), else running t_hat[i].
// e_hat[i] lives in bank_se at relative slot (slot_e_rt-slot_s_rt + m_i) = K+m_i.
// t_hat[i] lives in bank_t at relative slot m_i.
// Registered read-ahead: present the index that the NEXT pm_vo will consume
// m_acc_radr = pm_vo ? m_oidx+1 : m_oidx
// and register the selected bank into m_acc_rd, consumed one cycle later when
// pm_vo is high. The same-index read-modify-write on bank_t is read-old:
// the running t_hat source was written by the previous (j-1) term, long
// settled; the value consumed this cycle was registered before this cycle's
// write to m_oidx. Cadence is CALC(vo=0)/C0(vo=1)/C1(vo=1).
wire [7:0] m_acc_radr = pm_vo ? (m_oidx + 8'd1) : m_oidx;
wire [13:0] m_eacc_full = ({2'b0, k_r} + {2'b0, m_i})*256 + m_acc_radr; // K+m_i
wire [13:0] m_tacc_full = m_i*256 + m_acc_radr;
wire [11:0] m_acc_rd_next = (m_j == 2'd0) ? bank_se[m_eacc_full[PSE_AW-1:0]]
: bank_t [m_tacc_full[PT_AW-1:0]];
reg [11:0] m_acc_rd;
wire [11:0] m_acc_src = m_acc_rd;
// (a + b) mod Q (both < Q, sum < 2Q): one conditional subtract
wire [12:0] m_sum = {1'b0, m_acc_src} + {1'b0, pm_coeff};
wire [11:0] m_accq = (m_sum >= 13'(Q)) ? (m_sum - 13'(Q)) : m_sum[11:0];
@@ -506,6 +564,8 @@ module mlkem_top #(
n_ridx <= 9'd0;
n_widx <= 8'd0;
n_valid <= 1'b0;
n_loading <= 1'b0;
n_rd_data <= 12'd0;
n_pending <= 1'b0;
m_i <= 2'd0;
m_j <= 2'd0;
@@ -514,6 +574,9 @@ module mlkem_top #(
m_loading <= 1'b0;
m_pending <= 1'b0;
pm_valid <= 1'b0;
pm_a_rd <= 12'd0;
pm_b_rd <= 12'd0;
m_acc_rd <= 12'd0;
e_poly <= 3'd0;
e_pair <= 8'd0;
e_byte <= 2'd0;
@@ -578,7 +641,7 @@ module mlkem_top #(
// store each output coefficient only while busy (ignore stale last coeff from prior poly)
if (a_busy && snt_vo && snt_ack) begin
polymem[a_slot*256 + a_widx] <= snt_coeff;
bank_a[(a_slot*256 + a_widx) & ((1<<PA_AW)-1)] <= snt_coeff;
if (snt_last) begin
// finished this poly; advance (i,j) in row-major order
a_pair <= a_pair + 5'd1;
@@ -615,7 +678,8 @@ module mlkem_top #(
end
if (c_busy && cbd_vo && cbd_ack) begin
polymem[c_slot*256 + c_widx] <= cbd_modq;
// bank_se relative slot = c_slot - slot_s_rt = c_poly
bank_se[(c_poly*256 + c_widx) & ((1<<PSE_AW)-1)] <= cbd_modq;
if (cbd_last) begin
c_poly <= c_poly + 3'd1;
c_widx <= 8'd0;
@@ -627,30 +691,40 @@ module mlkem_top #(
end
end
// Arm N stage when C finishes: start NTT on slot S0
// Arm N stage when C finishes: prime load of slot S0. n_ridx is a
// read-ahead pointer; bank_se read is registered into n_rd_data and
// fed to ntt_core one cycle later, so valid starts low (priming).
if (st == ST_C && st_next == ST_N) begin
n_slot <= 3'd0;
n_ridx <= 9'd0;
n_widx <= 8'd0;
n_valid <= 1'b1; // begin loading first poly
n_valid <= 1'b0;
n_loading <= 1'b1; // begin presenting load addresses
n_pending <= 1'b0;
end
// ---- ST_N: forward NTT each of S0,S1,E0,E1 in place ----
if (st == ST_N) begin
// LOAD phase: stream 256 coeffs into ntt_core
if (n_valid && ntt_ready) begin
if (n_ridx == 9'd255) begin
n_valid <= 1'b0; // last coeff presented this cycle
n_ridx <= 9'd0;
// LOAD phase: present read-ahead addr to bank_se; the value
// registered last cycle (n_rd_data) is consumed by ntt_core
// this cycle (n_valid). Cores hold ready high through LOAD, so
// a fixed 1-cycle skew suffices (no backpressure gating).
if (n_loading) begin
if (n_ridx == 9'd256) begin
// 256th coeff (bank_se[255]) consumed this cycle; stop
n_loading <= 1'b0;
n_valid <= 1'b0;
end else begin
n_ridx <= n_ridx + 9'd1;
n_rd_data <= bank_se[ntt_rd_full[PSE_AW-1:0]];
n_ridx <= n_ridx + 9'd1;
n_valid <= 1'b1; // data presented last cycle is valid
end
end
// OUTPUT phase: collect 256 results, write back to same slot
if (ntt_vo) begin
polymem[n_slot_addr*256 + n_widx] <= ntt_coeff;
// bank_se relative slot = n_slot_addr - slot_s_rt = n_slot
bank_se[(n_slot*256 + n_widx) & ((1<<PSE_AW)-1)] <= ntt_coeff;
n_widx <= n_widx + 8'd1; // wraps 255->0 after last
end
@@ -665,15 +739,18 @@ module mlkem_top #(
end
end
// Kick next slot's load once core is back IDLE
// Kick next slot's load once core is back IDLE (re-prime)
if (n_pending && ntt_ready && !ntt_done) begin
n_valid <= 1'b1;
n_ridx <= 9'd0;
n_valid <= 1'b0;
n_loading <= 1'b1;
n_pending <= 1'b0;
end
end
// Arm M stage when N finishes: start first (i=0,j=0) poly_mul load
// Arm M stage when N finishes: prime first (i=0,j=0) poly_mul load.
// m_ld read-ahead pointer; pm_a_rd/pm_b_rd registered, pm_valid
// asserted one cycle after an address is presented (like ST_N).
if (st == ST_N && st_next == ST_M) begin
m_i <= 2'd0;
m_j <= 2'd0;
@@ -681,26 +758,38 @@ module mlkem_top #(
m_oidx <= 8'd0;
m_loading <= 1'b1;
m_pending <= 1'b0;
pm_valid <= 1'b1;
pm_valid <= 1'b0;
end
// ---- ST_M: t_hat[i] = e_hat[i] + sum_j A[i][j] o s_hat[j] ----
if (st == ST_M) begin
// LOAD: stream 256 (A,shat) pairs into poly_mul
if (m_loading && pm_valid && pm_ready) begin
if (m_ld == 9'd255) begin
pm_valid <= 1'b0; // last pair presented
// registered accumulator read (sd_bram timing): captures the
// index the next pm_vo will consume. Free-running within ST_M;
// primed during LOAD (m_oidx held at 0).
m_acc_rd <= m_acc_rd_next;
// LOAD: present read-ahead addr to bank_a/bank_se; the pair
// registered last cycle (pm_a_rd/pm_b_rd) is consumed by
// poly_mul this cycle (pm_valid). poly_mul holds ready high
// through LOAD, so a fixed 1-cycle skew suffices.
if (m_loading) begin
if (m_ld == 9'd256) begin
pm_valid <= 1'b0; // 256th pair consumed this cycle
m_loading <= 1'b0;
m_ld <= 9'd0;
m_oidx <= 8'd0;
end else begin
m_ld <= m_ld + 9'd1;
pm_a_rd <= bank_a [pm_a_full[PA_AW-1:0]];
pm_b_rd <= bank_se[pm_b_full[PSE_AW-1:0]];
m_ld <= m_ld + 9'd1;
pm_valid <= 1'b1; // pair presented last cycle is valid
end
end
// ACCUMULATE: each product coeff += e_hat (j==0) or running t_hat
if (pm_vo) begin
polymem[m_tslot*256 + m_oidx] <= m_accq;
// bank_t relative slot = m_tslot - slot_t_rt = m_i
bank_t[(m_i*256 + m_oidx) & ((1<<PT_AW)-1)] <= m_accq;
if (m_oidx == 8'd255) begin
// finished this (i,j) term; advance
if (m_j + 2'd1 < k_r) begin
@@ -716,12 +805,12 @@ module mlkem_top #(
end
end
// Start next (i,j) poly_mul load once core is IDLE again
// Start next (i,j) poly_mul load once core is IDLE again (re-prime)
if (m_pending && pm_ready && !pm_vo) begin
pm_valid <= 1'b1;
m_loading <= 1'b1;
m_ld <= 9'd0;
m_oidx <= 8'd0;
pm_valid <= 1'b0;
m_pending <= 1'b0;
end
end