From 1678f6b7de74d991c6ad1894b9e86c6bef2667c2 Mon Sep 17 00:00:00 2001 From: cah Date: Wed, 25 Feb 2026 22:28:46 -0700 Subject: [PATCH] feat: add LDPC demo firmware and cocotb test Demo firmware runs clean decode + noisy decode (vector 0) and reports pass/fail on GPIO[7:0]. All 5 cocotb tests pass: ldpc_basic, ldpc_noisy, ldpc_max_iter, ldpc_back_to_back, and ldpc_demo. Also adds .cf/project.json with GPIO configuration. Co-Authored-By: Claude Opus 4.6 --- .cf/project.json | 49 +++ firmware/ldpc_demo/ldpc_demo.c | 337 ++++++++++++++++++ verilog/dv/cocotb/cocotb_tests.py | 1 + .../cocotb/ldpc_tests/ldpc_demo/ldpc_demo.c | 116 ++++++ .../cocotb/ldpc_tests/ldpc_demo/ldpc_demo.py | 76 ++++ .../ldpc_tests/ldpc_demo/ldpc_demo.yaml | 19 + 6 files changed, 598 insertions(+) create mode 100644 .cf/project.json create mode 100644 firmware/ldpc_demo/ldpc_demo.c create mode 100644 verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.c create mode 100644 verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.py create mode 100644 verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.yaml diff --git a/.cf/project.json b/.cf/project.json new file mode 100644 index 0000000..25f4951 --- /dev/null +++ b/.cf/project.json @@ -0,0 +1,49 @@ +{ + "project_name": "ldpc_decoder_optical", + "description": "LDPC decoder for photon-starved optical communication", + "project_type": "digital", + "pdk": "sky130A", + "created": "2026-02-25", + "project": { + "gpio_config": { + "0": "13'h1809", + "1": "13'h1809", + "2": "13'h1809", + "3": "13'h1809", + "4": "13'h1809", + "5": "13'h1809", + "6": "13'h1809", + "7": "13'h1809", + "8": "13'h1809", + "9": "13'h1809", + "10": "13'h1809", + "11": "13'h1809", + "12": "13'h1809", + "13": "13'h1809", + "14": "13'h1809", + "15": "13'h1809", + "16": "13'h1809", + "17": "13'h1809", + "18": "13'h1809", + "19": "13'h1809", + "20": "13'h1809", + "21": "13'h1809", + "22": "13'h1809", + "23": "13'h1809", + "24": "13'h1809", + "25": "13'h1809", + "26": "13'h1809", + "27": "13'h1809", + "28": "13'h1809", + "29": "13'h1809", + "30": "13'h1809", + "31": "13'h1809", + "32": "13'h1809", + "33": "13'h1809", + "34": "13'h1809", + "35": "13'h1809", + "36": "13'h1809", + "37": "13'h1809" + } + } +} diff --git a/firmware/ldpc_demo/ldpc_demo.c b/firmware/ldpc_demo/ldpc_demo.c new file mode 100644 index 0000000..88af2de --- /dev/null +++ b/firmware/ldpc_demo/ldpc_demo.c @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2024 LDPC Optical Project +// SPDX-License-Identifier: Apache-2.0 + +/* + * LDPC Decoder Demo Firmware + * + * Runs on PicoRV32 inside Caravel. Demonstrates the LDPC decoder by running + * three scenarios and reporting results via UART and GPIO: + * + * Scenario 1: Clean all-zero codeword (should decode in 1 iteration) + * Scenario 2: Noisy but correctable codeword (test vector 0) + * Scenario 3: Stress test - all 20 test vectors back to back + * + * UART output format (115200 baud, 8N1): + * LDPC Decoder Demo v1.0 + * VERSION: 1D010001 + * --- Scenario 1: Clean decode --- + * LLR: all +31 (zero codeword) + * STATUS: 00001E02 DECODED: 00000000 + * PASS: converged in 1 iter, syndrome=0 + * --- Scenario 2: Noisy decode --- + * ... + * + * GPIO[7:0] final status: + * 0xAB = all scenarios passed + * 0xFF = at least one scenario failed + */ + +#include +#include "test_vectors.h" + +// LDPC register word offsets (byte_addr / 4) +#define LDPC_CTRL 0 // 0x00 +#define LDPC_STATUS 1 // 0x04 +#define LDPC_LLR_BASE 4 // 0x10 +#define LDPC_DECODED 20 // 0x50 +#define LDPC_VERSION 21 // 0x54 + +// CTRL register fields +#define CTRL_START (1 << 0) +#define CTRL_EARLY_TERM (1 << 1) +#define CTRL_MAX_ITER(n) (((n) & 0x1F) << 8) + +// STATUS register fields +#define STATUS_BUSY (1 << 0) +#define STATUS_CONVERGED (1 << 1) +#define STATUS_ITER_SHIFT 8 +#define STATUS_ITER_MASK (0x1F << STATUS_ITER_SHIFT) +#define STATUS_SYN_SHIFT 16 +#define STATUS_SYN_MASK (0xFF << STATUS_SYN_SHIFT) + +#define EXPECTED_VERSION 0x1D010001 +#define PASS_SIGNATURE 0xAB +#define FAIL_SIGNATURE 0xFF + +// All-zero codeword LLR word (5x +31) +#define ALL_ZERO_LLR_WORD 0x1F7DF7DF + +// Simple hex print (8 chars, uppercase) +static void print_hex(unsigned int val) { + static const char hex[] = "0123456789ABCDEF"; + for (int i = 28; i >= 0; i -= 4) { + UART_sendChar(hex[(val >> i) & 0xF]); + } +} + +static void print_dec(unsigned int val) { + if (val == 0) { + UART_sendChar('0'); + return; + } + char buf[10]; + int n = 0; + while (val > 0) { + buf[n++] = '0' + (val % 10); + val /= 10; + } + for (int i = n - 1; i >= 0; i--) { + UART_sendChar(buf[i]); + } +} + +static void println(const char *s) { + while (*s) UART_sendChar(*s++); + UART_sendChar('\r'); + UART_sendChar('\n'); +} + +static void print_str(const char *s) { + while (*s) UART_sendChar(*s++); +} + +// Write LLR words to decoder and start decode +static unsigned int run_decode(const unsigned int *llr_words, int count) { + // Write LLRs + for (int i = 0; i < count; i++) { + USER_writeWord(llr_words[i], LDPC_LLR_BASE + i); + } + + // Start: early_term=1, max_iter=30, start=1 + USER_writeWord(CTRL_START | CTRL_EARLY_TERM | CTRL_MAX_ITER(30), LDPC_CTRL); + + // Poll until not busy + unsigned int status; + do { + status = USER_readWord(LDPC_STATUS); + } while (status & STATUS_BUSY); + + return status; +} + +void main() { + int total_pass = 1; + int scenario_pass; + unsigned int status, decoded, version; + + // --- Hardware setup --- + ManagmentGpio_outputEnable(); + ManagmentGpio_write(0); + enableHkSpi(0); + + // GPIO[7:0] as management output, GPIO[5:6] for UART + GPIOs_configure(5, GPIO_MODE_MGMT_STD_OUTPUT); // UART TX + GPIOs_configure(6, GPIO_MODE_MGMT_STD_INPUT_NOPULL); // UART RX + for (int i = 0; i < 8; i++) { + if (i != 5 && i != 6) + GPIOs_configure(i, GPIO_MODE_MGMT_STD_OUTPUT); + } + GPIOs_loadConfigs(); + GPIOs_writeLow(0x00000000); + + // Enable UART (115200 baud) and Wishbone + UART_enableTX(1); + User_enableIF(); + + // Signal ready + ManagmentGpio_write(1); + + // --- Banner --- + println("LDPC Decoder Demo v1.0"); + println("Rate 1/8, n=256, k=32, Z=32"); + println("Offset min-sum, layered scheduling"); + println(""); + + // --- Version check --- + version = USER_readWord(LDPC_VERSION); + print_str("VERSION: "); + print_hex(version); + println(""); + if (version != EXPECTED_VERSION) { + println("ERROR: unexpected version"); + total_pass = 0; + } + + // ============================================================ + // Scenario 1: Clean all-zero codeword + // ============================================================ + println("--- Scenario 1: Clean decode ---"); + println("Input: all-zero codeword, LLR=+31"); + + // Build LLR words on stack (all +31) + unsigned int clean_llr[LLR_WORDS_PER_VECTOR]; + for (int i = 0; i < LLR_WORDS_PER_VECTOR - 1; i++) { + clean_llr[i] = ALL_ZERO_LLR_WORD; + } + clean_llr[LLR_WORDS_PER_VECTOR - 1] = 0x0000001F; // last word: 1 LLR + + status = run_decode(clean_llr, LLR_WORDS_PER_VECTOR); + decoded = USER_readWord(LDPC_DECODED); + + print_str("STATUS: "); + print_hex(status); + print_str(" DECODED: "); + print_hex(decoded); + println(""); + + scenario_pass = 1; + if (!(status & STATUS_CONVERGED)) { + println("FAIL: did not converge"); + scenario_pass = 0; + } + if ((status & STATUS_SYN_MASK) != 0) { + println("FAIL: nonzero syndrome"); + scenario_pass = 0; + } + if (decoded != 0x00000000) { + println("FAIL: wrong decoded bits"); + scenario_pass = 0; + } + if (scenario_pass) { + unsigned int iters = (status & STATUS_ITER_MASK) >> STATUS_ITER_SHIFT; + print_str("PASS: converged in "); + print_dec(iters); + println(" iterations"); + } else { + total_pass = 0; + } + println(""); + + // ============================================================ + // Scenario 2: Noisy but correctable codeword (vector 0) + // ============================================================ + println("--- Scenario 2: Noisy decode ---"); + print_str("Expected decoded: "); + print_hex(tv0_decoded); + println(""); + + status = run_decode(tv0_llr, LLR_WORDS_PER_VECTOR); + decoded = USER_readWord(LDPC_DECODED); + + print_str("STATUS: "); + print_hex(status); + print_str(" DECODED: "); + print_hex(decoded); + println(""); + + scenario_pass = 1; + if (!(status & STATUS_CONVERGED)) { + println("FAIL: did not converge"); + scenario_pass = 0; + } + if (decoded != tv0_decoded) { + println("FAIL: decoded mismatch"); + scenario_pass = 0; + } + if (scenario_pass) { + unsigned int iters = (status & STATUS_ITER_MASK) >> STATUS_ITER_SHIFT; + print_str("PASS: corrected in "); + print_dec(iters); + println(" iterations"); + } else { + total_pass = 0; + } + println(""); + + // ============================================================ + // Scenario 3: Stress test - all 20 vectors + // ============================================================ + println("--- Scenario 3: Stress test (20 vectors) ---"); + + // Pointers to all test vector LLR arrays + const unsigned int * const tv_llr[NUM_TEST_VECTORS] = { + tv0_llr, tv1_llr, tv2_llr, tv3_llr, tv4_llr, + tv5_llr, tv6_llr, tv7_llr, tv8_llr, tv9_llr, + tv10_llr, tv11_llr, tv12_llr, tv13_llr, tv14_llr, + tv15_llr, tv16_llr, tv17_llr, tv18_llr, tv19_llr + }; + const unsigned int tv_decoded[NUM_TEST_VECTORS] = { + tv0_decoded, tv1_decoded, tv2_decoded, tv3_decoded, tv4_decoded, + tv5_decoded, tv6_decoded, tv7_decoded, tv8_decoded, tv9_decoded, + tv10_decoded, tv11_decoded, tv12_decoded, tv13_decoded, tv14_decoded, + tv15_decoded, tv16_decoded, tv17_decoded, tv18_decoded, tv19_decoded + }; + const int tv_converged[NUM_TEST_VECTORS] = { + tv0_converged, tv1_converged, tv2_converged, tv3_converged, tv4_converged, + tv5_converged, tv6_converged, tv7_converged, tv8_converged, tv9_converged, + tv10_converged, tv11_converged, tv12_converged, tv13_converged, tv14_converged, + tv15_converged, tv16_converged, tv17_converged, tv18_converged, tv19_converged + }; + + int pass_count = 0; + int fail_count = 0; + + for (int v = 0; v < NUM_TEST_VECTORS; v++) { + status = run_decode(tv_llr[v], LLR_WORDS_PER_VECTOR); + decoded = USER_readWord(LDPC_DECODED); + + unsigned int iters = (status & STATUS_ITER_MASK) >> STATUS_ITER_SHIFT; + int converged = (status & STATUS_CONVERGED) ? 1 : 0; + + print_str("V"); + print_dec(v); + print_str(": "); + + // For converged vectors, check decoded matches expected + if (tv_converged[v]) { + if (converged && decoded == tv_decoded[v]) { + print_str("PASS "); + print_dec(iters); + println(" iters"); + pass_count++; + } else { + print_str("FAIL got="); + print_hex(decoded); + print_str(" exp="); + print_hex(tv_decoded[v]); + println(""); + fail_count++; + total_pass = 0; + } + } else { + // Unconverged vector: just check it didn't falsely converge + // (or if it did converge with correct result, that's also OK) + if (!converged) { + print_str("OK uncorrectable "); + print_dec(iters); + println(" iters"); + pass_count++; + } else if (decoded == tv_decoded[v]) { + print_str("OK converged "); + print_dec(iters); + println(" iters"); + pass_count++; + } else { + print_str("FAIL false-converge got="); + print_hex(decoded); + println(""); + fail_count++; + total_pass = 0; + } + } + } + + print_str("Results: "); + print_dec(pass_count); + print_str("/"); + print_dec(pass_count + fail_count); + println(" passed"); + println(""); + + // ============================================================ + // Final result + // ============================================================ + if (total_pass) { + println("=== ALL SCENARIOS PASSED ==="); + GPIOs_writeLow(PASS_SIGNATURE); + } else { + println("=== SOME SCENARIOS FAILED ==="); + GPIOs_writeLow(FAIL_SIGNATURE); + } + + // Signal test complete + ManagmentGpio_write(0); + + // Halt + while (1); +} diff --git a/verilog/dv/cocotb/cocotb_tests.py b/verilog/dv/cocotb/cocotb_tests.py index afcf5d6..e5336c9 100644 --- a/verilog/dv/cocotb/cocotb_tests.py +++ b/verilog/dv/cocotb/cocotb_tests.py @@ -9,3 +9,4 @@ from ldpc_tests.ldpc_basic.ldpc_basic import ldpc_basic from ldpc_tests.ldpc_noisy.ldpc_noisy import ldpc_noisy from ldpc_tests.ldpc_max_iter.ldpc_max_iter import ldpc_max_iter from ldpc_tests.ldpc_back_to_back.ldpc_back_to_back import ldpc_back_to_back +from ldpc_tests.ldpc_demo.ldpc_demo import ldpc_demo diff --git a/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.c b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.c new file mode 100644 index 0000000..4bebaa7 --- /dev/null +++ b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.c @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2024 LDPC Optical Project +// SPDX-License-Identifier: Apache-2.0 + +// Symlink/copy to the actual demo firmware +// This file just includes the demo firmware from its canonical location +// so that caravel_cocotb can find it in the test directory. + +// Note: caravel_cocotb expects the C file in the test directory. +// We keep the actual firmware in firmware/ldpc_demo/ for standalone builds. +// For cocotb, we duplicate the essential parts here. + +#include + +// Pull in test vectors (these are in firmware/ldpc_demo/) +// For cocotb, we need the vectors accessible from include path +// The test vector data is embedded directly via test_data definitions + +// LDPC register word offsets (byte_addr / 4) +#define LDPC_CTRL 0 // 0x00 +#define LDPC_STATUS 1 // 0x04 +#define LDPC_LLR_BASE 4 // 0x10 +#define LDPC_DECODED 20 // 0x50 +#define LDPC_VERSION 21 // 0x54 + +#define CTRL_START (1 << 0) +#define CTRL_EARLY_TERM (1 << 1) +#define CTRL_MAX_ITER(n) (((n) & 0x1F) << 8) + +#define STATUS_BUSY (1 << 0) +#define STATUS_CONVERGED (1 << 1) +#define STATUS_ITER_SHIFT 8 +#define STATUS_ITER_MASK (0x1F << STATUS_ITER_SHIFT) + +#define EXPECTED_VERSION 0x1D010001 +#define PASS_SIGNATURE 0xAB +#define FAIL_SIGNATURE 0xFF + +#define ALL_ZERO_LLR_WORD 0x1F7DF7DF +#define LLR_WORD_COUNT 52 + +// Write LLRs and start decode, return STATUS +static unsigned int run_decode(const unsigned int *llr, int count) { + for (int i = 0; i < count; i++) + USER_writeWord(llr[i], LDPC_LLR_BASE + i); + USER_writeWord(CTRL_START | CTRL_EARLY_TERM | CTRL_MAX_ITER(30), LDPC_CTRL); + unsigned int st; + do { st = USER_readWord(LDPC_STATUS); } while (st & STATUS_BUSY); + return st; +} + +void main() { + int pass = 1; + unsigned int status, decoded, version; + + // Setup + ManagmentGpio_outputEnable(); + ManagmentGpio_write(0); + enableHkSpi(0); + + GPIOs_configureAll(GPIO_MODE_MGMT_STD_OUTPUT); + GPIOs_loadConfigs(); + GPIOs_writeLow(0x00000000); + User_enableIF(); + ManagmentGpio_write(1); + + // Check version + version = USER_readWord(LDPC_VERSION); + if (version != EXPECTED_VERSION) pass = 0; + + // Scenario 1: clean all-zero codeword + { + unsigned int llr[LLR_WORD_COUNT]; + for (int i = 0; i < LLR_WORD_COUNT - 1; i++) llr[i] = ALL_ZERO_LLR_WORD; + llr[LLR_WORD_COUNT - 1] = 0x0000001F; + + status = run_decode(llr, LLR_WORD_COUNT); + decoded = USER_readWord(LDPC_DECODED); + + if (!(status & STATUS_CONVERGED)) pass = 0; + if (decoded != 0x00000000) pass = 0; + } + + // Scenario 2: noisy decode (vector 0 from test_data.py) + // Inline test vector 0 LLR words + { + static const unsigned int tv0_llr[52] = { + 0x1F7DF81F, 0x20C9F7E0, 0x207CC7DF, 0x1F82081F, + 0x328207E0, 0x20820820, 0x208207CC, 0x1F81F7DF, + 0x1F7F27DF, 0x1F7CC81F, 0x0C81F81F, 0x207E07F2, + 0x1F820820, 0x207DF7CC, 0x1F81F7E0, 0x2082081F, + 0x0C31F81F, 0x2081F7DF, 0x1FCA081F, 0x20820820, + 0x1F7DF7DF, 0x207E07E0, 0x208207CC, 0x1F8207DF, + 0x0C7DF7DF, 0x2030C820, 0x207DF7E0, 0x1F82081F, + 0x203207DF, 0x20832820, 0x2081F820, 0x20820832, + 0x1F82081F, 0x207E081F, 0x207DF820, 0x1F7E0320, + 0x1F7E07E0, 0x1F81F820, 0x20CA07CC, 0x0C81F7E0, + 0x1F820820, 0x1FCA07DF, 0x1F7E080C, 0x208207F2, + 0x207E081F, 0x20820820, 0x207E07DF, 0x2082081F, + 0x1F7E07DF, 0x1F7DF7E0, 0x207DF820, 0x00000020 + }; + status = run_decode(tv0_llr, 52); + decoded = USER_readWord(LDPC_DECODED); + + if (!(status & STATUS_CONVERGED)) pass = 0; + if (decoded != 0x3FD74222) pass = 0; + } + + // Report result + if (pass) { + GPIOs_writeLow(PASS_SIGNATURE); + } else { + GPIOs_writeLow(FAIL_SIGNATURE); + } + ManagmentGpio_write(0); + while (1); +} diff --git a/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.py b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.py new file mode 100644 index 0000000..107ba77 --- /dev/null +++ b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2024 LDPC Optical Project + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# SPDX-License-Identifier: Apache-2.0 + +""" +LDPC Demo Firmware Test - cocotb Monitor + +Runs the demo firmware which exercises: + 1. Clean all-zero codeword decode + 2. Noisy correctable codeword decode (test vector 0) + +Monitors GPIO[7:0] for pass (0xAB) / fail (0xFF) via management GPIO. +""" + +from caravel_cocotb.caravel_interfaces import test_configure +from caravel_cocotb.caravel_interfaces import report_test +import cocotb + +PASS_SIGNATURE = 0xAB +FAIL_SIGNATURE = 0xFF + + +@cocotb.test() +@report_test +async def ldpc_demo(dut): + caravelEnv = await test_configure(dut, timeout_cycles=500000) + + cocotb.log.info("[TEST] Starting LDPC demo firmware test") + + await caravelEnv.release_csb() + + # Wait for firmware ready + cocotb.log.info("[TEST] Waiting for firmware ready (mgmt_gpio=1)...") + await caravelEnv.wait_mgmt_gpio(1) + cocotb.log.info("[TEST] Firmware ready, running demo scenarios...") + + # Wait for test complete + await caravelEnv.wait_mgmt_gpio(0) + cocotb.log.info("[TEST] Firmware signaled test complete") + + # Read result + gpio_val = caravelEnv.monitor_gpio(7, 0) + + if not gpio_val.is_resolvable: + cocotb.log.error( + f"[TEST] FAIL - GPIO[7:0] is unresolvable: {gpio_val.binstr}" + ) + assert False, "GPIO[7:0] has X/Z values" + + result = gpio_val.integer + cocotb.log.info(f"[TEST] GPIO[7:0] = 0x{result:02X}") + + if result == PASS_SIGNATURE: + cocotb.log.info("[TEST] PASS - Demo firmware all scenarios passed (0xAB)") + elif result == FAIL_SIGNATURE: + cocotb.log.error( + "[TEST] FAIL - Demo firmware reported failure (0xFF)" + ) + assert False, "Demo firmware reported failure" + else: + cocotb.log.error( + f"[TEST] FAIL - Unexpected GPIO value: 0x{result:02X}" + ) + assert False, f"Unexpected GPIO result: 0x{result:02X}" diff --git a/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.yaml b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.yaml new file mode 100644 index 0000000..8e3d793 --- /dev/null +++ b/verilog/dv/cocotb/ldpc_tests/ldpc_demo/ldpc_demo.yaml @@ -0,0 +1,19 @@ +--- +# SPDX-FileCopyrightText: 2024 LDPC Optical Project + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# SPDX-License-Identifier: Apache-2.0 + +Tests: + - {name: ldpc_demo, sim: RTL}