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:
cah
2026-02-25 19:00:41 -07:00
parent 74baf3cd05
commit 3372f84a3a
4 changed files with 325 additions and 31 deletions

28
tb/Makefile Normal file
View 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
View 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