Files
ldpc_optical/docs/plans/2026-02-25-chipfoundry-contest-impl.md
cah 9a28e30bed docs: add detailed implementation plan for ChipFoundry contest
20 tasks across 8 weeks covering RTL integration, verification,
OpenLane hardening, firmware, PCBA, and final submission.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:10:38 -07:00

34 KiB

ChipFoundry Contest Submission Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Integrate the LDPC decoder into the Caravel chip_ignite template, verify with cocotb, harden with OpenLane, and submit proposal by March 25.

Architecture: Wire existing ldpc_decoder_top into user_project_wrapper.v, inverting reset polarity and connecting lower 8 address bits. PicoRV32 firmware drives test vectors over Wishbone. cocotb monitors UART/GPIO for pass/fail.

Tech Stack: SystemVerilog RTL, OpenLane 2.0 (LibreLane), cocotb + caravel_cocotb, PicoRV32 bare-metal C, Python 3 (test vector generation), KiCad (PCBA, later weeks)


Week 1: RTL Integration & Standalone Verification

Task 1: Copy RTL into chip_ignite and adapt for Caravel

Files:

  • Copy: rtl/ldpc_decoder_core.sv -> chip_ignite/verilog/rtl/ldpc_decoder_core.sv
  • Copy: rtl/wishbone_interface.sv -> chip_ignite/verilog/rtl/wishbone_interface.sv
  • Create: chip_ignite/verilog/rtl/ldpc_decoder_top.sv (adapted from rtl/ldpc_decoder_top.sv)
  • Modify: chip_ignite/verilog/rtl/user_project_wrapper.v

Step 1: Copy unmodified RTL files

cp rtl/ldpc_decoder_core.sv chip_ignite/verilog/rtl/
cp rtl/wishbone_interface.sv chip_ignite/verilog/rtl/

Step 2: Create Caravel-adapted ldpc_decoder_top.sv

The original ldpc_decoder_top.sv uses rst_n (active-low) and 8-bit address. Caravel provides wb_rst_i (active-high) and 32-bit address. Create an adapted version:

// LDPC Decoder Top - Caravel-adapted wrapper
// Handles reset polarity inversion and address bus width adaptation
//
// Changes from standalone version:
//   - rst_n derived from wb_rst_i (active-high -> active-low)
//   - wb_adr_i connected from lower 8 bits of 32-bit Caravel address
//   - USE_POWER_PINS support for Caravel integration

module ldpc_decoder_top #(
    parameter N_BASE    = 8,
    parameter M_BASE    = 7,
    parameter Z         = 32,
    parameter N         = N_BASE * Z,  // 256
    parameter K         = Z,           // 32
    parameter M         = M_BASE * Z,  // 224
    parameter Q         = 6,
    parameter MAX_ITER  = 30,
    parameter DC        = 8,
    parameter DV_MAX    = 7
)(
`ifdef USE_POWER_PINS
    inout vccd1,
    inout vssd1,
`endif
    input  logic        clk,
    input  logic        rst_n,

    // Wishbone B4 slave interface
    input  logic        wb_cyc_i,
    input  logic        wb_stb_i,
    input  logic        wb_we_i,
    input  logic [3:0]  wb_sel_i,      // byte selects (unused, for Caravel compat)
    input  logic [31:0] wb_adr_i,      // full 32-bit address from Caravel
    input  logic [31:0] wb_dat_i,
    output logic [31:0] wb_dat_o,
    output logic        wb_ack_o,

    // Interrupt
    output logic        irq_o
);

    // Internal signals
    logic        ctrl_start;
    logic        ctrl_early_term;
    logic [4:0]  ctrl_max_iter;
    logic        stat_busy;
    logic        stat_converged;
    logic [4:0]  stat_iter_used;
    logic signed [Q-1:0] llr_input [N];
    logic [K-1:0]  decoded_bits;
    logic [7:0]    syndrome_weight;

    wishbone_interface #(
        .N(N), .K(K), .Q(Q)
    ) u_wb (
        .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[7:0]),  // lower 8 bits only
        .wb_dat_i       (wb_dat_i),
        .wb_dat_o       (wb_dat_o),
        .wb_ack_o       (wb_ack_o),
        .ctrl_start     (ctrl_start),
        .ctrl_early_term(ctrl_early_term),
        .ctrl_max_iter  (ctrl_max_iter),
        .stat_busy      (stat_busy),
        .stat_converged (stat_converged),
        .stat_iter_used (stat_iter_used),
        .llr_input      (llr_input),
        .decoded_bits   (decoded_bits),
        .syndrome_weight(syndrome_weight),
        .irq_o          (irq_o)
    );

    ldpc_decoder_core #(
        .N_BASE   (N_BASE),
        .M_BASE   (M_BASE),
        .Z        (Z),
        .Q        (Q),
        .MAX_ITER (MAX_ITER),
        .DC       (DC),
        .DV_MAX   (DV_MAX)
    ) u_core (
        .clk            (clk),
        .rst_n          (rst_n),
        .start          (ctrl_start),
        .early_term_en  (ctrl_early_term),
        .max_iter       (ctrl_max_iter),
        .llr_in         (llr_input),
        .busy           (stat_busy),
        .converged      (stat_converged),
        .iter_used      (stat_iter_used),
        .decoded_bits   (decoded_bits),
        .syndrome_weight(syndrome_weight)
    );

endmodule

Step 3: Modify user_project_wrapper.v

Replace the user_proj_example instantiation with ldpc_decoder_top:

/* In user_project_wrapper.v, replace the user_proj_example instantiation block with: */

ldpc_decoder_top mprj (
`ifdef USE_POWER_PINS
    .vccd1(vccd1),
    .vssd1(vssd1),
`endif
    .clk        (wb_clk_i),
    .rst_n      (~wb_rst_i),          // Caravel active-high -> LDPC active-low

    // Wishbone slave
    .wb_cyc_i   (wbs_cyc_i),
    .wb_stb_i   (wbs_stb_i),
    .wb_we_i    (wbs_we_i),
    .wb_sel_i   (wbs_sel_i),
    .wb_adr_i   (wbs_adr_i),
    .wb_dat_i   (wbs_dat_i),
    .wb_dat_o   (wbs_dat_o),
    .wb_ack_o   (wbs_ack_o),

    // Interrupt (active high, directly to Caravel IRQ[0])
    .irq_o      (user_irq[0])
);

// Tie off unused outputs
assign la_data_out = 128'b0;
assign io_out = {`MPRJ_IO_PADS{1'b0}};
assign io_oeb = {`MPRJ_IO_PADS{1'b1}};  // all inputs
assign user_irq[2:1] = 2'b0;

Step 4: Update RTL include file

Edit chip_ignite/verilog/includes/includes.rtl.caravel_user_project:

-v $(USER_PROJECT_VERILOG)/rtl/defines.v
-v $(USER_PROJECT_VERILOG)/rtl/user_project_wrapper.v
-v $(USER_PROJECT_VERILOG)/rtl/ldpc_decoder_top.sv
-v $(USER_PROJECT_VERILOG)/rtl/ldpc_decoder_core.sv
-v $(USER_PROJECT_VERILOG)/rtl/wishbone_interface.sv

Remove the user_proj_example.v line.

Step 5: Lint check

cd chip_ignite
verilator --lint-only -Wall \
    verilog/rtl/ldpc_decoder_top.sv \
    verilog/rtl/ldpc_decoder_core.sv \
    verilog/rtl/wishbone_interface.sv \
    -DUSE_POWER_PINS \
    --top-module ldpc_decoder_top

Expected: 0 errors, warnings acceptable (unused wb_sel_i).

Step 6: Commit

cd chip_ignite
git add verilog/rtl/ldpc_decoder_top.sv \
        verilog/rtl/ldpc_decoder_core.sv \
        verilog/rtl/wishbone_interface.sv \
        verilog/rtl/user_project_wrapper.v \
        verilog/includes/includes.rtl.caravel_user_project
git commit -m "feat: integrate LDPC decoder into Caravel wrapper"

Task 2: Generate test vectors from Python model

Files:

  • Run: model/ldpc_sim.py --gen-vectors
  • Create: model/gen_firmware_vectors.py
  • Output: data/test_vectors.json
  • Output: chip_ignite/verilog/dv/cocotb/ldpc_tests/test_data.py

Step 1: Generate test vectors

cd /home/cah/r2d2/code/fpga/claude_project/ldpc_optical
python3 model/ldpc_sim.py --gen-vectors --lam-s 2.0 --lam-b 0.1 --seed 42

Expected: Creates data/test_vectors.json with 20 test vectors.

Step 2: Verify test vectors are usable

python3 -c "
import json
with open('data/test_vectors.json') as f:
    vecs = json.load(f)
print(f'Vectors: {len(vecs)}')
v = vecs[0]
print(f'LLR count: {len(v[\"llr_quantized\"])}')
print(f'Info bits: {len(v[\"info_bits\"])}')
print(f'Converged: {v[\"converged\"]}')
print(f'Iterations: {v[\"iterations\"]}')
"

Expected: 20 vectors, 256 LLRs each, 32 info bits each.

Step 3: Create Python converter for cocotb test data

Create model/gen_firmware_vectors.py:

#!/usr/bin/env python3
"""Convert test_vectors.json to Python test data module for cocotb tests
and C header for PicoRV32 firmware."""

import json
import sys
import os

def pack_llrs_to_words(llr_quantized, q_bits=6):
    """Pack 256 6-bit signed LLRs into 52 32-bit words (5 LLRs per word)."""
    words = []
    for i in range(0, 256, 5):
        word = 0
        for p in range(5):
            idx = i + p
            if idx < 256:
                # Convert signed to unsigned 6-bit
                val = llr_quantized[idx] & 0x3F
                word |= val << (p * q_bits)
        words.append(word)
    return words

def gen_cocotb_test_data(vectors, output_path):
    """Generate Python module with test vector data for cocotb."""
    with open(output_path, 'w') as f:
        f.write("# Auto-generated test vectors from ldpc_sim.py\n")
        f.write("# DO NOT EDIT - regenerate with: python3 model/gen_firmware_vectors.py\n\n")
        f.write("TEST_VECTORS = [\n")
        for i, v in enumerate(vectors):
            llr_words = pack_llrs_to_words(v['llr_quantized'])
            info_bits = v['info_bits']
            # Pack 32 info bits into a single uint32
            decoded_word = sum(b << j for j, b in enumerate(info_bits))
            f.write(f"    {{\n")
            f.write(f"        'index': {i},\n")
            f.write(f"        'llr_words': {llr_words},\n")
            f.write(f"        'decoded_word': 0x{decoded_word:08X},\n")
            f.write(f"        'converged': {v['converged']},\n")
            f.write(f"        'iterations': {v['iterations']},\n")
            f.write(f"        'syndrome_weight': {v['syndrome_weight']},\n")
            f.write(f"    }},\n")
        f.write("]\n")

def gen_c_header(vectors, output_path):
    """Generate C header with test vector data for PicoRV32 firmware."""
    with open(output_path, 'w') as f:
        f.write("// Auto-generated test vectors from ldpc_sim.py\n")
        f.write("// DO NOT EDIT - regenerate with: python3 model/gen_firmware_vectors.py\n\n")
        f.write("#ifndef TEST_VECTORS_H\n#define TEST_VECTORS_H\n\n")
        f.write(f"#define NUM_TEST_VECTORS {len(vectors)}\n")
        f.write("#define LLR_WORDS_PER_VECTOR 52\n\n")

        for i, v in enumerate(vectors):
            llr_words = pack_llrs_to_words(v['llr_quantized'])
            info_bits = v['info_bits']
            decoded_word = sum(b << j for j, b in enumerate(info_bits))
            f.write(f"// Vector {i}: converged={v['converged']}, "
                    f"iters={v['iterations']}, syndrome={v['syndrome_weight']}\n")
            f.write(f"static const uint32_t tv{i}_llr[52] = {{\n    ")
            for j, w in enumerate(llr_words):
                f.write(f"0x{w:08X}")
                if j < len(llr_words) - 1:
                    f.write(", ")
                if (j + 1) % 8 == 0 and j < len(llr_words) - 1:
                    f.write("\n    ")
            f.write(f"\n}};\n")
            f.write(f"static const uint32_t tv{i}_decoded = 0x{decoded_word:08X};\n")
            f.write(f"static const int tv{i}_converged = {int(v['converged'])};\n\n")

        f.write("#endif // TEST_VECTORS_H\n")

if __name__ == '__main__':
    project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    json_path = os.path.join(project_root, 'data', 'test_vectors.json')

    with open(json_path) as f:
        vectors = json.load(f)

    # Generate cocotb test data
    cocotb_path = os.path.join(project_root,
        'chip_ignite/verilog/dv/cocotb/ldpc_tests/test_data.py')
    os.makedirs(os.path.dirname(cocotb_path), exist_ok=True)
    gen_cocotb_test_data(vectors, cocotb_path)
    print(f"Generated {cocotb_path}")

    # Generate C header
    c_path = os.path.join(project_root,
        'chip_ignite/firmware/ldpc_demo/test_vectors.h')
    os.makedirs(os.path.dirname(c_path), exist_ok=True)
    gen_c_header(vectors, c_path)
    print(f"Generated {c_path}")

Step 4: Run the converter

python3 model/gen_firmware_vectors.py

Expected: Creates chip_ignite/verilog/dv/cocotb/ldpc_tests/test_data.py and chip_ignite/firmware/ldpc_demo/test_vectors.h.

Step 5: Commit

git add model/gen_firmware_vectors.py
cd chip_ignite
git add verilog/dv/cocotb/ldpc_tests/test_data.py firmware/ldpc_demo/test_vectors.h
git commit -m "feat: add test vector generation for cocotb and firmware"

Task 3: Write standalone Verilator testbench

Files:

  • Create: tb/tb_ldpc_decoder.sv
  • Create: tb/Makefile

This is a fast-iteration standalone testbench (no Caravel dependency). Tests the decoder core directly via Wishbone.

Step 1: Write the testbench

Create tb/tb_ldpc_decoder.sv:

// Standalone Verilator testbench for LDPC decoder
// Tests Wishbone register access and decode operation
// Uses test vectors generated by model/ldpc_sim.py

`timescale 1ns/1ps

module tb_ldpc_decoder;

    parameter CLK_PERIOD = 20;  // 50 MHz

    logic        clk;
    logic        rst_n;
    logic        wb_cyc;
    logic        wb_stb;
    logic        wb_we;
    logic [7:0]  wb_adr;
    logic [31:0] wb_dat_w;
    logic [31:0] wb_dat_r;
    logic        wb_ack;
    logic        irq;

    // DUT: standalone ldpc_decoder_top (original, not Caravel-adapted)
    ldpc_decoder_top dut (
        .clk      (clk),
        .rst_n    (rst_n),
        .wb_cyc_i (wb_cyc),
        .wb_stb_i (wb_stb),
        .wb_we_i  (wb_we),
        .wb_adr_i (wb_adr),
        .wb_dat_i (wb_dat_w),
        .wb_dat_o (wb_dat_r),
        .wb_ack_o (wb_ack),
        .irq_o    (irq)
    );

    // Clock generation
    initial clk = 0;
    always #(CLK_PERIOD/2) clk = ~clk;

    // Wishbone write task
    task automatic wb_write(input [7:0] addr, input [31:0] data);
        @(posedge clk);
        wb_cyc <= 1;
        wb_stb <= 1;
        wb_we  <= 1;
        wb_adr <= addr;
        wb_dat_w <= data;
        do @(posedge clk); while (!wb_ack);
        wb_cyc <= 0;
        wb_stb <= 0;
        wb_we  <= 0;
    endtask

    // Wishbone read task
    task automatic wb_read(input [7:0] addr, output [31:0] data);
        @(posedge clk);
        wb_cyc <= 1;
        wb_stb <= 1;
        wb_we  <= 0;
        wb_adr <= addr;
        do @(posedge clk); while (!wb_ack);
        data = wb_dat_r;
        wb_cyc <= 0;
        wb_stb <= 0;
    endtask

    // Test vector data (clean codeword: all-zero message, all LLRs = +31)
    initial begin
        logic [31:0] rdata;
        int errors;

        errors = 0;
        rst_n = 0;
        wb_cyc = 0;
        wb_stb = 0;
        wb_we  = 0;
        wb_adr = 0;
        wb_dat_w = 0;

        // Reset
        repeat(10) @(posedge clk);
        rst_n = 1;
        repeat(5) @(posedge clk);

        // ---- Test 1: Read version register ----
        $display("[TEST 1] Read VERSION register");
        wb_read(8'h54, rdata);
        if (rdata !== 32'hLD01_0001) begin
            $display("  FAIL: VERSION = 0x%08X, expected 0xLD010001", rdata);
            errors++;
        end else begin
            $display("  PASS: VERSION = 0x%08X", rdata);
        end

        // ---- Test 2: Decode all-zero codeword (clean) ----
        $display("[TEST 2] Decode clean all-zero codeword");

        // Write LLRs: all +31 (= bit 0 very likely)
        // Packed: 5 LLRs of +31 = {6'h1F, 6'h1F, 6'h1F, 6'h1F, 6'h1F}
        //       = {5'b11111 x5} in 30 bits = 0x1F7DF7DF
        for (int i = 0; i < 52; i++) begin
            wb_write(8'h10 + i*4, 32'h1F7D_F7DF);
        end

        // Start decode: early_term=1, max_iter=30, start=1
        wb_write(8'h00, 32'h0000_1E03);  // [12:8]=30, [1]=1, [0]=1

        // Poll status until not busy
        do begin
            repeat(10) @(posedge clk);
            wb_read(8'h04, rdata);
        end while (rdata[0]);  // bit 0 = busy

        $display("  STATUS = 0x%08X", rdata);
        $display("  Busy=%0d, Converged=%0d, Iters=%0d, Syndrome=%0d",
            rdata[0], rdata[1], rdata[12:8], rdata[23:16]);

        if (!rdata[1]) begin
            $display("  FAIL: did not converge");
            errors++;
        end
        if (rdata[23:16] != 0) begin
            $display("  FAIL: syndrome weight = %0d, expected 0", rdata[23:16]);
            errors++;
        end

        // Read decoded bits
        wb_read(8'h50, rdata);
        $display("  Decoded = 0x%08X", rdata);
        if (rdata !== 32'h0000_0000) begin
            $display("  FAIL: decoded = 0x%08X, expected 0x00000000", rdata);
            errors++;
        end else begin
            $display("  PASS: decoded bits correct");
        end

        // ---- Summary ----
        repeat(10) @(posedge clk);
        if (errors == 0)
            $display("\n=== ALL TESTS PASSED ===\n");
        else
            $display("\n=== %0d ERRORS ===\n", errors);

        $finish;
    end

    // Timeout
    initial begin
        #(CLK_PERIOD * 100_000);
        $display("TIMEOUT");
        $finish;
    end

    // VCD dump
    initial begin
        $dumpfile("ldpc_decoder.vcd");
        $dumpvars(0, tb_ldpc_decoder);
    end

endmodule

Step 2: Create Makefile

Create tb/Makefile:

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 $(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 \
		--unroll-count 1024 \
		tb_ldpc_decoder.sv $(RTL_FILES) \
		--top-module tb_ldpc_decoder

clean:
	rm -rf obj_dir *.vcd

Step 3: Run lint

cd tb
make lint

Expected: Clean (0 errors).

Step 4: Run simulation

cd tb
make sim

Expected:

[TEST 1] Read VERSION register
  PASS: VERSION = 0xLD010001
[TEST 2] Decode clean all-zero codeword
  ...
  PASS: decoded bits correct

=== ALL TESTS PASSED ===

Step 5: Commit

git add tb/tb_ldpc_decoder.sv tb/Makefile
git commit -m "test: add standalone Verilator testbench for LDPC decoder"

Task 4: Extend Verilator testbench with generated test vectors

Files:

  • Create: tb/tb_ldpc_vectors.sv
  • Modify: tb/Makefile (add target)
  • Depends on: data/test_vectors.json from Task 2

Step 1: Create a script to convert JSON vectors to $readmemh format

Create model/gen_verilator_vectors.py:

#!/usr/bin/env python3
"""Convert test_vectors.json to hex files for Verilator $readmemh."""

import json
import os

def main():
    project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    json_path = os.path.join(project_root, 'data', 'test_vectors.json')
    out_dir = os.path.join(project_root, 'tb', 'vectors')
    os.makedirs(out_dir, exist_ok=True)

    with open(json_path) as f:
        vectors = json.load(f)

    # Write LLR file: one line per LLR, 6-bit hex (2 chars), 256 lines per vector
    with open(os.path.join(out_dir, 'llr_input.hex'), 'w') as f:
        for v in vectors:
            for llr in v['llr_quantized']:
                f.write(f"{llr & 0x3F:02X}\n")

    # Write expected results: decoded_word, converged, iterations, syndrome_weight
    with open(os.path.join(out_dir, 'expected.hex'), 'w') as f:
        for v in vectors:
            decoded = sum(b << j for j, b in enumerate(v['info_bits']))
            f.write(f"{decoded:08X}\n")
            f.write(f"{int(v['converged']):02X}\n")
            f.write(f"{v['iterations']:02X}\n")
            f.write(f"{v['syndrome_weight']:02X}\n")

    # Write metadata
    with open(os.path.join(out_dir, 'num_vectors.txt'), 'w') as f:
        f.write(f"{len(vectors)}\n")

    print(f"Generated {len(vectors)} vectors in {out_dir}/")

if __name__ == '__main__':
    main()

Step 2: Run it

python3 model/gen_verilator_vectors.py

Expected: Creates tb/vectors/llr_input.hex, tb/vectors/expected.hex.

Step 3: Write vector-driven testbench

Create tb/tb_ldpc_vectors.sv — a testbench that reads hex files and tests each vector against RTL. Structure matches tb_ldpc_decoder.sv but loops over all 20 vectors, compares decoded bits, convergence, and iteration count against the Python model reference.

Step 4: Run vector testbench

cd tb
make sim_vectors

Expected: All 20 vectors pass bit-exact match against Python model.

Step 5: Commit

git add model/gen_verilator_vectors.py tb/tb_ldpc_vectors.sv tb/vectors/ tb/Makefile
git commit -m "test: add vector-driven Verilator testbench with Python model cross-check"

Task 5: Write cocotb firmware for LDPC Wishbone test

Files:

  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_basic/ldpc_basic.c
  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_basic/ldpc_basic.py
  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_basic/ldpc_basic.yaml

The cocotb Caravel test pattern requires paired firmware (C) + monitor (Python).

Step 1: Write firmware C code

// ldpc_basic.c - PicoRV32 firmware for LDPC decoder Wishbone test
// Writes LLRs, starts decode, reads result, signals pass/fail on GPIO

#include <defs.h>
#include <stub.c>

// LDPC decoder registers (Caravel user project base = 0x30000000)
#define LDPC_BASE       0x30000000
#define LDPC_CTRL       (*(volatile uint32_t*)(LDPC_BASE + 0x00))
#define LDPC_STATUS     (*(volatile uint32_t*)(LDPC_BASE + 0x04))
#define LDPC_LLR(i)     (*(volatile uint32_t*)(LDPC_BASE + 0x10 + (i)*4))
#define LDPC_DECODED    (*(volatile uint32_t*)(LDPC_BASE + 0x50))
#define LDPC_VERSION    (*(volatile uint32_t*)(LDPC_BASE + 0x54))

void main()
{
    // Configure GPIO[7:0] as user output
    reg_mprj_io_7 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_6 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_5 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_4 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_3 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_2 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_1 = GPIO_MODE_USER_STD_OUTPUT;
    reg_mprj_io_0 = GPIO_MODE_USER_STD_OUTPUT;

    // Apply GPIO config
    reg_mprj_xfer = 1;
    while (reg_mprj_xfer == 1);

    // Signal test start via management GPIO
    reg_mprj_datal = 0x00000000;
    reg_gpio_out = 1;  // mgmt GPIO = 1 -> "firmware ready"

    // --- Test: Read version register ---
    uint32_t version = LDPC_VERSION;
    if (version != 0xLD010001) {
        reg_mprj_datal = 0x000000FF;  // all 1s = FAIL
        while(1);
    }

    // --- Test: Decode all-zero codeword ---
    // Write LLRs: all +31 (strong confidence in bit=0)
    // Pack: 5x 6'b011111 = 0x1F7DF7DF
    for (int i = 0; i < 52; i++) {
        LDPC_LLR(i) = 0x1F7DF7DF;
    }

    // Start decode: early_term=1, max_iter=30, start=1
    LDPC_CTRL = 0x00001E03;

    // Poll until not busy
    while (LDPC_STATUS & 0x1);

    uint32_t status = LDPC_STATUS;
    uint32_t decoded = LDPC_DECODED;

    // Check: converged, syndrome=0, decoded=0x00000000
    int pass = 1;
    if (!(status & 0x2)) pass = 0;       // not converged
    if ((status >> 16) & 0xFF) pass = 0;  // syndrome != 0
    if (decoded != 0x00000000) pass = 0;  // wrong decoded bits

    if (pass) {
        reg_mprj_datal = 0x000000AB;  // 0xAB = PASS signature
    } else {
        reg_mprj_datal = 0x000000FF;  // 0xFF = FAIL signature
    }
}

Step 2: Write cocotb monitor

# ldpc_basic.py - cocotb test monitoring LDPC decode via GPIO
from caravel_cocotb.caravel_interfaces import test_configure
from caravel_cocotb.caravel_interfaces import report_test
import cocotb

@cocotb.test()
@report_test
async def ldpc_basic(dut):
    caravelEnv = await test_configure(dut, timeout_cycles=100000)
    await caravelEnv.release_csb()

    # Wait for firmware to signal ready
    await caravelEnv.wait_mgmt_gpio(1)
    cocotb.log.info("[TEST] Firmware started, waiting for result...")

    # Wait for GPIO output to change (firmware writes result)
    for _ in range(50000):
        await cocotb.triggers.ClockCycles(caravelEnv.clk, 10)
        gpio_val = caravelEnv.monitor_gpio(7, 0).integer
        if gpio_val != 0:
            break

    cocotb.log.info(f"[TEST] GPIO[7:0] = 0x{gpio_val:02X}")

    if gpio_val == 0xAB:
        cocotb.log.info("[TEST] LDPC basic decode: PASS")
    else:
        cocotb.log.error(f"[TEST] LDPC basic decode: FAIL (GPIO=0x{gpio_val:02X})")

Step 3: Write test YAML

# ldpc_basic.yaml
Tests:
    - {name: ldpc_basic, sim: RTL}

Step 4: Register test in cocotb_tests.py

Add to chip_ignite/verilog/dv/cocotb/cocotb_tests.py:

from ldpc_tests.ldpc_basic.ldpc_basic import ldpc_basic

Step 5: Run cocotb test

cd chip_ignite
cf verify ldpc_basic

Expected: PASS with GPIO[7:0] = 0xAB.

Step 6: Commit

cd chip_ignite
git add verilog/dv/cocotb/ldpc_tests/ verilog/dv/cocotb/cocotb_tests.py
git commit -m "test: add cocotb LDPC basic decode test with firmware"

Task 6: Write additional cocotb tests

Files:

  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_noisy/ldpc_noisy.c
  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_noisy/ldpc_noisy.py
  • Create: chip_ignite/verilog/dv/cocotb/ldpc_tests/ldpc_noisy/ldpc_noisy.yaml

Same pattern as Task 5 but uses a noisy test vector from test_vectors.h. Firmware loads the first noisy vector, decodes, compares against expected. Reports pass/fail on GPIO.

Additional tests following same pattern:

  • ldpc_max_iter — uncorrectable vector, verify decoder hits max iterations and reports non-zero syndrome
  • ldpc_back_to_back — two decodes in sequence, verify no state leakage

Step 1-4: Write firmware + cocotb + YAML for each test (same pattern as Task 5)

Step 5: Run all tests

cd chip_ignite
cf verify ldpc_basic
cf verify ldpc_noisy
cf verify ldpc_max_iter
cf verify ldpc_back_to_back

Expected: All PASS.

Step 6: Commit

cd chip_ignite
git add verilog/dv/cocotb/ldpc_tests/
git commit -m "test: add noisy, max_iter, and back-to-back cocotb tests"

Week 2: OpenLane Hardening

Task 7: Create OpenLane configuration for LDPC decoder macro

Files:

  • Create: chip_ignite/openlane/ldpc_decoder_top/config.json
  • Create: chip_ignite/openlane/ldpc_decoder_top/pin_order.cfg
  • Create: chip_ignite/openlane/ldpc_decoder_top/base_ldpc.sdc

Step 1: Write config.json

{
    "DESIGN_NAME": "ldpc_decoder_top",
    "VERILOG_FILES": [
        "dir::../../verilog/rtl/defines.v",
        "dir::../../verilog/rtl/ldpc_decoder_top.sv",
        "dir::../../verilog/rtl/ldpc_decoder_core.sv",
        "dir::../../verilog/rtl/wishbone_interface.sv"
    ],
    "CLOCK_PERIOD": 20,
    "CLOCK_PORT": "clk",
    "CLOCK_NET": "u_core.clk",
    "DIE_AREA": [0, 0, 1400, 1200],
    "FP_PIN_ORDER_CFG": "dir::pin_order.cfg",
    "FP_SIZING": "absolute",
    "FP_PDN_MULTILAYER": false,
    "VDD_NETS": ["vccd1"],
    "GND_NETS": ["vssd1"],
    "FALLBACK_SDC_FILE": "dir::base_ldpc.sdc",
    "MAX_TRANSITION_CONSTRAINT": 1.5,
    "MAX_FANOUT_CONSTRAINT": 16,
    "SYNTH_STRATEGY": "AREA 2",
    "PL_TARGET_DENSITY_PCT": 40,
    "DIODE_INSERTION_STRATEGY": "heuristic",
    "RUN_LINTER": false,
    "pdk::sky130*": {
        "SYNTH_MAX_FANOUT": 10,
        "scl::sky130_fd_sc_hd": {
            "CLOCK_PERIOD": 20
        }
    }
}

Step 2: Write pin_order.cfg

#BUS_SORT
#S
clk
rst_n
wb_cyc_i
wb_stb_i
wb_we_i
wb_sel_i\[.*\]
wb_adr_i\[.*\]
wb_dat_i\[.*\]

#N
wb_dat_o\[.*\]
wb_ack_o
irq_o

Step 3: Write SDC constraints

# base_ldpc.sdc - Timing constraints for LDPC decoder
create_clock -name clk -period 20.0 [get_ports {clk}]
set_input_delay -clock clk -max 5.0 [all_inputs]
set_input_delay -clock clk -min 1.0 [all_inputs]
set_output_delay -clock clk -max 5.0 [all_outputs]
set_output_delay -clock clk -min 0.0 [all_outputs]

Step 4: Run hardening

cd chip_ignite
cf harden ldpc_decoder_top

Expected: Synthesis + P&R completes. Note WNS (worst negative slack) — should be positive at 50 MHz.

Step 5: Check results

# Check timing summary in OpenLane logs
grep -r "wns" chip_ignite/openlane/ldpc_decoder_top/runs/*/logs/
# Check area
grep -r "DIEAREA" chip_ignite/gds/ldpc_decoder_top.gds || true

Step 6: Commit

cd chip_ignite
git add openlane/ldpc_decoder_top/
git commit -m "feat: add OpenLane hardening config for LDPC decoder (50 MHz)"

Task 8: Update wrapper hardening config

Files:

  • Modify: chip_ignite/openlane/user_project_wrapper/config.json

Step 1: Update MACROS section

Replace user_proj_example references with ldpc_decoder_top:

  • Change macro name from user_proj_example to ldpc_decoder_top
  • Update GDS/LEF/netlist/SPEF paths
  • Adjust placement coordinates if needed (keep (60, 15) as starting point)

Step 2: Update VERILOG_FILES

Point to LDPC RTL instead of example.

Step 3: Run wrapper hardening

cd chip_ignite
cf harden user_project_wrapper

Step 4: Commit

cd chip_ignite
git add openlane/user_project_wrapper/config.json
git commit -m "feat: update wrapper config for LDPC decoder macro"

Week 3: Gate-Level Sim & Precheck

Task 9: Run gate-level simulation

Step 1: Update GL include file

Edit chip_ignite/verilog/includes/includes.gl.caravel_user_project:

-v $(USER_PROJECT_VERILOG)/gl/user_project_wrapper.v
-v $(USER_PROJECT_VERILOG)/gl/ldpc_decoder_top.v

Step 2: Run GLS for basic test

cd chip_ignite
cf verify ldpc_basic --sim gl

Expected: PASS (same as RTL sim but with gate-level netlist).

Step 3: Run all GLS tests

cf verify ldpc_noisy --sim gl
cf verify ldpc_max_iter --sim gl
cf verify ldpc_back_to_back --sim gl

Step 4: Commit

cd chip_ignite
git add verilog/includes/
git commit -m "test: gate-level simulation passing for all LDPC tests"

Task 10: Timing push to 75 MHz (stretch goal)

Step 1: Check current timing margin

If WNS > 5 ns at 50 MHz (20 ns period), there's room to push.

Step 2: Tighten clock to 75 MHz

Update openlane/ldpc_decoder_top/config.json:

"CLOCK_PERIOD": 13.3

Step 3: Re-run hardening

cf harden ldpc_decoder_top
cf harden user_project_wrapper

Step 4: Check timing

If WNS is negative, either:

  • Pipeline the critical path in ldpc_decoder_core.sv (CN update min-finding)
  • Or revert to 50 MHz (20 ns) and document the achieved frequency

Step 5: Commit

git commit -am "perf: tighten clock to 75 MHz (or document 50 MHz)"

Task 11: Run precheck

Step 1: Configure GPIO

cd chip_ignite
cf gpio-config

Set GPIO[0:7] as user standard output. Set remaining as management input (default).

Step 2: Run precheck

cf precheck

Expected: All checks pass (DRC, LVS, power, clock, GPIO, wrapper, manifest).

Step 3: Fix any issues and re-run until clean

Step 4: Commit

git commit -am "chore: precheck passing"

Week 4: Proposal Submission

Task 12: Add LICENSE and AI disclosure

Files:

  • Create: chip_ignite/LICENSE (Apache 2.0)
  • Create: chip_ignite/docs/ai-disclosure.md

Step 1: Add Apache 2.0 license

cd chip_ignite
# Copy standard Apache 2.0 license text
cat > LICENSE << 'HEREDOC'
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/
...
HEREDOC

Step 2: Write AI disclosure

# AI Disclosure

This project was developed with assistance from Claude (Anthropic), an AI assistant.

## Usage

Claude was used for:
- RTL design review and optimization
- Testbench generation
- Documentation writing
- Build system configuration
- Design space exploration (density evolution analysis, code rate comparison)

## Session Logs

Session transcripts are available upon request. Key AI-assisted contributions:
- `rtl/ldpc_decoder_core.sv` — algorithmic implementation reviewed and refined with AI
- `model/*.py` — analysis scripts co-developed with AI
- `docs/` — documentation drafted with AI assistance
- `openlane/` — configuration guided by AI

## Human Contributions

All architectural decisions, algorithm selection, code review, and final verification
were performed by the human designer. The AI served as a coding assistant and
technical reference.

Step 3: Commit

git add LICENSE docs/ai-disclosure.md
git commit -m "docs: add Apache 2.0 license and AI disclosure"

Task 13: Write proposal README

Files:

  • Modify: chip_ignite/README.md

Step 1: Write comprehensive README

Cover:

  • Project title and one-line description
  • Application: photon-starved free-space optical communication
  • Architecture diagram (ASCII)
  • Code parameters table
  • Performance specs (clock frequency, throughput, latency, area)
  • Verification status (test list, pass/fail)
  • Timing report summary (WNS/TNS)
  • Directory structure
  • How to build and run
  • Roadmap (Approaches B and C from design doc)
  • License and AI disclosure link

Step 2: Commit

git add README.md
git commit -m "docs: write proposal README for ChipFoundry contest"

Task 14: Submit proposal

Step 1: Push to GitHub

cd chip_ignite
git push origin main

Step 2: Submit repo URL via contest form

Go to https://chipfoundry.io/challenges/application/#submit_form and submit the GitHub repo URL.


Weeks 5-8: Post-Proposal (Final Submission)

Task 15: Write PicoRV32 demo firmware

Files:

  • Create: chip_ignite/firmware/ldpc_demo/ldpc_demo.c
  • Create: chip_ignite/firmware/ldpc_demo/Makefile
  • Depends on: chip_ignite/firmware/ldpc_demo/test_vectors.h from Task 2

Full demo firmware that runs 3 scenarios on boot (clean, noisy, stress test) and prints results over UART. Uses the Caravel BSP for UART and GPIO.

Task 16: Design PCBA in KiCad

Files:

  • Create: chip_ignite/pcba/ldpc_eval_board.kicad_pro
  • Create: chip_ignite/pcba/ldpc_eval_board.kicad_sch
  • Create: chip_ignite/pcba/ldpc_eval_board.kicad_pcb
  • Create: chip_ignite/pcba/BOM.csv

Schematic sections:

  1. Caravel QFN-64 with decoupling
  2. Power (LDO 3.3V + 1.8V, USB or barrel jack)
  3. 25 MHz crystal oscillator
  4. FTDI USB-UART bridge
  5. SPI flash (firmware storage)
  6. Status LEDs + reset button
  7. GMAPD connector (DNP, reference only)
  8. TIA + comparator section (DNP, reference only)

Task 17: Design mechanical enclosure

Files:

  • Create: chip_ignite/mechanical/enclosure.scad
  • Output: chip_ignite/mechanical/enclosure.stl

Parametric OpenSCAD box with PCB standoffs, USB/barrel cutouts.

Task 18: Record demo video

3-minute screen recording:

  1. Show architecture diagram and explain the application (30s)
  2. Run cocotb simulation showing decode pass (30s)
  3. Show OpenLane GDS layout viewer (30s)
  4. Walk through firmware UART output (30s)
  5. Show KiCad 3D board render (30s)
  6. Summarize specs and roadmap (30s)

Task 19: Create how-to screenshots

Step-by-step PNG screenshots:

  1. Clone repo and run cf setup
  2. Run cf harden ldpc_decoder_top
  3. Run cf verify ldpc_basic
  4. Run cf precheck
  5. Build and flash firmware

Task 20: Final submission

  • Verify all deliverables present
  • Run cf precheck one final time
  • Push to GitHub
  • Submit via contest form