From ad0d44616bdfc6280b590b199c3e3f79436e087b Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Thu, 13 Feb 2025 20:18:17 -0800 Subject: [PATCH] ptp: Add PTP TD leaf clock module and testbench Signed-off-by: Alex Forencich --- rtl/ptp/taxi_ptp_td_leaf.sv | 1011 +++++++++++++++++ tb/ptp/taxi_ptp_td_leaf/Makefile | 52 + tb/ptp/taxi_ptp_td_leaf/ptp_td.py | 1 + .../taxi_ptp_td_leaf/test_taxi_ptp_td_leaf.py | 541 +++++++++ 4 files changed, 1605 insertions(+) create mode 100644 rtl/ptp/taxi_ptp_td_leaf.sv create mode 100644 tb/ptp/taxi_ptp_td_leaf/Makefile create mode 120000 tb/ptp/taxi_ptp_td_leaf/ptp_td.py create mode 100644 tb/ptp/taxi_ptp_td_leaf/test_taxi_ptp_td_leaf.py diff --git a/rtl/ptp/taxi_ptp_td_leaf.sv b/rtl/ptp/taxi_ptp_td_leaf.sv new file mode 100644 index 0000000..41822ae --- /dev/null +++ b/rtl/ptp/taxi_ptp_td_leaf.sv @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1fs +`default_nettype none + +/* + * PTP time distribution leaf + */ +module taxi_ptp_td_leaf # +( + parameter logic TS_REL_EN = 1'b1, + parameter logic TS_TOD_EN = 1'b1, + 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 logic clk, + input wire logic rst, + input wire logic sample_clk, + + /* + * PTP clock interface + */ + input wire logic ptp_clk, + input wire logic ptp_rst, + input wire logic ptp_td_sdi, + + /* + * Timestamp output + */ + output wire logic [TS_REL_W-1:0] output_ts_rel, + output wire logic output_ts_rel_step, + output wire logic [TS_TOD_W-1:0] output_ts_tod, + output wire logic output_ts_tod_step, + + /* + * PPS output (ToD format only) + */ + output wire logic output_pps, + output wire logic output_pps_str, + + /* + * Status + */ + output wire logic 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-1; + +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; + +for (genvar 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_ff @(posedge ptp_clk) begin + ptp_td_sdi_reg <= ptp_td_sdi_pipe[n]; + end + +end + +// deserialize data +logic [15:0] td_shift_reg = '0; +logic [4:0] bit_cnt_reg = '0; +logic td_valid_reg = 1'b0; +logic [3:0] td_index_reg = '0; +logic [3:0] td_msg_reg = '0; + +logic [15:0] td_tdata_reg = '0; +logic td_tvalid_reg = 1'b0; +logic td_tlast_reg = 1'b0; +logic [7:0] td_tid_reg = '0; +logic td_sync_reg = 1'b0; + +always_ff @(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 != 0) 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 +logic [15:0] dst_td_tdata_reg = '0; +logic dst_td_tvalid_reg = 1'b0; +logic [7:0] dst_td_tid_reg = '0; + +(* shreg_extract = "no" *) +logic td_sync_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +logic td_sync_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +logic td_sync_sync3_reg = 1'b0; + +always_ff @(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_ff @(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 +logic [5:0] src_sync_delay_reg = '0; +logic src_load_reg = 1'b0; + +logic [PHASE_CNT_W-1:0] src_phase_reg = '0; +logic src_update_reg = 1'b0; +logic src_sync_reg = 1'b0; +logic src_marker_reg = 1'b0; + +logic [PERIOD_NS_W+32-1:0] src_period_reg = '0; +logic [PERIOD_NS_W+32-1:0] src_period_shadow_reg = '0; + +logic [9+SRC_FNS_W-1:0] src_ns_reg = '0; +logic [9+32-1:0] src_ns_shadow_reg = '0; + +always_ff @(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 + (9+SRC_FNS_W)'({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; + end + if (td_tid_reg[3:0] == 4'd7) begin + src_ns_shadow_reg[31:16] <= td_tdata_reg; + end + if (td_tid_reg[3:0] == 4'd8) begin + src_ns_shadow_reg[40:32] <= td_tdata_reg[8:0]; + 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[7:0]; + end + end + + if (src_load_reg) begin + src_ns_reg <= (9+SRC_FNS_W)'(src_ns_shadow_reg >> (32-SRC_FNS_W)); + src_period_reg <= src_period_shadow_reg; + src_sync_reg <= 1'b1; + 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 != 0) begin + src_sync_delay_reg <= src_sync_delay_reg - 1; + end + + if (td_tvalid_reg && td_tlast_reg) begin + src_sync_delay_reg <= 6'(SYNC_DELAY); + end +end + +logic [PERIOD_NS_W+FNS_W-1:0] period_ns_reg = '0, period_ns_next; + +logic [9+CMP_FNS_W-1:0] dst_ns_capt_reg = '0; +logic [9+CMP_FNS_W-1:0] src_ns_sync_reg = '0; + +logic [FNS_W-1:0] ts_fns_lsb_reg = '0, ts_fns_lsb_next; +logic [FNS_W-1:0] ts_fns_reg = '0, ts_fns_next; + +logic [8:0] ts_rel_ns_lsb_reg = '0, ts_rel_ns_lsb_next; +logic [TS_NS_W-1:0] ts_rel_ns_reg = '0, ts_rel_ns_next; +logic ts_rel_step_reg = 1'b0, ts_rel_step_next; + +logic [TS_TOD_S_W-1:0] ts_tod_s_reg = '0, ts_tod_s_next; +logic [TS_TOD_NS_W-1:0] ts_tod_ns_reg = '0, ts_tod_ns_next; +logic [8:0] ts_tod_offset_ns_reg = '0, ts_tod_offset_ns_next; +logic ts_tod_step_reg = 1'b0, ts_tod_step_next; + +logic pps_reg = 1'b0, pps_next; +logic pps_str_reg = 1'b0, pps_str_next; + +logic [PHASE_ACC_W-1:0] dst_phase_reg = '0, dst_phase_next; +logic [PHASE_ACC_W-1:0] dst_phase_inc_reg = '0, dst_phase_inc_next; + +logic dst_sync_reg = 1'b0; +logic dst_update_reg = 1'b0, dst_update_next; + +(* shreg_extract = "no" *) +logic src_sync_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_sync_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_sync_sync3_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_marker_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_marker_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_marker_sync3_reg = 1'b0; + +(* shreg_extract = "no" *) +logic src_sync_sample_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_sync_sample_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +logic src_sync_sample_sync3_reg = 1'b0; +(* shreg_extract = "no" *) +logic dst_sync_sample_sync1_reg = 1'b0; +(* shreg_extract = "no" *) +logic dst_sync_sample_sync2_reg = 1'b0; +(* shreg_extract = "no" *) +logic dst_sync_sample_sync3_reg = 1'b0; + +logic [SAMPLE_ACC_W-1:0] sample_acc_reg = '0; +logic [SAMPLE_ACC_W-1:0] sample_acc_out_reg = '0; +logic [LOG_SAMPLE_SYNC_RATE-1:0] sample_cnt_reg = '0; +logic sample_update_reg = 1'b0; +logic sample_update_sync1_reg = 1'b0; +logic sample_update_sync2_reg = 1'b0; +logic sample_update_sync3_reg = 1'b0; + +// CDC logic +always_ff @(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_ff @(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 + +logic edge_1_reg = 1'b0; +logic edge_2_reg = 1'b0; + +logic [3:0] active_reg = '0; + +always_ff @(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) + SAMPLE_ACC_W'($signed({1'b0, edge_2_reg})) - SAMPLE_ACC_W'($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[2:0], src_sync_sample_sync2_reg && !src_sync_sample_sync3_reg}; + sample_acc_reg <= SAMPLE_ACC_W'($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_ff @(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 + +logic [SAMPLE_ACC_W-1:0] sample_acc_sync_reg = '0; +logic sample_acc_sync_valid_reg = '0; + +logic [PHASE_ACC_W-1:0] dst_err_int_reg = '0, dst_err_int_next; +logic [1:0] dst_ovf; + +logic [DST_SYNC_LOCK_W-1:0] dst_sync_lock_count_reg = '0, dst_sync_lock_count_next; +logic dst_sync_locked_reg = 1'b0, dst_sync_locked_next; + +logic dst_gain_sel_reg = '0, dst_gain_sel_next; + +always_comb 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 + casez (sample_acc_sync_reg[SAMPLE_ACC_W-4 +: 4]) + 4'b01zz: dst_gain_sel_next = 1'b1; + 4'b001z: dst_gain_sel_next = 1'b1; + 4'b0001: dst_gain_sel_next = 1'b1; + 4'b0000: dst_gain_sel_next = 1'b0; + 4'b1111: dst_gain_sel_next = 1'b0; + 4'b1110: dst_gain_sel_next = 1'b1; + 4'b110z: dst_gain_sel_next = 1'b1; + 4'b10zz: dst_gain_sel_next = 1'b1; + default: dst_gain_sel_next = 1'b0; + endcase + + // time integral of error + case (dst_gain_sel_reg) + 1'd0: {dst_ovf, dst_err_int_next} = $signed({1'b0, dst_err_int_reg}) + (PHASE_ACC_W+2)'($signed(sample_acc_sync_reg)); + 1'd1: {dst_ovf, dst_err_int_next} = $signed({1'b0, dst_err_int_reg}) + (PHASE_ACC_W+2)'($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 = '0; + end else if (dst_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + dst_err_int_next = '1; + 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 = '0; + end else if (dst_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + dst_phase_inc_next = '1; + 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 + +logic [LOAD_CNT_W-1:0] dst_load_cnt_reg = '0; + +logic [PHASE_ERR_ACC_W-1:0] phase_err_acc_reg = '0; +logic [PHASE_ERR_ACC_W-1:0] phase_err_out_reg = '0; +logic [LOG_PHASE_ERR_RATE-1:0] phase_err_cnt_reg = '0; +logic phase_err_out_valid_reg = '0; + +logic phase_last_src_reg = 1'b0; +logic phase_last_dst_reg = 1'b0; +logic phase_edge_1_reg = 1'b0; +logic phase_edge_2_reg = 1'b0; + +logic ts_sync_valid_reg = 1'b0; + +always_ff @(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 <= (9+CMP_FNS_W)'({ts_rel_ns_reg, ts_fns_reg} >> (FNS_W-CMP_FNS_W)); + + dst_sync_reg <= !dst_sync_reg; + + if (dst_sync_reg) begin + dst_load_cnt_reg <= dst_load_cnt_reg + 1; + end + 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 <= (9+CMP_FNS_W)'(src_ns_reg >> (SRC_FNS_W-CMP_FNS_W)); + + ts_sync_valid_reg <= 1'b1; + 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) + PHASE_ERR_ACC_W'($signed({1'b0, phase_edge_2_reg})) - PHASE_ERR_ACC_W'($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 <= PHASE_ERR_ACC_W'($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 <= '0; + dst_phase_inc_reg <= '0; + 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; + end +end + +logic dst_rel_step_shadow_reg = 1'b0, dst_rel_step_shadow_next; +logic [47:0] dst_rel_ns_shadow_reg = '0, dst_rel_ns_shadow_next; +logic dst_rel_shadow_valid_reg = '0, dst_rel_shadow_valid_next; + +logic dst_tod_step_shadow_reg = 1'b0, dst_tod_step_shadow_next; +logic [29:0] dst_tod_ns_shadow_reg = '0, dst_tod_ns_shadow_next; +logic [47:0] dst_tod_s_shadow_reg = '0, dst_tod_s_shadow_next; +logic dst_tod_shadow_valid_reg = '0, dst_tod_shadow_valid_next; + +logic ts_rel_diff_reg = 1'b0, ts_rel_diff_next; +logic ts_rel_diff_valid_reg = 1'b0, ts_rel_diff_valid_next; +logic [1:0] ts_rel_mismatch_cnt_reg = '0, ts_rel_mismatch_cnt_next; +logic ts_rel_load_ts_reg = 1'b0, ts_rel_load_ts_next; + +logic ts_tod_diff_reg = 1'b0, ts_tod_diff_next; +logic ts_tod_diff_valid_reg = 1'b0, ts_tod_diff_valid_next; +logic [1:0] ts_tod_mismatch_cnt_reg = '0, ts_tod_mismatch_cnt_next; +logic ts_tod_load_ts_reg = 1'b0, ts_tod_load_ts_next; + +logic [9+CMP_FNS_W-1:0] ts_ns_diff_reg = '0, ts_ns_diff_next; +logic ts_ns_diff_valid_reg = 1'b0, ts_ns_diff_valid_next; + +logic [TIME_ERR_INT_W-1:0] time_err_int_reg = '0, time_err_int_next; + +logic [1:0] ptp_ovf; + +logic [FREQ_LOCK_W-1:0] freq_lock_count_reg = '0, freq_lock_count_next; +logic freq_locked_reg = 1'b0, freq_locked_next; +logic [PTP_LOCK_W-1:0] ptp_lock_count_reg = '0, ptp_lock_count_next; +logic ptp_locked_reg = 1'b0, ptp_locked_next; + +logic gain_sel_reg = 0, gain_sel_next; + +if (TS_REL_EN) begin + assign output_ts_rel = TS_REL_W'({ts_rel_ns_reg, ts_fns_reg, {TS_FNS_W{1'b0}}} >> FNS_W); + assign output_ts_rel_step = ts_rel_step_reg; +end else begin + assign output_ts_rel = '0; + assign output_ts_rel_step = 1'b0; +end + +if (TS_TOD_EN) begin + assign output_ts_tod = TS_TOD_W'({ts_tod_s_reg, 2'b00, ts_tod_ns_reg, ts_fns_reg, {TS_FNS_W{1'b0}}} >> FNS_W); + assign output_ts_tod_step = ts_tod_step_reg; + + assign output_pps = pps_reg; + assign output_pps_str = pps_str_reg; +end else begin + assign output_ts_tod = '0; + assign output_ts_tod_step = 0; + + assign output_pps = 1'b0; + assign output_pps_str = 1'b0; +end + +assign locked = ptp_locked_reg && freq_locked_reg && dst_sync_locked_reg; + +always_comb begin + period_ns_next = period_ns_reg; + + ts_fns_lsb_next = ts_fns_lsb_reg; + ts_fns_next = ts_fns_reg; + + ts_rel_ns_lsb_next = ts_rel_ns_lsb_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[3:0] == 4'd1) begin + // prevent stale data from being used in time sync + dst_tod_shadow_valid_next = 1'b0; + end + 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[13:0]; + 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[8:0]; + 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 and relative timestamp least significant bits + {ts_rel_ns_lsb_next, ts_fns_lsb_next} = ({ts_rel_ns_lsb_reg, ts_fns_lsb_reg} + period_ns_reg); + ts_fns_next = ts_fns_lsb_reg; + + // relative timestamp + ts_rel_ns_next[8:0] = ts_rel_ns_lsb_reg; + + if (TS_REL_EN) begin + if (!ts_rel_ns_next[8] && ts_rel_ns_reg[8]) begin + ts_rel_ns_next[TS_REL_NS_W-1:9] = ts_rel_ns_reg[TS_REL_NS_W-1:9] + 1; + end + + if (dst_update_reg && !dst_sync_reg && dst_rel_shadow_valid_reg && (dst_load_cnt_reg == 0)) 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 + + // absolute time-of-day timestamp + if (TS_TOD_EN) begin + ts_tod_ns_next[8:0] = ts_rel_ns_lsb_reg + 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 == 30'(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_sync_reg && dst_tod_shadow_valid_reg && (dst_load_cnt_reg == 0)) 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 != 0) 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 + casez (ts_ns_diff_reg[9+CMP_FNS_W-5 +: 5]) + 5'b01zzz: gain_sel_next = 1'b1; + 5'b001zz: gain_sel_next = 1'b1; + 5'b0001z: gain_sel_next = 1'b1; + 5'b00001: gain_sel_next = 1'b1; + 5'b00000: gain_sel_next = 1'b0; + 5'b11111: gain_sel_next = 1'b0; + 5'b11110: gain_sel_next = 1'b1; + 5'b1110z: gain_sel_next = 1'b1; + 5'b110zz: gain_sel_next = 1'b1; + 5'b10zzz: gain_sel_next = 1'b1; + default: gain_sel_next = 1'b0; + endcase + + // time integral of error + case (gain_sel_reg) + 1'b0: {ptp_ovf, time_err_int_next} = $signed({1'b0, time_err_int_reg}) + (TIME_ERR_INT_W+2)'($signed(ts_ns_diff_reg) / 2**4); + 1'b1: {ptp_ovf, time_err_int_next} = $signed({1'b0, time_err_int_reg}) + (TIME_ERR_INT_W+2)'($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 = '0; + end else if (ptp_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + time_err_int_next = '1; + 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 = '0; + end else if (ptp_ovf[0]) begin + // sign bit clear but carry bit set indicating overflow; saturate to all 1 + period_ns_next = '1; + end + + // adjust period if integrator is saturated + if (time_err_int_reg == 0) begin + period_ns_next = '0; + end else if (~time_err_int_reg == 0) begin + period_ns_next = '1; + 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 != 0) begin + ptp_lock_count_next = ptp_lock_count_reg - 1; + end else begin + ptp_locked_next = 1'b0; + end + end + end +end + +always_ff @(posedge clk) begin + period_ns_reg <= period_ns_next; + + ts_fns_lsb_reg <= ts_fns_lsb_next; + ts_fns_reg <= ts_fns_next; + + ts_rel_ns_lsb_reg <= ts_rel_ns_lsb_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_lsb_reg <= '0; + ts_fns_reg <= '0; + ts_rel_ns_lsb_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/tb/ptp/taxi_ptp_td_leaf/Makefile b/tb/ptp/taxi_ptp_td_leaf/Makefile new file mode 100644 index 0000000..82e32b8 --- /dev/null +++ b/tb/ptp/taxi_ptp_td_leaf/Makefile @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2023-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_ptp_td_leaf +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = $(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += ../../../rtl/ptp/$(DUT).sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_TS_REL_EN := "1'b1" +export PARAM_TS_TOD_EN := "1'b1" +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 $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/ptp/taxi_ptp_td_leaf/ptp_td.py b/tb/ptp/taxi_ptp_td_leaf/ptp_td.py new file mode 120000 index 0000000..fec11b6 --- /dev/null +++ b/tb/ptp/taxi_ptp_td_leaf/ptp_td.py @@ -0,0 +1 @@ +../ptp_td.py \ No newline at end of file diff --git a/tb/ptp/taxi_ptp_td_leaf/test_taxi_ptp_td_leaf.py b/tb/ptp/taxi_ptp_td_leaf/test_taxi_ptp_td_leaf.py new file mode 100644 index 0000000..472a991 --- /dev/null +++ b/tb/ptp/taxi_ptp_td_leaf/test_taxi_ptp_td_leaf.py @@ -0,0 +1,541 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +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("Coherent tracking (250 MHz +0/-0.5%)") + + tb.set_ptp_clock_period(6.4) + tb.set_clock_period(4.0) + + await RisingEdge(dut.clk) + + period = 4.000 + step = 0.0002 + period_min = 4.0 + period_max = 4.0*(1+0.005) + + 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("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')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +def test_taxi_ptp_td_leaf(request): + dut = "taxi_ptp_td_leaf" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, "ptp", f"{dut}.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['TS_REL_EN'] = "1'b1" + parameters['TS_TOD_EN'] = "1'b1" + 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( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )