From 3372f84a3ae6814577ecf79ab28fd5ff6ede1c71 Mon Sep 17 00:00:00 2001 From: cah Date: Wed, 25 Feb 2026 19:00:41 -0700 Subject: [PATCH] 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 --- rtl/ldpc_decoder_core.sv | 81 ++++++++----- rtl/wishbone_interface.sv | 2 +- tb/Makefile | 28 +++++ tb/tb_ldpc_decoder.sv | 245 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 325 insertions(+), 31 deletions(-) create mode 100644 tb/Makefile create mode 100644 tb/tb_ldpc_decoder.sv diff --git a/rtl/ldpc_decoder_core.sv b/rtl/ldpc_decoder_core.sv index 1ceaf81..7cfcdcf 100644 --- a/rtl/ldpc_decoder_core.sv +++ b/rtl/ldpc_decoder_core.sv @@ -112,13 +112,14 @@ module ldpc_decoder_core #( // Decoder FSM // ========================================================================= - typedef enum logic [2:0] { + typedef enum logic [3:0] { IDLE, INIT, // Initialize beliefs from channel LLRs, zero messages LAYER_READ, // Read Z beliefs for each of DC columns in current row CN_UPDATE, // Run min-sum CN update on gathered messages LAYER_WRITE, // Write updated beliefs and new CN->VN messages SYNDROME, // Check syndrome after full iteration + SYNDROME_DONE, // Read registered syndrome result DONE } state_t; @@ -167,7 +168,8 @@ module ldpc_decoder_core #( state_next = LAYER_READ; // next row end end - SYNDROME: begin + SYNDROME: state_next = SYNDROME_DONE; + SYNDROME_DONE: begin if (syndrome_ok && early_term_en) state_next = DONE; else if (iter_cnt >= effective_max_iter) @@ -192,6 +194,7 @@ module ldpc_decoder_core #( converged <= 1'b0; iter_used <= '0; syndrome_weight <= '0; + syndrome_ok <= 1'b0; end else begin case (state) IDLE: begin @@ -199,6 +202,7 @@ module ldpc_decoder_core #( row_idx <= '0; col_idx <= '0; converged <= 1'b0; + syndrome_ok <= 1'b0; end INIT: begin @@ -221,18 +225,25 @@ module ldpc_decoder_core #( // VN->CN = belief - old CN->VN message // (belief already contains the sum of ALL CN->VN messages, // so subtracting the current row's message gives the extrinsic) - for (int z = 0; z < Z; z++) begin - int bit_idx; - int shifted_z; - logic signed [Q-1:0] old_msg; - logic signed [Q-1:0] belief_val; + // Skip unconnected columns (H_BASE == -1) + if (H_BASE[row_idx][col_idx] >= 0) begin + for (int z = 0; z < Z; z++) begin + int bit_idx; + int shifted_z; + logic signed [Q-1:0] old_msg; + logic signed [Q-1:0] belief_val; - shifted_z = (z + H_BASE[row_idx][col_idx]) % Z; - bit_idx = int'(col_idx) * Z + shifted_z; - old_msg = msg_cn2vn[row_idx][col_idx][z]; - belief_val = beliefs[bit_idx]; + shifted_z = (z + H_BASE[row_idx][col_idx]) % Z; + bit_idx = int'(col_idx) * Z + shifted_z; + old_msg = msg_cn2vn[row_idx][col_idx][z]; + belief_val = beliefs[bit_idx]; - 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 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) @@ -261,22 +272,25 @@ module ldpc_decoder_core #( LAYER_WRITE: begin // Write back: update beliefs and store new CN->VN messages - for (int z = 0; z < Z; z++) begin - int bit_idx; - int shifted_z; - logic signed [Q-1:0] new_msg; - logic signed [Q-1:0] old_extrinsic; + // Skip unconnected columns (H_BASE == -1) + if (H_BASE[row_idx][col_idx] >= 0) begin + for (int z = 0; z < Z; z++) begin + int bit_idx; + int shifted_z; + logic signed [Q-1:0] new_msg; + logic signed [Q-1:0] old_extrinsic; - shifted_z = (z + H_BASE[row_idx][col_idx]) % Z; - bit_idx = int'(col_idx) * Z + shifted_z; - new_msg = cn_to_vn[col_idx][z]; - old_extrinsic = vn_to_cn[col_idx][z]; + shifted_z = (z + H_BASE[row_idx][col_idx]) % Z; + bit_idx = int'(col_idx) * Z + shifted_z; + new_msg = cn_to_vn[col_idx][z]; + old_extrinsic = vn_to_cn[col_idx][z]; - // belief = extrinsic (VN->CN) + new CN->VN message - beliefs[bit_idx] <= sat_add(old_extrinsic, new_msg); + // belief = extrinsic (VN->CN) + new CN->VN message + beliefs[bit_idx] <= sat_add(old_extrinsic, new_msg); - // Store new message for next iteration - msg_cn2vn[row_idx][col_idx][z] <= new_msg; + // Store new message for next iteration + msg_cn2vn[row_idx][col_idx][z] <= new_msg; + end end if (col_idx == N_BASE - 1) begin @@ -292,25 +306,32 @@ module ldpc_decoder_core #( SYNDROME: begin // Check H * c_hat == 0 (compute syndrome weight) + // Only include connected columns (H_BASE >= 0) syndrome_cnt = '0; for (int r = 0; r < M_BASE; r++) begin for (int z = 0; z < Z; z++) begin logic parity; parity = 1'b0; for (int c = 0; c < N_BASE; c++) begin - int shifted_z, bit_idx; - shifted_z = (z + H_BASE[r][c]) % Z; - bit_idx = c * Z + shifted_z; - parity = parity ^ beliefs[bit_idx][Q-1]; // sign bit = hard decision + if (H_BASE[r][c] >= 0) begin + int shifted_z, bit_idx; + shifted_z = (z + H_BASE[r][c]) % Z; + bit_idx = c * Z + shifted_z; + parity = parity ^ beliefs[bit_idx][Q-1]; + end end if (parity) syndrome_cnt = syndrome_cnt + 1; end end syndrome_weight <= syndrome_cnt; - syndrome_ok = (syndrome_cnt == 0); + syndrome_ok <= (syndrome_cnt == 0); iter_cnt <= iter_cnt + 1; iter_used <= iter_cnt + 1; + end + + SYNDROME_DONE: begin + // Check registered syndrome result if (syndrome_ok) converged <= 1'b1; end diff --git a/rtl/wishbone_interface.sv b/rtl/wishbone_interface.sv index 1163253..a5ea6f2 100644 --- a/rtl/wishbone_interface.sv +++ b/rtl/wishbone_interface.sv @@ -40,7 +40,7 @@ module wishbone_interface #( 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 logic wb_valid; diff --git a/tb/Makefile b/tb/Makefile new file mode 100644 index 0000000..98ef009 --- /dev/null +++ b/tb/Makefile @@ -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 diff --git a/tb/tb_ldpc_decoder.sv b/tb/tb_ldpc_decoder.sv new file mode 100644 index 0000000..c4fc61b --- /dev/null +++ b/tb/tb_ldpc_decoder.sv @@ -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