diff --git a/rtl/ptp/taxi_ptp_perout.sv b/rtl/ptp/taxi_ptp_perout.sv new file mode 100644 index 0000000..5d4d3b6 --- /dev/null +++ b/rtl/ptp/taxi_ptp_perout.sv @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2019-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * PTP period out module + */ +module taxi_ptp_perout # +( + parameter logic FNS_EN = 1'b1, + parameter OUT_START_S = 48'h0, + parameter OUT_START_NS = 30'h0, + parameter OUT_START_FNS = 16'h0000, + parameter OUT_PERIOD_S = 48'd1, + parameter OUT_PERIOD_NS = 30'd0, + parameter OUT_PERIOD_FNS = 16'h0000, + parameter OUT_WIDTH_S = 48'h0, + parameter OUT_WIDTH_NS = 30'd1000, + parameter OUT_WIDTH_FNS = 16'h0000 +) +( + input wire logic clk, + input wire logic rst, + + /* + * Timestamp input from PTP clock + */ + input wire logic [95:0] input_ts_tod, + input wire logic input_ts_tod_step, + + /* + * Control + */ + input wire logic enable, + input wire logic [95:0] input_start, + input wire logic input_start_valid, + input wire logic [95:0] input_period, + input wire logic input_period_valid, + input wire logic [95:0] input_width, + input wire logic input_width_valid, + + /* + * Status + */ + output wire logic locked, + output wire logic error, + + /* + * Pulse output + */ + output wire logic output_pulse +); + +localparam [1:0] + STATE_IDLE = 2'd0, + STATE_UPDATE_RISE = 2'd1, + STATE_UPDATE_FALL = 2'd2; + +logic [1:0] state_reg = STATE_IDLE, state_next; + +logic [47:0] time_s_reg = '0; +logic [29:0] time_ns_reg = '0; +logic [15:0] time_fns_reg = '0; + +logic [47:0] next_rise_s_reg = '0, next_rise_s_next; +logic [29:0] next_rise_ns_reg = '0, next_rise_ns_next; +logic [15:0] next_rise_fns_reg = '0, next_rise_fns_next; + +logic [47:0] next_edge_s_reg = '0, next_edge_s_next; +logic [29:0] next_edge_ns_reg = '0, next_edge_ns_next; +logic [15:0] next_edge_fns_reg = '0, next_edge_fns_next; + +logic [47:0] start_s_reg = 48'(OUT_START_S); +logic [29:0] start_ns_reg = 30'(OUT_START_NS); +logic [15:0] start_fns_reg = 16'(OUT_START_FNS); + +logic [47:0] period_s_reg = 48'(OUT_PERIOD_S); +logic [29:0] period_ns_reg = 30'(OUT_PERIOD_NS); +logic [15:0] period_fns_reg = 16'(OUT_PERIOD_FNS); + +logic [47:0] width_s_reg = 48'(OUT_WIDTH_S); +logic [29:0] width_ns_reg = 30'(OUT_WIDTH_NS); +logic [15:0] width_fns_reg = 16'(OUT_WIDTH_FNS); + +logic [29:0] ts_tod_ns_inc_reg = '0, ts_tod_ns_inc_next; +logic [15:0] ts_tod_fns_inc_reg = '0, ts_tod_fns_inc_next; +logic [30:0] ts_tod_ns_ovf_reg = '0, ts_tod_ns_ovf_next; +logic [15:0] ts_tod_fns_ovf_reg = '0, ts_tod_fns_ovf_next; + +logic restart_reg = 1'b1; +logic locked_reg = 1'b0, locked_next; +logic error_reg = 1'b0, error_next; +logic ffwd_reg = 1'b0, ffwd_next; +logic level_reg = 1'b0, level_next; +logic output_reg = 1'b0, output_next; + +assign locked = locked_reg; +assign error = error_reg; +assign output_pulse = output_reg; + +always_comb begin + state_next = STATE_IDLE; + + next_rise_s_next = next_rise_s_reg; + next_rise_ns_next = next_rise_ns_reg; + next_rise_fns_next = next_rise_fns_reg; + + next_edge_s_next = next_edge_s_reg; + next_edge_ns_next = next_edge_ns_reg; + next_edge_fns_next = next_edge_fns_reg; + + ts_tod_ns_inc_next = ts_tod_ns_inc_reg; + ts_tod_fns_inc_next = ts_tod_fns_inc_reg; + + ts_tod_ns_ovf_next = ts_tod_ns_ovf_reg; + ts_tod_fns_ovf_next = ts_tod_fns_ovf_reg; + + locked_next = locked_reg; + error_next = error_reg; + ffwd_next = ffwd_reg; + level_next = level_reg; + output_next = output_reg; + + case (state_reg) + STATE_IDLE: begin + if (ffwd_reg || level_reg) begin + // fast forward or falling edge, set up for next rising edge + // set next rise time to previous rise time plus period + {ts_tod_ns_inc_next, ts_tod_fns_inc_next} = {next_rise_ns_reg, next_rise_fns_reg} + {period_ns_reg, period_fns_reg}; + {ts_tod_ns_ovf_next, ts_tod_fns_ovf_next} = {next_rise_ns_reg, next_rise_fns_reg} + {period_ns_reg, period_fns_reg} - {30'd1_000_000_000, 16'd0}; + end else begin + // rising edge; set up for next falling edge + // set next fall time to previous rise time plus width + {ts_tod_ns_inc_next, ts_tod_fns_inc_next} = {next_rise_ns_reg, next_rise_fns_reg} + {width_ns_reg, width_fns_reg}; + {ts_tod_ns_ovf_next, ts_tod_fns_ovf_next} = {next_rise_ns_reg, next_rise_fns_reg} + {width_ns_reg, width_fns_reg} - {30'd1_000_000_000, 16'd0}; + end + + // wait for edge + if ((time_s_reg > next_edge_s_reg) || (time_s_reg == next_edge_s_reg && {time_ns_reg, time_fns_reg} > {next_edge_ns_reg, next_edge_fns_reg})) begin + if (ffwd_reg || level_reg) begin + // fast forward or falling edge, set up for next rising edge + output_next = 1'b0; + level_next = 1'b0; + state_next = STATE_UPDATE_RISE; + end else begin + // rising edge; set up for next falling edge + locked_next = 1'b1; + error_next = 1'b0; + output_next = enable; + level_next = 1'b1; + state_next = STATE_UPDATE_FALL; + end + end else begin + ffwd_next = 1'b0; + state_next = STATE_IDLE; + end + end + STATE_UPDATE_RISE: begin + if (!ts_tod_ns_ovf_reg[30]) begin + // if the overflow lookahead did not borrow, one second has elapsed + next_edge_s_next = next_rise_s_reg + period_s_reg + 1; + next_edge_ns_next = ts_tod_ns_ovf_reg[29:0]; + next_edge_fns_next = ts_tod_fns_ovf_reg; + end else begin + // no increment seconds field + next_edge_s_next = next_rise_s_reg + period_s_reg; + next_edge_ns_next = ts_tod_ns_inc_reg; + next_edge_fns_next = ts_tod_fns_inc_reg; + end + next_rise_s_next = next_edge_s_next; + next_rise_ns_next = next_edge_ns_next; + next_rise_fns_next = next_edge_fns_next; + state_next = STATE_IDLE; + end + STATE_UPDATE_FALL: begin + if (!ts_tod_ns_ovf_reg[30]) begin + // if the overflow lookahead did not borrow, one second has elapsed + next_edge_s_next = next_rise_s_reg + width_s_reg + 1; + next_edge_ns_next = ts_tod_ns_ovf_reg[29:0]; + next_edge_fns_next = ts_tod_fns_ovf_reg; + end else begin + // no increment seconds field + next_edge_s_next = next_rise_s_reg + width_s_reg; + next_edge_ns_next = ts_tod_ns_inc_reg; + next_edge_fns_next = ts_tod_fns_inc_reg; + end + state_next = STATE_IDLE; + end + default: begin + state_next = STATE_IDLE; + end + endcase + + if (restart_reg || input_ts_tod_step) begin + // set next rise and next edge to start time + next_rise_s_next = start_s_reg; + next_rise_ns_next = start_ns_reg; + if (FNS_EN) begin + next_rise_fns_next = start_fns_reg; + end + next_edge_s_next = start_s_reg; + next_edge_ns_next = start_ns_reg; + if (FNS_EN) begin + next_edge_fns_next = start_fns_reg; + end + locked_next = 1'b0; + ffwd_next = 1'b1; + output_next = 1'b0; + level_next = 1'b0; + error_next = input_ts_tod_step; + state_next = STATE_IDLE; + end +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + restart_reg <= 1'b0; + + time_s_reg <= input_ts_tod[95:48]; + time_ns_reg <= input_ts_tod[45:16]; + if (FNS_EN) begin + time_fns_reg <= input_ts_tod[15:0]; + end + + if (input_start_valid) begin + start_s_reg <= input_start[95:48]; + start_ns_reg <= input_start[45:16]; + if (FNS_EN) begin + start_fns_reg <= input_start[15:0]; + end + restart_reg <= 1'b1; + end + + if (input_period_valid) begin + period_s_reg <= input_period[95:48]; + period_ns_reg <= input_period[45:16]; + if (FNS_EN) begin + period_fns_reg <= input_period[15:0]; + end + restart_reg <= 1'b1; + end + + if (input_width_valid) begin + width_s_reg <= input_width[95:48]; + width_ns_reg <= input_width[45:16]; + if (FNS_EN) begin + width_fns_reg <= input_width[15:0]; + end + end + + next_rise_s_reg <= next_rise_s_next; + next_rise_ns_reg <= next_rise_ns_next; + if (FNS_EN) begin + next_rise_fns_reg <= next_rise_fns_next; + end + + next_edge_s_reg <= next_edge_s_next; + next_edge_ns_reg <= next_edge_ns_next; + if (FNS_EN) begin + next_edge_fns_reg <= next_edge_fns_next; + end + + ts_tod_ns_inc_reg <= ts_tod_ns_inc_next; + if (FNS_EN) begin + ts_tod_fns_inc_reg <= ts_tod_fns_inc_next; + end + + ts_tod_ns_ovf_reg <= ts_tod_ns_ovf_next; + if (FNS_EN) begin + ts_tod_fns_ovf_reg <= ts_tod_fns_ovf_next; + end + + locked_reg <= locked_next; + error_reg <= error_next; + ffwd_reg <= ffwd_next; + level_reg <= level_next; + output_reg <= output_next; + + if (rst) begin + state_reg <= STATE_IDLE; + + start_s_reg <= 48'(OUT_START_S); + start_ns_reg <= 30'(OUT_START_NS); + start_fns_reg <= 16'(OUT_START_FNS); + + period_s_reg <= 48'(OUT_PERIOD_S); + period_ns_reg <= 30'(OUT_PERIOD_NS); + period_fns_reg <= 16'(OUT_PERIOD_FNS); + + width_s_reg <= 48'(OUT_WIDTH_S); + width_ns_reg <= 30'(OUT_WIDTH_NS); + width_fns_reg <= 16'(OUT_WIDTH_FNS); + + restart_reg <= 1'b1; + locked_reg <= 1'b0; + error_reg <= 1'b0; + output_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/tb/ptp/taxi_ptp_perout/Makefile b/tb/ptp/taxi_ptp_perout/Makefile new file mode 100644 index 0000000..62f8ca1 --- /dev/null +++ b/tb/ptp/taxi_ptp_perout/Makefile @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2020-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_ptp_perout +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = $(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += ../../../rtl/ptp/$(DUT).sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_FNS_EN := "1'b1" +export PARAM_OUT_START_S := 0 +export PARAM_OUT_START_NS := 0 +export PARAM_OUT_START_FNS := 0 +export PARAM_OUT_PERIOD_S := 1 +export PARAM_OUT_PERIOD_NS := 0 +export PARAM_OUT_PERIOD_FNS := 0 +export PARAM_OUT_WIDTH_S := 0 +export PARAM_OUT_WIDTH_NS := 1000 +export PARAM_OUT_WIDTH_FNS := 0 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/ptp/taxi_ptp_perout/test_taxi_ptp_perout.py b/tb/ptp/taxi_ptp_perout/test_taxi_ptp_perout.py new file mode 100644 index 0000000..ca878c3 --- /dev/null +++ b/tb/ptp/taxi_ptp_perout/test_taxi_ptp_perout.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2020-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import logging +import os + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer + +from cocotbext.eth import PtpClock + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 6.4, units="ns").start()) + + self.ptp_clock = PtpClock( + ts_tod=dut.input_ts_tod, + ts_step=dut.input_ts_tod_step, + clock=dut.clk, + reset=dut.rst, + period_ns=6.4 + ) + + dut.enable.setimmediatevalue(0) + dut.input_start.setimmediatevalue(0) + dut.input_start_valid.setimmediatevalue(0) + dut.input_period.setimmediatevalue(0) + dut.input_period_valid.setimmediatevalue(0) + dut.input_width.setimmediatevalue(0) + dut.input_width_valid.setimmediatevalue(0) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +@cocotb.test() +async def run_test(dut): + + tb = TB(dut) + + await tb.reset() + + dut.enable.value = 1 + + await RisingEdge(dut.clk) + + dut.input_start.value = 100 << 16 + dut.input_start_valid.value = 1 + dut.input_period.value = 100 << 16 + dut.input_period_valid.value = 1 + dut.input_width.value = 50 << 16 + dut.input_width_valid.value = 1 + + await RisingEdge(dut.clk) + + dut.input_start_valid.value = 0 + dut.input_period_valid.value = 0 + dut.input_width_valid.value = 0 + + await Timer(10000, 'ns') + + await RisingEdge(dut.clk) + + dut.input_start.value = 0 << 16 + dut.input_start_valid.value = 1 + dut.input_period.value = 100 << 16 + dut.input_period_valid.value = 1 + dut.input_width.value = 50 << 16 + dut.input_width_valid.value = 1 + + await RisingEdge(dut.clk) + + dut.input_start_valid.value = 0 + dut.input_period_valid.value = 0 + dut.input_width_valid.value = 0 + + await Timer(10000, 'ns') + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +def test_taxi_ptp_perout(request): + dut = "taxi_ptp_perout" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, "ptp", f"{dut}.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['FNS_EN'] = "1'b1" + parameters['OUT_START_S'] = 0 + parameters['OUT_START_NS'] = 0 + parameters['OUT_START_FNS'] = 0x0000 + parameters['OUT_PERIOD_S'] = 1 + parameters['OUT_PERIOD_NS'] = 0 + parameters['OUT_PERIOD_FNS'] = 0x0000 + parameters['OUT_WIDTH_S'] = 0 + parameters['OUT_WIDTH_NS'] = 1000 + parameters['OUT_WIDTH_FNS'] = 0x0000 + + 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( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )