Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mac: Only use one CRC engine for Checker #183

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 117 additions & 4 deletions liteeth/mac/crc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
114 changes: 114 additions & 0 deletions test/test_crc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#
# This file is part of LiteEth.
#
# Copyright (c) 2025 David Sawatzke <d-git@sawatzke.dev>
# 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)
12 changes: 1 addition & 11 deletions test/test_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 11 additions & 0 deletions test/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand Down