From 909dcff0f37912b07ff670f016e1942d64c80ab5 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Mon, 16 Nov 2020 23:42:42 -0800 Subject: [PATCH] Add GMII models --- cocotbext/eth/__init__.py | 1 + cocotbext/eth/gmii.py | 316 ++++++++++++++++++++++++++++++++++++++ tests/gmii/Makefile | 73 +++++++++ tests/gmii/__init__.py | 0 tests/gmii/test_gmii.py | 132 ++++++++++++++++ tests/gmii/test_gmii.v | 45 ++++++ 6 files changed, 567 insertions(+) create mode 100644 cocotbext/eth/gmii.py create mode 100644 tests/gmii/Makefile create mode 100644 tests/gmii/__init__.py create mode 100644 tests/gmii/test_gmii.py create mode 100644 tests/gmii/test_gmii.v diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py index da56d2e..1fa0d88 100644 --- a/cocotbext/eth/__init__.py +++ b/cocotbext/eth/__init__.py @@ -22,5 +22,6 @@ THE SOFTWARE. """ +from .gmii import GmiiFrame, GmiiSource, GmiiSink from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink diff --git a/cocotbext/eth/gmii.py b/cocotbext/eth/gmii.py new file mode 100644 index 0000000..d424136 --- /dev/null +++ b/cocotbext/eth/gmii.py @@ -0,0 +1,316 @@ +""" + +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 cocotb +from cocotb.triggers import RisingEdge, ReadOnly, Timer, First, Event +from cocotb.bus import Bus +from cocotb.log import SimLog +from cocotb.utils import get_sim_time + +from collections import deque + +from .constants import EthPre, ETH_PREAMBLE + +class GmiiFrame(object): + def __init__(self, data=None, error=None): + self.data = bytearray() + self.error = None + self.rx_sim_time = None + + if type(data) is GmiiFrame: + self.data = bytearray(data.data) + self.error = data.error + self.rx_sim_time = data.rx_sim_time + else: + self.data = bytearray(data) + self.error = error + + @classmethod + def from_payload(cls, payload): + data = bytearray(ETH_PREAMBLE) + data.extend(payload) + return cls(data) + + def get_preamble(self): + return self.data[0:8] + + def get_payload(self): + return self.data[8:] + + def normalize(self): + n = len(self.data) + + if self.error is not None: + self.error = self.error[:n] + [self.error[-1]]*(n-len(self.error)) + else: + self.error = [0]*n + + def compact(self): + if not any(self.error): + self.error = None + + def __eq__(self, other): + if type(other) is GmiiFrame: + return self.data == other.data + + def __repr__(self): + return ( + f"{type(self).__name__}(data={repr(self.data)}, " + + f"error={repr(self.error)}, " + + f"rx_sim_time={repr(self.rx_sim_time)})" + ) + + def __len__(self): + return len(self.data) + + def __iter__(self): + return self.data.__iter__() + + +class GmiiSource(object): + + _signals = ["d"] + _optional_signals = ["er", "en", "dv"] + + def __init__(self, entity, name, clock, reset=None, enable=None, *args, **kwargs): + self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) + self.entity = entity + self.clock = clock + self.reset = reset + self.enable = enable + self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs) + + 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 = 8 + self.byte_width = 1 + + self.reset = reset + + assert len(self.bus.d) == 8 + self.bus.d.setimmediatevalue(0) + if self.bus.er is not None: + assert len(self.bus.er) == 1 + self.bus.er.setimmediatevalue(0) + if self.bus.en is not None: + assert len(self.bus.en) == 1 + self.bus.en.setimmediatevalue(0) + self.bus.dv = self.bus.en + if self.bus.dv is not None: + assert len(self.bus.dv) == 1 + self.bus.dv.setimmediatevalue(0) + + cocotb.fork(self._run()) + + def send(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.bus.d <= 0 + if self.bus.er is not None: + self.bus.er <= 0 + self.bus.en <= 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(f"TX frame: {frame}") + frame.normalize() + self.active = True + + if frame is not None: + self.bus.d <= frame.data.pop(0) + if self.bus.er is not None: + self.bus.er <= frame.error.pop(0) + self.bus.en <= 1 + + if not frame.data: + ifg_cnt = max(self.ifg, 1) + frame = None + else: + self.bus.d <= 0 + if self.bus.er is not None: + self.bus.er <= 0 + self.bus.en <= 0 + self.active = False + + +class GmiiSink(object): + + _signals = ["d"] + _optional_signals = ["er", "en", "dv"] + + def __init__(self, entity, name, clock, reset=None, enable=None, *args, **kwargs): + self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) + self.entity = entity + self.clock = clock + self.reset = reset + self.enable = enable + self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs) + + 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 = 8 + self.byte_width = 1 + + self.reset = reset + + assert len(self.bus.d) == 8 + if self.bus.er is not None: + assert len(self.bus.er) == 1 + if self.bus.en is not None: + assert len(self.bus.en) == 1 + self.bus.dv = self.bus.en + if self.bus.dv is not None: + assert len(self.bus.dv) == 1 + + cocotb.fork(self._run()) + + def recv(self): + 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.bus.d.value.integer + dv_val = self.bus.dv.value + er_val = 0 if self.bus.er is None else self.bus.er.value + + 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 + + frame.compact() + self.log.info(f"RX frame: {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) + + +class GmiiMonitor(GmiiSink): + pass + diff --git a/tests/gmii/Makefile b/tests/gmii/Makefile new file mode 100644 index 0000000..26bf821 --- /dev/null +++ b/tests/gmii/Makefile @@ -0,0 +1,73 @@ +# 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_gmii +TOPLEVEL = $(DUT) +MODULE = $(DUT) +VERILOG_SOURCES += $(DUT).v + +# module parameters +# export PARAM_DATA_WIDTH ?= 64 + +SIM_BUILD ?= sim_build_$(MODULE) + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + #COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + #COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace + #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/gmii/__init__.py b/tests/gmii/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/gmii/test_gmii.py b/tests/gmii/test_gmii.py new file mode 100644 index 0000000..f48195c --- /dev/null +++ b/tests/gmii/test_gmii.py @@ -0,0 +1,132 @@ +#!/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 os + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.eth import GmiiFrame, GmiiSource, GmiiSink + +class TB(object): + def __init__(self, dut): + self.dut = dut + + cocotb.fork(Clock(dut.clk, 2, units="ns").start()) + + self.source = GmiiSource(dut, "gmii", dut.clk, dut.rst) + self.sink = GmiiSink(dut, "gmii", dut.clk, dut.rst) + + 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) + +async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut) + + byte_width = tb.source.width // 8 + + tb.source.ifg = ifg + + await tb.reset() + + test_frames = [payload_data(l) for l in payload_lengths()] + + for test_data in test_frames: + test_frame = GmiiFrame.from_payload(test_data) + tb.source.send(test_frame) + + for test_data in test_frames: + await tb.sink.wait() + rx_frame = tb.sink.recv() + + assert rx_frame.get_payload() == test_data + assert rx_frame.error is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + +def size_list(): + return list(range(64, 128))+[512, 1514, 9214]+[64]*10 + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + +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.generate_tests() + + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + +def test_gmii(request): + dut = "test_gmii" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(tests_dir, f"{dut}.v"), + ] + + parameters = {} + + # parameters['DATA_WIDTH'] = data_width + + 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/gmii/test_gmii.v b/tests/gmii/test_gmii.v new file mode 100644 index 0000000..431a97e --- /dev/null +++ b/tests/gmii/test_gmii.v @@ -0,0 +1,45 @@ +/* + +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 + +/* + * GMII test + */ +module test_gmii # +( + parameter DATA_WIDTH = 8 +) +( + input wire clk, + input wire rst, + + inout wire [DATA_WIDTH-1:0] gmii_d, + inout wire gmii_er, + inout wire gmii_en +); + +endmodule