// 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