From f479a851553310d350b44f2f1a79dfc38aa57256 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Wed, 5 Feb 2025 15:29:12 -0800 Subject: [PATCH] lfsr: Add LFSR descrambler module and testbench Signed-off-by: Alex Forencich --- rtl/lfsr/taxi_lfsr_descramble.sv | 185 +++++++++++++++++ tb/lfsr/taxi_lfsr_descramble/Makefile | 51 +++++ .../test_taxi_lfsr_descramble.py | 187 ++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 rtl/lfsr/taxi_lfsr_descramble.sv create mode 100644 tb/lfsr/taxi_lfsr_descramble/Makefile create mode 100644 tb/lfsr/taxi_lfsr_descramble/test_taxi_lfsr_descramble.py diff --git a/rtl/lfsr/taxi_lfsr_descramble.sv b/rtl/lfsr/taxi_lfsr_descramble.sv new file mode 100644 index 0000000..49a50f7 --- /dev/null +++ b/rtl/lfsr/taxi_lfsr_descramble.sv @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2016-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * LFSR descrambler + */ +module taxi_lfsr_descramble # +( + // width of LFSR + parameter LFSR_W = 58, + // LFSR polynomial + parameter logic [LFSR_W-1:0] LFSR_POLY = 58'h8000000001, + // Initial state + parameter logic [LFSR_W-1:0] LFSR_INIT = '1, + // LFSR configuration: 0 for Fibonacci (PRBS), 1 for Galois (CRC) + parameter logic LFSR_GALOIS = 1'b0, + // bit-reverse input and output + parameter logic REVERSE = 1'b1, + // width of data bus + parameter DATA_W = 64 +) +( + input wire logic clk, + input wire logic rst, + + input wire logic [DATA_W-1:0] data_in, + input wire logic data_in_valid, + output wire logic [DATA_W-1:0] data_out +); + +/* + +Fully parametrizable combinatorial parallel LFSR CRC module. Implements an unrolled LFSR +next state computation. + +Ports: + +clk + +Clock input + +rst + +Reset module, set state to LFSR_INIT + +data_in + +Scrambled data input (DATA_W bits) + +data_in_valid + +Shift input data through CRC when asserted + +data_out + +Descrambled data output (DATA_W bits) + +Parameters: + +LFSR_W + +Specify width of LFSR/CRC register + +LFSR_POLY + +Specify the LFSR/CRC polynomial in hex format. For example, the polynomial + +x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1 + +would be represented as + +32'h04c11db7 + +Note that the largest term (x^32) is suppressed. This term is generated automatically based +on LFSR_W. + +LFSR_INIT + +Initial state of LFSR. Defaults to all 1s. + +LFSR_GALOIS + +Specify the LFSR configuration, either Fibonacci (0) or Galois (1). Fibonacci is generally used +for linear-feedback shift registers (LFSR) for pseudorandom binary sequence (PRBS) generators, +scramblers, and descrambers, while Galois is generally used for cyclic redundancy check +generators and checkers. + +Fibonacci style (example for 64b66b scrambler, 0x8000000001) + + DIN (LSB first) + | + V + (+)<---------------------------(+)<-----------------------------. + | ^ | + | .----. .----. .----. | .----. .----. .----. | + +->| 0 |->| 1 |->...->| 38 |-+->| 39 |->...->| 56 |->| 57 |--' + | '----' '----' '----' '----' '----' '----' + V + DOUT + +Galois style (example for CRC16, 0x8005) + + ,-------------------+-------------------------+----------(+)<-- DIN (MSB first) + | | | ^ + | .----. .----. V .----. .----. V .----. | + `->| 0 |->| 1 |->(+)->| 2 |->...->| 14 |->(+)->| 15 |--+---> DOUT + '----' '----' '----' '----' '----' + +REVERSE + +Bit-reverse LFSR input and output. + +DATA_W + +Specify width of the data bus. The module will perform one shift per input data bit. + +Settings for common LFSR/CRC implementations: + +Name Configuration Length Polynomial Initial value Notes +CRC32 Galois, bit-reverse 32 32'h04c11db7 32'hffffffff Ethernet FCS; invert final output +PRBS6 Fibonacci 6 6'h21 any +PRBS7 Fibonacci 7 7'h41 any +PRBS9 Fibonacci 9 9'h021 any ITU V.52 +PRBS10 Fibonacci 10 10'h081 any ITU +PRBS11 Fibonacci 11 11'h201 any ITU O.152 +PRBS15 Fibonacci, inverted 15 15'h4001 any ITU O.152 +PRBS17 Fibonacci 17 17'h04001 any +PRBS20 Fibonacci 20 20'h00009 any ITU V.57 +PRBS23 Fibonacci, inverted 23 23'h040001 any ITU O.151 +PRBS29 Fibonacci, inverted 29 29'h08000001 any +PRBS31 Fibonacci, inverted 31 31'h10000001 any +64b66b Fibonacci, bit-reverse 58 58'h8000000001 any 10G Ethernet +128b130b Galois, bit-reverse 23 23'h210125 any PCIe gen 3 + +*/ + +logic [LFSR_W-1:0] state_reg = LFSR_INIT; +logic [DATA_W-1:0] output_reg = '0; + +wire [DATA_W-1:0] lfsr_data; +wire [LFSR_W-1:0] lfsr_state; + +assign data_out = output_reg; + +taxi_lfsr #( + .LFSR_W(LFSR_W), + .LFSR_POLY(LFSR_POLY), + .LFSR_GALOIS(LFSR_GALOIS), + .LFSR_FEED_FORWARD('1), + .REVERSE(REVERSE), + .DATA_W(DATA_W) +) +lfsr_inst ( + .data_in(data_in), + .state_in(state_reg), + .data_out(lfsr_data), + .state_out(lfsr_state) +); + +always_ff @(posedge clk) begin + if (data_in_valid) begin + state_reg <= lfsr_state; + output_reg <= lfsr_data; + end + + if (rst) begin + state_reg <= LFSR_INIT; + output_reg <= '0; + end +end + +endmodule + +`resetall diff --git a/tb/lfsr/taxi_lfsr_descramble/Makefile b/tb/lfsr/taxi_lfsr_descramble/Makefile new file mode 100644 index 0000000..9eb95a7 --- /dev/null +++ b/tb/lfsr/taxi_lfsr_descramble/Makefile @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2023-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_lfsr_descramble +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = $(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += ../../../rtl/lfsr/$(DUT).sv +VERILOG_SOURCES += ../../../rtl/lfsr/taxi_lfsr.sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_LFSR_W ?= 58 +export PARAM_LFSR_POLY ?= "58'h8000000001" +export PARAM_LFSR_INIT ?= "'1" +export PARAM_LFSR_GALOIS ?= "1'b0" +export PARAM_REVERSE ?= "1'b1" +export PARAM_DATA_W ?= 8 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/lfsr/taxi_lfsr_descramble/test_taxi_lfsr_descramble.py b/tb/lfsr/taxi_lfsr_descramble/test_taxi_lfsr_descramble.py new file mode 100644 index 0000000..fc0b434 --- /dev/null +++ b/tb/lfsr/taxi_lfsr_descramble/test_taxi_lfsr_descramble.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + + dut.data_in.setimmediatevalue(0) + dut.data_in_valid.setimmediatevalue(0) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +def chunks(lst, n, padvalue=None): + return itertools.zip_longest(*[iter(lst)]*n, fillvalue=padvalue) + + +def scramble_64b66b(data, state=0x3ffffffffffffff): + data_out = bytearray() + for d in data: + b = 0 + for i in range(8): + if bool(state & (1 << 38)) ^ bool(state & (1 << 57)) ^ bool(d & (1 << i)): + state = ((state & 0x1ffffffffffffff) << 1) | 1 + b = b | (1 << i) + else: + state = (state & 0x1ffffffffffffff) << 1 + data_out.append(b) + return data_out + + +def descramble_64b66b(data, state=0x3ffffffffffffff): + data_out = bytearray() + for d in data: + b = 0 + for i in range(8): + if bool(state & (1 << 38)) ^ bool(state & (1 << 57)) ^ bool(d & (1 << i)): + b = b | (1 << i) + state = (state & 0x1ffffffffffffff) << 1 | bool(d & (1 << i)) + data_out += bytearray([b]) + return data_out + + +async def run_test_descramble(dut, ref_scramble): + + data_width = len(dut.data_in) + byte_lanes = data_width // 8 + + tb = TB(dut) + + await tb.reset() + + block = bytearray(itertools.islice(itertools.cycle(range(256)), 1024)) + + scr = scramble_64b66b(block) + + dscr = descramble_64b66b(scr) + + assert dscr == block + + ref_iter = iter(chunks(block, byte_lanes)) + + first = True + for b in chunks(scr, byte_lanes): + dut.data_in.value = int.from_bytes(b, 'little') + dut.data_in_valid.value = 1 + await RisingEdge(dut.clk) + + val = dut.data_out.value.integer + + if not first: + ref = int.from_bytes(bytes(next(ref_iter)), 'little') + + tb.log.info("Descrambled: 0x%x (ref: 0x%x)", val, ref) + + assert ref == val + + first = False + + dut.data_in_valid.value = 0 + + await RisingEdge(dut.clk) + + +if cocotb.SIM_NAME: + + # if cocotb.top.LFSR_POLY.value == 0x8000000001: + if int(cocotb.top.LFSR_W.value) == 58: + factory = TestFactory(run_test_descramble) + factory.add_option("ref_scramble", [scramble_64b66b]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +@pytest.mark.parametrize(("lfsr_w", "lfsr_poly", "lfsr_init", "lfsr_galois", "reverse", "data_w"), [ + (58, "58'h8000000001", "'1", 0, 1, 8), + (58, "58'h8000000001", "'1", 0, 1, 64), + ]) +def test_taxi_lfsr_descramble(request, lfsr_w, lfsr_poly, lfsr_init, lfsr_galois, reverse, data_w): + dut = "taxi_lfsr_descramble" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, "lfsr", f"{dut}.sv"), + os.path.join(rtl_dir, "lfsr", "taxi_lfsr.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['LFSR_W'] = lfsr_w + parameters['LFSR_POLY'] = lfsr_poly + parameters['LFSR_INIT'] = lfsr_init + parameters['LFSR_GALOIS'] = f"1'b{lfsr_galois}" + parameters['REVERSE'] = f"1'b{reverse}" + parameters['DATA_W'] = data_w + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )