From a54121b503ea3186d373746fe81b7032f4e8c00a Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Tue, 30 Mar 2021 21:18:36 -0700 Subject: [PATCH] Add PTP clock model that generates timestamps from sim time --- README.md | 47 +++++++ cocotbext/eth/__init__.py | 2 +- cocotbext/eth/ptp.py | 77 +++++++++++ tests/ptp_clock_sim_time/Makefile | 61 +++++++++ .../test_ptp_clock_sim_time.py | 125 ++++++++++++++++++ .../test_ptp_clock_sim_time.v | 41 ++++++ 6 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 tests/ptp_clock_sim_time/Makefile create mode 100644 tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py create mode 100644 tests/ptp_clock_sim_time/test_ptp_clock_sim_time.v diff --git a/README.md b/README.md index 0950fab..5eed3db 100644 --- a/README.md +++ b/README.md @@ -506,3 +506,50 @@ Once the clock is instantiated, it will generate a continuous stream of monotoni * `get_ts_64()`: return current 64-bit timestamp as an integer * `get_ts_64_ns()`: return current 64-bit timestamp in ns (float) * `get_ts_64_s()`: return current 64-bit timestamp in seconds (float) + +### PTP clock (sim time) + +The `PtpClockSimTime` class implements a PTP hardware clock that produces IEEE 1588 format 96 and 64 bit PTP timestamps, derived from the current simulation time. This module can be used in place of `PtpClock` so that captured PTP timestamps can be easily compared to captured simulation time. + +To use this module, import it and connect it to the DUT: + + from cocotbext.eth import PtpClockSimTime + + ptp_clock = PtpClockSimTime( + ts_96=dut.ts_96, + ts_64=dut.ts_64, + pps=dut.pps, + clock=dut.clk + ) + +Once the clock is instantiated, it will generate a continuous stream of monotonically increasing PTP timestamps on every clock edge. + +#### Signals + +* `ts_96`: 96-bit timestamp (48 bit seconds, 32 bit ns, 16 bit fractional ns) +* `ts_64`: 64-bit timestamp (48 bit ns, 16 bit fractional ns) +* `pps`: pulse-per-second output, pulsed when ts_96 seconds field increments + +#### Constructor parameters: + +* _ts_96_: 96-bit timestamp signal (optional) +* _ts_64_: 64-bit timestamp signal (optional) +* _pps_: pulse-per-second signal (optional) +* _clock_: clock + +#### Attributes: + +* _ts_96_s_: current 96-bit timestamp seconds field +* _ts_96_ns_: current 96-bit timestamp ns field +* _ts_96_fns_: current 96-bit timestamp fractional ns field +* _ts_64_ns_: current 64-bit timestamp ns field +* _ts_64_fns_: current 64-bit timestamp fractional ns field + +#### Methods + +* `get_ts_96()`: return current 96-bit timestamp as an integer +* `get_ts_96_ns()`: return current 96-bit timestamp in ns (float) +* `get_ts_96_s()`: return current 96-bit timestamp in seconds (float) +* `get_ts_64()`: return current 64-bit timestamp as an integer +* `get_ts_64_ns()`: return current 64-bit timestamp in ns (float) +* `get_ts_64_s()`: return current 64-bit timestamp in seconds (float) diff --git a/cocotbext/eth/__init__.py b/cocotbext/eth/__init__.py index 544996d..2b11871 100644 --- a/cocotbext/eth/__init__.py +++ b/cocotbext/eth/__init__.py @@ -29,4 +29,4 @@ from .mii import MiiSource, MiiSink, MiiPhy from .rgmii import RgmiiSource, RgmiiSink, RgmiiPhy from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink -from .ptp import PtpClock +from .ptp import PtpClock, PtpClockSimTime diff --git a/cocotbext/eth/ptp.py b/cocotbext/eth/ptp.py index d87e09b..408c379 100644 --- a/cocotbext/eth/ptp.py +++ b/cocotbext/eth/ptp.py @@ -28,6 +28,7 @@ from fractions import Fraction import cocotb from cocotb.triggers import RisingEdge +from cocotb.utils import get_sim_time from .version import __version__ from .reset import Reset @@ -253,3 +254,79 @@ class PtpClock(Reset): self.drift_cnt -= 1 else: self.drift_cnt = self.drift_rate-1 + + +class PtpClockSimTime: + + def __init__(self, ts_96=None, ts_64=None, pps=None, clock=None, *args, **kwargs): + self.log = logging.getLogger(f"cocotb.eth.{type(self).__name__}") + self.ts_96 = ts_96 + self.ts_64 = ts_64 + self.pps = pps + self.clock = clock + + self.log.info("PTP clock (sim time)") + 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.ts_96_s = 0 + self.ts_96_ns = 0 + self.ts_96_fns = 0 + + self.ts_64_ns = 0 + self.ts_64_fns = 0 + + self.last_ts_96_s = 0 + + if self.ts_96 is not None: + self.ts_96.setimmediatevalue(0) + if self.ts_64 is not None: + self.ts_64.setimmediatevalue(0) + if self.pps is not None: + self.pps <= 0 + + self._run_cr = cocotb.fork(self._run()) + + def get_ts_96(self): + return (self.ts_96_s << 48) | (self.ts_96_ns << 16) | self.ts_96_fns + + def get_ts_96_ns(self): + return self.ts_96_s*1e9+self.ts_96_ns+self.ts_96_fns/2**16 + + def get_ts_96_s(self): + return self.get_ts_96_ns()*1e-9 + + def get_ts_64(self): + return (self.ts_64_ns << 16) | self.ts_64_fns + + def get_ts_64_ns(self): + return self.get_ts_64()/2**16 + + def get_ts_64_s(self): + return self.get_ts_64()*1e-9 + + async def _run(self): + while True: + await RisingEdge(self.clock) + + self.ts_64_fns, self.ts_64_ns = math.modf(get_sim_time('ns')) + + self.ts_64_ns = int(self.ts_64_ns) + self.ts_64_fns = int(self.ts_64_fns*0x10000) + + self.ts_96_s, self.ts_96_ns = divmod(self.ts_64_ns, 1000000000) + self.ts_96_fns = self.ts_64_fns + + if self.ts_96 is not None: + self.ts_96 <= (self.ts_96_s << 48) | (self.ts_96_ns << 16) | self.ts_96_fns + + if self.ts_64 is not None: + self.ts_64 <= (self.ts_64_ns << 16) | self.ts_64_fns + + if self.pps is not None: + self.pps <= int(self.last_ts_96_s != self.ts_96_s) + + self.last_ts_96_s = self.ts_96_s diff --git a/tests/ptp_clock_sim_time/Makefile b/tests/ptp_clock_sim_time/Makefile new file mode 100644 index 0000000..019cc3c --- /dev/null +++ b/tests/ptp_clock_sim_time/Makefile @@ -0,0 +1,61 @@ +# 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 = 1ps + +DUT = test_ptp_clock_sim_time +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/ptp_clock_sim_time/test_ptp_clock_sim_time.py b/tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py new file mode 100644 index 0000000..5a3f023 --- /dev/null +++ b/tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py @@ -0,0 +1,125 @@ +#!/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 logging +import os + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.utils import get_sim_time + +from cocotbext.eth import PtpClockSimTime + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.fork(Clock(dut.clk, 6.4, units="ns").start()) + + self.ptp_clock = PtpClockSimTime( + ts_96=dut.ts_96, + ts_64=dut.ts_64, + pps=dut.pps, + clock=dut.clk + ) + + +@cocotb.test() +async def run_test(dut): + + tb = TB(dut) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = get_sim_time('sec') + start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9) + start_ts_64 = dut.ts_64.value.integer/2**16*1e-9 + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = get_sim_time('sec') + stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9) + stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9 + + time_delta = stop_time-start_time + ts_96_delta = stop_ts_96-start_ts_96 + ts_64_delta = stop_ts_64-start_ts_64 + + ts_96_diff = time_delta - ts_96_delta + ts_64_diff = time_delta - ts_64_delta + + tb.log.info("sim time delta : %g s", time_delta) + tb.log.info("96 bit ts delta : %g s", ts_96_delta) + tb.log.info("64 bit ts delta : %g s", ts_64_delta) + tb.log.info("96 bit ts diff : %g s", ts_96_diff) + tb.log.info("64 bit ts diff : %g s", ts_64_diff) + + assert abs(ts_96_diff) < 1e-12 + assert abs(ts_64_diff) < 1e-12 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) + + +def test_ptp_clock(request): + dut = "test_ptp_clock_sim_time" + 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/ptp_clock_sim_time/test_ptp_clock_sim_time.v b/tests/ptp_clock_sim_time/test_ptp_clock_sim_time.v new file mode 100644 index 0000000..da55b44 --- /dev/null +++ b/tests/ptp_clock_sim_time/test_ptp_clock_sim_time.v @@ -0,0 +1,41 @@ +/* + +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 + +/* + * PTP clock test + */ +module test_ptp_clock_sim_time +( + input wire clk, + + inout wire [95:0] ts_96, + inout wire [63:0] ts_64, + inout wire pps +); + +endmodule