test: add standalone Verilator testbench for LDPC decoder
Add tb/tb_ldpc_decoder.sv with Wishbone read/write tasks, version register test, and all-zero codeword decode test. Add tb/Makefile with lint and sim targets. Fix two RTL bugs found during testbench bring-up: - ldpc_decoder_core.sv: skip unconnected H_BASE columns (shift=-1) in LAYER_READ, LAYER_WRITE, and SYNDROME states to prevent out-of-bounds array access and belief corruption - ldpc_decoder_core.sv: fix syndrome_ok timing race by adding SYNDROME_DONE state so the registered result is available before the early-termination decision - wishbone_interface.sv: fix VERSION_ID typo (0xLD01 -> 0x1D01) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,13 +112,14 @@ module ldpc_decoder_core #(
|
|||||||
// Decoder FSM
|
// Decoder FSM
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
typedef enum logic [2:0] {
|
typedef enum logic [3:0] {
|
||||||
IDLE,
|
IDLE,
|
||||||
INIT, // Initialize beliefs from channel LLRs, zero messages
|
INIT, // Initialize beliefs from channel LLRs, zero messages
|
||||||
LAYER_READ, // Read Z beliefs for each of DC columns in current row
|
LAYER_READ, // Read Z beliefs for each of DC columns in current row
|
||||||
CN_UPDATE, // Run min-sum CN update on gathered messages
|
CN_UPDATE, // Run min-sum CN update on gathered messages
|
||||||
LAYER_WRITE, // Write updated beliefs and new CN->VN messages
|
LAYER_WRITE, // Write updated beliefs and new CN->VN messages
|
||||||
SYNDROME, // Check syndrome after full iteration
|
SYNDROME, // Check syndrome after full iteration
|
||||||
|
SYNDROME_DONE, // Read registered syndrome result
|
||||||
DONE
|
DONE
|
||||||
} state_t;
|
} state_t;
|
||||||
|
|
||||||
@@ -167,7 +168,8 @@ module ldpc_decoder_core #(
|
|||||||
state_next = LAYER_READ; // next row
|
state_next = LAYER_READ; // next row
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
SYNDROME: begin
|
SYNDROME: state_next = SYNDROME_DONE;
|
||||||
|
SYNDROME_DONE: begin
|
||||||
if (syndrome_ok && early_term_en)
|
if (syndrome_ok && early_term_en)
|
||||||
state_next = DONE;
|
state_next = DONE;
|
||||||
else if (iter_cnt >= effective_max_iter)
|
else if (iter_cnt >= effective_max_iter)
|
||||||
@@ -192,6 +194,7 @@ module ldpc_decoder_core #(
|
|||||||
converged <= 1'b0;
|
converged <= 1'b0;
|
||||||
iter_used <= '0;
|
iter_used <= '0;
|
||||||
syndrome_weight <= '0;
|
syndrome_weight <= '0;
|
||||||
|
syndrome_ok <= 1'b0;
|
||||||
end else begin
|
end else begin
|
||||||
case (state)
|
case (state)
|
||||||
IDLE: begin
|
IDLE: begin
|
||||||
@@ -199,6 +202,7 @@ module ldpc_decoder_core #(
|
|||||||
row_idx <= '0;
|
row_idx <= '0;
|
||||||
col_idx <= '0;
|
col_idx <= '0;
|
||||||
converged <= 1'b0;
|
converged <= 1'b0;
|
||||||
|
syndrome_ok <= 1'b0;
|
||||||
end
|
end
|
||||||
|
|
||||||
INIT: begin
|
INIT: begin
|
||||||
@@ -221,6 +225,8 @@ module ldpc_decoder_core #(
|
|||||||
// VN->CN = belief - old CN->VN message
|
// VN->CN = belief - old CN->VN message
|
||||||
// (belief already contains the sum of ALL CN->VN messages,
|
// (belief already contains the sum of ALL CN->VN messages,
|
||||||
// so subtracting the current row's message gives the extrinsic)
|
// so subtracting the current row's message gives the extrinsic)
|
||||||
|
// Skip unconnected columns (H_BASE == -1)
|
||||||
|
if (H_BASE[row_idx][col_idx] >= 0) begin
|
||||||
for (int z = 0; z < Z; z++) begin
|
for (int z = 0; z < Z; z++) begin
|
||||||
int bit_idx;
|
int bit_idx;
|
||||||
int shifted_z;
|
int shifted_z;
|
||||||
@@ -234,6 +240,11 @@ module ldpc_decoder_core #(
|
|||||||
|
|
||||||
vn_to_cn[col_idx][z] <= sat_sub(belief_val, old_msg);
|
vn_to_cn[col_idx][z] <= sat_sub(belief_val, old_msg);
|
||||||
end
|
end
|
||||||
|
end else begin
|
||||||
|
// Unconnected: set VN->CN messages to 0
|
||||||
|
for (int z = 0; z < Z; z++)
|
||||||
|
vn_to_cn[col_idx][z] <= '0;
|
||||||
|
end
|
||||||
|
|
||||||
if (col_idx == N_BASE - 1)
|
if (col_idx == N_BASE - 1)
|
||||||
col_idx <= '0;
|
col_idx <= '0;
|
||||||
@@ -261,6 +272,8 @@ module ldpc_decoder_core #(
|
|||||||
|
|
||||||
LAYER_WRITE: begin
|
LAYER_WRITE: begin
|
||||||
// Write back: update beliefs and store new CN->VN messages
|
// Write back: update beliefs and store new CN->VN messages
|
||||||
|
// Skip unconnected columns (H_BASE == -1)
|
||||||
|
if (H_BASE[row_idx][col_idx] >= 0) begin
|
||||||
for (int z = 0; z < Z; z++) begin
|
for (int z = 0; z < Z; z++) begin
|
||||||
int bit_idx;
|
int bit_idx;
|
||||||
int shifted_z;
|
int shifted_z;
|
||||||
@@ -278,6 +291,7 @@ module ldpc_decoder_core #(
|
|||||||
// Store new message for next iteration
|
// Store new message for next iteration
|
||||||
msg_cn2vn[row_idx][col_idx][z] <= new_msg;
|
msg_cn2vn[row_idx][col_idx][z] <= new_msg;
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if (col_idx == N_BASE - 1) begin
|
if (col_idx == N_BASE - 1) begin
|
||||||
col_idx <= '0;
|
col_idx <= '0;
|
||||||
@@ -292,25 +306,32 @@ module ldpc_decoder_core #(
|
|||||||
|
|
||||||
SYNDROME: begin
|
SYNDROME: begin
|
||||||
// Check H * c_hat == 0 (compute syndrome weight)
|
// Check H * c_hat == 0 (compute syndrome weight)
|
||||||
|
// Only include connected columns (H_BASE >= 0)
|
||||||
syndrome_cnt = '0;
|
syndrome_cnt = '0;
|
||||||
for (int r = 0; r < M_BASE; r++) begin
|
for (int r = 0; r < M_BASE; r++) begin
|
||||||
for (int z = 0; z < Z; z++) begin
|
for (int z = 0; z < Z; z++) begin
|
||||||
logic parity;
|
logic parity;
|
||||||
parity = 1'b0;
|
parity = 1'b0;
|
||||||
for (int c = 0; c < N_BASE; c++) begin
|
for (int c = 0; c < N_BASE; c++) begin
|
||||||
|
if (H_BASE[r][c] >= 0) begin
|
||||||
int shifted_z, bit_idx;
|
int shifted_z, bit_idx;
|
||||||
shifted_z = (z + H_BASE[r][c]) % Z;
|
shifted_z = (z + H_BASE[r][c]) % Z;
|
||||||
bit_idx = c * Z + shifted_z;
|
bit_idx = c * Z + shifted_z;
|
||||||
parity = parity ^ beliefs[bit_idx][Q-1]; // sign bit = hard decision
|
parity = parity ^ beliefs[bit_idx][Q-1];
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if (parity) syndrome_cnt = syndrome_cnt + 1;
|
if (parity) syndrome_cnt = syndrome_cnt + 1;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
syndrome_weight <= syndrome_cnt;
|
syndrome_weight <= syndrome_cnt;
|
||||||
syndrome_ok = (syndrome_cnt == 0);
|
syndrome_ok <= (syndrome_cnt == 0);
|
||||||
|
|
||||||
iter_cnt <= iter_cnt + 1;
|
iter_cnt <= iter_cnt + 1;
|
||||||
iter_used <= iter_cnt + 1;
|
iter_used <= iter_cnt + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
SYNDROME_DONE: begin
|
||||||
|
// Check registered syndrome result
|
||||||
if (syndrome_ok) converged <= 1'b1;
|
if (syndrome_ok) converged <= 1'b1;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ module wishbone_interface #(
|
|||||||
output logic irq_o
|
output logic irq_o
|
||||||
);
|
);
|
||||||
|
|
||||||
localparam VERSION_ID = 32'hLD01_0001; // LDPC v0.1 build 1
|
localparam VERSION_ID = 32'h1D01_0001; // LDPC v0.1 build 1
|
||||||
|
|
||||||
// Wishbone handshake: ack on valid cycle
|
// Wishbone handshake: ack on valid cycle
|
||||||
logic wb_valid;
|
logic wb_valid;
|
||||||
|
|||||||
28
tb/Makefile
Normal file
28
tb/Makefile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
RTL_DIR = ../rtl
|
||||||
|
RTL_FILES = $(RTL_DIR)/ldpc_decoder_top.sv \
|
||||||
|
$(RTL_DIR)/ldpc_decoder_core.sv \
|
||||||
|
$(RTL_DIR)/wishbone_interface.sv
|
||||||
|
|
||||||
|
.PHONY: lint sim clean
|
||||||
|
|
||||||
|
lint:
|
||||||
|
verilator --lint-only -Wall \
|
||||||
|
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-CASEINCOMPLETE \
|
||||||
|
-Wno-BLKSEQ -Wno-BLKLOOPINIT -Wno-UNUSEDSIGNAL -Wno-UNUSEDPARAM \
|
||||||
|
--unroll-count 1024 \
|
||||||
|
$(RTL_FILES) --top-module ldpc_decoder_top
|
||||||
|
|
||||||
|
sim: obj_dir/Vtb_ldpc_decoder
|
||||||
|
./obj_dir/Vtb_ldpc_decoder
|
||||||
|
|
||||||
|
obj_dir/Vtb_ldpc_decoder: tb_ldpc_decoder.sv $(RTL_FILES)
|
||||||
|
verilator --binary --timing --trace \
|
||||||
|
-o Vtb_ldpc_decoder \
|
||||||
|
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-CASEINCOMPLETE \
|
||||||
|
-Wno-BLKSEQ -Wno-BLKLOOPINIT -Wno-UNUSEDSIGNAL -Wno-UNUSEDPARAM \
|
||||||
|
--unroll-count 1024 \
|
||||||
|
tb_ldpc_decoder.sv $(RTL_FILES) \
|
||||||
|
--top-module tb_ldpc_decoder
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf obj_dir *.vcd
|
||||||
245
tb/tb_ldpc_decoder.sv
Normal file
245
tb/tb_ldpc_decoder.sv
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// Standalone Verilator testbench for LDPC decoder
|
||||||
|
// Tests the decoder core directly via Wishbone (no Caravel dependency)
|
||||||
|
//
|
||||||
|
// Test 1: Read VERSION register (expect 0x1D010001)
|
||||||
|
// Test 2: Decode all-zero codeword with strong +31 LLRs
|
||||||
|
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
module tb_ldpc_decoder;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Clock and reset
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
logic clk;
|
||||||
|
logic rst_n;
|
||||||
|
logic wb_cyc_i;
|
||||||
|
logic wb_stb_i;
|
||||||
|
logic wb_we_i;
|
||||||
|
logic [7:0] wb_adr_i;
|
||||||
|
logic [31:0] wb_dat_i;
|
||||||
|
logic [31:0] wb_dat_o;
|
||||||
|
logic wb_ack_o;
|
||||||
|
logic irq_o;
|
||||||
|
|
||||||
|
// 50 MHz clock (20 ns period)
|
||||||
|
initial clk = 0;
|
||||||
|
always #10 clk = ~clk;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DUT instantiation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
ldpc_decoder_top dut (
|
||||||
|
.clk (clk),
|
||||||
|
.rst_n (rst_n),
|
||||||
|
.wb_cyc_i (wb_cyc_i),
|
||||||
|
.wb_stb_i (wb_stb_i),
|
||||||
|
.wb_we_i (wb_we_i),
|
||||||
|
.wb_adr_i (wb_adr_i),
|
||||||
|
.wb_dat_i (wb_dat_i),
|
||||||
|
.wb_dat_o (wb_dat_o),
|
||||||
|
.wb_ack_o (wb_ack_o),
|
||||||
|
.irq_o (irq_o)
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// VCD dump
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
$dumpfile("tb_ldpc_decoder.vcd");
|
||||||
|
$dumpvars(0, tb_ldpc_decoder);
|
||||||
|
end
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Watchdog timeout
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
int cycle_cnt;
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
cycle_cnt = 0;
|
||||||
|
forever begin
|
||||||
|
@(posedge clk);
|
||||||
|
cycle_cnt++;
|
||||||
|
if (cycle_cnt > 100000) begin
|
||||||
|
$display("TIMEOUT: exceeded 100000 cycles");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Wishbone tasks
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
task automatic wb_write(input logic [7:0] addr, input logic [31:0] data);
|
||||||
|
@(posedge clk);
|
||||||
|
wb_cyc_i = 1'b1;
|
||||||
|
wb_stb_i = 1'b1;
|
||||||
|
wb_we_i = 1'b1;
|
||||||
|
wb_adr_i = addr;
|
||||||
|
wb_dat_i = data;
|
||||||
|
|
||||||
|
// Wait for ack
|
||||||
|
do begin
|
||||||
|
@(posedge clk);
|
||||||
|
end while (!wb_ack_o);
|
||||||
|
|
||||||
|
// Deassert
|
||||||
|
wb_cyc_i = 1'b0;
|
||||||
|
wb_stb_i = 1'b0;
|
||||||
|
wb_we_i = 1'b0;
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic wb_read(input logic [7:0] addr, output logic [31:0] data);
|
||||||
|
@(posedge clk);
|
||||||
|
wb_cyc_i = 1'b1;
|
||||||
|
wb_stb_i = 1'b1;
|
||||||
|
wb_we_i = 1'b0;
|
||||||
|
wb_adr_i = addr;
|
||||||
|
|
||||||
|
// Wait for ack
|
||||||
|
do begin
|
||||||
|
@(posedge clk);
|
||||||
|
end while (!wb_ack_o);
|
||||||
|
|
||||||
|
data = wb_dat_o;
|
||||||
|
|
||||||
|
// Deassert
|
||||||
|
wb_cyc_i = 1'b0;
|
||||||
|
wb_stb_i = 1'b0;
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Test variables
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
int pass_cnt;
|
||||||
|
int fail_cnt;
|
||||||
|
logic [31:0] rd_data;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Main test sequence
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
pass_cnt = 0;
|
||||||
|
fail_cnt = 0;
|
||||||
|
|
||||||
|
// Initialize Wishbone signals
|
||||||
|
wb_cyc_i = 1'b0;
|
||||||
|
wb_stb_i = 1'b0;
|
||||||
|
wb_we_i = 1'b0;
|
||||||
|
wb_adr_i = 8'h00;
|
||||||
|
wb_dat_i = 32'h0;
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
rst_n = 1'b0;
|
||||||
|
repeat (10) @(posedge clk);
|
||||||
|
rst_n = 1'b1;
|
||||||
|
repeat (5) @(posedge clk);
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// TEST 1: Read VERSION register
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
$display("[TEST 1] Read VERSION register");
|
||||||
|
wb_read(8'h54, rd_data);
|
||||||
|
|
||||||
|
if (rd_data === 32'h1D01_0001) begin
|
||||||
|
$display(" PASS: VERSION = 0x%08X", rd_data);
|
||||||
|
pass_cnt++;
|
||||||
|
end else begin
|
||||||
|
$display(" FAIL: VERSION = 0x%08X (expected 0x1D010001)", rd_data);
|
||||||
|
fail_cnt++;
|
||||||
|
end
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// TEST 2: Decode clean all-zero codeword
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
$display("[TEST 2] Decode clean all-zero codeword");
|
||||||
|
|
||||||
|
// Write 52 LLR words at addresses 0x10..0xDC
|
||||||
|
// Each word = 5x +31 packed: {6'h1F, 6'h1F, 6'h1F, 6'h1F, 6'h1F}
|
||||||
|
// = 0x1F | (0x1F<<6) | (0x1F<<12) | (0x1F<<18) | (0x1F<<24)
|
||||||
|
// = 0x1F7DF7DF
|
||||||
|
begin
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 52; i++) begin
|
||||||
|
wb_write(8'h10 + i * 4, 32'h1F7D_F7DF);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Start decode: write CTRL
|
||||||
|
// bit[0]=1 (start), bit[1]=1 (early_term), bits[12:8]=0x1E=30 (max_iter)
|
||||||
|
// 0x00001E03
|
||||||
|
wb_write(8'h00, 32'h0000_1E03);
|
||||||
|
|
||||||
|
// Poll STATUS (addr 0x04) until busy (bit[0]) = 0
|
||||||
|
// Allow a few cycles for busy to assert first
|
||||||
|
repeat (5) @(posedge clk);
|
||||||
|
|
||||||
|
begin
|
||||||
|
int poll_cnt;
|
||||||
|
poll_cnt = 0;
|
||||||
|
do begin
|
||||||
|
wb_read(8'h04, rd_data);
|
||||||
|
poll_cnt++;
|
||||||
|
if (poll_cnt > 10000) begin
|
||||||
|
$display(" FAIL: decoder stuck busy after %0d polls", poll_cnt);
|
||||||
|
fail_cnt++;
|
||||||
|
$display("=== %0d PASSED, %0d FAILED ===", pass_cnt, fail_cnt);
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
end while (rd_data[0] == 1'b1);
|
||||||
|
end
|
||||||
|
|
||||||
|
// Check convergence: bit[1] of STATUS
|
||||||
|
if (rd_data[1] == 1'b1) begin
|
||||||
|
$display(" converged=1 (OK)");
|
||||||
|
end else begin
|
||||||
|
$display(" FAIL: converged=0 (expected 1)");
|
||||||
|
fail_cnt++;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Check syndrome weight: bits[23:16] of STATUS
|
||||||
|
if (rd_data[23:16] == 8'd0) begin
|
||||||
|
$display(" syndrome_weight=0 (OK)");
|
||||||
|
end else begin
|
||||||
|
$display(" FAIL: syndrome_weight=%0d (expected 0)", rd_data[23:16]);
|
||||||
|
fail_cnt++;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Check iterations used: bits[12:8] of STATUS
|
||||||
|
$display(" iterations_used=%0d", rd_data[12:8]);
|
||||||
|
|
||||||
|
// Read DECODED register (addr 0x50)
|
||||||
|
wb_read(8'h50, rd_data);
|
||||||
|
|
||||||
|
if (rd_data === 32'h0000_0000) begin
|
||||||
|
$display(" PASS: decoded=0x%08X", rd_data);
|
||||||
|
pass_cnt++;
|
||||||
|
end else begin
|
||||||
|
$display(" FAIL: decoded=0x%08X (expected 0x00000000)", rd_data);
|
||||||
|
fail_cnt++;
|
||||||
|
end
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Summary
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
$display("");
|
||||||
|
if (fail_cnt == 0) begin
|
||||||
|
$display("=== ALL %0d TESTS PASSED ===", pass_cnt);
|
||||||
|
end else begin
|
||||||
|
$display("=== %0d PASSED, %0d FAILED ===", pass_cnt, fail_cnt);
|
||||||
|
end
|
||||||
|
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
Reference in New Issue
Block a user