diff --git a/rtl/lfsr/taxi_lfsr.sv b/rtl/lfsr/taxi_lfsr.sv new file mode 100644 index 0000000..5542510 --- /dev/null +++ b/rtl/lfsr/taxi_lfsr.sv @@ -0,0 +1,313 @@ +// 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 + +/* + * Parametrizable combinatorial parallel LFSR/CRC + */ +module taxi_lfsr # +( + // width of LFSR + parameter LFSR_W = 31, + // LFSR polynomial + parameter logic [LFSR_W-1:0] LFSR_POLY = 31'h10000001, + // LFSR configuration: 0 for Fibonacci (PRBS), 1 for Galois (CRC) + parameter logic LFSR_GALOIS = 1'b0, + // LFSR feed forward enable + parameter logic LFSR_FEED_FORWARD = 1'b0, + // bit-reverse input and output + parameter logic REVERSE = 1'b0, + // width of data input + parameter DATA_W = 8 +) +( + input wire logic [DATA_W-1:0] data_in, + input wire logic [LFSR_W-1:0] state_in, + output wire logic [DATA_W-1:0] data_out, + output wire logic [LFSR_W-1:0] state_out +); + +/* + +Fully parametrizable combinatorial parallel LFSR/CRC module. Implements an unrolled LFSR +next state computation, shifting DATA_W bits per pass through the module. Input data +is XORed with LFSR feedback path, tie data_in to zero if this is not required. + +Works in two parts: statically computes a set of bit masks, then uses these bit masks to +select bits for XORing to compute the next state. + +Ports: + +data_in + +Data bits to be shifted through the LFSR (DATA_W bits) + +state_in + +LFSR/CRC current state input (LFSR_W bits) + +data_out + +Data bits shifted out of LFSR (DATA_W bits) + +state_out + +LFSR/CRC next state output (LFSR_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_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 + '----' '----' '----' '----' '----' + +LFSR_FEED_FORWARD + +Generate feed forward instead of feed back LFSR. Enable this for PRBS checking and self- +synchronous descrambling. + +Fibonacci feed-forward style (example for 64b66b descrambler, 0x8000000001) + + DIN (LSB first) + | + | .----. .----. .----. .----. .----. .----. + +->| 0 |->| 1 |->...->| 38 |-+->| 39 |->...->| 56 |->| 57 |--. + | '----' '----' '----' | '----' '----' '----' | + | V | + (+)<---------------------------(+)------------------------------' + | + V + DOUT + +Galois feed-forward style + + ,-------------------+-------------------------+------------+--- DIN (MSB first) + | | | | + | .----. .----. V .----. .----. V .----. V + `->| 0 |->| 1 |->(+)->| 2 |->...->| 14 |->(+)->| 15 |->(+)-> DOUT + '----' '----' '----' '----' '----' + +REVERSE + +Bit-reverse LFSR input and output. Shifts MSB first by default, set REVERSE for LSB first. + +DATA_W + +Specify width of input and output data bus. The module will perform one shift per input +data bit, so if the input data bus is not required tie data_in to zero and set DATA_W +to the required number of shifts per clock cycle. + +Settings for common LFSR/CRC implementations: + +Name Configuration Length Polynomial Initial value Notes +CRC16-IBM Galois, bit-reverse 16 16'h8005 16'hffff +CRC16-CCITT Galois 16 16'h1021 16'h1d0f +CRC32 Galois, bit-reverse 32 32'h04c11db7 32'hffffffff Ethernet FCS; invert final output +CRC32C Galois, bit-reverse 32 32'h1edc6f41 32'hffffffff iSCSI, Intel CRC32 instruction; 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 + +*/ + +function [LFSR_W+DATA_W-1:0][LFSR_W+DATA_W-1:0] lfsr_mask(); + logic [LFSR_W-1:0] lfsr_mask_state[LFSR_W-1:0]; + logic [DATA_W-1:0] lfsr_mask_data[LFSR_W-1:0]; + logic [LFSR_W-1:0] output_mask_state[DATA_W-1:0]; + logic [DATA_W-1:0] output_mask_data[DATA_W-1:0]; + + logic [LFSR_W-1:0] state_val; + logic [DATA_W-1:0] data_val; + + logic [DATA_W-1:0] data_mask; + + // init bit masks + for (integer i = 0; i < LFSR_W; i = i + 1) begin + lfsr_mask_state[i] = '0; + lfsr_mask_state[i][i] = 1'b1; + lfsr_mask_data[i] = '0; + end + for (integer i = 0; i < DATA_W; i = i + 1) begin + output_mask_state[i] = '0; + if (i < LFSR_W) begin + output_mask_state[i][i] = 1'b1; + end + output_mask_data[i] = '0; + end + + // simulate shift register + if (LFSR_GALOIS) begin + // Galois configuration + for (data_mask = {1'b1, {DATA_W-1{1'b0}}}; data_mask != 0; data_mask = data_mask >> 1) begin + // determine shift in value + // current value in last FF, XOR with input data bit (MSB first) + state_val = lfsr_mask_state[LFSR_W-1]; + data_val = lfsr_mask_data[LFSR_W-1]; + data_val = data_val ^ data_mask; + + // shift + for (integer j = LFSR_W-1; j > 0; j = j - 1) begin + lfsr_mask_state[j] = lfsr_mask_state[j-1]; + lfsr_mask_data[j] = lfsr_mask_data[j-1]; + end + for (integer j = DATA_W-1; j > 0; j = j - 1) begin + output_mask_state[j] = output_mask_state[j-1]; + output_mask_data[j] = output_mask_data[j-1]; + end + output_mask_state[0] = state_val; + output_mask_data[0] = data_val; + if (LFSR_FEED_FORWARD) begin + // only shift in new input data + state_val = '0; + data_val = data_mask; + end + lfsr_mask_state[0] = state_val; + lfsr_mask_data[0] = data_val; + + // add XOR inputs at correct indicies + for (integer j = 1; j < LFSR_W; j = j + 1) begin + if (LFSR_POLY[j]) begin + lfsr_mask_state[j] = lfsr_mask_state[j] ^ state_val; + lfsr_mask_data[j] = lfsr_mask_data[j] ^ data_val; + end + end + end + end else begin + // Fibonacci configuration + for (data_mask = {1'b1, {DATA_W-1{1'b0}}}; data_mask != 0; data_mask = data_mask >> 1) begin + // determine shift in value + // current value in last FF, XOR with input data bit (MSB first) + state_val = lfsr_mask_state[LFSR_W-1]; + data_val = lfsr_mask_data[LFSR_W-1]; + data_val = data_val ^ data_mask; + + // add XOR inputs from correct indicies + for (integer j = 1; j < LFSR_W; j = j + 1) begin + if (LFSR_POLY[j]) begin + state_val = lfsr_mask_state[j-1] ^ state_val; + data_val = lfsr_mask_data[j-1] ^ data_val; + end + end + + // shift + for (integer j = LFSR_W-1; j > 0; j = j - 1) begin + lfsr_mask_state[j] = lfsr_mask_state[j-1]; + lfsr_mask_data[j] = lfsr_mask_data[j-1]; + end + for (integer j = DATA_W-1; j > 0; j = j - 1) begin + output_mask_state[j] = output_mask_state[j-1]; + output_mask_data[j] = output_mask_data[j-1]; + end + output_mask_state[0] = state_val; + output_mask_data[0] = data_val; + if (LFSR_FEED_FORWARD) begin + // only shift in new input data + state_val = '0; + data_val = data_mask; + end + lfsr_mask_state[0] = state_val; + lfsr_mask_data[0] = data_val; + end + end + + if (REVERSE) begin + // output reversed + for (integer i = 0; i < LFSR_W; i = i + 1) begin + for (integer j = 0; j < LFSR_W; j = j + 1) begin + lfsr_mask[i][j] = lfsr_mask_state[LFSR_W-i-1][LFSR_W-j-1]; + end + for (integer j = 0; j < DATA_W; j = j + 1) begin + lfsr_mask[i][j+LFSR_W] = lfsr_mask_data[LFSR_W-i-1][DATA_W-j-1]; + end + end + for (integer i = 0; i < DATA_W; i = i + 1) begin + for (integer j = 0; j < LFSR_W; j = j + 1) begin + lfsr_mask[i+LFSR_W][j] = output_mask_state[DATA_W-i-1][LFSR_W-j-1]; + end + for (integer j = 0; j < DATA_W; j = j + 1) begin + lfsr_mask[i+LFSR_W][j+LFSR_W] = output_mask_data[DATA_W-i-1][DATA_W-j-1]; + end + end + end else begin + // output normal + for (integer i = 0; i < LFSR_W; i = i + 1) begin + lfsr_mask[i] = {lfsr_mask_data[i], lfsr_mask_state[i]}; + end + for (integer i = 0; i < DATA_W; i = i + 1) begin + lfsr_mask[i+LFSR_W] = {output_mask_data[i], output_mask_state[i]}; + end + end +endfunction + +wire [LFSR_W+DATA_W-1:0][LFSR_W+DATA_W-1:0] mask = lfsr_mask(); + +for (genvar n = 0; n < LFSR_W; n = n + 1) begin : lfsr_state + assign state_out[n] = ^({data_in, state_in} & mask[n]); +end +for (genvar n = 0; n < DATA_W; n = n + 1) begin : lfsr_data + assign data_out[n] = ^({data_in, state_in} & mask[n+LFSR_W]); +end + +endmodule + +`resetall diff --git a/tb/lfsr/taxi_lfsr/Makefile b/tb/lfsr/taxi_lfsr/Makefile new file mode 100644 index 0000000..e621060 --- /dev/null +++ b/tb/lfsr/taxi_lfsr/Makefile @@ -0,0 +1,50 @@ +# 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 +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = $(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += ../../../rtl/lfsr/$(DUT).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 ?= 32 +export PARAM_LFSR_POLY ?= "32'h4c11db7" +export PARAM_LFSR_GALOIS ?= "1'b1" +export PARAM_LFSR_FEED_FORWARD ?= "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/test_taxi_lfsr.py b/tb/lfsr/taxi_lfsr/test_taxi_lfsr.py new file mode 100644 index 0000000..d8052ce --- /dev/null +++ b/tb/lfsr/taxi_lfsr/test_taxi_lfsr.py @@ -0,0 +1,238 @@ +#!/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 zlib + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.triggers import Timer +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) + + dut.data_in.setimmediatevalue(0) + dut.state_in.setimmediatevalue(0) + + +def chunks(lst, n, padvalue=None): + return itertools.zip_longest(*[iter(lst)]*n, fillvalue=padvalue) + + +def crc32(data): + return zlib.crc32(data) & 0xffffffff + + +def crc32c(data, crc=0xffffffff, poly=0x82f63b78): + for d in data: + crc = crc ^ d + for bit in range(0, 8): + if crc & 1: + crc = (crc >> 1) ^ poly + else: + crc = crc >> 1 + return ~crc & 0xffffffff + + +async def run_test_crc(dut, ref_crc): + + data_width = len(dut.data_in) + byte_lanes = data_width // 8 + + state_width = len(dut.state_in) + state_mask = 2**state_width-1 + + tb = TB(dut) + + await Timer(10, 'ns') + + block = bytes([(x+1)*0x11 for x in range(byte_lanes)]) + + dut.state_in.value = state_mask + dut.data_in.value = int.from_bytes(block, 'little') + await Timer(10, 'ns') + + val = ~dut.state_out.value.integer & state_mask + ref = ref_crc(block) + + tb.log.info("CRC: 0x%x (ref: 0x%x)", val, ref) + + assert val == ref + + await Timer(10, 'ns') + + block = bytearray(itertools.islice(itertools.cycle(range(256)), 1024)) + + dut.state_in.value = state_mask + for b in chunks(block, byte_lanes): + dut.data_in.value = int.from_bytes(b, 'little') + await Timer(10, 'ns') + dut.state_in.value = dut.state_out.value + + val = ~int(dut.state_out.value) & state_mask + ref = ref_crc(block) + + tb.log.info("CRC: 0x%x (ref: 0x%x)", val, ref) + + assert val == ref + + await Timer(10, 'ns') + + +def prbs9(state=0x1ff): + while True: + for i in range(8): + if bool(state & 0x10) ^ bool(state & 0x100): + state = ((state & 0xff) << 1) | 1 + else: + state = (state & 0xff) << 1 + yield ~state & 0xff + + +def prbs31(state=0x7fffffff): + while True: + for i in range(8): + if bool(state & 0x08000000) ^ bool(state & 0x40000000): + state = ((state & 0x3fffffff) << 1) | 1 + else: + state = (state & 0x3fffffff) << 1 + yield ~state & 0xff + + +async def run_test_prbs(dut, ref_prbs): + + data_width = len(dut.data_in) + byte_lanes = data_width // 8 + data_mask = 2**data_width-1 + + state_width = len(dut.state_in) + state_mask = 2**state_width-1 + + tb = TB(dut) + + await Timer(10, 'ns') + + dut.state_in.value = state_mask + dut.data_in.value = 0 + gen = chunks(ref_prbs(), byte_lanes) + + await Timer(10, 'ns') + + for i in range(512): + ref = int.from_bytes(bytes(next(gen)), 'big') + val = ~int(dut.data_out.value) & data_mask + + tb.log.info("PRBS: 0x%x (ref: 0x%x)", val, ref) + + assert ref == val + + dut.state_in.value = dut.state_out.value + + await Timer(10, 'ns') + + +if cocotb.SIM_NAME: + + if cocotb.top.LFSR_POLY.value == 0x4c11db7: + factory = TestFactory(run_test_crc) + factory.add_option("ref_crc", [crc32]) + factory.generate_tests() + + if cocotb.top.LFSR_POLY.value == 0x1edc6f41: + factory = TestFactory(run_test_crc) + factory.add_option("ref_crc", [crc32c]) + factory.generate_tests() + + if cocotb.top.LFSR_POLY.value == 0x021: + factory = TestFactory(run_test_prbs) + factory.add_option("ref_prbs", [prbs9]) + factory.generate_tests() + + if cocotb.top.LFSR_POLY.value == 0x10000001: + factory = TestFactory(run_test_prbs) + factory.add_option("ref_prbs", [prbs31]) + 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_galois", "reverse", "data_w"), [ + (32, "32'h4c11db7", 1, 1, 8), + (32, "32'h4c11db7", 1, 1, 64), + (32, "32'h1edc6f41", 1, 1, 8), + (32, "32'h1edc6f41", 1, 1, 64), + (9, "9'h021", 0, 0, 8), + (9, "9'h021", 0, 0, 64), + (31, "31'h10000001", 0, 0, 8), + (31, "31'h10000001", 0, 0, 64), + ]) +def test_taxi_lfsr(request, lfsr_w, lfsr_poly, lfsr_galois, reverse, data_w): + dut = "taxi_lfsr" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, "lfsr", f"{dut}.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['LFSR_W'] = lfsr_w + parameters['LFSR_POLY'] = lfsr_poly + parameters['LFSR_GALOIS'] = f"1'b{lfsr_galois}" + parameters['LFSR_FEED_FORWARD'] = "1'b0" + 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, + )