From bd8e8e5b20b3c7ed4348c69f91f34dd834ea1456 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Tue, 7 Nov 2023 13:07:15 -0800 Subject: [PATCH] Add PTP time distribution components Signed-off-by: Alex Forencich --- README.md | 10 + rtl/ptp_td_leaf.v | 1015 ++++++++++++++++++++++++++++ rtl/ptp_td_phc.v | 603 +++++++++++++++++ syn/vivado/ptp_td_leaf.tcl | 104 +++ tb/ptp_td.py | 604 +++++++++++++++++ tb/ptp_td_leaf/Makefile | 75 ++ tb/ptp_td_leaf/ptp_td.py | 1 + tb/ptp_td_leaf/test_ptp_td_leaf.py | 507 ++++++++++++++ tb/ptp_td_phc/Makefile | 69 ++ tb/ptp_td_phc/ptp_td.py | 1 + tb/ptp_td_phc/test_ptp_td_phc.py | 550 +++++++++++++++ 11 files changed, 3539 insertions(+) create mode 100644 rtl/ptp_td_leaf.v create mode 100644 rtl/ptp_td_phc.v create mode 100644 syn/vivado/ptp_td_leaf.tcl create mode 100644 tb/ptp_td.py create mode 100644 tb/ptp_td_leaf/Makefile create mode 120000 tb/ptp_td_leaf/ptp_td.py create mode 100644 tb/ptp_td_leaf/test_ptp_td_leaf.py create mode 100644 tb/ptp_td_phc/Makefile create mode 120000 tb/ptp_td_phc/ptp_td.py create mode 100644 tb/ptp_td_phc/test_ptp_td_phc.py diff --git a/README.md b/README.md index c22744ee9..f535cb525 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,14 @@ PTP clock CDC module with PPS output. Use this module to transfer and deskew a free-running PTP clock across clock domains. Supports both 64 and 96 bit timestamp formats. +### `ptp_td_leaf` module + +PTP time distribution leaf clock module. Accepts PTP time distribution messages from the `ptp_td_phc` module, and outputs both the 96-bit time-of-day timestamp and 64-bit relative timestamp in the destination clock domain, as well as both single-cycle and stretched PPS outputs. Also supports pipelining the serial data input, automatically compensating for the pipeline delay. + +### `ptp_td_phc` module + +PTP time distribution master clock module. Generates PTP time distribution messages over a serial interface that can provide PTP time to one or more leaf clocks (`ptp_td_leaf`), as well as both single-cycle and stretched PPS outputs. The fractional nanoseconds portion is shared between the time-of-day and relative timestamps to support reconstruction of the 96-bit time-of-day timestamp from a truncated relative timestamp. The module supports coarse setting of both the ToD and relative timestamps as well as atomically applying offsets to the ToD and relative timestamps and the shared fractional nanoseconds. + ### `ptp_ts_extract` module PTP timestamp extract module. Use this module to extract a PTP timestamp @@ -466,6 +474,8 @@ and data lines. rtl/oddr.v : Generic DDR output register rtl/ptp_clock.v : PTP clock rtl/ptp_clock_cdc.v : PTP clock CDC + rtl/ptp_td_leaf.v : PTP time distribution leaf clock + rtl/ptp_td_phc.v : PTP time distribution master clock rtl/ptp_ts_extract.v : PTP timestamp extract rtl/ptp_perout.v : PTP period out rtl/rgmii_phy_if.v : RGMII PHY interface diff --git a/rtl/ptp_td_leaf.v b/rtl/ptp_td_leaf.v new file mode 100644 index 000000000..e4690d5f8 --- /dev/null +++ b/rtl/ptp_td_leaf.v @@ -0,0 +1,1015 @@ +/* + +Copyright (c) 2023 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 + +`resetall +`timescale 1ns / 1fs +`default_nettype none + +/* + * PTP time distribution leaf + */ +module ptp_td_leaf # +( + parameter TS_REL_EN = 1, + parameter TS_TOD_EN = 1, + parameter TS_FNS_W = 16, + parameter TS_REL_NS_W = 48, + parameter TS_TOD_S_W = 48, + parameter TS_REL_W = TS_REL_NS_W + TS_FNS_W, + parameter TS_TOD_W = TS_TOD_S_W + 32 + TS_FNS_W, + parameter TD_SDI_PIPELINE = 2 +) +( + input wire clk, + input wire rst, + input wire sample_clk, + + /* + * PTP clock interface + */ + input wire ptp_clk, + input wire ptp_rst, + input wire ptp_td_sdi, + + /* + * Timestamp output + */ + output wire [TS_REL_W-1:0] output_ts_rel, + output wire output_ts_rel_step, + output wire [TS_TOD_W-1:0] output_ts_tod, + output wire output_ts_tod_step, + + /* + * PPS output (ToD format only) + */ + output wire output_pps, + output wire output_pps_str, + + /* + * Status + */ + output wire locked +); + +localparam SYNC_DELAY = 32-2-TD_SDI_PIPELINE; + +localparam TS_NS_W = TS_REL_NS_W < 9 ? 9 : TS_REL_NS_W; +localparam TS_TOD_NS_W = 30; +localparam PERIOD_NS_W = 8; + +localparam FNS_W = 16; + +localparam CMP_FNS_W = 4; +localparam SRC_FNS_W = CMP_FNS_W+8; + +localparam LOG_RATE = 3; + +localparam PHASE_CNT_W = LOG_RATE; +localparam PHASE_ACC_W = PHASE_CNT_W+16; +localparam LOAD_CNT_W = 8-LOG_RATE; + +localparam LOG_SAMPLE_SYNC_RATE = 4; +localparam SAMPLE_ACC_W = LOG_SAMPLE_SYNC_RATE+2; + +localparam LOG_PHASE_ERR_RATE = 3; +localparam PHASE_ERR_ACC_W = LOG_PHASE_ERR_RATE+2; + +localparam DST_SYNC_LOCK_W = 5; +localparam FREQ_LOCK_W = 5; +localparam PTP_LOCK_W = 8; + +localparam TIME_ERR_INT_W = PERIOD_NS_W+FNS_W; + +localparam [30:0] NS_PER_S = 31'd1_000_000_000; + +// pipeline to facilitate long input path +wire ptp_td_sdi_pipe[0:TD_SDI_PIPELINE]; + +assign ptp_td_sdi_pipe[0] = ptp_td_sdi; + +generate + +genvar n; + +for (n = 0; n < TD_SDI_PIPELINE; n = n + 1) begin : pipe_stage + + (* shreg_extract = "no" *) + reg ptp_td_sdi_reg = 0; + + assign ptp_td_sdi_pipe[n+1] = ptp_td_sdi_reg; + + always @(posedge ptp_clk) begin + ptp_td_sdi_reg <= ptp_td_sdi_pipe[n]; + end + +end + +endgenerate + +// deserialize data +reg [15:0] td_shift_reg = 0; +reg [4:0] bit_cnt_reg = 0; +reg td_valid_reg = 1'b0; +reg [3:0] td_index_reg = 0; +reg [3:0] td_msg_reg = 0; + +reg [15:0] td_tdata_reg = 0; +reg td_tvalid_reg = 1'b0; +reg td_tlast_reg = 1'b0; +reg [7:0] td_tid_reg = 0; +reg td_sync_reg = 1'b0; + +always @(posedge ptp_clk) begin + td_shift_reg <= {ptp_td_sdi_pipe[TD_SDI_PIPELINE], td_shift_reg[15:1]}; + + td_tvalid_reg <= 1'b0; + + if (bit_cnt_reg) begin + bit_cnt_reg <= bit_cnt_reg - 1; + end else begin + td_valid_reg <= 1'b0; + if (td_valid_reg) begin + td_tdata_reg <= td_shift_reg; + td_tvalid_reg <= 1'b1; + td_tlast_reg <= ptp_td_sdi_pipe[TD_SDI_PIPELINE]; + td_tid_reg <= {td_msg_reg, td_index_reg}; + if (td_index_reg == 0) begin + td_msg_reg <= td_shift_reg[3:0]; + td_tid_reg[7:4] <= td_shift_reg[3:0]; + end + td_index_reg <= td_index_reg + 1; + td_sync_reg = !td_sync_reg; + end + if (ptp_td_sdi_pipe[TD_SDI_PIPELINE] == 0) begin + bit_cnt_reg <= 16; + td_valid_reg <= 1'b1; + end else begin + td_index_reg <= 0; + end + end + + if (ptp_rst) begin + bit_cnt_reg <= 0; + td_valid_reg <= 1'b0; + + td_tvalid_reg <= 1'b0; + end +end + +// sync TD data +reg [15:0] dst_td_tdata_reg = 0; +reg dst_td_tvalid_reg = 1'b0; +reg [7:0] dst_td_tid_reg = 0; + +(* shreg_extract = "no" *) +reg td_sync_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +reg td_sync_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +reg td_sync_sync3_reg = 1'b0; + +always @(posedge clk) begin + td_sync_sync1_reg <= td_sync_reg; + td_sync_sync2_reg <= td_sync_sync1_reg; + td_sync_sync3_reg <= td_sync_sync2_reg; +end + +always @(posedge clk) begin + dst_td_tvalid_reg <= 1'b0; + + if (td_sync_sync3_reg ^ td_sync_sync2_reg) begin + dst_td_tdata_reg <= td_tdata_reg; + dst_td_tvalid_reg <= 1'b1; + dst_td_tid_reg <= td_tid_reg; + end + + if (rst) begin + dst_td_tvalid_reg <= 1'b0; + end +end + +// source clock and sync generation +reg [5:0] src_sync_delay_reg = 0; +reg src_load_reg = 1'b0; + +reg [PHASE_CNT_W-1:0] src_phase_reg = 0; +reg src_update_reg = 1'b0; +reg src_sync_reg = 1'b0; +reg src_marker_reg = 1'b0; + +reg [PERIOD_NS_W+32-1:0] src_period_reg = 0; +reg [PERIOD_NS_W+32-1:0] src_period_shadow_reg = 0; + +reg [9+SRC_FNS_W-1:0] src_ns_reg = 0; +reg [9+32-1:0] src_ns_shadow_reg = 0; +reg src_fns_shadow_valid_reg = 1'b0; +reg src_ns_shadow_valid_reg = 1'b0; + +always @(posedge ptp_clk) begin + src_load_reg <= 1'b0; + + {src_update_reg, src_phase_reg} <= src_phase_reg+1; + + if (src_update_reg) begin + src_ns_reg <= src_ns_reg + ({src_period_reg, {PHASE_CNT_W{1'b0}}} >> (32-SRC_FNS_W)); + src_sync_reg <= !src_sync_reg; + end + + // extract data + if (td_tvalid_reg) begin + if (td_tid_reg[3:0] == 4'd6) begin + src_ns_shadow_reg[15:0] <= td_tdata_reg; + src_fns_shadow_valid_reg <= 1'b0; + end + if (td_tid_reg[3:0] == 4'd7) begin + src_ns_shadow_reg[31:16] <= td_tdata_reg; + src_fns_shadow_valid_reg <= 1'b1; + end + if (td_tid_reg[3:0] == 4'd8) begin + src_ns_shadow_reg[40:32] <= td_tdata_reg; + src_ns_shadow_valid_reg <= 1'b1; + end + if (td_tid_reg[3:0] == 4'd11) begin + src_period_shadow_reg[15:0] <= td_tdata_reg; + end + if (td_tid_reg[3:0] == 4'd12) begin + src_period_shadow_reg[31:16] <= td_tdata_reg; + end + if (td_tid_reg[3:0] == 4'd13) begin + src_period_shadow_reg[39:32] <= td_tdata_reg; + end + end + + if (src_load_reg) begin + if (src_ns_shadow_valid_reg && src_fns_shadow_valid_reg) begin + src_ns_reg <= src_ns_shadow_reg >> (32-SRC_FNS_W); + end + src_fns_shadow_valid_reg <= 1'b0; + src_ns_shadow_valid_reg <= 1'b0; + src_period_reg <= src_period_shadow_reg; + src_marker_reg <= !src_marker_reg; + end + + if (src_sync_delay_reg == 1) begin + src_load_reg <= 1'b1; + src_phase_reg <= 0; + end + + if (src_sync_delay_reg) begin + src_sync_delay_reg <= src_sync_delay_reg - 1; + end + + if (td_tvalid_reg && td_tlast_reg) begin + src_sync_delay_reg <= SYNC_DELAY; + end +end + +reg [PERIOD_NS_W+FNS_W-1:0] period_ns_reg = 0, period_ns_next = 0; + +reg [9+CMP_FNS_W-1:0] dst_ns_capt_reg = 0; +reg [9+CMP_FNS_W-1:0] src_ns_sync_reg = 0; + +reg [FNS_W-1:0] ts_fns_reg = 0, ts_fns_next = 0; + +reg [TS_NS_W-1:0] ts_rel_ns_reg = 0, ts_rel_ns_next = 0; +reg ts_tod_step_reg = 1'b0, ts_tod_step_next; + +reg [TS_TOD_S_W-1:0] ts_tod_s_reg = 0, ts_tod_s_next = 0; +reg [TS_TOD_NS_W-1:0] ts_tod_ns_reg = 0, ts_tod_ns_next = 0; +reg [8:0] ts_tod_offset_ns_reg = 0, ts_tod_offset_ns_next = 0; +reg ts_rel_step_reg = 1'b0, ts_rel_step_next; + +reg pps_reg = 1'b0, pps_next; +reg pps_str_reg = 1'b0, pps_str_next; + +reg [PHASE_ACC_W-1:0] dst_phase_reg = {PHASE_ACC_W{1'b0}}, dst_phase_next; +reg [PHASE_ACC_W-1:0] dst_phase_inc_reg = {PHASE_ACC_W{1'b0}}, dst_phase_inc_next; + +reg dst_sync_reg = 1'b0; +reg dst_update_reg = 1'b0, dst_update_next = 1'b0; + +(* shreg_extract = "no" *) +reg src_sync_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_sync_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_sync_sync3_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_marker_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_marker_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_marker_sync3_reg = 1'b0; + +(* shreg_extract = "no" *) +reg src_sync_sample_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_sync_sample_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +reg src_sync_sample_sync3_reg = 1'b0; +(* shreg_extract = "no" *) +reg dst_sync_sample_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +reg dst_sync_sample_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +reg dst_sync_sample_sync3_reg = 1'b0; + +reg [SAMPLE_ACC_W-1:0] sample_acc_reg = 0; +reg [SAMPLE_ACC_W-1:0] sample_acc_out_reg = 0; +reg [LOG_SAMPLE_SYNC_RATE-1:0] sample_cnt_reg = 0; +reg sample_update_reg = 1'b0; +reg sample_update_sync1_reg = 1'b0; +reg sample_update_sync2_reg = 1'b0; +reg sample_update_sync3_reg = 1'b0; + +// CDC logic +always @(posedge clk) begin + src_sync_sync1_reg <= src_sync_reg; + src_sync_sync2_reg <= src_sync_sync1_reg; + src_sync_sync3_reg <= src_sync_sync2_reg; + src_marker_sync1_reg <= src_marker_reg; + src_marker_sync2_reg <= src_marker_sync1_reg; + src_marker_sync3_reg <= src_marker_sync2_reg; +end + +always @(posedge sample_clk) begin + src_sync_sample_sync1_reg <= src_sync_reg; + src_sync_sample_sync2_reg <= src_sync_sample_sync1_reg; + src_sync_sample_sync3_reg <= src_sync_sample_sync2_reg; + dst_sync_sample_sync1_reg <= dst_sync_reg; + dst_sync_sample_sync2_reg <= dst_sync_sample_sync1_reg; + dst_sync_sample_sync3_reg <= dst_sync_sample_sync2_reg; +end + +reg edge_1_reg = 1'b0; +reg edge_2_reg = 1'b0; + +reg [3:0] active_reg = 0; + +always @(posedge sample_clk) begin + // phase and frequency detector + if (dst_sync_sample_sync2_reg && !dst_sync_sample_sync3_reg) begin + if (src_sync_sample_sync2_reg && !src_sync_sample_sync3_reg) begin + edge_1_reg <= 1'b0; + edge_2_reg <= 1'b0; + end else begin + edge_1_reg <= !edge_2_reg; + edge_2_reg <= 1'b0; + end + end else if (src_sync_sample_sync2_reg && !src_sync_sample_sync3_reg) begin + edge_1_reg <= 1'b0; + edge_2_reg <= !edge_1_reg; + end + + // accumulator + sample_acc_reg <= $signed(sample_acc_reg) + $signed({1'b0, edge_2_reg}) - $signed({1'b0, edge_1_reg}); + + sample_cnt_reg <= sample_cnt_reg + 1; + + if (src_sync_sample_sync2_reg && !src_sync_sample_sync3_reg) begin + active_reg[0] <= 1'b1; + end + + if (sample_cnt_reg == 0) begin + active_reg <= {active_reg, src_sync_sample_sync2_reg && !src_sync_sample_sync3_reg}; + sample_acc_reg <= $signed({1'b0, edge_2_reg}) - $signed({1'b0, edge_1_reg}); + sample_acc_out_reg <= sample_acc_reg; + if (active_reg != 0) begin + sample_update_reg <= !sample_update_reg; + end + end +end + +always @(posedge clk) begin + sample_update_sync1_reg <= sample_update_reg; + sample_update_sync2_reg <= sample_update_sync1_reg; + sample_update_sync3_reg <= sample_update_sync2_reg; +end + +reg [SAMPLE_ACC_W-1:0] sample_acc_sync_reg = 0; +reg sample_acc_sync_valid_reg = 0; + +reg [PHASE_ACC_W-1:0] dst_err_int_reg = 0, dst_err_int_next = 0; +reg [1:0] dst_ovf; + +reg [DST_SYNC_LOCK_W-1:0] dst_sync_lock_count_reg = 0, dst_sync_lock_count_next; +reg dst_sync_locked_reg = 1'b0, dst_sync_locked_next; + +reg dst_gain_sel_reg = 0, dst_gain_sel_next; + +always @* begin + {dst_update_next, dst_phase_next} = dst_phase_reg + dst_phase_inc_reg; + dst_phase_inc_next = dst_phase_inc_reg; + + dst_err_int_next = dst_err_int_reg; + + dst_sync_lock_count_next = dst_sync_lock_count_reg; + dst_sync_locked_next = dst_sync_locked_reg; + + dst_gain_sel_next = dst_gain_sel_reg; + + if (sample_acc_sync_valid_reg) begin + // updated sampled dst_phase error + + // gain scheduling + if (!sample_acc_sync_reg[SAMPLE_ACC_W-1]) begin + if (sample_acc_sync_reg[SAMPLE_ACC_W-4 +: 3]) begin + dst_gain_sel_next = 1'b1; + end else begin + dst_gain_sel_next = 1'b0; + end + end else begin + if (~sample_acc_sync_reg[SAMPLE_ACC_W-4 +: 3]) begin + dst_gain_sel_next = 1'b1; + end else begin + dst_gain_sel_next = 1'b0; + end + end + + // time integral of error + case (dst_gain_sel_reg) + 1'd0: {dst_ovf, dst_err_int_next} = $signed({1'b0, dst_err_int_reg}) + $signed(sample_acc_sync_reg); + 1'd1: {dst_ovf, dst_err_int_next} = $signed({1'b0, dst_err_int_reg}) + ($signed(sample_acc_sync_reg) * 2**7); + endcase + + // saturate + if (dst_ovf[1]) begin + // sign bit set indicating underflow across zero; saturate to zero + dst_err_int_next = {PHASE_ACC_W{1'b0}}; + end else if (dst_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + dst_err_int_next = {PHASE_ACC_W{1'b1}}; + end + + // compute output + case (dst_gain_sel_reg) + 1'd0: {dst_ovf, dst_phase_inc_next} = $signed({1'b0, dst_err_int_reg}) + ($signed(sample_acc_sync_reg) * 2**4); + 1'd1: {dst_ovf, dst_phase_inc_next} = $signed({1'b0, dst_err_int_reg}) + ($signed(sample_acc_sync_reg) * 2**11); + endcase + + // saturate + if (dst_ovf[1]) begin + // sign bit set indicating underflow across zero; saturate to zero + dst_phase_inc_next = {PHASE_ACC_W{1'b0}}; + end else if (dst_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + dst_phase_inc_next = {PHASE_ACC_W{1'b1}}; + end + + // locked status + if (dst_gain_sel_reg == 1'd0) begin + if (&dst_sync_lock_count_reg) begin + dst_sync_locked_next = 1'b1; + end else begin + dst_sync_lock_count_next = dst_sync_lock_count_reg + 1; + end + end else begin + if (|dst_sync_lock_count_reg) begin + dst_sync_lock_count_next = dst_sync_lock_count_reg - 1; + end else begin + dst_sync_locked_next = 1'b0; + end + end + end +end + +reg [LOAD_CNT_W-1:0] dst_load_cnt_reg = 0; + +reg [PHASE_ERR_ACC_W-1:0] phase_err_acc_reg = 0; +reg [PHASE_ERR_ACC_W-1:0] phase_err_out_reg = 0; +reg [LOG_PHASE_ERR_RATE-1:0] phase_err_cnt_reg = 0; +reg phase_err_out_valid_reg = 0; + +reg phase_last_src_reg = 1'b0; +reg phase_last_dst_reg = 1'b0; +reg phase_edge_1_reg = 1'b0; +reg phase_edge_2_reg = 1'b0; + +reg ts_sync_valid_reg = 1'b0; +reg ts_capt_valid_reg = 1'b0; + +always @(posedge clk) begin + dst_phase_reg <= dst_phase_next; + dst_phase_inc_reg <= dst_phase_inc_next; + dst_update_reg <= dst_update_next; + + sample_acc_sync_valid_reg <= 1'b0; + if (sample_update_sync2_reg ^ sample_update_sync3_reg) begin + // latch in synchronized counts from phase detector + sample_acc_sync_reg <= sample_acc_out_reg; + sample_acc_sync_valid_reg <= 1'b1; + end + + if (dst_update_reg) begin + // capture local TS + dst_ns_capt_reg <= {ts_rel_ns_reg, ts_fns_reg} >> (FNS_W-CMP_FNS_W); + + dst_sync_reg <= !dst_sync_reg; + ts_capt_valid_reg <= 1'b1; + + dst_load_cnt_reg <= dst_load_cnt_reg + 1; + end + + ts_sync_valid_reg <= 1'b0; + + if (src_sync_sync2_reg ^ src_sync_sync3_reg) begin + // store captured source TS + src_ns_sync_reg <= src_ns_reg >> (SRC_FNS_W-CMP_FNS_W); + + ts_sync_valid_reg <= ts_capt_valid_reg; + ts_capt_valid_reg <= 1'b0; + end + + if (src_marker_sync2_reg ^ src_marker_sync3_reg) begin + dst_load_cnt_reg <= 0; + end + + phase_err_out_valid_reg <= 1'b0; + if (ts_sync_valid_reg) begin + // coarse phase locking + + // phase and frequency detector + phase_last_src_reg <= src_ns_sync_reg[8+CMP_FNS_W]; + phase_last_dst_reg <= dst_ns_capt_reg[8+CMP_FNS_W]; + if (dst_ns_capt_reg[8+CMP_FNS_W] && !phase_last_dst_reg) begin + if (src_ns_sync_reg[8+CMP_FNS_W] && !phase_last_src_reg) begin + phase_edge_1_reg <= 1'b0; + phase_edge_2_reg <= 1'b0; + end else begin + phase_edge_1_reg <= !phase_edge_2_reg; + phase_edge_2_reg <= 1'b0; + end + end else if (src_ns_sync_reg[8+CMP_FNS_W] && !phase_last_src_reg) begin + phase_edge_1_reg <= 1'b0; + phase_edge_2_reg <= !phase_edge_1_reg; + end + + // accumulator + phase_err_acc_reg <= $signed(phase_err_acc_reg) + $signed({1'b0, phase_edge_2_reg}) - $signed({1'b0, phase_edge_1_reg}); + + phase_err_cnt_reg <= phase_err_cnt_reg + 1; + + if (phase_err_cnt_reg == 0) begin + phase_err_acc_reg <= $signed({1'b0, phase_edge_2_reg}) - $signed({1'b0, phase_edge_1_reg}); + phase_err_out_reg <= phase_err_acc_reg; + phase_err_out_valid_reg <= 1'b1; + end + end + + dst_err_int_reg <= dst_err_int_next; + + dst_sync_lock_count_reg <= dst_sync_lock_count_next; + dst_sync_locked_reg <= dst_sync_locked_next; + + dst_gain_sel_reg <= dst_gain_sel_next; + + if (rst) begin + dst_phase_reg <= {PHASE_ACC_W{1'b0}}; + dst_phase_inc_reg <= {PHASE_ACC_W{1'b0}}; + dst_sync_reg <= 1'b0; + dst_update_reg <= 1'b0; + + dst_err_int_reg <= 0; + + dst_sync_lock_count_reg <= 0; + dst_sync_locked_reg <= 1'b0; + + ts_sync_valid_reg <= 1'b0; + ts_capt_valid_reg <= 1'b0; + end +end + +reg dst_rel_step_shadow_reg = 1'b0, dst_rel_step_shadow_next; +reg [47:0] dst_rel_ns_shadow_reg = 0, dst_rel_ns_shadow_next = 0; +reg dst_rel_shadow_valid_reg = 0, dst_rel_shadow_valid_next; + +reg dst_tod_step_shadow_reg = 1'b0, dst_tod_step_shadow_next; +reg [29:0] dst_tod_ns_shadow_reg = 0, dst_tod_ns_shadow_next = 0; +reg [47:0] dst_tod_s_shadow_reg = 0, dst_tod_s_shadow_next = 0; +reg dst_tod_shadow_valid_reg = 0, dst_tod_shadow_valid_next; + +reg ts_rel_diff_reg = 1'b0, ts_rel_diff_next; +reg ts_rel_diff_valid_reg = 1'b0, ts_rel_diff_valid_next; +reg [1:0] ts_rel_mismatch_cnt_reg = 0, ts_rel_mismatch_cnt_next; +reg ts_rel_load_ts_reg = 1'b0, ts_rel_load_ts_next; + +reg ts_tod_diff_reg = 1'b0, ts_tod_diff_next; +reg ts_tod_diff_valid_reg = 1'b0, ts_tod_diff_valid_next; +reg [1:0] ts_tod_mismatch_cnt_reg = 0, ts_tod_mismatch_cnt_next; +reg ts_tod_load_ts_reg = 1'b0, ts_tod_load_ts_next; + +reg [9+CMP_FNS_W-1:0] ts_ns_diff_reg = 0, ts_ns_diff_next; +reg ts_ns_diff_valid_reg = 1'b0, ts_ns_diff_valid_next; + +reg [TIME_ERR_INT_W-1:0] time_err_int_reg = 0, time_err_int_next; + +reg [1:0] ptp_ovf; + +reg [FREQ_LOCK_W-1:0] freq_lock_count_reg = 0, freq_lock_count_next; +reg freq_locked_reg = 1'b0, freq_locked_next; +reg [PTP_LOCK_W-1:0] ptp_lock_count_reg = 0, ptp_lock_count_next; +reg ptp_locked_reg = 1'b0, ptp_locked_next; + +reg gain_sel_reg = 0, gain_sel_next; + +assign output_ts_rel = TS_REL_EN ? {ts_rel_ns_reg, ts_fns_reg, {TS_FNS_W{1'b0}}} >> FNS_W : 0; +assign output_ts_rel_step = TS_REL_EN ? ts_rel_step_reg : 0; + +assign output_ts_tod = TS_TOD_EN ? {ts_tod_s_reg, 2'b00, ts_tod_ns_reg, ts_fns_reg, {TS_FNS_W{1'b0}}} >> FNS_W : 0; +assign output_ts_tod_step = TS_TOD_EN ? ts_tod_step_reg : 0; + +assign output_pps = TS_TOD_EN ? pps_reg : 1'b0; +assign output_pps_str = TS_TOD_EN ? pps_str_reg : 1'b0; + +assign locked = ptp_locked_reg && freq_locked_reg && dst_sync_locked_reg; + +always @* begin + period_ns_next = period_ns_reg; + + ts_fns_next = ts_fns_reg; + + ts_rel_ns_next = ts_rel_ns_reg; + ts_rel_step_next = 1'b0; + + ts_tod_s_next = ts_tod_s_reg; + ts_tod_ns_next = ts_tod_ns_reg; + ts_tod_offset_ns_next = ts_tod_offset_ns_reg; + ts_tod_step_next = 1'b0; + + dst_rel_step_shadow_next = dst_rel_step_shadow_reg; + dst_rel_ns_shadow_next = dst_rel_ns_shadow_reg; + dst_rel_shadow_valid_next = dst_rel_shadow_valid_reg; + + dst_tod_step_shadow_next = dst_tod_step_shadow_reg; + dst_tod_ns_shadow_next = dst_tod_ns_shadow_reg; + dst_tod_s_shadow_next = dst_tod_s_shadow_reg; + dst_tod_shadow_valid_next = dst_tod_shadow_valid_reg; + + ts_rel_diff_next = ts_rel_diff_reg; + ts_rel_diff_valid_next = 1'b0; + ts_rel_mismatch_cnt_next = ts_rel_mismatch_cnt_reg; + ts_rel_load_ts_next = ts_rel_load_ts_reg; + + ts_tod_diff_next = ts_tod_diff_reg; + ts_tod_diff_valid_next = 1'b0; + ts_tod_mismatch_cnt_next = ts_tod_mismatch_cnt_reg; + ts_tod_load_ts_next = ts_tod_load_ts_reg; + + ts_ns_diff_next = ts_ns_diff_reg; + ts_ns_diff_valid_next = 1'b0; + + time_err_int_next = time_err_int_reg; + + freq_lock_count_next = freq_lock_count_reg; + freq_locked_next = freq_locked_reg; + ptp_lock_count_next = ptp_lock_count_reg; + ptp_locked_next = ptp_locked_reg; + + gain_sel_next = gain_sel_reg; + + pps_next = 1'b0; + pps_str_next = pps_str_reg; + + // extract data + if (dst_td_tvalid_reg) begin + if (TS_TOD_EN) begin + if (dst_td_tid_reg == {4'd0, 4'd1}) begin + dst_tod_ns_shadow_next[15:0] = dst_td_tdata_reg; + dst_tod_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg == {4'd0, 4'd2}) begin + dst_tod_ns_shadow_next[29:16] = dst_td_tdata_reg; + dst_tod_step_shadow_next = dst_tod_step_shadow_reg | dst_td_tdata_reg[15]; + dst_tod_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg == {4'd0, 4'd3}) begin + dst_tod_s_shadow_next[15:0] = dst_td_tdata_reg; + dst_tod_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg == {4'd0, 4'd4}) begin + dst_tod_s_shadow_next[31:16] = dst_td_tdata_reg; + dst_tod_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg == {4'd0, 4'd5}) begin + dst_tod_s_shadow_next[47:32] = dst_td_tdata_reg; + dst_tod_shadow_valid_next = 1'b1; + end + if (dst_td_tid_reg == {4'd1, 4'd1}) begin + ts_tod_offset_ns_next = dst_td_tdata_reg; + end + end + if (TS_REL_EN) begin + if (dst_td_tid_reg[3:0] == 4'd0) begin + dst_rel_step_shadow_next = dst_rel_step_shadow_reg | dst_td_tdata_reg[8]; + end + if (dst_td_tid_reg[3:0] == 4'd8) begin + dst_rel_ns_shadow_next[15:0] = dst_td_tdata_reg; + dst_rel_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg[3:0] == 4'd9) begin + dst_rel_ns_shadow_next[31:16] = dst_td_tdata_reg; + dst_rel_shadow_valid_next = 1'b0; + end + if (dst_td_tid_reg[3:0] == 4'd10) begin + dst_rel_ns_shadow_next[47:32] = dst_td_tdata_reg; + dst_rel_shadow_valid_next = 1'b1; + end + end + end + + // PTP clock + + // shared fractional ns + ts_fns_next = ts_fns_reg + period_ns_reg; + + // relative timestamp + ts_rel_ns_next = ({ts_rel_ns_reg, ts_fns_reg} + period_ns_reg) >> FNS_W; + + if (TS_REL_EN) begin + if (dst_update_reg && dst_rel_shadow_valid_reg && (dst_load_cnt_reg == {LOAD_CNT_W{1'b1}})) begin + // check timestamp MSBs + if (dst_rel_step_shadow_reg || ts_rel_load_ts_reg) begin + // input stepped + ts_rel_ns_next[TS_NS_W-1:9] = dst_rel_ns_shadow_reg[TS_NS_W-1:9]; + ts_rel_step_next = 1'b1; + end + ts_rel_diff_next = dst_rel_ns_shadow_reg[TS_NS_W-1:9] != ts_rel_ns_reg[TS_NS_W-1:9]; + + ts_rel_load_ts_next = 1'b0; + dst_rel_shadow_valid_next = 1'b0; + dst_rel_step_shadow_next = 1'b0; + ts_rel_diff_valid_next = 1'b1; + end + + if (ts_rel_diff_valid_reg) begin + if (ts_rel_diff_reg) begin + if (&ts_rel_mismatch_cnt_reg) begin + ts_rel_load_ts_next = 1'b1; + ts_rel_mismatch_cnt_next = 0; + end else begin + ts_rel_mismatch_cnt_next = ts_rel_mismatch_cnt_reg + 1; + end + end else begin + ts_rel_mismatch_cnt_next = 0; + end + end + end + + if (TS_TOD_EN) begin + // absolute time-of-day timestamp + ts_tod_ns_next[8:0] = ts_rel_ns_next[8:0] + ts_tod_offset_ns_reg; + + if (ts_tod_ns_reg[TS_TOD_NS_W-1]) begin + pps_str_next = 1'b0; + end + + if (!ts_tod_ns_next[8] && ts_tod_ns_reg[8]) begin + if (ts_tod_ns_reg >> 9 == NS_PER_S-1 >> 9) begin + ts_tod_ns_next[TS_TOD_NS_W-1:9] = 0; + ts_tod_s_next = ts_tod_s_reg + 1; + pps_next = 1'b1; + pps_str_next = 1'b1; + end else begin + ts_tod_ns_next[TS_TOD_NS_W-1:9] = ts_tod_ns_reg[TS_TOD_NS_W-1:9] + 1; + end + end + + if (dst_update_reg && dst_tod_shadow_valid_reg && (dst_load_cnt_reg == {LOAD_CNT_W{1'b1}})) begin + // check timestamp MSBs + if (dst_tod_step_shadow_reg || ts_tod_load_ts_reg) begin + // input stepped + ts_tod_s_next = dst_tod_s_shadow_reg; + ts_tod_ns_next[TS_TOD_NS_W-1:9] = dst_tod_ns_shadow_reg[TS_TOD_NS_W-1:9]; + ts_tod_step_next = 1'b1; + end + ts_tod_diff_next = dst_tod_s_shadow_reg != ts_tod_s_reg || dst_tod_ns_shadow_reg[TS_TOD_NS_W-1:9] != ts_tod_ns_reg[TS_TOD_NS_W-1:9]; + + ts_tod_load_ts_next = 1'b0; + dst_tod_shadow_valid_next = 1'b0; + dst_tod_step_shadow_next = 1'b0; + ts_tod_diff_valid_next = 1'b1; + end + + if (ts_tod_diff_valid_reg) begin + if (ts_tod_diff_reg) begin + if (&ts_tod_mismatch_cnt_reg) begin + ts_tod_load_ts_next = 1'b1; + ts_tod_mismatch_cnt_next = 0; + end else begin + ts_tod_mismatch_cnt_next = ts_tod_mismatch_cnt_reg + 1; + end + end else begin + ts_tod_mismatch_cnt_next = 0; + end + end + end + + if (ts_sync_valid_reg) begin + // compute difference + ts_ns_diff_valid_next = freq_locked_reg; + ts_ns_diff_next = src_ns_sync_reg - dst_ns_capt_reg; + end + + if (phase_err_out_valid_reg) begin + // coarse phase/frequency lock of PTP clock + if ($signed(phase_err_out_reg) > 4 || $signed(phase_err_out_reg) < -4) begin + if (freq_lock_count_reg) begin + freq_lock_count_next = freq_lock_count_reg - 1; + end else begin + freq_locked_next = 1'b0; + end + end else begin + if (&freq_lock_count_reg) begin + freq_locked_next = 1'b1; + end else begin + freq_lock_count_next = freq_lock_count_reg + 1; + end + end + + if (!freq_locked_reg) begin + ts_ns_diff_next = $signed(phase_err_out_reg) * 16 * 2**CMP_FNS_W; + ts_ns_diff_valid_next = 1'b1; + end + end + + if (ts_ns_diff_valid_reg) begin + // PI control + + // gain scheduling + if (!ts_ns_diff_reg[8+CMP_FNS_W]) begin + if (ts_ns_diff_reg[4+CMP_FNS_W +: 4]) begin + gain_sel_next = 1'b1; + end else begin + gain_sel_next = 1'b0; + end + end else begin + if (~ts_ns_diff_reg[4+CMP_FNS_W +: 4]) begin + gain_sel_next = 1'b1; + end else begin + gain_sel_next = 1'b0; + end + end + + // time integral of error + case (gain_sel_reg) + 1'b0: {ptp_ovf, time_err_int_next} = $signed({1'b0, time_err_int_reg}) + ($signed(ts_ns_diff_reg) / 2**4); + 1'b1: {ptp_ovf, time_err_int_next} = $signed({1'b0, time_err_int_reg}) + ($signed(ts_ns_diff_reg) * 2**2); + endcase + + // saturate + if (ptp_ovf[1]) begin + // sign bit set indicating underflow across zero; saturate to zero + time_err_int_next = {TIME_ERR_INT_W{1'b0}}; + end else if (ptp_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + time_err_int_next = {TIME_ERR_INT_W{1'b1}}; + end + + // compute output + case (gain_sel_reg) + 1'b0: {ptp_ovf, period_ns_next} = $signed({1'b0, time_err_int_reg}) + ($signed(ts_ns_diff_reg) * 2**2); + 1'b1: {ptp_ovf, period_ns_next} = $signed({1'b0, time_err_int_reg}) + ($signed(ts_ns_diff_reg) * 2**6); + endcase + + // saturate + if (ptp_ovf[1]) begin + // sign bit set indicating underflow across zero; saturate to zero + period_ns_next = {PERIOD_NS_W+FNS_W{1'b0}}; + end else if (ptp_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + period_ns_next = {PERIOD_NS_W+FNS_W{1'b1}}; + end + + // adjust period if integrator is saturated + if (time_err_int_reg == 0) begin + period_ns_next = {PERIOD_NS_W+FNS_W{1'b0}}; + end else if (~time_err_int_reg == 0) begin + period_ns_next = {PERIOD_NS_W+FNS_W{1'b1}}; + end + + // locked status + if (!freq_locked_reg) begin + ptp_lock_count_next = 0; + ptp_locked_next = 1'b0; + end else if (gain_sel_reg == 1'b0) begin + if (&ptp_lock_count_reg) begin + ptp_locked_next = 1'b1; + end else begin + ptp_lock_count_next = ptp_lock_count_reg + 1; + end + end else begin + if (ptp_lock_count_reg) begin + ptp_lock_count_next = ptp_lock_count_reg - 1; + end else begin + ptp_locked_next = 1'b0; + end + end + end +end + +always @(posedge clk) begin + period_ns_reg <= period_ns_next; + + ts_fns_reg <= ts_fns_next; + + ts_rel_ns_reg <= ts_rel_ns_next; + ts_rel_step_reg <= ts_rel_step_next; + + ts_tod_s_reg <= ts_tod_s_next; + ts_tod_ns_reg <= ts_tod_ns_next; + ts_tod_offset_ns_reg <= ts_tod_offset_ns_next; + ts_tod_step_reg <= ts_tod_step_next; + + dst_rel_step_shadow_reg <= dst_rel_step_shadow_next; + dst_rel_ns_shadow_reg <= dst_rel_ns_shadow_next; + dst_rel_shadow_valid_reg <= dst_rel_shadow_valid_next; + + dst_tod_step_shadow_reg <= dst_tod_step_shadow_next; + dst_tod_ns_shadow_reg <= dst_tod_ns_shadow_next; + dst_tod_s_shadow_reg <= dst_tod_s_shadow_next; + dst_tod_shadow_valid_reg <= dst_tod_shadow_valid_next; + + ts_rel_diff_reg <= ts_rel_diff_next; + ts_rel_diff_valid_reg <= ts_rel_diff_valid_next; + ts_rel_mismatch_cnt_reg <= ts_rel_mismatch_cnt_next; + ts_rel_load_ts_reg <= ts_rel_load_ts_next; + + ts_tod_diff_reg <= ts_tod_diff_next; + ts_tod_diff_valid_reg <= ts_tod_diff_valid_next; + ts_tod_mismatch_cnt_reg <= ts_tod_mismatch_cnt_next; + ts_tod_load_ts_reg <= ts_tod_load_ts_next; + + ts_ns_diff_reg <= ts_ns_diff_next; + ts_ns_diff_valid_reg <= ts_ns_diff_valid_next; + + time_err_int_reg <= time_err_int_next; + + freq_lock_count_reg <= freq_lock_count_next; + freq_locked_reg <= freq_locked_next; + ptp_lock_count_reg <= ptp_lock_count_next; + ptp_locked_reg <= ptp_locked_next; + + gain_sel_reg <= gain_sel_next; + + pps_reg <= pps_next; + pps_str_reg <= pps_str_next; + + if (rst) begin + period_ns_reg <= 0; + ts_fns_reg <= 0; + ts_rel_ns_reg <= 0; + ts_rel_step_reg <= 1'b0; + ts_tod_s_reg <= 0; + ts_tod_ns_reg <= 0; + ts_tod_step_reg <= 1'b0; + dst_rel_shadow_valid_reg <= 1'b0; + pps_reg <= 1'b0; + pps_str_reg <= 1'b0; + + ts_rel_diff_reg <= 1'b0; + ts_rel_diff_valid_reg <= 1'b0; + ts_rel_mismatch_cnt_reg <= 0; + ts_rel_load_ts_reg <= 0; + + ts_tod_diff_reg <= 1'b0; + ts_tod_diff_valid_reg <= 1'b0; + ts_tod_mismatch_cnt_reg <= 0; + ts_tod_load_ts_reg <= 0; + + ts_ns_diff_valid_reg <= 1'b0; + + time_err_int_reg <= 0; + + freq_lock_count_reg <= 0; + freq_locked_reg <= 1'b0; + ptp_lock_count_reg <= 0; + ptp_locked_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/rtl/ptp_td_phc.v b/rtl/ptp_td_phc.v new file mode 100644 index 000000000..4e687b490 --- /dev/null +++ b/rtl/ptp_td_phc.v @@ -0,0 +1,603 @@ +/* + +Copyright (c) 2023 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 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * PTP time distribution PHC + */ +module ptp_td_phc # +( + parameter PERIOD_NS_NUM = 32, + parameter PERIOD_NS_DENOM = 5 +) +( + input wire clk, + input wire rst, + + /* + * ToD timestamp control + */ + input wire [47:0] input_ts_tod_s, + input wire [29:0] input_ts_tod_ns, + input wire input_ts_tod_valid, + output wire input_ts_tod_ready, + input wire [29:0] input_ts_tod_offset_ns, + input wire input_ts_tod_offset_valid, + output wire input_ts_tod_offset_ready, + + /* + * Relative timestamp control + */ + input wire [47:0] input_ts_rel_ns, + input wire input_ts_rel_valid, + output wire input_ts_rel_ready, + input wire [31:0] input_ts_rel_offset_ns, + input wire input_ts_rel_offset_valid, + output wire input_ts_rel_offset_ready, + + /* + * Fractional ns control + */ + input wire [31:0] input_ts_offset_fns, + input wire input_ts_offset_valid, + output wire input_ts_offset_ready, + + /* + * Period control + */ + input wire [7:0] input_period_ns, + input wire [31:0] input_period_fns, + input wire input_period_valid, + output wire input_period_ready, + input wire [15:0] input_drift_num, + input wire [15:0] input_drift_denom, + input wire input_drift_valid, + output wire input_drift_ready, + + /* + * Time distribution serial data output + */ + output wire ptp_td_sdo, + + /* + * PPS output + */ + output wire output_pps, + output wire output_pps_str +); + +localparam INC_NS_W = 9+8; + +localparam FNS_W = 32; + +localparam PERIOD_NS = PERIOD_NS_NUM / PERIOD_NS_DENOM; +localparam PERIOD_NS_REM = PERIOD_NS_NUM - PERIOD_NS*PERIOD_NS_DENOM; +localparam PERIOD_FNS = (PERIOD_NS_REM * {32'd1, {FNS_W{1'b0}}}) / PERIOD_NS_DENOM; +localparam PERIOD_FNS_REM = (PERIOD_NS_REM * {32'd1, {FNS_W{1'b0}}}) - PERIOD_FNS*PERIOD_NS_DENOM; + +localparam [30:0] NS_PER_S = 31'd1_000_000_000; + +reg [7:0] period_ns_reg = PERIOD_NS; +reg [FNS_W-1:0] period_fns_reg = PERIOD_FNS; + +reg [15:0] drift_num_reg = PERIOD_FNS_REM; +reg [15:0] drift_denom_reg = PERIOD_NS_DENOM; +reg [15:0] drift_cnt_reg = 0; +reg [15:0] drift_cnt_d1_reg = 0; +reg drift_apply_reg = 1'b0; +reg [23:0] drift_acc_reg = 0; + +reg [INC_NS_W-1:0] ts_inc_ns_reg = 0; +reg [FNS_W-1:0] ts_fns_reg = 0; + +reg [32:0] ts_rel_ns_inc_reg = 0; +reg [47:0] ts_rel_ns_reg = 0; +reg ts_rel_updated_reg = 1'b0; + +reg [47:0] ts_tod_s_reg = 0; +reg [29:0] ts_tod_ns_reg = 0; +reg ts_tod_updated_reg = 1'b0; + +reg [31:0] ts_tod_offset_ns_reg = 0; + +reg [47:0] ts_tod_alt_s_reg = 0; +reg [31:0] ts_tod_alt_offset_ns_reg = 0; + +reg [7:0] td_update_cnt_reg = 0; +reg td_update_reg = 1'b0; +reg [1:0] td_msg_i_reg = 0; + +reg input_ts_tod_ready_reg = 1'b0; +reg input_ts_tod_offset_ready_reg = 1'b0; +reg input_ts_rel_ready_reg = 1'b0; +reg input_ts_rel_offset_ready_reg = 1'b0; +reg input_ts_offset_ready_reg = 1'b0; + +reg [17*14-1:0] td_shift_reg = {17*14{1'b1}}; + +reg [15:0] pps_gen_fns_reg = 0; +reg [9:0] pps_gen_ns_inc_reg = 0; +reg [30:0] pps_gen_ns_reg = 31'h40000000; + +reg [9:0] pps_delay_reg = 0; +reg pps_reg = 0; +reg pps_str_reg = 0; + +reg [3:0] update_state_reg = 0; + +reg [47:0] adder_a_reg = 0; +reg [47:0] adder_b_reg = 0; +reg adder_cin_reg = 0; +wire [47:0] adder_sum; +wire adder_cout; + +assign {adder_cout, adder_sum} = adder_a_reg + adder_b_reg + adder_cin_reg; + +assign input_ts_tod_ready = input_ts_tod_ready_reg; +assign input_ts_tod_offset_ready = input_ts_tod_offset_ready_reg; +assign input_ts_rel_ready = input_ts_rel_ready_reg; +assign input_ts_rel_offset_ready = input_ts_rel_offset_ready_reg; +assign input_ts_offset_ready = input_ts_offset_ready_reg; + +assign input_period_ready = 1'b1; +assign input_drift_ready = 1'b1; + +assign output_pps = pps_reg; +assign output_pps_str = pps_str_reg; + +assign ptp_td_sdo = td_shift_reg[0]; + +always @(posedge clk) begin + drift_apply_reg <= 1'b0; + + input_ts_tod_ready_reg <= 1'b0; + input_ts_tod_offset_ready_reg <= 1'b0; + input_ts_rel_ready_reg <= 1'b0; + input_ts_rel_offset_ready_reg <= 1'b0; + input_ts_offset_ready_reg <= 1'b0; + + // update and message generation cadence + {td_update_reg, td_update_cnt_reg} <= td_update_cnt_reg + 1; + + // latch drift setting + if (input_drift_valid) begin + drift_num_reg <= input_drift_num; + drift_denom_reg <= input_drift_denom; + end + + // drift + if (drift_denom_reg) begin + if (drift_cnt_reg == 0) begin + drift_cnt_reg <= drift_denom_reg - 1; + drift_apply_reg <= 1'b1; + end else begin + drift_cnt_reg <= drift_cnt_reg - 1; + end + end else begin + drift_cnt_reg <= 0; + end + + drift_cnt_d1_reg <= drift_cnt_reg; + + // drift accumulation + if (drift_apply_reg) begin + drift_acc_reg <= drift_acc_reg + drift_num_reg; + end + + // latch period setting + if (input_period_valid) begin + period_ns_reg <= input_period_ns; + period_fns_reg <= input_period_fns; + end + + // PPS generation + if (td_update_reg) begin + {pps_gen_ns_inc_reg, pps_gen_fns_reg} <= {period_ns_reg, period_fns_reg[31:16]} + ts_fns_reg[31:16]; + end else begin + {pps_gen_ns_inc_reg, pps_gen_fns_reg} <= {period_ns_reg, period_fns_reg[31:16]} + pps_gen_fns_reg; + end + pps_gen_ns_reg <= pps_gen_ns_reg + pps_gen_ns_inc_reg; + + if (!pps_gen_ns_reg[30]) begin + // pps_delay_reg <= 14*17 + 32 + 1; + pps_delay_reg <= 14*17 + 32 + 248; + pps_gen_ns_reg[30] <= 1'b1; + end + + pps_reg <= 1'b0; + + if (ts_tod_ns_reg[29]) begin + pps_str_reg <= 1'b0; + end + + if (pps_delay_reg) begin + pps_delay_reg <= pps_delay_reg - 1; + if (pps_delay_reg == 1) begin + pps_reg <= 1'b1; + pps_str_reg <= 1'b1; + end + end + + // update state machine + case (update_state_reg) + 0: begin + // idle + + // set relative timestamp + if (input_ts_rel_valid) begin + ts_rel_ns_reg <= input_ts_rel_ns; + input_ts_rel_ready_reg <= 1'b1; + ts_rel_updated_reg <= 1'b1; + end + + // set ToD timestamp + if (input_ts_tod_valid) begin + ts_tod_s_reg <= input_ts_tod_s; + ts_tod_ns_reg <= input_ts_tod_ns; + input_ts_tod_ready_reg <= 1'b1; + ts_tod_updated_reg <= 1'b1; + end + + // compute period 1 - add drift and requested offset + if (drift_apply_reg) begin + adder_a_reg <= drift_acc_reg + drift_num_reg; + end else begin + adder_a_reg <= drift_acc_reg; + end + adder_b_reg <= input_ts_offset_valid ? $signed(input_ts_offset_fns) : 0; + adder_cin_reg <= 0; + + if (td_update_reg) begin + drift_acc_reg <= 0; + input_ts_offset_ready_reg <= input_ts_offset_valid; + update_state_reg <= 1; + end else begin + update_state_reg <= 0; + end + end + 1: begin + // compute period 2 - add drift and offset to period + adder_a_reg <= adder_sum; + adder_b_reg <= {period_ns_reg, period_fns_reg, 8'd0}; + adder_cin_reg <= 0; + + update_state_reg <= 2; + end + 2: begin + // compute next fns + adder_a_reg <= adder_sum; + adder_b_reg <= ts_fns_reg; + adder_cin_reg <= 0; + + update_state_reg <= 3; + end + 3: begin + // store fns; compute relative timestamp 1 - add previous value and offset + {ts_inc_ns_reg, ts_fns_reg} <= {adder_cout, adder_sum}; + + adder_a_reg <= ts_rel_ns_reg; + adder_b_reg <= 0; + adder_cin_reg <= 0; + + // offset relative timestamp if requested + if (input_ts_rel_offset_valid) begin + adder_b_reg <= $signed(input_ts_rel_offset_ns); + input_ts_rel_offset_ready_reg <= 1'b1; + ts_rel_updated_reg <= 1'b1; + end + + update_state_reg <= 4; + end + 4: begin + // compute relative timestamp 2 - add increment + adder_a_reg <= adder_sum; + adder_b_reg <= ts_inc_ns_reg; + adder_cin_reg <= 0; + + update_state_reg <= 5; + end + 5: begin + // store relative timestamp; compute ToD timestamp 1 - add previous value and increment + ts_rel_ns_reg <= adder_sum; + + adder_a_reg <= ts_tod_ns_reg; + adder_b_reg <= ts_inc_ns_reg; + adder_cin_reg <= 0; + + update_state_reg <= 6; + end + 6: begin + // compute ToD timestamp 2 - add offset + adder_a_reg <= adder_sum; + adder_b_reg <= 0; + adder_cin_reg <= 0; + + // offset ToD timestamp if requested + if (input_ts_tod_offset_valid) begin + adder_b_reg <= $signed(input_ts_tod_offset_ns); + input_ts_tod_offset_ready_reg <= 1'b1; + ts_tod_updated_reg <= 1'b1; + end + + update_state_reg <= 7; + end + 7: begin + // compute ToD timestamp 2 - check for underflow/overflow + ts_tod_ns_reg <= adder_sum; + + if (adder_b_reg[47] && !adder_cout) begin + // borrowed; add 1 billion + adder_a_reg <= adder_sum; + adder_b_reg <= NS_PER_S; + adder_cin_reg <= 0; + + update_state_reg <= 8; + end else begin + // did not borrow; subtract 1 billion to check for overflow + adder_a_reg <= adder_sum; + adder_b_reg <= -NS_PER_S; + adder_cin_reg <= 0; + + update_state_reg <= 9; + end + end + 8: begin + // seconds decrement + ts_tod_ns_reg <= adder_sum; + pps_gen_ns_reg[30] <= 1'b1; + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= -1; + adder_cin_reg <= 0; + + update_state_reg <= 10; + end + 9: begin + // seconds increment + pps_gen_ns_reg <= adder_sum; + + if (!adder_cout) begin + // borrowed; leave seconds alone + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 0; + adder_cin_reg <= 0; + end else begin + // did not borrow; decrement seconds + ts_tod_ns_reg <= adder_sum; + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 1; + adder_cin_reg <= 0; + end + + update_state_reg <= 10; + end + 10: begin + // store seconds; compute offset + ts_tod_s_reg <= adder_sum; + + if (adder_sum == ts_tod_alt_s_reg) begin + // store previous offset as alternate + ts_tod_alt_s_reg <= ts_tod_s_reg; + ts_tod_alt_offset_ns_reg <= ts_tod_offset_ns_reg; + end + + adder_a_reg <= ts_tod_ns_reg; + adder_b_reg <= ~ts_rel_ns_reg; + adder_cin_reg <= 1; + + update_state_reg <= 11; + end + 11: begin + // store offset + ts_tod_offset_ns_reg <= adder_sum; + + adder_a_reg <= adder_sum; + adder_b_reg <= -NS_PER_S; + adder_cin_reg <= 0; + + if (ts_tod_ns_reg[29]) begin + // latter half of second; compute offset for next second + adder_b_reg <= -NS_PER_S; + update_state_reg <= 12; + end else begin + // former half of second; compute offset for previous second + adder_b_reg <= NS_PER_S; + update_state_reg <= 14; + end + end + 12: begin + // store alternate offset for next second + ts_tod_alt_offset_ns_reg <= adder_sum; + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 1; + adder_cin_reg <= 0; + + update_state_reg <= 13; + end + 13: begin + // store alternate second for next second + ts_tod_alt_s_reg <= adder_sum; + + update_state_reg <= 0; + end + 14: begin + // store alternate offset for previous second + ts_tod_alt_offset_ns_reg <= adder_sum; + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= -1; + adder_cin_reg <= 0; + + update_state_reg <= 15; + end + 15: begin + // store alternate second for previous second + ts_tod_alt_s_reg <= adder_sum; + + update_state_reg <= 0; + end + default: begin + // invalid state; return to idle + update_state_reg <= 0; + end + endcase + + // time distribution message generation + td_shift_reg <= {1'b1, td_shift_reg} >> 1; + + if (td_update_reg) begin + // word 0: control + td_shift_reg[17*0+0 +: 1] <= 1'b0; + td_shift_reg[17*0+1 +: 16] <= 0; + td_shift_reg[17*0+1+0 +: 4] <= td_msg_i_reg; + td_shift_reg[17*0+1+8 +: 1] <= ts_rel_updated_reg; + td_shift_reg[17*0+1+9 +: 1] <= ts_tod_s_reg[0]; + ts_rel_updated_reg <= 1'b0; + + case (td_msg_i_reg) + 2'd0: begin + // msg 0 word 1: current ToD ns 15:0 + td_shift_reg[17*1+0 +: 1] <= 1'b0; + td_shift_reg[17*1+1 +: 16] <= ts_tod_ns_reg[15:0]; + // msg 0 word 2: current ToD ns 29:16 + td_shift_reg[17*2+0 +: 1] <= 1'b0; + td_shift_reg[17*2+1+0 +: 15] <= ts_tod_ns_reg[29:16]; + td_shift_reg[17*2+1+15 +: 1] <= ts_tod_updated_reg; + ts_tod_updated_reg <= 1'b0; + // msg 0 word 3: current ToD seconds 15:0 + td_shift_reg[17*3+0 +: 1] <= 1'b0; + td_shift_reg[17*3+1 +: 16] <= ts_tod_s_reg[15:0]; + // msg 0 word 4: current ToD seconds 31:16 + td_shift_reg[17*4+0 +: 1] <= 1'b0; + td_shift_reg[17*4+1 +: 16] <= ts_tod_s_reg[31:16]; + // msg 0 word 5: current ToD seconds 47:32 + td_shift_reg[17*5+0 +: 1] <= 1'b0; + td_shift_reg[17*5+1 +: 16] <= ts_tod_s_reg[47:32]; + + td_msg_i_reg <= 2'd1; + end + 2'd1: begin + // msg 1 word 1: current ToD ns offset 15:0 + td_shift_reg[17*1+0 +: 1] <= 1'b0; + td_shift_reg[17*1+1 +: 16] <= ts_tod_offset_ns_reg[15:0]; + // msg 1 word 2: current ToD ns offset 31:16 + td_shift_reg[17*2+0 +: 1] <= 1'b0; + td_shift_reg[17*2+1 +: 16] <= ts_tod_offset_ns_reg[31:16]; + // msg 1 word 3: drift num + td_shift_reg[17*3+0 +: 1] <= 1'b0; + td_shift_reg[17*3+1 +: 16] <= drift_num_reg; + // msg 1 word 4: drift denom + td_shift_reg[17*4+0 +: 1] <= 1'b0; + td_shift_reg[17*4+1 +: 16] <= drift_denom_reg; + // msg 1 word 5: drift state + td_shift_reg[17*5+0 +: 1] <= 1'b0; + td_shift_reg[17*5+1 +: 16] <= drift_cnt_d1_reg; + + td_msg_i_reg <= 2'd2; + end + 2'd2: begin + // msg 2 word 1: alternate ToD ns offset 15:0 + td_shift_reg[17*1+0 +: 1] <= 1'b0; + td_shift_reg[17*1+1 +: 16] <= ts_tod_alt_offset_ns_reg[15:0]; + // msg 2 word 2: alternate ToD ns offset 31:16 + td_shift_reg[17*2+0 +: 1] <= 1'b0; + td_shift_reg[17*2+1 +: 16] <= ts_tod_alt_offset_ns_reg[31:16]; + // msg 2 word 3: alternate ToD seconds 15:0 + td_shift_reg[17*3+0 +: 1] <= 1'b0; + td_shift_reg[17*3+1 +: 16] <= ts_tod_alt_s_reg[15:0]; + // msg 2 word 4: alternate ToD seconds 31:16 + td_shift_reg[17*4+0 +: 1] <= 1'b0; + td_shift_reg[17*4+1 +: 16] <= ts_tod_alt_s_reg[31:16]; + // msg 2 word 5: alternate ToD seconds 47:32 + td_shift_reg[17*5+0 +: 1] <= 1'b0; + td_shift_reg[17*5+1 +: 16] <= ts_tod_alt_s_reg[47:32]; + + td_msg_i_reg <= 2'd0; + end + endcase + + // word 6: current fns 15:0 + td_shift_reg[17*6+0 +: 1] <= 1'b0; + td_shift_reg[17*6+1 +: 16] <= ts_fns_reg[15:0]; + // word 7: current fns 31:16 + td_shift_reg[17*7+0 +: 1] <= 1'b0; + td_shift_reg[17*7+1 +: 16] <= ts_fns_reg[31:16]; + // word 8: current ns 15:0 + td_shift_reg[17*8+0 +: 1] <= 1'b0; + td_shift_reg[17*8+1 +: 16] <= ts_rel_ns_reg[15:0]; + // word 9: current ns 31:16 + td_shift_reg[17*9+0 +: 1] <= 1'b0; + td_shift_reg[17*9+1 +: 16] <= ts_rel_ns_reg[31:16]; + // word 10: current ns 47:32 + td_shift_reg[17*10+0 +: 1] <= 1'b0; + td_shift_reg[17*10+1 +: 16] <= ts_rel_ns_reg[47:32]; + // word 11: current phase increment fns 15:0 + td_shift_reg[17*11+0 +: 1] <= 1'b0; + td_shift_reg[17*11+1 +: 16] <= period_fns_reg[15:0]; + // word 12: current phase increment fns 31:16 + td_shift_reg[17*12+0 +: 1] <= 1'b0; + td_shift_reg[17*12+1 +: 16] <= period_fns_reg[31:16]; + // word 13: current phase increment ns 7:0 + crc + td_shift_reg[17*13+0 +: 1] <= 1'b0; + td_shift_reg[17*13+1 +: 16] <= period_ns_reg[7:0]; + end + + if (rst) begin + period_ns_reg <= PERIOD_NS; + period_fns_reg <= PERIOD_FNS; + drift_num_reg <= PERIOD_FNS_REM; + drift_denom_reg <= PERIOD_NS_DENOM; + drift_cnt_reg <= 0; + drift_acc_reg <= 0; + ts_fns_reg <= 0; + ts_rel_ns_reg <= 0; + ts_rel_updated_reg <= 0; + ts_tod_s_reg <= 0; + ts_tod_ns_reg <= 0; + ts_tod_updated_reg <= 0; + + pps_gen_ns_reg[30] <= 1'b1; + pps_delay_reg <= 0; + pps_reg <= 0; + pps_str_reg <= 0; + + td_update_cnt_reg <= 0; + td_update_reg <= 1'b0; + td_msg_i_reg <= 0; + + td_shift_reg <= {17*14{1'b1}}; + end +end + +endmodule + +`resetall diff --git a/syn/vivado/ptp_td_leaf.tcl b/syn/vivado/ptp_td_leaf.tcl new file mode 100644 index 000000000..65f641a4e --- /dev/null +++ b/syn/vivado/ptp_td_leaf.tcl @@ -0,0 +1,104 @@ +# Copyright (c) 2019-2023 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. + +# PTP time distribution leaf module + +foreach inst [get_cells -hier -filter {(ORIG_REF_NAME == ptp_td_leaf || REF_NAME == ptp_td_leaf)}] { + puts "Inserting timing constraints for ptp_td_leaf instance $inst" + + # get clock periods + set input_clk [get_clocks -of_objects [get_pins "$inst/src_sync_reg_reg/C"]] + set output_clk [get_clocks -of_objects [get_pins "$inst/dst_sync_reg_reg/C"]] + + set input_clk_period [if {[llength $input_clk]} {get_property -min PERIOD $input_clk} {expr 1.0}] + set output_clk_period [if {[llength $output_clk]} {get_property -min PERIOD $output_clk} {expr 1.0}] + + # TD data sync + set_property ASYNC_REG TRUE [get_cells -hier -regexp ".*/dst_td_(tdata|tid)_reg_reg(\\\[\\d+\\\])?" -filter "PARENT == $inst"] + + set_max_delay -from [get_cells "$inst/td_tdata_reg_reg[*]"] -to [get_cells "$inst/dst_td_tdata_reg_reg[*]"] -datapath_only $output_clk_period + set_bus_skew -from [get_cells "$inst/td_tdata_reg_reg[*]"] -to [get_cells "$inst/dst_td_tdata_reg_reg[*]"] $input_clk_period + set_max_delay -from [get_cells "$inst/td_tid_reg_reg[*]"] -to [get_cells "$inst/dst_td_tid_reg_reg[*]"] -datapath_only $output_clk_period + set_bus_skew -from [get_cells "$inst/td_tid_reg_reg[*]"] -to [get_cells "$inst/dst_td_tid_reg_reg[*]"] $input_clk_period + + set sync_ffs [get_cells -quiet -hier -regexp ".*/td_sync_sync\[12\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set_max_delay -from [get_cells "$inst/td_sync_reg_reg"] -to [get_cells "$inst/td_sync_sync1_reg_reg"] -datapath_only $input_clk_period + } + + # timestamp sync + set_property ASYNC_REG TRUE [get_cells -hier -regexp ".*/src_ns_sync_reg_reg(\\\[\\d+\\\])?" -filter "PARENT == $inst"] + + set_max_delay -from [get_cells "$inst/src_ns_reg_reg[*]"] -to [get_cells "$inst/src_ns_sync_reg_reg[*]"] -datapath_only $output_clk_period + set_bus_skew -from [get_cells "$inst/src_ns_reg_reg[*]"] -to [get_cells "$inst/src_ns_sync_reg_reg[*]"] $input_clk_period + + # sample clock + set sync_ffs [get_cells -quiet -hier -regexp ".*/src_sync_sample_sync\[12\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set_max_delay -from [get_cells "$inst/src_sync_reg_reg"] -to [get_cells "$inst/src_sync_sample_sync1_reg_reg"] -datapath_only $input_clk_period + } + + set sync_ffs [get_cells -quiet -hier -regexp ".*/dst_sync_sample_sync\[12\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set_max_delay -from [get_cells "$inst/dst_sync_reg_reg"] -to [get_cells "$inst/dst_sync_sample_sync1_reg_reg"] -datapath_only $output_clk_period + } + + # sample update sync + set sync_ffs [get_cells -quiet -hier -regexp ".*/sample_update_sync\[123\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set src_clk [get_clocks -of_objects [get_pins "$inst/sample_update_reg_reg/C"]] + + set src_clk_period [if {[llength $src_clk]} {get_property -min PERIOD $src_clk} {expr 1.0}] + + set_max_delay -from [get_cells "$inst/sample_update_reg_reg"] -to [get_cells "$inst/sample_update_sync1_reg_reg"] -datapath_only $src_clk_period + + set_max_delay -from [get_cells "$inst/sample_acc_out_reg_reg[*]"] -to [get_cells $inst/sample_acc_sync_reg_reg[*]] -datapath_only $src_clk_period + set_bus_skew -from [get_cells "$inst/sample_acc_out_reg_reg[*]"] -to [get_cells $inst/sample_acc_sync_reg_reg[*]] $output_clk_period + } + + # timestamp transfer sync + set sync_ffs [get_cells -quiet -hier -regexp ".*/src_sync_sync\[12\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set_max_delay -from [get_cells "$inst/src_sync_reg_reg"] -to [get_cells "$inst/src_sync_sync1_reg_reg"] -datapath_only $input_clk_period + } + + set sync_ffs [get_cells -quiet -hier -regexp ".*/src_marker_sync\[12\]_reg_reg" -filter "PARENT == $inst"] + + if {[llength $sync_ffs]} { + set_property ASYNC_REG TRUE $sync_ffs + + set_max_delay -from [get_cells "$inst/src_marker_reg_reg"] -to [get_cells "$inst/src_marker_sync1_reg_reg"] -datapath_only $input_clk_period + } +} diff --git a/tb/ptp_td.py b/tb/ptp_td.py new file mode 100644 index 000000000..e90aafb95 --- /dev/null +++ b/tb/ptp_td.py @@ -0,0 +1,604 @@ +""" + +Copyright (c) 2023 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 +from decimal import Decimal, Context +from fractions import Fraction + +import cocotb +from cocotb.triggers import RisingEdge, Event +from cocotb.utils import get_sim_time + +from cocotbext.eth.reset import Reset + + +class PtpTdSource(Reset): + def __init__(self, + data=None, + clock=None, + reset=None, + reset_active_level=True, + period_ns=6.4, + td_delay=32, + *args, **kwargs): + + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.clock = clock + self.reset = reset + + self.log.info("PTP time distribution source") + self.log.info("Copyright (c) 2023 Alex Forencich") + self.log.info("https://github.com/alexforencich/verilog-ethernet") + + super().__init__(*args, **kwargs) + + self.ctx = Context(prec=60) + + self.period_ns = 0 + self.period_fns = 0 + self.drift_num = 0 + self.drift_denom = 0 + self.drift_cnt = 0 + self.set_period_ns(period_ns) + + self.ts_fns = 0 + + self.ts_rel_ns = 0 + self.ts_rel_updated = False + + self.ts_tod_s = 0 + self.ts_tod_ns = 0 + self.ts_tod_updated = False + + self.ts_tod_offset_ns = 0 + + self.ts_tod_alt_s = 0 + self.ts_tod_alt_offset_ns = 0 + + self.td_delay = td_delay + + self.timestamp_delay = [(0, 0, 0, 0)] + + self.data.setimmediatevalue(1) + + self.pps = Event() + + self._run_cr = None + + self._init_reset(reset, reset_active_level) + + def set_period(self, ns, fns): + self.period_ns = int(ns) + self.period_fns = int(fns) & 0xffffffff + + def set_drift(self, num, denom): + self.drift_num = int(num) + self.drift_denom = int(denom) + + def set_period_ns(self, t): + t = Decimal(t) + period, drift = self.ctx.divmod(Decimal(t) * Decimal(2**32), Decimal(1)) + period = int(period) + frac = Fraction(drift).limit_denominator(2**16-1) + self.set_period(period >> 32, period & 0xffffffff) + self.set_drift(frac.numerator, frac.denominator) + + self.log.info("Set period: %s ns", t) + self.log.info("Period: 0x%x ns 0x%08x fns", self.period_ns, self.period_fns) + self.log.info("Drift: 0x%04x / 0x%04x fns", self.drift_num, self.drift_denom) + + def get_period_ns(self): + p = Decimal((self.period_ns << 32) | self.period_fns) + if self.drift_denom: + p += Decimal(self.drift_num) / Decimal(self.drift_denom) + return p / Decimal(2**32) + + def set_ts_tod(self, ts_s, ts_ns, ts_fns): + self.ts_tod_s = int(ts_s) + self.ts_tod_ns = int(ts_ns) + self.ts_fns = int(ts_fns) + self.ts_tod_updated = True + + def set_ts_tod_64(self, ts): + ts = int(ts) + self.set_ts_tod(ts >> 48, (ts >> 32) & 0x3fffffff, (ts & 0xffff) << 16) + + def set_ts_tod_ns(self, t): + ts_s, ts_ns = self.ctx.divmod(Decimal(t), Decimal(1000000000)) + ts_s = ts_s.scaleb(-9).to_integral_value() + ts_ns, ts_fns = self.ctx.divmod(ts_ns, Decimal(1)) + ts_ns = ts_ns.to_integral_value() + ts_fns = (ts_fns * Decimal(2**32)).to_integral_value() + self.set_ts_tod(ts_s, ts_ns, ts_fns) + + def set_ts_tod_s(self, t): + self.set_ts_tod_ns(Decimal(t).scaleb(9, self.ctx)) + + def set_ts_tod_sim_time(self): + self.set_ts_tod_ns(Decimal(get_sim_time('fs')).scaleb(-6)) + + def get_ts_tod(self): + ts_tod_s, ts_tod_ns, ts_rel_ns, ts_fns = self.timestamp_delay[0] + return (ts_tod_s, ts_tod_ns, ts_fns) + + def get_ts_tod_96(self): + ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod() + return (ts_tod_s << 48) | (ts_tod_ns << 16) | (ts_fns >> 16) + + def get_ts_tod_ns(self): + ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod() + ns = Decimal(ts_fns) / Decimal(2**32) + ns = self.ctx.add(ns, Decimal(ts_tod_ns)) + return self.ctx.add(ns, Decimal(ts_tod_s).scaleb(9)) + + def get_ts_tod_s(self): + return self.get_ts_tod_ns().scaleb(-9, self.ctx) + + def set_ts_rel(self, ts_ns, ts_fns): + self.ts_rel_ns = int(ts_ns) + self.ts_fns = int(ts_fns) + self.ts_rel_updated = True + + def set_ts_rel_64(self, ts): + ts = int(ts) + self.set_ts_rel(ts >> 16, (ts & 0xffff) << 16) + + def set_ts_rel_ns(self, t): + ts_ns, ts_fns = self.ctx.divmod(Decimal(t), Decimal(1)) + ts_ns = ts_ns.to_integral_value() + ts_fns = (ts_fns * Decimal(2**32)).to_integral_value() + self.set_ts_rel(ts_ns, ts_fns) + + def set_ts_rel_s(self, t): + self.set_ts_rel_ns(Decimal(t).scaleb(9, self.ctx)) + + def set_ts_rel_sim_time(self): + self.set_ts_rel_ns(Decimal(get_sim_time('fs')).scaleb(-6)) + + def get_ts_rel(self): + ts_tod_s, ts_tod_ns, ts_rel_ns, ts_fns = self.timestamp_delay[0] + return (ts_rel_ns, ts_fns) + + def get_ts_rel_64(self): + ts_rel_ns, ts_fns = self.get_ts_rel() + return (ts_rel_ns << 16) | (ts_fns >> 16) + + def get_ts_rel_ns(self): + ts_rel_ns, ts_fns = self.get_ts_rel() + return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_rel_ns)) + + def get_ts_rel_s(self): + return self.get_ts_rel_ns().scaleb(-9, self.ctx) + + def _handle_reset(self, state): + if state: + self.log.info("Reset asserted") + if self._run_cr is not None: + self._run_cr.kill() + self._run_cr = None + + self.ts_tod_s = 0 + self.ts_tod_ns = 0 + self.ts_rel_ns = 0 + self.ts_fns = 0 + self.drift_cnt = 0 + + self.data.value = 1 + else: + self.log.info("Reset de-asserted") + if self._run_cr is None: + self._run_cr = cocotb.start_soon(self._run()) + + async def _run(self): + clock_edge_event = RisingEdge(self.clock) + msg_index = 0 + msg = None + msg_delay = 0 + word = None + bit_index = 0 + + while True: + await clock_edge_event + + # delay timestamp + self.timestamp_delay.append((self.ts_tod_s, self.ts_tod_ns, self.ts_rel_ns, self.ts_fns)) + while len(self.timestamp_delay) > 14*17+self.td_delay: + self.timestamp_delay.pop(0) + + # increment fns portion + self.ts_fns += ((self.period_ns << 32) + self.period_fns) + + if self.drift_denom: + if self.drift_cnt > 0: + self.drift_cnt -= 1 + else: + self.drift_cnt = self.drift_denom-1 + self.ts_fns += self.drift_num + + ns_inc = self.ts_fns >> 32 + self.ts_fns &= 0xffffffff + + # increment relative timestamp + self.ts_rel_ns = (self.ts_rel_ns + ns_inc) & 0xffffffffffff + + # increment ToD timestamp + self.ts_tod_ns = self.ts_tod_ns + ns_inc + + if self.ts_tod_ns >= 1000000000: + self.log.info("Seconds rollover") + self.pps.set() + self.ts_tod_s += 1 + self.ts_tod_ns -= 1000000000 + + # compute offset for current second + self.ts_tod_offset_ns = (self.ts_tod_ns - self.ts_rel_ns) & 0xffffffff + + # compute alternate offset + if self.ts_tod_ns & (1 << 29): + # latter half of second; compute offset for next second + self.ts_tod_alt_s = self.ts_tod_s+1 + self.ts_tod_alt_offset_ns = (self.ts_tod_offset_ns - 1000000000) & 0xffffffff + else: + # former half of second; compute offset for previous second + self.ts_tod_alt_s = self.ts_tod_s-1 + self.ts_tod_alt_offset_ns = (self.ts_tod_offset_ns + 1000000000) & 0xffffffff + + if msg_delay <= 0: + # build message + + msg = [] + + # word 0: control + ctrl = 0 + ctrl |= msg_index & 0xf + ctrl |= bool(self.ts_rel_updated) << 8 + ctrl |= bool(self.ts_tod_s & 1) << 9 + self.ts_rel_updated = False + msg.append(ctrl) + + if msg_index == 0: + # msg 0 word 1: current ToD TS ns 15:0 + msg.append(self.ts_tod_ns & 0xffff) + # msg 0 word 2: current ToD TS ns 29:16 and flag bit + msg.append(((self.ts_tod_ns >> 16) & 0x3fff) | (0x8000 if self.ts_tod_updated else 0)) + self.ts_tod_updated = False + # msg 0 word 3: current ToD TS seconds 15:0 + msg.append(self.ts_tod_s & 0xffff) + # msg 0 word 4: current ToD TS seconds 31:16 + msg.append((self.ts_tod_s >> 16) & 0xffff) + # msg 0 word 5: current ToD TS seconds 47:32 + msg.append((self.ts_tod_s >> 32) & 0xffff) + msg_index = 1 + elif msg_index == 1: + # msg 1 word 1: current ToD TS ns offset 15:0 + msg.append(self.ts_tod_offset_ns & 0xffff) + # msg 1 word 2: current ToD TS ns offset 31:16 + msg.append((self.ts_tod_offset_ns >> 16) & 0xffff) + # msg 1 word 3: drift num + msg.append(self.drift_num) + # msg 1 word 4: drift denom + msg.append(self.drift_denom) + # msg 1 word 5: drift state + msg.append(self.drift_cnt) + msg_index = 2 + elif msg_index == 2: + # msg 2 word 1: alternate ToD TS ns offset 15:0 + msg.append(self.ts_tod_alt_offset_ns & 0xffff) + # msg 2 word 2: alternate ToD TS ns offset 31:16 + msg.append((self.ts_tod_alt_offset_ns >> 16) & 0xffff) + # msg 2 word 3: alternate ToD TS seconds 15:0 + msg.append(self.ts_tod_alt_s & 0xffff) + # msg 2 word 4: alternate ToD TS seconds 31:16 + msg.append((self.ts_tod_alt_s >> 16) & 0xffff) + # msg 2 word 5: alternate ToD TS seconds 47:32 + msg.append((self.ts_tod_alt_s >> 32) & 0xffff) + msg_index = 0 + + # word 6: current fns 15:0 + msg.append(self.ts_fns & 0xffff) + # word 7: current fns 31:16 + msg.append((self.ts_fns >> 16) & 0xffff) + # word 8: current relative TS ns 15:0 + msg.append(self.ts_rel_ns & 0xffff) + # word 9: current relative TS ns 31:16 + msg.append((self.ts_rel_ns >> 16) & 0xffff) + # word 10: current relative TS ns 47:32 + msg.append((self.ts_rel_ns >> 32) & 0xffff) + # word 11: current phase increment fns 15:0 + msg.append(self.period_fns & 0xffff) + # word 12: current phase increment fns 31:16 + msg.append((self.period_fns >> 16) & 0xffff) + # word 13: current phase increment ns 7:0 + crc + msg.append(self.period_ns & 0xff) + + msg_delay = 255 + else: + msg_delay -= 1 + + # serialize message + if word is None: + if msg: + word = msg.pop(0) + bit_index = 0 + self.data.value = 0 + else: + self.data.value = 1 + else: + self.data.value = bool((word >> bit_index) & 1) + bit_index += 1 + if bit_index == 16: + word = None + + +class PtpTdSink(Reset): + def __init__(self, + data=None, + clock=None, + reset=None, + reset_active_level=True, + period_ns=6.4, + td_delay=32, + *args, **kwargs): + + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.clock = clock + self.reset = reset + + self.log.info("PTP time distribution sink") + self.log.info("Copyright (c) 2023 Alex Forencich") + self.log.info("https://github.com/alexforencich/verilog-ethernet") + + super().__init__(*args, **kwargs) + + self.ctx = Context(prec=60) + + self.period_ns = 0 + self.period_fns = 0 + self.drift_num = 0 + self.drift_denom = 0 + + self.ts_fns = 0 + + self.ts_rel_ns = 0 + + self.ts_tod_s = 0 + self.ts_tod_ns = 0 + + self.ts_tod_offset_ns = 0 + + self.ts_tod_alt_s = 0 + self.ts_tod_alt_offset_ns = 0 + + self.td_delay = td_delay + + self.drift_cnt = 0 + + self.pps = Event() + + self._run_cr = None + + self._init_reset(reset, reset_active_level) + + def get_period_ns(self): + p = Decimal((self.period_ns << 32) | self.period_fns) + if self.drift_denom: + return p + Decimal(self.drift_num) / Decimal(self.drift_denom) + return p / Decimal(2**32) + + def get_ts_tod(self): + return (self.ts_tod_s, self.ts_tod_ns, self.ts_fns) + + def get_ts_tod_96(self): + ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod() + return (ts_tod_s << 48) | (ts_tod_ns << 16) | (ts_fns >> 16) + + def get_ts_tod_ns(self): + ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod() + ns = Decimal(ts_fns) / Decimal(2**32) + ns = self.ctx.add(ns, Decimal(ts_tod_ns)) + return self.ctx.add(ns, Decimal(ts_tod_s).scaleb(9)) + + def get_ts_tod_s(self): + return self.get_ts_tod_ns().scaleb(-9, self.ctx) + + def get_ts_rel(self): + return (self.ts_rel_ns, self.ts_fns) + + def get_ts_rel_64(self): + ts_rel_ns, ts_fns = self.get_ts_rel() + return (ts_rel_ns << 16) | (ts_fns >> 16) + + def get_ts_rel_ns(self): + ts_rel_ns, ts_fns = self.get_ts_rel() + return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_rel_ns)) + + def get_ts_rel_s(self): + return self.get_ts_rel_ns().scaleb(-9, self.ctx) + + def _handle_reset(self, state): + if state: + self.log.info("Reset asserted") + if self._run_cr is not None: + self._run_cr.kill() + self._run_cr = None + + self.ts_tod_s = 0 + self.ts_tod_ns = 0 + self.ts_rel_ns = 0 + self.ts_fns = 0 + self.drift_cnt = 0 + + self.data.value = 1 + else: + self.log.info("Reset de-asserted") + if self._run_cr is None: + self._run_cr = cocotb.start_soon(self._run()) + + async def _run(self): + clock_edge_event = RisingEdge(self.clock) + msg_index = 0 + msg = None + msg_delay = 0 + cur_msg = [] + word = None + bit_index = 0 + + while True: + await clock_edge_event + + sdi_sample = self.data.value.integer + + # increment fns portion + self.ts_fns += ((self.period_ns << 32) + self.period_fns) + + if self.drift_denom: + if self.drift_cnt > 0: + self.drift_cnt -= 1 + else: + self.drift_cnt = self.drift_denom-1 + self.ts_fns += self.drift_num + + ns_inc = self.ts_fns >> 32 + self.ts_fns &= 0xffffffff + + # increment relative timestamp + self.ts_rel_ns = (self.ts_rel_ns + ns_inc) & 0xffffffffffff + + # increment ToD timestamp + self.ts_tod_ns = self.ts_tod_ns + ns_inc + + if self.ts_tod_ns >= 1000000000: + self.log.info("Seconds rollover") + self.pps.set() + self.ts_tod_s += 1 + self.ts_tod_ns -= 1000000000 + + # process messages + if msg_delay > 0: + msg_delay -= 1 + + if msg_delay == 0 and msg: + self.log.info("process message %r", msg) + + # word 0: control + msg_index = msg[0] & 0xf + + if msg_index == 0: + # msg 0 word 1: current ToD TS ns 15:0 + # msg 0 word 2: current ToD TS ns 29:16 + val = ((msg[2] & 0x3fff) << 16) | msg[1] + if self.ts_tod_ns != val: + self.log.info("update ts_tod_ns: old 0x%x, new 0x%x", self.ts_tod_ns, val) + self.ts_tod_ns = val + # msg 0 word 3: current ToD TS seconds 15:0 + # msg 0 word 4: current ToD TS seconds 31:16 + # msg 0 word 5: current ToD TS seconds 47:32 + val = (msg[5] << 32) | (msg[4] << 16) | msg[3] + if self.ts_tod_s != val: + self.log.info("update ts_tod_s: old 0x%x, new 0x%x", self.ts_tod_s, val) + self.ts_tod_s = val + elif msg_index == 1: + # msg 1 word 1: current ToD TS ns offset 15:0 + # msg 1 word 2: current ToD TS ns offset 31:16 + val = (msg[2] << 16) | msg[1] + if self.ts_tod_offset_ns != val: + self.log.info("update ts_tod_offset_ns: old 0x%x, new 0x%x", self.ts_tod_offset_ns, val) + self.ts_tod_offset_ns = val + # msg 1 word 3: drift num + val = msg[3] + if self.drift_num != val: + self.log.info("update drift_num: old 0x%x, new 0x%x", self.drift_num, val) + self.drift_num = val + # msg 1 word 4: drift denom + val = msg[4] + if self.drift_denom != val: + self.log.info("update drift_denom: old 0x%x, new 0x%x", self.drift_denom, val) + self.drift_denom = val + # msg 1 word 5: drift state + val = msg[5] + if self.drift_cnt != val: + self.log.info("update drift_cnt: old 0x%x, new 0x%x", self.drift_cnt, val) + self.drift_cnt = val + elif msg_index == 2: + # msg 2 word 1: alternate ToD TS ns offset 15:0 + # msg 2 word 2: alternate ToD TS ns offset 31:16 + val = (msg[2] << 16) | msg[1] + if self.ts_tod_alt_offset_ns != val: + self.log.info("update ts_tod_alt_offset_ns: old 0x%x, new 0x%x", self.ts_tod_alt_offset_ns, val) + self.ts_tod_alt_offset_ns = val + # msg 2 word 3: alternate ToD TS seconds 15:0 + # msg 2 word 4: alternate ToD TS seconds 31:16 + # msg 2 word 5: alternate ToD TS seconds 47:32 + val = (msg[5] << 32) | (msg[4] << 16) | msg[3] + if self.ts_tod_alt_s != val: + self.log.info("update ts_tod_alt_s: old 0x%x, new 0x%x", self.ts_tod_alt_s, val) + self.ts_tod_alt_s = val + + # word 6: current fns 15:0 + # word 7: current fns 31:16 + val = (msg[7] << 16) | msg[6] + if self.ts_fns != val: + self.log.info("update ts_fns: old 0x%x, new 0x%x", self.ts_fns, val) + self.ts_fns = val + # word 8: current relative TS ns 15:0 + # word 9: current relative TS ns 31:16 + # word 10: current relative TS ns 47:32 + val = (msg[10] << 32) | (msg[9] << 16) | msg[8] + if self.ts_rel_ns != val: + self.log.info("update ts_rel_ns: old 0x%x, new 0x%x", self.ts_rel_ns, val) + self.ts_rel_ns = val + # word 11: current phase increment fns 15:0 + # word 12: current phase increment fns 31:16 + val = (msg[12] << 16) | msg[11] + if self.period_fns != val: + self.log.info("update period_fns: old 0x%x, new 0x%x", self.period_fns, val) + self.period_fns = val + # word 13: current phase increment ns 7:0 + crc + val = msg[13] & 0xff + if self.period_ns != val: + self.log.info("update period_ns: old 0x%x, new 0x%x", self.period_ns, val) + self.period_ns = val + + msg = None + + # deserialize message + if word is not None: + word = word | (sdi_sample << bit_index) + bit_index += 1 + + if bit_index == 16: + cur_msg.append(word) + word = None + else: + if not sdi_sample: + # start bit + word = 0 + bit_index = 0 + elif cur_msg: + # idle + msg = cur_msg + msg_delay = self.td_delay + cur_msg = [] diff --git a/tb/ptp_td_leaf/Makefile b/tb/ptp_td_leaf/Makefile new file mode 100644 index 000000000..cc1f48de7 --- /dev/null +++ b/tb/ptp_td_leaf/Makefile @@ -0,0 +1,75 @@ +# 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 = ptp_td_leaf +TOPLEVEL = $(DUT) +MODULE = test_$(DUT) +VERILOG_SOURCES += ../../rtl/$(DUT).v + +# module parameters +export PARAM_TS_REL_EN := 1 +export PARAM_TS_TOD_EN := 1 +export PARAM_TS_FNS_W := 16 +export PARAM_TS_REL_NS_W := 48 +export PARAM_TS_TOD_S_W := 48 +export PARAM_TS_REL_W := $(shell expr $(PARAM_TS_REL_NS_W) + $(PARAM_TS_FNS_W)) +export PARAM_TS_TOD_W := $(shell expr $(PARAM_TS_TOD_S_W) + 32 + $(PARAM_TS_FNS_W)) +export PARAM_TD_SDI_PIPELINE := 2 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) + + 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 += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + 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/tb/ptp_td_leaf/ptp_td.py b/tb/ptp_td_leaf/ptp_td.py new file mode 120000 index 000000000..fec11b651 --- /dev/null +++ b/tb/ptp_td_leaf/ptp_td.py @@ -0,0 +1 @@ +../ptp_td.py \ No newline at end of file diff --git a/tb/ptp_td_leaf/test_ptp_td_leaf.py b/tb/ptp_td_leaf/test_ptp_td_leaf.py new file mode 100644 index 000000000..49cb2f5e2 --- /dev/null +++ b/tb/ptp_td_leaf/test_ptp_td_leaf.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2023 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 sys +from decimal import Decimal +from statistics import mean, stdev + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer +from cocotb.utils import get_sim_steps, get_sim_time + +try: + from ptp_td import PtpTdSource +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from ptp_td import PtpTdSource + finally: + del sys.path[0] + + +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.sample_clk, 9.9, units="ns").start()) + + self.ptp_td_source = PtpTdSource( + data=dut.ptp_td_sdi, + clock=dut.ptp_clk, + reset=dut.ptp_rst, + period_ns=6.4 + ) + + self.ptp_clock_period = 6.4 + dut.ptp_clk.setimmediatevalue(0) + cocotb.start_soon(self._run_ptp_clock()) + + self.clock_period = 6.4 + dut.clk.setimmediatevalue(0) + cocotb.start_soon(self._run_clock()) + + self.ref_ts_rel = [] + self.ref_ts_tod = [] + self.output_ts_rel = [] + self.output_ts_tod = [] + + cocotb.start_soon(self._run_collect_ref_ts()) + cocotb.start_soon(self._run_collect_output_ts()) + + async def reset(self): + self.dut.ptp_rst.setimmediatevalue(0) + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.ptp_clk) + await RisingEdge(self.dut.ptp_clk) + self.dut.ptp_rst.value = 1 + self.dut.rst.value = 1 + for k in range(10): + await RisingEdge(self.dut.ptp_clk) + self.dut.ptp_rst.value = 0 + self.dut.rst.value = 0 + for k in range(10): + await RisingEdge(self.dut.ptp_clk) + + def set_ptp_clock_period(self, period): + self.ptp_clock_period = period + + async def _run_ptp_clock(self): + period = None + steps_per_ns = get_sim_steps(1.0, 'ns') + + while True: + if period != self.ptp_clock_period: + period = self.ptp_clock_period + t = Timer(int(steps_per_ns * period / 2.0)) + await t + self.dut.ptp_clk.value = 1 + await t + self.dut.ptp_clk.value = 0 + + def set_clock_period(self, period): + self.clock_period = period + + def get_output_ts_tod_ns(self): + ts = self.dut.output_ts_tod.value.integer + return Decimal(ts >> 48).scaleb(9) + (Decimal(ts & 0xffffffffffff) / Decimal(2**16)) + + def get_output_ts_rel_ns(self): + ts = self.dut.output_ts_rel.value.integer + return Decimal(ts) / Decimal(2**16) + + async def _run_clock(self): + period = None + steps_per_ns = get_sim_steps(1.0, 'ns') + + while True: + if period != self.clock_period: + period = self.clock_period + t = Timer(int(steps_per_ns * period / 2.0)) + await t + self.dut.clk.value = 1 + await t + self.dut.clk.value = 0 + + async def _run_collect_ref_ts(self): + clk_event = RisingEdge(self.dut.ptp_clk) + while True: + await clk_event + st = Decimal(get_sim_time('fs')).scaleb(-6) + self.ref_ts_rel.append((st, self.ptp_td_source.get_ts_rel_ns())) + self.ref_ts_tod.append((st, self.ptp_td_source.get_ts_tod_ns())) + + async def _run_collect_output_ts(self): + clk_event = RisingEdge(self.dut.clk) + while True: + await clk_event + st = Decimal(get_sim_time('fs')).scaleb(-6) + self.output_ts_rel.append((st, self.get_output_ts_rel_ns())) + self.output_ts_tod.append((st, self.get_output_ts_tod_ns())) + + def compute_ts_diff(self, ts_lst_1, ts_lst_2): + ts_lst_1 = [x for x in ts_lst_1] + + diffs = [] + + its1 = ts_lst_1.pop(0) + its2 = ts_lst_1.pop(0) + + for ots in ts_lst_2: + while its2[0] < ots[0] and ts_lst_1: + its1 = its2 + its2 = ts_lst_1.pop(0) + + if its2[0] < ots[0]: + break + + dt = its2[0] - its1[0] + dts = its2[1] - its1[1] + + its = its1[1]+dts/dt*(ots[0]-its1[0]) + + # diffs.append(ots[1] - its) + diffs.append(float(ots[1] - its)) + + return diffs + + async def measure_ts_diff(self, N=100): + self.ref_ts_rel = [] + self.ref_ts_tod = [] + self.output_ts_rel = [] + self.output_ts_tod = [] + + for k in range(N): + await RisingEdge(self.dut.clk) + + rel_diffs = self.compute_ts_diff(self.ref_ts_rel, self.output_ts_rel) + tod_diffs = self.compute_ts_diff(self.ref_ts_tod, self.output_ts_tod) + + return rel_diffs, tod_diffs + + +@cocotb.test() +async def run_test(dut): + + tb = TB(dut) + + await tb.reset() + + # set small offset between timestamps + tb.ptp_td_source.set_ts_rel_ns(0) + tb.ptp_td_source.set_ts_tod_ns(10000) + + await RisingEdge(dut.clk) + tb.log.info("Same clock speed") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("10 ppm slower") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4*(1+.00001)) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("10 ppm faster") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4*(1-.00001)) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("200 ppm slower") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4*(1+.0002)) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("200 ppm faster") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4*(1-.0002)) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Coherent tracking (+/- 10 ppm)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4) + + await RisingEdge(dut.clk) + + period = 6.400 + step = 0.000002 + period_min = 6.4*(1-.00001) + period_max = 6.4*(1+.00001) + + for i in range(500): + period += step + + if period <= period_min: + step = abs(step) + if period >= period_max: + step = -abs(step) + + tb.set_clock_period(period) + + for i in range(200): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Coherent tracking (+/- 200 ppm)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.4) + + await RisingEdge(dut.clk) + + period = 6.400 + step = 0.000002 + period_min = 6.4*(1-.0002) + period_max = 6.4*(1+.0002) + + for i in range(5000): + period += step + + if period <= period_min: + step = abs(step) + if period >= period_max: + step = -abs(step) + + tb.set_clock_period(period) + + for i in range(20): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Slightly faster (6.3 ns)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.3) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Slightly slower (6.5 ns)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(6.5) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Significantly faster (250 MHz)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(4.0) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Significantly slower (100 MHz)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(10.0) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + await RisingEdge(dut.clk) + tb.log.info("Significantly faster (390.625 MHz)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(2.56) + + await RisingEdge(dut.clk) + + for i in range(100000): + await RisingEdge(dut.clk) + + assert tb.dut.locked.value.integer + + rel_diffs, tod_diffs = await tb.measure_ts_diff() + tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})") + tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})") + assert abs(mean(rel_diffs)) < 5 + assert abs(mean(tod_diffs)) < 5 + + 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')) +lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib')) +axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl')) + + +def test_ptp_td_leaf(request): + dut = "ptp_td_leaf" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, f"{dut}.v"), + ] + + parameters = {} + + parameters['TS_REL_EN'] = 1 + parameters['TS_TOD_EN'] = 1 + parameters['TS_FNS_W'] = 16 + parameters['TS_REL_NS_W'] = 48 + parameters['TS_TOD_S_W'] = 48 + parameters['TS_REL_W'] = parameters['TS_REL_NS_W'] + parameters['TS_FNS_W'] + parameters['TS_TOD_W'] = parameters['TS_TOD_S_W'] + 32 + parameters['TS_FNS_W'] + parameters['TD_SDI_PIPELINE'] = 2 + + 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/tb/ptp_td_phc/Makefile b/tb/ptp_td_phc/Makefile new file mode 100644 index 000000000..aa67c7739 --- /dev/null +++ b/tb/ptp_td_phc/Makefile @@ -0,0 +1,69 @@ +# 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 = ptp_td_phc +TOPLEVEL = $(DUT) +MODULE = test_$(DUT) +VERILOG_SOURCES += ../../rtl/$(DUT).v + +# module parameters +export PARAM_PERIOD_NS_NUM := 32 +export PARAM_PERIOD_NS_DENOM := 5 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) + + 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 += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + 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/tb/ptp_td_phc/ptp_td.py b/tb/ptp_td_phc/ptp_td.py new file mode 120000 index 000000000..fec11b651 --- /dev/null +++ b/tb/ptp_td_phc/ptp_td.py @@ -0,0 +1 @@ +../ptp_td.py \ No newline at end of file diff --git a/tb/ptp_td_phc/test_ptp_td_phc.py b/tb/ptp_td_phc/test_ptp_td_phc.py new file mode 100644 index 000000000..1fa5dd567 --- /dev/null +++ b/tb/ptp_td_phc/test_ptp_td_phc.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2023 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 sys +from decimal import Decimal + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.utils import get_sim_time + +try: + from ptp_td import PtpTdSink +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from ptp_td import PtpTdSink + finally: + del sys.path[0] + + +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_td_sink = PtpTdSink( + data=dut.ptp_td_sdo, + clock=dut.clk, + reset=dut.rst, + period_ns=6.4 + ) + + dut.input_ts_rel_ns.setimmediatevalue(0) + dut.input_ts_rel_valid.setimmediatevalue(0) + dut.input_ts_rel_offset_ns.setimmediatevalue(0) + dut.input_ts_rel_offset_valid.setimmediatevalue(0) + + dut.input_ts_tod_s.setimmediatevalue(0) + dut.input_ts_tod_ns.setimmediatevalue(0) + dut.input_ts_tod_valid.setimmediatevalue(0) + dut.input_ts_tod_offset_ns.setimmediatevalue(0) + dut.input_ts_tod_offset_valid.setimmediatevalue(0) + + dut.input_ts_offset_fns.setimmediatevalue(0) + dut.input_ts_offset_valid.setimmediatevalue(0) + + dut.input_period_ns.setimmediatevalue(0) + dut.input_period_fns.setimmediatevalue(0) + dut.input_period_valid.setimmediatevalue(0) + dut.input_drift_num.setimmediatevalue(0) + dut.input_drift_denom.setimmediatevalue(0) + dut.input_drift_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_default_rate(dut): + + tb = TB(dut) + + await tb.reset() + + for k in range(256*6): + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta + ts_rel_diff = time_delta - ts_rel_delta + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +@cocotb.test() +async def run_load_timestamps(dut): + + tb = TB(dut) + + await tb.reset() + + await RisingEdge(dut.clk) + + dut.input_ts_tod_s.value = 12 + dut.input_ts_tod_ns.value = 123456789 + dut.input_ts_tod_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_tod_valid.value = 0 + + dut.input_ts_rel_ns.value = 123456789 + dut.input_ts_rel_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_rel_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_rel_valid.value = 0 + + for k in range(256*6): + await RisingEdge(dut.clk) + + # assert tb.ptp_td_sink.get_ts_tod_s() - (12.123456789 + (256*6-(14*17+32)-2)*6.4e-9) < 6.4e-9 + # assert tb.ptp_td_sink.get_ts_rel_ns() - (123456789 + (256*6-(14*17+32)-1)*6.4) < 6.4 + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta + ts_rel_diff = time_delta - ts_rel_delta + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +@cocotb.test() +async def run_offsets(dut): + + tb = TB(dut) + + await tb.reset() + + for k in range(256*6): + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset FNS (positive)") + + await RisingEdge(dut.clk) + + dut.input_ts_offset_fns.value = 0x78000000 & 0xffffffff + dut.input_ts_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_offset_valid.value = 0 + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset FNS (negative)") + + await RisingEdge(dut.clk) + + dut.input_ts_offset_fns.value = -0x70000000 & 0xffffffff + dut.input_ts_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_offset_valid.value = 0 + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset relative TS (positive)") + + dut.input_ts_rel_offset_ns.value = 30000 & 0xffffffff + dut.input_ts_rel_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_rel_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_rel_offset_valid.value = 0 + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset relative TS (negative)") + + dut.input_ts_rel_offset_ns.value = -10000 & 0xffffffff + dut.input_ts_rel_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_rel_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_rel_offset_valid.value = 0 + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset ToD TS (positive)") + + dut.input_ts_tod_offset_ns.value = 510000000 & 0x3fffffff + dut.input_ts_tod_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_offset_ready.value: + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_tod_offset_valid.value = 0 + + for k in range(2000): + await RisingEdge(dut.clk) + + tb.log.info("Offset ToD TS (negative)") + + dut.input_ts_tod_offset_ns.value = -500000000 & 0x3fffffff + dut.input_ts_tod_offset_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_offset_ready.value: + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_offset_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_tod_offset_valid.value = 0 + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta + Decimal(0.03125) + Decimal(20000000) + ts_rel_diff = time_delta - ts_rel_delta + Decimal(0.03125) + Decimal(20000) + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +@cocotb.test() +async def run_seconds_increment(dut): + + tb = TB(dut) + + await tb.reset() + + await RisingEdge(dut.clk) + + dut.input_ts_tod_s.value = 0 + dut.input_ts_tod_ns.value = 999990000 + dut.input_ts_tod_valid.value = 1 + + await RisingEdge(dut.clk) + while not dut.input_ts_tod_ready.value: + await RisingEdge(dut.clk) + + dut.input_ts_tod_valid.value = 0 + + for k in range(256*6): + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + saw_pps = False + + for k in range(3000): + await RisingEdge(dut.clk) + + if dut.output_pps.value.integer: + saw_pps = True + tb.log.info("Got PPS with sink ToD TS %s", tb.ptp_td_sink.get_ts_tod_ns()) + assert (tb.ptp_td_sink.get_ts_tod_s() - 1) < 6.4e-9 + + assert saw_pps + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta + ts_rel_diff = time_delta - ts_rel_delta + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +@cocotb.test() +async def run_frequency_adjustment(dut): + + tb = TB(dut) + + await tb.reset() + + await RisingEdge(dut.clk) + + dut.input_period_ns.value = 0x6 + dut.input_period_fns.value = 0x66240000 + dut.input_period_valid.value = 1 + + await RisingEdge(dut.clk) + + dut.input_period_valid.value = 0 + + for k in range(256*6): + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4/(6+(0x66240000+2/5)/2**32)) + ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4/(6+(0x66240000+2/5)/2**32)) + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +@cocotb.test() +async def run_drift_adjustment(dut): + + tb = TB(dut) + + await tb.reset() + + dut.input_drift_num.value = 20000 + dut.input_drift_denom.value = 5 + dut.input_drift_valid.value = 1 + + await RisingEdge(dut.clk) + + dut.input_drift_valid.value = 0 + + for k in range(256*6): + await RisingEdge(dut.clk) + + await RisingEdge(dut.clk) + start_time = Decimal(get_sim_time('fs')).scaleb(-6) + start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + for k in range(10000): + await RisingEdge(dut.clk) + + stop_time = Decimal(get_sim_time('fs')).scaleb(-6) + stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns() + stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns() + + time_delta = stop_time-start_time + ts_tod_delta = stop_ts_tod-start_ts_tod + ts_rel_delta = stop_ts_rel-start_ts_rel + + tb.log.info("sim time delta : %s ns", time_delta) + tb.log.info("ToD ts delta : %s ns", ts_tod_delta) + tb.log.info("Rel ts delta : %s ns", ts_rel_delta) + + ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4/(6+(0x66666666+20000/5)/2**32)) + ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4/(6+(0x66666666+20000/5)/2**32)) + + tb.log.info("ToD ts diff : %s ns", ts_tod_diff) + tb.log.info("Rel ts diff : %s ns", ts_rel_diff) + + assert abs(ts_tod_diff) < 1e-3 + assert abs(ts_rel_diff) < 1e-3 + + 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')) +lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib')) +axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl')) + + +def test_ptp_td_phc(request): + dut = "ptp_td_phc" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, f"{dut}.v"), + ] + + parameters = {} + + parameters['PERIOD_NS_NUM'] = 32 + parameters['PERIOD_NS_DENOM'] = 5 + + 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, + )