From e858721dcc8b38e9767bf343be8d60dca373e495 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Tue, 22 Dec 2020 16:54:41 -0800 Subject: [PATCH] Add MII models --- cocotbext/eth/__init__.py | 1 + cocotbext/eth/mii.py | 294 ++++++++++++++++++++++++++++++++++++++ tests/mii/Makefile | 65 +++++++++ tests/mii/test_mii.py | 171 ++++++++++++++++++++++ tests/mii/test_mii.v | 46 ++++++ 5 files changed, 577 insertions(+) create mode 100644 cocotbext/eth/mii.py create mode 100644 tests/mii/Makefile create mode 100644 tests/mii/test_mii.py create mode 100644 tests/mii/test_mii.v diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py index 9620985..861f050 100644 --- a/cocotbext/eth/__init__.py +++ b/cocotbext/eth/__init__.py @@ -25,6 +25,7 @@ THE SOFTWARE. from .version import __version__ from .gmii import GmiiFrame, GmiiSource, GmiiSink +from .mii import MiiSource, MiiSink from .rgmii import RgmiiSource, RgmiiSink from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink diff --git a/cocotbext/eth/mii.py b/cocotbext/eth/mii.py new file mode 100644 index 0000000..8d5f112 --- /dev/null +++ b/cocotbext/eth/mii.py @@ -0,0 +1,294 @@ +""" + +Copyright (c) 2020 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 +from collections import deque + +import cocotb +from cocotb.triggers import RisingEdge, ReadOnly, Timer, First, Event +from cocotb.utils import get_sim_time + +from .version import __version__ +from .gmii import GmiiFrame +from .constants import EthPre + + +class MiiSource(object): + + def __init__(self, data, er, dv, clock, reset=None, enable=None, *args, **kwargs): + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.er = er + self.dv = dv + self.clock = clock + self.reset = reset + self.enable = enable + + self.log.info("MII source") + 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.active = False + self.queue = deque() + + self.ifg = 12 + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.width = 4 + self.byte_width = 1 + + self.reset = reset + + assert len(self.data) == 4 + self.data.setimmediatevalue(0) + if self.er is not None: + assert len(self.er) == 1 + self.er.setimmediatevalue(0) + assert len(self.dv) == 1 + self.dv.setimmediatevalue(0) + + cocotb.fork(self._run()) + + async def send(self, frame): + self.send_nowait(frame) + + def send_nowait(self, frame): + frame = GmiiFrame(frame) + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + self.queue.append(frame) + + def count(self): + return len(self.queue) + + def empty(self): + return not self.queue + + def idle(self): + return self.empty() and not self.active + + async def wait(self): + while not self.idle(): + await RisingEdge(self.clock) + + async def _run(self): + frame = None + ifg_cnt = 0 + self.active = False + + while True: + await ReadOnly() + + if self.reset is not None and self.reset.value: + await RisingEdge(self.clock) + frame = None + ifg_cnt = 0 + self.active = False + self.data <= 0 + if self.er is not None: + self.er <= 0 + self.dv <= 0 + continue + + await RisingEdge(self.clock) + + if self.enable is None or self.enable.value: + if ifg_cnt > 0: + # in IFG + ifg_cnt -= 1 + + elif frame is None and self.queue: + # send frame + frame = self.queue.popleft() + self.queue_occupancy_bytes -= len(frame) + self.queue_occupancy_frames -= 1 + self.log.info("TX frame: %s", frame) + frame.normalize() + + mii_data = [] + mii_error = [] + for b, e in zip(frame.data, frame.error): + mii_data.append(b & 0x0F) + mii_data.append(b >> 4) + mii_error.append(e) + mii_error.append(e) + frame.data = mii_data + frame.error = mii_error + + self.active = True + + if frame is not None: + self.data <= frame.data.pop(0) + if self.er is not None: + self.er <= frame.error.pop(0) + self.dv <= 1 + + if not frame.data: + ifg_cnt = max(self.ifg, 1) + frame = None + else: + self.data <= 0 + if self.er is not None: + self.er <= 0 + self.dv <= 0 + self.active = False + + +class MiiSink(object): + + def __init__(self, data, er, dv, clock, reset=None, enable=None, *args, **kwargs): + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.er = er + self.dv = dv + self.clock = clock + self.reset = reset + self.enable = enable + + self.log.info("MII sink") + 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.active = False + self.queue = deque() + self.sync = Event() + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.width = 4 + self.byte_width = 1 + + self.reset = reset + + assert len(self.data) == 4 + if self.er is not None: + assert len(self.er) == 1 + if self.dv is not None: + assert len(self.dv) == 1 + + cocotb.fork(self._run()) + + async def recv(self, compact=True): + while self.empty(): + self.sync.clear() + await self.sync.wait() + return self.recv_nowait(compact) + + def recv_nowait(self, compact=True): + if self.queue: + frame = self.queue.popleft() + self.queue_occupancy_bytes -= len(frame) + self.queue_occupancy_frames -= 1 + return frame + return None + + def count(self): + return len(self.queue) + + def empty(self): + return not self.queue + + def idle(self): + return not self.active + + async def wait(self, timeout=0, timeout_unit=None): + if not self.empty(): + return + self.sync.clear() + if timeout: + await First(self.sync.wait(), Timer(timeout, timeout_unit)) + else: + await self.sync.wait() + + async def _run(self): + frame = None + self.active = False + + while True: + await ReadOnly() + + if self.reset is not None and self.reset.value: + await RisingEdge(self.clock) + frame = None + self.active = False + continue + + if self.enable is None or self.enable.value: + d_val = self.data.value.integer + dv_val = self.dv.value.integer + er_val = 0 if self.er is None else self.er.value.integer + + if frame is None: + if dv_val: + # start of frame + frame = GmiiFrame(bytearray(), []) + frame.rx_sim_time = get_sim_time() + else: + if not dv_val: + # end of frame + odd = True + sync = False + b = 0 + be = 0 + data = bytearray() + error = [] + for n, e in zip(frame.data, frame.error): + odd = not odd + b = (n & 0x0F) << 4 | b >> 4 + be |= e + if not sync and b == EthPre.SFD: + odd = True + sync = True + if odd: + data.append(b) + error.append(be) + be = 0 + frame.data = data + frame.error = error + + frame.compact() + self.log.info("RX frame: %s", frame) + + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + self.queue.append(frame) + self.sync.set() + + frame = None + + if frame is not None: + frame.data.append(d_val) + frame.error.append(er_val) + + await RisingEdge(self.clock) diff --git a/tests/mii/Makefile b/tests/mii/Makefile new file mode 100644 index 0000000..4658172 --- /dev/null +++ b/tests/mii/Makefile @@ -0,0 +1,65 @@ +# Copyright (c) 2020 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_mii +TOPLEVEL = $(DUT) +MODULE = $(DUT) +VERILOG_SOURCES += $(DUT).v + +SIM_BUILD ?= sim_build_$(MODULE) + +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 + +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 sim_build_* + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst + +include $(shell cocotb-config --makefiles)/Makefile.sim + diff --git a/tests/mii/test_mii.py b/tests/mii/test_mii.py new file mode 100644 index 0000000..e5f359f --- /dev/null +++ b/tests/mii/test_mii.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2020 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 GmiiFrame, MiiSource, MiiSink + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + self._enable_generator = None + self._enable_cr = None + + cocotb.fork(Clock(dut.clk, 2, units="ns").start()) + + self.source = MiiSource(dut.mii_d, dut.mii_er, dut.mii_en, + dut.clk, dut.rst, dut.mii_clk_en) + self.sink = MiiSink(dut.mii_d, dut.mii_er, dut.mii_en, + dut.clk, dut.rst, dut.mii_clk_en) + + dut.mii_clk_en.setimmediatevalue(1) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst <= 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + def set_enable_generator(self, generator=None): + if self._enable_cr is not None: + self._enable_cr.kill() + self._enable_cr = None + + self._enable_generator = generator + + if self._enable_generator is not None: + self._enable_cr = cocotb.fork(self._run_enable()) + + def clear_enable_generator(self): + self.set_enable_generator(None) + + async def _run_enable(self): + for val in self._enable_generator: + self.dut.mii_clk_en <= val + await RisingEdge(self.dut.clk) + + +async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None): + + tb = TB(dut) + + tb.source.ifg = ifg + + if enable_gen is not None: + tb.set_enable_generator(enable_gen()) + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + + for test_data in test_frames: + test_frame = GmiiFrame.from_payload(test_data) + await tb.source.send(test_frame) + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.error is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.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)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("ifg", [12, 0]) + factory.add_option("enable_gen", [None, cycle_en]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + + +def test_mii(request): + dut = "test_mii" + 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/mii/test_mii.v b/tests/mii/test_mii.v new file mode 100644 index 0000000..0982049 --- /dev/null +++ b/tests/mii/test_mii.v @@ -0,0 +1,46 @@ +/* + +Copyright (c) 2020 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 / 1ns + +/* + * MII test + */ +module test_mii # +( + parameter DATA_WIDTH = 4 +) +( + input wire clk, + input wire rst, + + inout wire [DATA_WIDTH-1:0] mii_d, + inout wire mii_er, + inout wire mii_en, + inout wire mii_clk_en +); + +endmodule