From e242c85d83906c9bebe5d4a069ecf631a6b60837 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Mon, 16 Nov 2020 22:31:32 -0800 Subject: [PATCH] Initial commit --- LICENSE | 21 +++ README.md | 8 + cocotbext/eth/__init__.py | 26 +++ cocotbext/eth/constants.py | 83 +++++++++ cocotbext/eth/version.py | 1 + cocotbext/eth/xgmii.py | 350 +++++++++++++++++++++++++++++++++++++ setup.py | 32 ++++ tests/Makefile | 30 ++++ tests/xgmii/Makefile | 76 ++++++++ tests/xgmii/__init__.py | 0 tests/xgmii/test_xgmii.py | 217 +++++++++++++++++++++++ tests/xgmii/test_xgmii.v | 45 +++++ 12 files changed, 889 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cocotbext/eth/__init__.py create mode 100644 cocotbext/eth/constants.py create mode 100644 cocotbext/eth/version.py create mode 100644 cocotbext/eth/xgmii.py create mode 100644 setup.py create mode 100644 tests/Makefile create mode 100644 tests/xgmii/Makefile create mode 100644 tests/xgmii/__init__.py create mode 100644 tests/xgmii/test_xgmii.py create mode 100644 tests/xgmii/test_xgmii.v diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7affb14 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f81cda --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Ethernet interface modules for Cocotb + +GitHub repository: https://github.com/alexforencich/cocotbext-eth + +## Introduction + +Ethernet interface models for cocotb. + diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py new file mode 100644 index 0000000..da56d2e --- /dev/null +++ b/cocotbext/eth/__init__.py @@ -0,0 +1,26 @@ +""" + +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. + +""" + +from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink + diff --git a/cocotbext/eth/constants.py b/cocotbext/eth/constants.py new file mode 100644 index 0000000..c8dea0a --- /dev/null +++ b/cocotbext/eth/constants.py @@ -0,0 +1,83 @@ +""" + +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 enum + +class EthPre(enum.IntEnum): + PRE = 0x55 + SFD = 0xD5 + +ETH_PREAMBLE = b'\x55\x55\x55\x55\x55\x55\x55\xd5' + +class XgmiiCtrl(enum.IntEnum): + IDLE = 0x07 + LPI = 0x06 + START = 0xfb + TERM = 0xfd + ERROR = 0xfe + SEQ_OS = 0x9c + RES0 = 0x1c + RES1 = 0x3c + RES2 = 0x7c + RES3 = 0xbc + RES4 = 0xdc + RES5 = 0xf7 + SIG_OS = 0x5c + +class BaseRCtrl(enum.IntEnum): + IDLE = 0x00 + LPI = 0x06 + ERROR = 0x1e + RES_0 = 0x2d + RES_1 = 0x33 + RES_2 = 0x4b + RES_3 = 0x55 + RES_4 = 0x66 + RES_5 = 0x78 + +class BaseRO(enum.IntEnum): + SEQ_OS = 0x0 + SIG_OS = 0xf + +class BaseRSync(enum.IntEnum): + DATA = 0b10 + CTRL = 0b01 + +class BaseRBlockType(enum.IntEnum): + CTRL = 0x1e # C7 C6 C5 C4 C3 C2 C1 C0 BT + OS_4 = 0x2d # D7 D6 D5 O4 C3 C2 C1 C0 BT + START_4 = 0x33 # D7 D6 D5 C3 C2 C1 C0 BT + OS_START = 0x66 # D7 D6 D5 O0 D3 D2 D1 BT + OS_04 = 0x55 # D7 D6 D5 O4 O0 D3 D2 D1 BT + START_0 = 0x78 # D7 D6 D5 D4 D3 D2 D1 BT + OS_0 = 0x4b # C7 C6 C5 C4 O0 D3 D2 D1 BT + TERM_0 = 0x87 # C7 C6 C5 C4 C3 C2 C1 BT + TERM_1 = 0x99 # C7 C6 C5 C4 C3 C2 D0 BT + TERM_2 = 0xaa # C7 C6 C5 C4 C3 D1 D0 BT + TERM_3 = 0xb4 # C7 C6 C5 C4 D2 D1 D0 BT + TERM_4 = 0xcc # C7 C6 C5 D3 D2 D1 D0 BT + TERM_5 = 0xd2 # C7 C6 D4 D3 D2 D1 D0 BT + TERM_6 = 0xe1 # C7 D5 D4 D3 D2 D1 D0 BT + TERM_7 = 0xff # D6 D5 D4 D3 D2 D1 D0 BT + diff --git a/cocotbext/eth/version.py b/cocotbext/eth/version.py new file mode 100644 index 0000000..1c6be3a --- /dev/null +++ b/cocotbext/eth/version.py @@ -0,0 +1 @@ +__version__ = 0.1 diff --git a/cocotbext/eth/xgmii.py b/cocotbext/eth/xgmii.py new file mode 100644 index 0000000..565d951 --- /dev/null +++ b/cocotbext/eth/xgmii.py @@ -0,0 +1,350 @@ +""" + +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, XgmiiCtrl + +class XgmiiFrame(object): + def __init__(self, data=None, ctrl=None): + self.data = bytearray() + self.ctrl = None + self.rx_sim_time = None + self.rx_start_lane = None + + if type(data) is XgmiiFrame: + self.data = bytearray(data.data) + self.ctrl = data.ctrl + self.rx_sim_time = data.rx_sim_time + self.rx_start_lane = data.rx_start_lane + else: + self.data = bytearray(data) + self.ctrl = ctrl + + @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.ctrl is not None: + self.ctrl = self.ctrl[:n] + [self.ctrl[-1]]*(n-len(self.ctrl)) + else: + self.ctrl = [0]*n + + def compact(self): + if not any(self.ctrl): + self.ctrl = None + + def __eq__(self, other): + if type(other) is XgmiiFrame: + return self.data == other.data + + def __repr__(self): + return ( + f"{type(self).__name__}(data={repr(self.data)}, " + + f"ctrl={repr(self.ctrl)}, " + + f"rx_sim_time={repr(self.rx_sim_time)}, " + + f"rx_start_lane={repr(self.rx_start_lane)})" + ) + + def __len__(self): + return len(self.data) + + def __iter__(self): + return self.data.__iter__() + + +class XgmiiSource(object): + + _signals = ["d", "c"] + _optional_signals = [] + + 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.enable_dic = True + self.ifg = 12 + self.force_offset_start = False + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.width = len(self.bus.d) + self.byte_width = len(self.bus.c) + + self.reset = reset + + assert self.width == self.byte_width * 8 + + self.idle_d = 0 + self.idle_c = 0 + + for k in range(self.byte_width): + self.idle_d |= XgmiiCtrl.IDLE << k*8 + self.idle_c |= 1 << k + + self.bus.d.setimmediatevalue(0) + self.bus.c.setimmediatevalue(0) + + cocotb.fork(self._run()) + + def send(self, frame): + frame = XgmiiFrame(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 + deficit_idle_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 + deficit_idle_cnt = 0 + self.active = False + self.bus.d <= 0 + self.bus.c <= 0 + continue + + await RisingEdge(self.clock) + + if self.enable is None or self.enable.value: + if ifg_cnt + deficit_idle_cnt > self.byte_width-1 or (not self.enable_dic and ifg_cnt > 4): + # in IFG + ifg_cnt = ifg_cnt - self.byte_width + if ifg_cnt < 0: + if self.enable_dic: + deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0) + ifg_cnt = 0 + + elif frame is None: + # idle + if 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() + assert frame.data[0] == EthPre.PRE + assert frame.ctrl[0] == 0 + frame.data[0] = XgmiiCtrl.START + frame.ctrl[0] = 1 + frame.data.append(XgmiiCtrl.TERM) + frame.ctrl.append(1) + + # offset start + if (self.byte_width > 4 and ifg_cnt > (3-deficit_idle_cnt if self.enable_dic else 0)) or self.force_offset_start: + ifg_cnt = ifg_cnt-4 + frame.data = bytearray([XgmiiCtrl.IDLE]*4)+frame.data + frame.ctrl = [1]*4+frame.ctrl + + if self.enable_dic: + deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0) + ifg_cnt = 0 + self.active = True + else: + # clear counters + deficit_idle_cnt = 0 + ifg_cnt = 0 + + if frame is not None: + d_val = 0 + c_val = 0 + + for k in range(self.byte_width): + if frame is not None: + d_val |= frame.data.pop(0) << k*8 + c_val |= frame.ctrl.pop(0) << k + + if not frame.data: + ifg_cnt = max(self.ifg - (self.byte_width-k), 0) + frame = None + else: + d_val |= XgmiiCtrl.IDLE << k*8 + c_val |= 1 << k + + self.bus.d <= d_val + self.bus.c <= c_val + else: + self.bus.d <= self.idle_d + self.bus.c <= self.idle_c + self.active = False + + +class XgmiiSink(object): + + _signals = ["d", "c"] + _optional_signals = [] + + 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 = len(self.bus.d) + self.byte_width = len(self.bus.c) + + self.reset = reset + + assert self.width == self.byte_width * 8 + + 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: + for offset in range(self.byte_width): + d_val = (self.bus.d.value.integer >> (offset*8)) & 0xff + c_val = (self.bus.c.value.integer >> offset) & 1 + + if frame is None: + if c_val and d_val == XgmiiCtrl.START: + # start + frame = XgmiiFrame(bytearray([EthPre.PRE]), [0]) + frame.rx_sim_time = get_sim_time() + frame.rx_start_lane = offset + else: + if c_val: + # got a control character; terminate frame reception + if d_val != XgmiiCtrl.TERM: + # store control character if it's not a termination + frame.data.append(d_val) + frame.ctrl.append(c_val) + + 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 + else: + frame.data.append(d_val) + frame.ctrl.append(c_val) + + await RisingEdge(self.clock) + + +class XgmiiMonitor(XgmiiSink): + pass + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f148e59 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup, find_namespace_packages +import os.path + +version_py = os.path.join(os.path.dirname(__file__), 'cocotbext', 'eth', 'version.py') +with open(version_py, 'r') as f: + d = dict() + exec(f.read(), d) + version = d['__version__'] + +with open("README.md", "r") as f: + long_description = f.read() + +setup( + name = "cocotbext-eth", + author="Alex Forencich", + author_email="alex@alexforencich.com", + description="Ethernet modules for Cocotb", + long_description=long_description, + long_description_content_type='text/markdown', + url="https://github.com/alexforencich/cocotbext-eth", + download_url = 'http://github.com/alexforencich/cocotbext-eth/tarball/master', + version = version, + packages = find_namespace_packages(include=['cocotbext.*']), + install_requires = ['cocotb', 'cocotbext-axi'], + python_requires = '>=3.6', + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)" + ] +) diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..651fe52 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,30 @@ +# 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. + +TOPTARGETS := all clean + +SUBDIRS := $(wildcard */.) + +$(TOPTARGETS): $(SUBDIRS) +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +.PHONY: $(TOPTARGETS) $(SUBDIRS) + diff --git a/tests/xgmii/Makefile b/tests/xgmii/Makefile new file mode 100644 index 0000000..ddb66d9 --- /dev/null +++ b/tests/xgmii/Makefile @@ -0,0 +1,76 @@ +# 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_xgmii +TOPLEVEL = $(DUT) +MODULE = $(DUT) +VERILOG_SOURCES += $(DUT).v + +# module parameters +export PARAM_DATA_WIDTH ?= 64 +export PARAM_CTRL_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) + +SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH) + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) + COMPILE_ARGS += -P $(TOPLEVEL).CTRL_WIDTH=$(PARAM_CTRL_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) + COMPILE_ARGS += -GCTRL_WIDTH=$(PARAM_CTRL_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/xgmii/__init__.py b/tests/xgmii/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/xgmii/test_xgmii.py b/tests/xgmii/test_xgmii.py new file mode 100644 index 0000000..c4f7cc1 --- /dev/null +++ b/tests/xgmii/test_xgmii.py @@ -0,0 +1,217 @@ +#!/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 XgmiiFrame, XgmiiSource, XgmiiSink + +class TB(object): + def __init__(self, dut): + self.dut = dut + + cocotb.fork(Clock(dut.clk, 2, units="ns").start()) + + self.source = XgmiiSource(dut, "xgmii", dut.clk, dut.rst) + self.sink = XgmiiSink(dut, "xgmii", 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, enable_dic=True, force_offset_start=False): + + tb = TB(dut) + + byte_width = tb.source.width // 8 + + tb.source.ifg = ifg + tb.source.enable_dic = enable_dic + tb.source.force_offset_start = force_offset_start + + await tb.reset() + + test_frames = [payload_data(l) for l in payload_lengths()] + + for test_data in test_frames: + test_frame = XgmiiFrame.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.ctrl is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + +async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True, force_offset_start=False): + + tb = TB(dut) + + byte_width = tb.source.width // 8 + + tb.source.ifg = ifg + tb.source.enable_dic = enable_dic + tb.source.force_offset_start = force_offset_start + + await tb.reset() + + for length in range(64, 96): + + test_frames = [payload_data(length) for k in range(10)] + start_lane = [] + + for test_data in test_frames: + test_frame = XgmiiFrame.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.ctrl is None + + start_lane.append(rx_frame.rx_start_lane) + + print(length) + print(start_lane) + + start_lane_ref = [] + + # compute expected starting lanes + lane = 0 + deficit_idle_count = 0 + + for test_data in test_frames: + if ifg == 0: + lane = 0 + if force_offset_start and byte_width > 4: + lane = 4 + + start_lane_ref.append(lane) + lane = (lane + len(test_data)+ifg) % byte_width + + if enable_dic: + offset = lane % 4 + if deficit_idle_count+offset >= 4: + offset += 4 + lane = (lane - offset) % byte_width + deficit_idle_count = (deficit_idle_count + offset) % 4 + else: + offset = lane % 4 + if offset > 0: + offset += 4 + lane = (lane - offset) % byte_width + + print(start_lane_ref) + + assert start_lane_ref == start_lane + + for k in range(10): + await RisingEdge(dut.clk) + + 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+[65]*10+[66]*10+[67]*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.add_option("enable_dic", [True, False]) + factory.add_option("force_offset_start", [False, True]) + factory.generate_tests() + + factory = TestFactory(run_test_alignment) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("ifg", [12, 0]) + factory.add_option("enable_dic", [True, False]) + factory.add_option("force_offset_start", [False, True]) + factory.generate_tests() + + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + +@pytest.mark.parametrize("data_width", [32, 64]) +def test_xgmii(request, data_width): + dut = "test_xgmii" + 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 + parameters['CTRL_WIDTH'] = parameters['DATA_WIDTH'] // 8 + + 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/xgmii/test_xgmii.v b/tests/xgmii/test_xgmii.v new file mode 100644 index 0000000..51e8df9 --- /dev/null +++ b/tests/xgmii/test_xgmii.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 + +/* + * XGMII test + */ +module test_xgmii # +( + parameter DATA_WIDTH = 8, + parameter CTRL_WIDTH = (DATA_WIDTH/8) +) +( + input wire clk, + input wire rst, + + inout wire [DATA_WIDTH-1:0] xgmii_d, + inout wire [CTRL_WIDTH-1:0] xgmii_c +); + +endmodule