From ceabdabb110c381b0ee9509fc966a804d66ecb48 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Wed, 25 Nov 2020 00:53:29 -0800 Subject: [PATCH] Add RGMII models --- cocotbext/eth/__init__.py | 1 + cocotbext/eth/rgmii.py | 314 ++++++++++++++++++++++++++++++++++++++ tests/rgmii/Makefile | 65 ++++++++ tests/rgmii/__init__.py | 0 tests/rgmii/test_rgmii.py | 172 +++++++++++++++++++++ tests/rgmii/test_rgmii.v | 46 ++++++ 6 files changed, 598 insertions(+) create mode 100644 cocotbext/eth/rgmii.py create mode 100644 tests/rgmii/Makefile create mode 100644 tests/rgmii/__init__.py create mode 100644 tests/rgmii/test_rgmii.py create mode 100644 tests/rgmii/test_rgmii.v diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py index f104b8d..a69e9ed 100644 --- a/cocotbext/eth/__init__.py +++ b/cocotbext/eth/__init__.py @@ -25,4 +25,5 @@ THE SOFTWARE. from .version import __version__ from .gmii import GmiiFrame, GmiiSource, GmiiSink +from .rgmii import RgmiiSource, RgmiiSink from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink diff --git a/cocotbext/eth/rgmii.py b/cocotbext/eth/rgmii.py new file mode 100644 index 0000000..f43396d --- /dev/null +++ b/cocotbext/eth/rgmii.py @@ -0,0 +1,314 @@ +""" + +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, FallingEdge, 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 .version import __version__ +from .gmii import GmiiFrame +from .constants import EthPre + + +class RgmiiSource(object): + + _signals = ["d", "ctl"] + _optional_signals = [] + + def __init__(self, entity, name, clock, reset=None, enable=None, mii_select=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.mii_select = mii_select + self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs) + + self.log.info("RGMII 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 = 8 + self.byte_width = 1 + + self.reset = reset + + assert len(self.bus.d) == 4 + self.bus.d.setimmediatevalue(0) + assert len(self.bus.ctl) == 1 + self.bus.ctl.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 + d = 0 + er = 0 + en = 0 + + 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 + self.bus.ctl <= 0 + continue + + await RisingEdge(self.clock) + + if self.mii_select is None or not self.mii_select.value: + # send high nibble after rising edge, leading in to falling edge + self.bus.d <= d >> 4 + self.bus.ctl <= en ^ er + + 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() + + if self.mii_select is not None and self.mii_select.value: + 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: + d = frame.data.pop(0) + er = frame.error.pop(0) + en = 1 + + if not frame.data: + ifg_cnt = max(self.ifg, 1) + frame = None + else: + d = 0 + er = 0 + en = 0 + self.active = False + + await FallingEdge(self.clock) + + # send low nibble after falling edge, leading in to rising edge + self.bus.d <= d & 0x0F + self.bus.ctl <= en + + +class RgmiiSink(object): + + _signals = ["d", "ctl"] + _optional_signals = [] + + def __init__(self, entity, name, clock, reset=None, enable=None, mii_select=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.mii_select = mii_select + self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs) + + self.log.info("RGMII 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 = 8 + self.byte_width = 1 + + self.reset = reset + + assert len(self.bus.d) == 4 + assert len(self.bus.ctl) == 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 + d_val = 0 + dv_val = 0 + er_val = 0 + + while True: + await ReadOnly() + + if self.reset is not None and self.reset.value: + await RisingEdge(self.clock) + frame = None + self.active = False + continue + + # capture high nibble after rising edge, leading in to falling edge + d_val |= self.bus.d.value.integer << 4 + er_val = dv_val ^ self.bus.ctl.value.integer + + if self.enable is None or self.enable.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 + + if self.mii_select is not None and self.mii_select.value: + 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 FallingEdge(self.clock) + await ReadOnly() + + # capture low nibble after falling edge, leading in to rising edge + d_val = self.bus.d.value.integer + dv_val = self.bus.ctl.value.integer + + await RisingEdge(self.clock) + + +class RgmiiMonitor(RgmiiSink): + pass diff --git a/tests/rgmii/Makefile b/tests/rgmii/Makefile new file mode 100644 index 0000000..f44dd8b --- /dev/null +++ b/tests/rgmii/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_rgmii +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/rgmii/__init__.py b/tests/rgmii/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/rgmii/test_rgmii.py b/tests/rgmii/test_rgmii.py new file mode 100644 index 0000000..e1f105b --- /dev/null +++ b/tests/rgmii/test_rgmii.py @@ -0,0 +1,172 @@ +#!/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.log import SimLog +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.eth import GmiiFrame, RgmiiSource, RgmiiSink + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + self.log = SimLog("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 = RgmiiSource(dut, "rgmii", dut.clk, dut.rst, dut.rgmii_clk_en, dut.rgmii_mii_sel) + self.sink = RgmiiSink(dut, "rgmii", dut.clk, dut.rst, dut.rgmii_clk_en, dut.rgmii_mii_sel) + + dut.rgmii_clk_en.setimmediatevalue(1) + dut.rgmii_mii_sel.setimmediatevalue(0) + + 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.rgmii_clk_en <= val + await RisingEdge(self.dut.clk) + + +async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None, mii_sel=False): + + tb = TB(dut) + + tb.source.ifg = ifg + tb.dut.rgmii_mii_sel <= mii_sel + + 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) + 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)) + + +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", "mii_sel"), [(None, False), (None, True), (cycle_en, True)]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) + + +def test_rgmii(request): + dut = "test_rgmii" + 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/rgmii/test_rgmii.v b/tests/rgmii/test_rgmii.v new file mode 100644 index 0000000..5d0551c --- /dev/null +++ b/tests/rgmii/test_rgmii.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 + +/* + * RGMII test + */ +module test_rgmii # +( + parameter DATA_WIDTH = 4 +) +( + input wire clk, + input wire rst, + + inout wire [DATA_WIDTH-1:0] rgmii_d, + inout wire rgmii_ctl, + inout wire rgmii_clk_en, + inout wire rgmii_mii_sel +); + +endmodule