diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py index 2b11871..f4f11eb 100644 --- a/cocotbext/eth/__init__.py +++ b/cocotbext/eth/__init__.py @@ -28,5 +28,6 @@ from .gmii import GmiiFrame, GmiiSource, GmiiSink, GmiiPhy from .mii import MiiSource, MiiSink, MiiPhy from .rgmii import RgmiiSource, RgmiiSink, RgmiiPhy from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink +from .eth_mac import EthMacFrame, EthMacTx, EthMacRx, EthMac from .ptp import PtpClock, PtpClockSimTime diff --git a/cocotbext/eth/eth_mac.py b/cocotbext/eth/eth_mac.py new file mode 100644 index 0000000..c6b912c --- /dev/null +++ b/cocotbext/eth/eth_mac.py @@ -0,0 +1,533 @@ +""" + +Copyright (c) 2021 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import logging +import struct +import zlib + +import cocotb +from cocotb.queue import Queue, QueueFull +from cocotb.triggers import RisingEdge, Timer, First, Event +from cocotb.utils import get_sim_time + +from cocotbext.axi.stream import define_stream + +from .version import __version__ +from .reset import Reset + +AxiStreamBus, AxiStreamTransaction, AxiStreamSource, AxiStreamSink, AxiStreamMonitor = define_stream("AxiStream", + signals=["tvalid", "tdata", "tkeep", "tlast", "tuser"], + optional_signals=["tready"] +) + + +class EthMacFrame: + def __init__(self, data=None, tx_complete=None): + self.data = bytearray() + self.sim_time_start = None + self.sim_time_sfd = None + self.sim_time_end = None + self.ptp_timestamp = None + self.tx_complete = None + + if type(data) is EthMacFrame: + self.data = bytearray(data.data) + self.sim_time_start = data.sim_time_start + self.sim_time_sfd = data.sim_time_sfd + self.sim_time_end = data.sim_time_end + self.ptp_timestamp = data.ptp_timestamp + self.tx_complete = data.tx_complete + else: + self.data = bytearray(data) + + if tx_complete is not None: + self.tx_complete = tx_complete + + @classmethod + def from_payload(cls, payload, min_len=60, tx_complete=None): + payload = bytearray(payload) + if len(payload) < min_len: + payload.extend(bytearray(min_len-len(payload))) + payload.extend(struct.pack('> offset) & 1: + frame.data.append((cycle.tdata.integer >> (offset * self.byte_size)) & self.byte_mask) + byte_count += 1 + + # wait for serialization time + await Timer(self.time_scale*byte_count*8//self.speed, 'step') + + if cycle.tlast.integer: + frame.sim_time_end = get_sim_time() + self.log.info("RX frame: %s", frame) + + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + await self.queue.put(frame) + self.active_event.set() + + frame = None + + break + + # get next cycle + # TODO improve underflow handling + assert not self.stream.empty(), "underflow" + cycle = await self.stream.recv() + + # wait for IFG + await Timer(self.time_scale*self.ifg*8//self.speed, 'step') + + async def _run_ts(self): + while True: + await RisingEdge(self.clock) + self.ptp_ts_valid <= 0 + + if not self.ts_queue.empty(): + ts = self.ts_queue.get_nowait() + self.ptp_ts <= ts + self.ptp_ts_valid <= 1 + + +class EthMacRx(Reset): + def __init__(self, bus, clock, reset=None, ptp_time=None, + reset_active_level=True, ifg=12, speed=1000e6, *args, **kwargs): + + self.bus = bus + self.clock = clock + self.reset = reset + self.ptp_time = ptp_time + self.ifg = ifg + self.speed = speed + self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}") + + self.log.info("Ethernet MAC RX model") + self.log.info("cocotbext-eth version %s", __version__) + self.log.info("Copyright (c) 2020 Alex Forencich") + self.log.info("https://github.com/alexforencich/cocotbext-eth") + + super().__init__(*args, **kwargs) + + self.stream = AxiStreamSource(bus, clock, reset, reset_active_level=reset_active_level) + self.stream.queue_occupancy_limit = 4 + + self.active = False + self.queue = Queue() + self.dequeue_event = Event() + self.current_frame = None + self.idle_event = Event() + self.idle_event.set() + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.queue_occupancy_limit_bytes = -1 + self.queue_occupancy_limit_frames = -1 + + self.time_scale = cocotb.utils.get_sim_steps(1, 'sec') + + self.width = len(self.bus.tdata) + self.byte_lanes = 1 + + if hasattr(self.bus, "tkeep"): + self.byte_lanes = len(self.bus.tkeep) + + self.byte_size = self.width // self.byte_lanes + self.byte_mask = 2**self.byte_size-1 + + self.log.info("Ethernet MAC RX model configuration") + self.log.info(" Byte size: %d bits", self.byte_size) + self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) + if hasattr(self.bus, "tkeep"): + self.log.info(" tkeep width: %d bits", len(self.bus.tkeep)) + else: + self.log.info(" tkeep: not present") + if hasattr(self.bus, "tuser"): + self.log.info(" tuser width: %d bits", len(self.bus.tuser)) + else: + self.log.info(" tuser: not present") + + if self.byte_size != 8: + raise ValueError("Byte size must be 8") + + if self.byte_lanes * self.byte_size != self.width: + raise ValueError(f"Bus does not evenly divide into byte lanes " + f"({self.byte_lanes} * {self.byte_size} != {self.width})") + + self._run_cr = None + + self._init_reset(reset, reset_active_level) + + async def send(self, frame): + while self.full(): + self.dequeue_event.clear() + await self.dequeue_event.wait() + frame = EthMacFrame(frame) + await self.queue.put(frame) + self.idle_event.clear() + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + def send_nowait(self, frame): + if self.full(): + raise QueueFull() + frame = EthMacFrame(frame) + self.queue.put_nowait(frame) + self.idle_event.clear() + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + def count(self): + return self.queue.qsize() + + def empty(self): + return self.queue.empty() + + def full(self): + if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes: + return True + elif self.queue_occupancy_limit_frames > 0 and self.queue_occupancy_frames > self.queue_occupancy_limit_frames: + return True + else: + return False + + def idle(self): + return self.empty() and not self.active + + def clear(self): + while not self.queue.empty(): + frame = self.queue.get_nowait() + frame.sim_time_end = None + frame.handle_tx_complete() + self.dequeue_event.set() + self.idle_event.set() + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + async def wait(self): + await self.idle_event.wait() + + def _handle_reset(self, state): + if state: + self.log.info("Reset asserted") + if self._run_cr is not None: + self._run_cr.kill() + self._run_cr = None + + self.active = False + + if self.current_frame: + self.log.warning("Flushed transmit frame during reset: %s", self.current_frame) + self.current_frame.handle_tx_complete() + self.current_frame = None + + if self.queue.empty(): + self.idle_event.set() + else: + self.log.info("Reset de-asserted") + if self._run_cr is None: + self._run_cr = cocotb.fork(self._run()) + + async def _run(self): + frame = None + tuser = 0 + self.active = False + + while True: + # wait for data + frame = await self.queue.get() + tuser = 0 + self.dequeue_event.set() + self.queue_occupancy_bytes -= len(frame) + self.queue_occupancy_frames -= 1 + self.current_frame = frame + frame.sim_time_start = get_sim_time() + frame.sim_time_sfd = None + frame.sim_time_end = None + self.log.info("TX frame: %s", frame) + + # wait for preamble time + await Timer(self.time_scale*8*8//self.speed, 'step') + + frame.sim_time_sfd = get_sim_time() + + if self.ptp_time: + frame.ptp_timestamp = self.ptp_time.value.integer + tuser |= frame.ptp_timestamp << 1 + + # process frame data + while frame is not None: + byte_count = 0 + + cycle = AxiStreamTransaction() + + cycle.tdata = 0 + cycle.tkeep = 0 + cycle.tlast = 0 + cycle.tuser = tuser + + for offset in range(self.byte_lanes): + cycle.tdata |= (frame.data.pop(0) & self.byte_mask) << (offset * self.byte_size) + cycle.tkeep |= 1 << offset + byte_count += 1 + + if len(frame.data) == 0: + cycle.tlast = 1 + frame.sim_time_end = get_sim_time() + frame.handle_tx_complete() + frame = None + self.current_frame = None + break + + await self.stream.send(cycle) + + # wait for serialization time + await Timer(self.time_scale*byte_count*8//self.speed, 'step') + + # wait for IFG + await Timer(self.time_scale*self.ifg*8//self.speed, 'step') + + +class EthMac: + def __init__(self, tx_bus=None, tx_clk=None, tx_rst=None, tx_ptp_time=None, tx_ptp_ts=None, tx_ptp_ts_valid=None, + rx_bus=None, rx_clk=None, rx_rst=None, rx_ptp_time=None, + reset_active_level=True, ifg=12, speed=1000e6, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.tx = EthMacTx(tx_bus, tx_clk, tx_rst, tx_ptp_time, tx_ptp_ts, tx_ptp_ts_valid, + reset_active_level=reset_active_level, ifg=ifg, speed=speed) + self.rx = EthMacRx(rx_bus, rx_clk, rx_rst, rx_ptp_time, + reset_active_level=reset_active_level, ifg=ifg, speed=speed) diff --git a/tests/eth_mac/Makefile b/tests/eth_mac/Makefile new file mode 100644 index 0000000..8002e5a --- /dev/null +++ b/tests/eth_mac/Makefile @@ -0,0 +1,61 @@ +# Copyright (c) 2021 Alex Forencich +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +TOPLEVEL_LANG = verilog + +SIM ?= icarus +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ns + +DUT = test_eth_mac +TOPLEVEL = $(DUT) +MODULE = $(DUT) +VERILOG_SOURCES += $(DUT).v + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim + +iverilog_dump.v: + echo 'module iverilog_dump();' > $@ + echo 'initial begin' >> $@ + echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@ + echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@ + echo 'end' >> $@ + echo 'endmodule' >> $@ + +clean:: + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst diff --git a/tests/eth_mac/test_eth_mac.py b/tests/eth_mac/test_eth_mac.py new file mode 100644 index 0000000..c1e8cdd --- /dev/null +++ b/tests/eth_mac/test_eth_mac.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2021 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import itertools +import logging +import os + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.eth import EthMacFrame, EthMac, PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink + + +class TB: + def __init__(self, dut, speed=10e9): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.fork(Clock(dut.tx_clk, 6.4, units="ns").start()) + cocotb.fork(Clock(dut.rx_clk, 6.4, units="ns").start()) + + self.mac = EthMac( + tx_clk=dut.tx_clk, + tx_rst=dut.tx_rst, + tx_bus=AxiStreamBus.from_prefix(dut, "tx_axis"), + tx_ptp_time=dut.tx_ptp_time, + tx_ptp_ts=dut.tx_ptp_ts, + tx_ptp_ts_valid=dut.tx_ptp_ts_valid, + rx_clk=dut.rx_clk, + rx_rst=dut.rx_rst, + rx_bus=AxiStreamBus.from_prefix(dut, "rx_axis"), + rx_ptp_time=dut.rx_ptp_time, + ifg=12, speed=speed + ) + + self.tx_ptp = PtpClockSimTime( + ts_96=dut.tx_ptp_time, + clock=dut.tx_clk + ) + + self.rx_ptp = PtpClockSimTime( + ts_96=dut.rx_ptp_time, + clock=dut.rx_clk + ) + + self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "tx_axis"), dut.tx_clk, dut.tx_rst) + self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "rx_axis"), dut.rx_clk, dut.rx_rst) + + async def reset(self): + self.dut.tx_rst.setimmediatevalue(0) + self.dut.rx_rst.setimmediatevalue(0) + await RisingEdge(self.dut.tx_clk) + await RisingEdge(self.dut.tx_clk) + self.dut.tx_rst <= 1 + self.dut.rx_rst <= 1 + await RisingEdge(self.dut.tx_clk) + await RisingEdge(self.dut.tx_clk) + self.dut.tx_rst <= 0 + self.dut.rx_rst <= 0 + await RisingEdge(self.dut.tx_clk) + await RisingEdge(self.dut.tx_clk) + + +async def run_test_tx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=10e9): + + tb = TB(dut, speed) + + tb.mac.tx.ifg = ifg + tb.mac.rx.ifg = ifg + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + + for test_data in test_frames: + test_frame = EthMacFrame.from_payload(test_data) + await tb.source.send(test_frame) + + for test_data in test_frames: + rx_frame = await tb.mac.tx.recv() + + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + + assert tb.mac.tx.empty() + + await RisingEdge(dut.tx_clk) + await RisingEdge(dut.tx_clk) + + +async def run_test_rx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=10e9): + + tb = TB(dut, speed) + + tb.mac.tx.ifg = ifg + tb.mac.rx.ifg = ifg + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + + for test_data in test_frames: + test_frame = EthMacFrame.from_payload(test_data) + await tb.mac.rx.send(test_frame) + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + + check_frame = EthMacFrame(rx_frame.tdata) + + assert check_frame.get_payload() == test_data + assert check_frame.check_fcs() + + assert tb.sink.empty() + + await RisingEdge(dut.rx_clk) + await RisingEdge(dut.rx_clk) + + +def size_list(): + return list(range(60, 128)) + [512, 1514, 9214] + [60]*10 + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + for test in [run_test_tx, run_test_rx]: + + factory = TestFactory(test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("speed", [10e9, 1e9]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) + + +def test_eth_mac(request): + dut = "test_eth_mac" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(tests_dir, f"{dut}.v"), + ] + + parameters = {} + + 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( + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tests/eth_mac/test_eth_mac.v b/tests/eth_mac/test_eth_mac.v new file mode 100644 index 0000000..b15a559 --- /dev/null +++ b/tests/eth_mac/test_eth_mac.v @@ -0,0 +1,56 @@ +/* + +Copyright (c) 2021 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`timescale 1ns / 1ps + +/* + * Ethernet MAC model test + */ +module test_eth_mac +( + inout wire tx_clk, + inout wire tx_rst, + inout wire [63:0] tx_axis_tdata, + inout wire [7:0] tx_axis_tkeep, + inout wire tx_axis_tlast, + inout wire tx_axis_tuser, + inout wire tx_axis_tvalid, + inout wire tx_axis_tready, + inout wire [95:0] tx_ptp_time, + inout wire [95:0] tx_ptp_ts, + inout wire tx_ptp_ts_valid, + + inout wire rx_clk, + inout wire rx_rst, + inout wire [63:0] rx_axis_tdata, + inout wire [7:0] rx_axis_tkeep, + inout wire rx_axis_tlast, + inout wire [96:0] rx_axis_tuser, + inout wire rx_axis_tvalid, + inout wire [95:0] rx_ptp_time +); + +endmodule