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>
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 fromrtl/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.jsonfrom 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 syndromeldpc_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_exampletoldpc_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.hfrom 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:
- Caravel QFN-64 with decoupling
- Power (LDO 3.3V + 1.8V, USB or barrel jack)
- 25 MHz crystal oscillator
- FTDI USB-UART bridge
- SPI flash (firmware storage)
- Status LEDs + reset button
- GMAPD connector (DNP, reference only)
- 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:
- Show architecture diagram and explain the application (30s)
- Run cocotb simulation showing decode pass (30s)
- Show OpenLane GDS layout viewer (30s)
- Walk through firmware UART output (30s)
- Show KiCad 3D board render (30s)
- Summarize specs and roadmap (30s)
Task 19: Create how-to screenshots
Step-by-step PNG screenshots:
- Clone repo and run
cf setup - Run
cf harden ldpc_decoder_top - Run
cf verify ldpc_basic - Run
cf precheck - Build and flash firmware
Task 20: Final submission
- Verify all deliverables present
- Run
cf precheckone final time - Push to GitHub
- Submit via contest form