diff --git a/rtl/ptp/taxi_ptp_td_phc.sv b/rtl/ptp/taxi_ptp_td_phc.sv new file mode 100644 index 0000000..5b233ba --- /dev/null +++ b/rtl/ptp/taxi_ptp_td_phc.sv @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * PTP time distribution PHC + */ +module taxi_ptp_td_phc # +( + parameter PERIOD_NS_NUM = 32, + parameter PERIOD_NS_DENOM = 5 +) +( + input wire logic clk, + input wire logic rst, + + /* + * ToD timestamp control + */ + input wire logic [47:0] input_ts_tod_s, + input wire logic [29:0] input_ts_tod_ns, + input wire logic input_ts_tod_valid, + output wire logic input_ts_tod_ready, + input wire logic [29:0] input_ts_tod_offset_ns, + input wire logic input_ts_tod_offset_valid, + output wire logic input_ts_tod_offset_ready, + + /* + * Relative timestamp control + */ + input wire logic [47:0] input_ts_rel_ns, + input wire logic input_ts_rel_valid, + output wire logic input_ts_rel_ready, + input wire logic [31:0] input_ts_rel_offset_ns, + input wire logic input_ts_rel_offset_valid, + output wire logic input_ts_rel_offset_ready, + + /* + * Fractional ns control + */ + input wire logic [31:0] input_ts_offset_fns, + input wire logic input_ts_offset_valid, + output wire logic input_ts_offset_ready, + + /* + * Period control + */ + input wire logic [7:0] input_period_ns, + input wire logic [31:0] input_period_fns, + input wire logic input_period_valid, + output wire logic input_period_ready, + input wire logic [15:0] input_drift_num, + input wire logic [15:0] input_drift_denom, + input wire logic input_drift_valid, + output wire logic input_drift_ready, + + /* + * Time distribution serial data output + */ + output wire logic ptp_td_sdo, + + /* + * PPS output + */ + output wire logic output_pps, + output wire logic output_pps_str +); + +localparam INC_NS_W = 9+8; + +localparam PERIOD_NS_W = 8; +localparam TS_REL_NS_W = 48; +localparam TS_TOD_S_W = 48; +localparam TS_TOD_NS_W = 30; +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}}}) / (32+FNS_W)'(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; + +logic [PERIOD_NS_W-1:0] period_ns_reg = PERIOD_NS_W'(PERIOD_NS); +logic [FNS_W-1:0] period_fns_reg = FNS_W'(PERIOD_FNS); + +logic [15:0] drift_num_reg = 16'(PERIOD_FNS_REM); +logic [15:0] drift_denom_reg = 16'(PERIOD_NS_DENOM); +logic [15:0] drift_cnt_reg = '0; +logic [15:0] drift_cnt_d1_reg = '0; +logic drift_apply_reg = 1'b0; +logic [23:0] drift_acc_reg = '0; + +logic [INC_NS_W-1:0] ts_inc_ns_reg = '0; +logic [FNS_W-1:0] ts_fns_reg = '0; + +logic [32:0] ts_rel_ns_inc_reg = '0; +logic [TS_REL_NS_W-1:0] ts_rel_ns_reg = '0; +logic ts_rel_updated_reg = 1'b0; + +logic [TS_TOD_S_W-1:0] ts_tod_s_reg = '0; +logic [TS_TOD_NS_W-1:0] ts_tod_ns_reg = '0; +logic ts_tod_updated_reg = 1'b0; + +logic [31:0] ts_tod_offset_ns_reg = '0; + +logic [TS_TOD_S_W-1:0] ts_tod_alt_s_reg = '0; +logic [31:0] ts_tod_alt_offset_ns_reg = '0; + +logic [7:0] td_update_cnt_reg = '0; +logic td_update_reg = 1'b0; +logic [1:0] td_msg_i_reg = '0; + +logic input_ts_tod_ready_reg = 1'b0; +logic input_ts_tod_offset_ready_reg = 1'b0; +logic input_ts_rel_ready_reg = 1'b0; +logic input_ts_rel_offset_ready_reg = 1'b0; +logic input_ts_offset_ready_reg = 1'b0; + +logic [17*14-1:0] td_shift_reg = '1; + +logic [15:0] pps_gen_fns_reg = '0; +logic [8:0] pps_gen_ns_inc_reg = '0; +logic [30:0] pps_gen_ns_reg = 31'h40000000; + +logic [9:0] pps_delay_reg = '0; +logic pps_reg = '0; +logic pps_str_reg = '0; + +logic [3:0] update_state_reg = '0; + +logic [47:0] adder_a_reg = '0; +logic [47:0] adder_b_reg = '0; +logic adder_cin_reg = '0; +logic [47:0] adder_sum_reg = '0; +logic adder_cout_reg = '0; +logic adder_busy_reg = '0; + +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_ff @(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 != 0) begin + if (drift_cnt_reg != 0) begin + drift_cnt_reg <= drift_cnt_reg - 1; + end else begin + drift_cnt_reg <= drift_denom_reg - 1; + drift_apply_reg <= 1'b1; + 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 + 24'(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]} + 24'(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]} + 24'(pps_gen_fns_reg); + end + pps_gen_ns_reg <= pps_gen_ns_reg + 31'(pps_gen_ns_inc_reg); + + if (!pps_gen_ns_reg[30]) begin + pps_delay_reg <= 14*17 + 32 + 240; + 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 != 0) 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 + {adder_cout_reg, adder_sum_reg} <= adder_a_reg + adder_b_reg + 48'(adder_cin_reg); + adder_busy_reg <= 1'b0; + + // computes the following: + // {ts_inc_ns_reg, ts_fns_reg} = drift_acc_reg + $signed(input_ts_offset_fns) + {period_ns_reg, period_fns_reg} * 256 + ts_fns_reg + // ts_rel_ns_reg = ts_rel_ns_reg + ts_inc_ns_reg + $signed(input_ts_rel_offset_ns); + // ts_tod_ns_reg = ts_tod_ns_reg + ts_inc_ns_reg + $signed(input_ts_tod_offset_ns); + // if that borrowed, + // ts_tod_ns_reg = ts_tod_ns_reg + NS_PER_S + // ts_tod_s_reg = ts_tod_s_reg - 1 + // else + // pps_gen_ns_reg = ts_tod_ns_reg - NS_PER_S + // if that did not borrow, + // ts_tod_ns_reg = ts_tod_ns_reg - NS_PER_S + // ts_tod_s_reg = ts_tod_s_reg + 1 + // ts_tod_offset_ns_reg = ts_tod_ns_reg - ts_rel_ns_reg + // if ts_tod_ns_reg[29] + // ts_tod_alt_offset_ns_reg = ts_tod_offset_ns_reg - NS_PER_S + // ts_tod_alt_s_reg = ts_tod_s_reg + 1 + // else + // ts_tod_alt_offset_ns_reg = ts_tod_offset_ns_reg + NS_PER_S + // ts_tod_alt_s_reg = ts_tod_s_reg - 1 + + if (!adder_busy_reg) begin + 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 <= 48'(drift_acc_reg + drift_num_reg); + end else begin + adder_a_reg <= 48'(drift_acc_reg); + end + adder_b_reg <= '0; + if (input_ts_offset_valid) begin + adder_b_reg <= 48'($signed(input_ts_offset_fns)); + end + 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; + adder_busy_reg <= 1'b1; + 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_reg; + adder_b_reg <= 48'({period_ns_reg, period_fns_reg, 8'd0}); + adder_cin_reg <= 0; + + update_state_reg <= 2; + adder_busy_reg <= 1'b1; + end + 2: begin + // compute next fns + adder_a_reg <= adder_sum_reg; + adder_b_reg <= 48'(ts_fns_reg); + adder_cin_reg <= 0; + + update_state_reg <= 3; + adder_busy_reg <= 1'b1; + end + 3: begin + // store fns + {ts_inc_ns_reg, ts_fns_reg} <= {adder_cout_reg, adder_sum_reg}; + + // compute relative timestamp 1 - add previous value and increment + adder_a_reg <= 48'(ts_rel_ns_reg); + adder_b_reg <= 48'({adder_cout_reg, adder_sum_reg} >> FNS_W); // ts_inc_ns_reg + adder_cin_reg <= 0; + + update_state_reg <= 4; + adder_busy_reg <= 1'b1; + end + 4: begin + // compute relative timestamp 2 - add offset + adder_a_reg <= adder_sum_reg; + adder_b_reg <= '0; + adder_cin_reg <= 0; + + // offset relative timestamp if requested + if (input_ts_rel_offset_valid) begin + adder_b_reg <= 48'($signed(input_ts_rel_offset_ns)); + input_ts_rel_offset_ready_reg <= 1'b1; + ts_rel_updated_reg <= 1'b1; + end + + update_state_reg <= 5; + adder_busy_reg <= 1'b1; + end + 5: begin + // store relative timestamp + ts_rel_ns_reg <= adder_sum_reg; + + // compute ToD timestamp 1 - add previous value and increment + adder_a_reg <= 48'(ts_tod_ns_reg); + adder_b_reg <= 48'(ts_inc_ns_reg); + adder_cin_reg <= 0; + + update_state_reg <= 6; + adder_busy_reg <= 1'b1; + end + 6: begin + // compute ToD timestamp 2 - add offset + adder_a_reg <= adder_sum_reg; + adder_b_reg <= '0; + adder_cin_reg <= 0; + + // offset ToD timestamp if requested + if (input_ts_tod_offset_valid) begin + adder_b_reg <= 48'($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; + adder_busy_reg <= 1'b1; + end + 7: begin + // compute ToD timestamp 3 - check for underflow/overflow + ts_tod_ns_reg <= TS_TOD_NS_W'(adder_sum_reg); + + if (adder_b_reg[47] && !adder_cout_reg) begin + // borrowed; add 1 billion + adder_a_reg <= adder_sum_reg; + adder_b_reg <= 48'(NS_PER_S); + adder_cin_reg <= 0; + + update_state_reg <= 8; + adder_busy_reg <= 1'b1; + end else begin + // did not borrow; subtract 1 billion to check for overflow + adder_a_reg <= adder_sum_reg; + adder_b_reg <= 48'(-NS_PER_S); + adder_cin_reg <= 0; + + update_state_reg <= 9; + adder_busy_reg <= 1'b1; + end + end + 8: begin + // seconds decrement + ts_tod_ns_reg <= TS_TOD_NS_W'(adder_sum_reg); + pps_gen_ns_reg[30] <= 1'b1; + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 48'(-1); + adder_cin_reg <= 0; + + update_state_reg <= 10; + adder_busy_reg <= 1'b1; + end + 9: begin + // seconds increment + pps_gen_ns_reg <= 31'(adder_sum_reg); + + if (!adder_cout_reg) 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; increment seconds + ts_tod_ns_reg <= TS_TOD_NS_W'(adder_sum_reg); + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 48'(1); + adder_cin_reg <= 0; + end + + update_state_reg <= 10; + adder_busy_reg <= 1'b1; + end + 10: begin + // store seconds + ts_tod_s_reg <= adder_sum_reg; + + // compute offset + adder_a_reg <= 48'(ts_tod_ns_reg); + adder_b_reg <= 48'(~ts_rel_ns_reg); + adder_cin_reg <= 1; + + update_state_reg <= 11; + adder_busy_reg <= 1'b1; + end + 11: begin + // store offset + ts_tod_offset_ns_reg <= 32'(adder_sum_reg); + + adder_a_reg <= adder_sum_reg; + adder_b_reg <= 48'(-NS_PER_S); + adder_cin_reg <= 0; + + if (ts_tod_ns_reg[29:27] == 3'b111) begin + // latter portion of second; compute offset for next second + adder_b_reg <= 48'(-NS_PER_S); + update_state_reg <= 12; + adder_busy_reg <= 1'b1; + end else begin + // former portion of second; compute offset for previous second + adder_b_reg <= 48'(NS_PER_S); + update_state_reg <= 14; + adder_busy_reg <= 1'b1; + end + end + 12: begin + // store alternate offset for next second + ts_tod_alt_offset_ns_reg <= 32'(adder_sum_reg); + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 48'(1); + adder_cin_reg <= 0; + + update_state_reg <= 13; + adder_busy_reg <= 1'b1; + end + 13: begin + // store alternate second for next second + ts_tod_alt_s_reg <= adder_sum_reg; + + update_state_reg <= 0; + end + 14: begin + // store alternate offset for previous second + ts_tod_alt_offset_ns_reg <= 32'(adder_sum_reg); + + adder_a_reg <= ts_tod_s_reg; + adder_b_reg <= 48'(-1); + adder_cin_reg <= 0; + + update_state_reg <= 15; + adder_busy_reg <= 1'b1; + end + 15: begin + // store alternate second for previous second + ts_tod_alt_s_reg <= adder_sum_reg; + + update_state_reg <= 0; + end + default: begin + // invalid state; return to idle + update_state_reg <= 0; + end + endcase + end + + // time distribution message generation + td_shift_reg <= {1'b1, td_shift_reg[17*14-1: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] <= 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] <= 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 + default: begin + td_shift_reg[17*1+0 +: 1] <= 1'b0; + td_shift_reg[17*1+1 +: 16] <= '0; + td_shift_reg[17*2+0 +: 1] <= 1'b0; + td_shift_reg[17*2+1 +: 16] <= '0; + td_shift_reg[17*3+0 +: 1] <= 1'b0; + td_shift_reg[17*3+1 +: 16] <= '0; + td_shift_reg[17*4+0 +: 1] <= 1'b0; + td_shift_reg[17*4+1 +: 16] <= '0; + td_shift_reg[17*5+0 +: 1] <= 1'b0; + td_shift_reg[17*5+1 +: 16] <= '0; + + 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+0 +: 8] <= period_ns_reg[7:0]; + td_shift_reg[17*13+1+8 +: 8] <= '0; + end + + if (rst) begin + period_ns_reg <= PERIOD_NS_W'(PERIOD_NS); + period_fns_reg <= FNS_W'(PERIOD_FNS); + drift_num_reg <= 16'(PERIOD_FNS_REM); + drift_denom_reg <= 16'(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 <= '1; + end +end + +endmodule + +`resetall diff --git a/tb/ptp/ptp_td.py b/tb/ptp/ptp_td.py new file mode 100644 index 0000000..de9c854 --- /dev/null +++ b/tb/ptp/ptp_td.py @@ -0,0 +1,590 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +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_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 >> 27 == 7: + # latter portion 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 portion 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/taxi_ptp_td_phc/Makefile b/tb/ptp/taxi_ptp_td_phc/Makefile new file mode 100644 index 0000000..bcdbcaa --- /dev/null +++ b/tb/ptp/taxi_ptp_td_phc/Makefile @@ -0,0 +1,46 @@ +# 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_phc +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_PERIOD_NS_NUM := 32 +export PARAM_PERIOD_NS_DENOM := 5 + +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_phc/ptp_td.py b/tb/ptp/taxi_ptp_td_phc/ptp_td.py new file mode 120000 index 0000000..fec11b6 --- /dev/null +++ b/tb/ptp/taxi_ptp_td_phc/ptp_td.py @@ -0,0 +1 @@ +../ptp_td.py \ No newline at end of file diff --git a/tb/ptp/taxi_ptp_td_phc/test_taxi_ptp_td_phc.py b/tb/ptp/taxi_ptp_td_phc/test_taxi_ptp_td_phc.py new file mode 100644 index 0000000..aa2de40 --- /dev/null +++ b/tb/ptp/taxi_ptp_td_phc/test_taxi_ptp_td_phc.py @@ -0,0 +1,550 @@ +#!/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 + +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')) + + +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_phc(request): + dut = "taxi_ptp_td_phc" + 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['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( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )