diff --git a/liteeth/mac/crc.py b/liteeth/mac/crc.py index ef3a1be..b825a3a 100644 --- a/liteeth/mac/crc.py +++ b/liteeth/mac/crc.py @@ -26,8 +26,10 @@ class LiteEthMACCRCEngine(LiteXModule): Parameters ---------- + data_width : int + The bit width of the data bus width : int - The bit width of the data bus and CRC value. + The bit width of CRC value. polynom : int The polynomial used for the CRC calculation, specified as an integer (e.g., 0x04C11DB7 for IEEE 802.3). """ @@ -67,6 +69,53 @@ def optimize_xors(bits): from collections import Counter return [bit for bit, count in Counter(bits).items() if count % 2 == 1] +def crc_calc(data_width, width, polynom, crc_prev, data): + """ + Calculate the next CRC value. Functionally equivalent to the migen CRCEngine, but as a python function + + Parameters + ---------- + data_width : int + The bit width of data + width : int + The bit width of the CRC value + polynom : int + The polynomial used for the CRC calculation, specified as an integer (e.g., 0x04C11DB7 for IEEE 802.3). + crc_prev : int + The previous CRC value + data : int + The new data word + + Returns + ------- + int + The next CRC value + """ + # Convert crc_prev into a list of bits (LSB first) for easier bitwise operations + state = [(crc_prev >> i) & 1 for i in range(width)] + + # Process each bit of the input data (assumed LSB-first). + for n in range(data_width): + d = (data >> n) & 1 + feedback = state[-1] ^ d + state.pop() + + # For each remaining bit position (positions 0 .. width-2), + # if the corresponding tap (at bit position pos+1 in the polynomial) + # is active, XOR the feedback into that bit. + for pos in range(width - 1): + if (polynom >> (pos + 1)) & 1: + state[pos] ^= feedback + # Insert the feedback at the beginning of the state (this is equivalent + # to shifting the register and feeding in the new bit). + state.insert(0, feedback) + + crc_next = 0 + for i, bit in enumerate(state): + if bit: + crc_next |= (1 << i) + return crc_next + # MAC CRC32 ---------------------------------------------------------------------------------------- @ResetInserter() @@ -131,6 +180,70 @@ def __init__(self, data_width): ) ] +# MAC CRC32 ---------------------------------------------------------------------------------------- + +@ResetInserter() +@CEInserter() +class LiteEthMACCRC32Check(LiteXModule): + """IEEE 802.3 CRC + + Implement an IEEE 802.3 CRC checker. + + Parameters + ---------- + data_width : int + Width of the data bus. + + Attributes + ---------- + data : in + Data input. + be : in + Data byte enable (optional, defaults to full word). + error : out + CRC error (used for checker). + """ + width = 32 + polynom = 0x04c11db7 + init = 2**width - 1 + check = 0xc704dd7b + def __init__(self, data_width): + self.data = Signal(data_width) + self.be = Signal(data_width//8, reset=2**data_width//8 - 1) + self.value = Signal(self.width) + self.error = Signal() + + check_be = [self.check] + for _ in range(1, data_width//8): + check_be.append(crc_calc(8, self.width, self.polynom, check_be[-1], 0)) + + # # # + + # Create a CRC Engine for the data_width + self.submodules.engine = engine = LiteEthMACCRCEngine( + data_width = data_width, + width = self.width, + polynom = self.polynom, + ) + + # Register Full-Word CRC Engine (last one). + reg = Signal(self.width, reset=self.init) + self.sync += reg.eq(engine.crc_next) + + # Select CRC Engine/Result. + self.comb += [ + # TODO mask data + engine.data.eq(self.data), + engine.crc_prev.eq(reg), + ] + for n in range(data_width//8): + self.comb += [ + If(self.be[n], + engine.data.eq(self.data & (2**((n + 1)*8) - 1)), + self.error.eq(engine.crc_next != check_be[-(n + 1)]), + ) + ] + # MAC CRC32 Inserter ------------------------------------------------------------------------------- class LiteEthMACCRC32Inserter(LiteXModule): @@ -159,7 +272,7 @@ def __init__(self, description): # Parameters. data_width = len(sink.data) ratio = 32//data_width - assert data_width in [8, 16, 32, 64] + assert data_width in [8, 32, 64] # Signals. crc_packet = Signal(32, reset_less=True) @@ -279,10 +392,10 @@ def __init__(self, description): # Parameters. data_width = len(sink.data) ratio = ceil(32/data_width) - assert data_width in [8, 16, 32, 64] + assert data_width in [8, 32, 64] # CRC32 Checker. - self.crc = crc = LiteEthMACCRC32(data_width) + self.crc = crc = LiteEthMACCRC32Check(data_width) # FIFO. self.fifo = fifo = ResetInserter()(stream.SyncFIFO(description, ratio + 1)) diff --git a/test/test_crc.py b/test/test_crc.py new file mode 100644 index 0000000..d8b57c5 --- /dev/null +++ b/test/test_crc.py @@ -0,0 +1,114 @@ +# +# This file is part of LiteEth. +# +# Copyright (c) 2025 David Sawatzke +# SPDX-License-Identifier: BSD-2-Clause + +import unittest +import random + +from migen import * + +from liteeth.common import * +from liteeth.mac.crc import * + +from litex.gen.sim import * + +from .test_stream import ( + mask_last_be, + StreamPacket, + stream_inserter, + stream_collector, + compare_packets, +) + + +def get_stream_desc(dw): + return [ + ("data", dw), + ("last_be", dw // 8), + ("error", dw // 8), + ] + + +class DUT(Module): + def __init__(self, dw): + self.submodules.inserter = LiteEthMACCRC32Inserter(eth_phy_description(dw)) + self.submodules.checker = LiteEthMACCRC32Checker(eth_phy_description(dw)) + self.comb += self.inserter.source.connect(self.checker.sink) + + +# ----------------------------------------------------------------------------- +# Simulation Test +# ----------------------------------------------------------------------------- + + +class TestCRC(unittest.TestCase): + def crc_inserter_checker_test(self, dw=32, seed=42, npackets=2, debug_print=False): + prng = random.Random(seed + 5) + + dut = DUT(dw) + desc = get_stream_desc(dw) + full_last_be = (1 << (dw // 8)) - 1 + + packets = [] + + for n in range(npackets): + header = {} + datas = [prng.randrange(2**8) for _ in range(prng.randrange(dw - 1) + 1)] + packets.append(StreamPacket(datas, header)) + + recvd_packets = [] + run_simulation( + dut, + [ + stream_inserter( + dut.inserter.sink, + src=packets, + seed=seed, + debug_print=debug_print, + valid_rand=50, + ), + stream_collector( + dut.checker.source, + dest=recvd_packets, + expect_npackets=npackets, + seed=seed, + debug_print=debug_print, + ready_rand=50, + ), + ], + vcd_name="crc_test_{}bit_seed{}.vcd".format(dw, seed), + ) + + if not compare_packets(packets, recvd_packets): + + print("crc_test_{}bit_seed{}".format(dw, seed)) + print(len(packets)) + for i in range(len(packets)): + print(i) + print(packets[i].data) + print(recvd_packets[i].data) + assert False + + def test_8bit_loopback(self): + for seed in range(42, 48): + with self.subTest(seed=seed): + self.crc_inserter_checker_test(dw=8, seed=seed) + + def test_32bit_loopback(self): + for seed in range(42, 48): + with self.subTest(seed=seed): + self.crc_inserter_checker_test(dw=32, seed=seed) + + # TODO the 64 bit case has a few issues unrelated to LiteEthMACCRC32Check + # def test_64bit_loopback(self): + # for seed in range(42, 70): + # with self.subTest(seed=seed): + # self.crc_inserter_checker_test(dw=64, seed=seed) + + # 16 bit is completely broken + # def test_16bit_loopback(self): + # for seed in range(42, 70): + # with self.subTest(seed=seed): + # self.crc_inserter_checker_test(dw=16, seed=seed) diff --git a/test/test_packet.py b/test/test_packet.py index 24a6ca6..4c49be4 100644 --- a/test/test_packet.py +++ b/test/test_packet.py @@ -12,17 +12,7 @@ from litex.soc.interconnect.stream import * from liteeth.packet import * -from .test_stream import StreamPacket, stream_inserter, stream_collector, compare_packets - -def mask_last_be(dw, data, last_be): - masked_data = 0 - - for byte in range(dw // 8): - if 2**byte > last_be: - break - masked_data |= data & (0xFF << (byte * 8)) - - return masked_data +from .test_stream import StreamPacket, stream_inserter, stream_collector, compare_packets, mask_last_be class TestPacket(unittest.TestCase): def loopback_test(self, dw, seed=42, with_last_be=False, debug_print=False): diff --git a/test/test_stream.py b/test/test_stream.py index a1a2f51..4071513 100644 --- a/test/test_stream.py +++ b/test/test_stream.py @@ -22,6 +22,16 @@ def grouper(iterable, n, fillvalue=None): args = [iter(iterable)] * n return itertools.zip_longest(*args, fillvalue=fillvalue) +def mask_last_be(dw, data, last_be): + masked_data = 0 + + for byte in range(dw // 8): + if 2**byte > last_be: + break + masked_data |= data & (0xFF << (byte * 8)) + + return masked_data + class StreamPacket: def __init__(self, data, params={}): # Data must be a list of bytes @@ -251,6 +261,7 @@ def stream_collector( if (yield source.last) == 1: read_last = True if hasattr(source, "last_be") and \ + dw != 8 and \ 2**byte > (yield source.last_be): break collected_bytes += [((data >> (byte * 8)) & 0xFF)]