ptp: Add PTP TD rel2tod timestamp reconstruction module and testbench

Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
Alex Forencich
2025-02-13 22:07:46 -08:00
parent ad0d44616b
commit fc1e0efad7
5 changed files with 645 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
// SPDX-License-Identifier: CERN-OHL-S-2.0
/*
Copyright (c) 2024-2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
*/
`resetall
`timescale 1ns / 1fs
`default_nettype none
/*
* PTP time distribution ToD timestamp reconstruction module
*/
module taxi_ptp_td_rel2tod #
(
parameter TS_FNS_W = 16,
parameter TS_REL_NS_W = 32,
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,
/*
* PTP clock interface
*/
input wire logic ptp_clk,
input wire logic ptp_rst,
input wire logic ptp_td_sdi,
/*
* Timestamp conversion
*/
taxi_axis_if.snk s_axis_ts_rel,
taxi_axis_if.src m_axis_ts_tod
);
localparam TS_ID_W = s_axis_ts_rel.ID_W;
localparam TS_DEST_W = s_axis_ts_rel.DEST_W;
localparam TS_USER_W = s_axis_ts_rel.USER_W;
localparam TS_TOD_NS_W = 30;
localparam TS_NS_W = TS_TOD_NS_W+1;
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
logic ts_sel_reg = 1'b0;
logic [47:0] ts_tod_s_0_reg = '0;
logic [31:0] ts_tod_offset_ns_0_reg = '0;
logic [47:0] ts_tod_s_1_reg = '0;
logic [31:0] ts_tod_offset_ns_1_reg = '0;
logic [TS_TOD_S_W-1:0] output_ts_tod_s_reg = '0, output_ts_tod_s_next;
logic [TS_TOD_NS_W-1:0] output_ts_tod_ns_reg = '0, output_ts_tod_ns_next;
logic [TS_FNS_W-1:0] output_ts_fns_reg = '0, output_ts_fns_next;
logic [TS_ID_W-1:0] output_ts_id_reg = '0, output_ts_id_next;
logic [TS_DEST_W-1:0] output_ts_dest_reg = '0, output_ts_dest_next;
logic [TS_USER_W-1:0] output_ts_user_reg = '0, output_ts_user_next;
logic output_ts_valid_reg = 1'b0, output_ts_valid_next;
logic [TS_NS_W-1:0] ts_tod_ns_0;
logic [TS_NS_W-1:0] ts_tod_ns_1;
assign s_axis_ts_rel.tready = 1'b1;
assign m_axis_ts_tod.tdata = {output_ts_tod_s_reg, 2'b00, output_ts_tod_ns_reg, output_ts_fns_reg};
assign m_axis_ts_tod.tkeep = '1;
assign m_axis_ts_tod.tstrb = m_axis_ts_tod.tkeep;
assign m_axis_ts_tod.tlast = 1'b1;
assign m_axis_ts_tod.tid = output_ts_id_reg;
assign m_axis_ts_tod.tdest = output_ts_dest_reg;
assign m_axis_ts_tod.tuser = output_ts_user_reg;
assign m_axis_ts_tod.tvalid = output_ts_valid_reg;
always_comb begin
// reconstruct timestamp
// apply both offsets
ts_tod_ns_0 = TS_NS_W'(s_axis_ts_rel.tdata[TS_FNS_W +: TS_REL_NS_W] + ts_tod_offset_ns_0_reg);
ts_tod_ns_1 = TS_NS_W'(s_axis_ts_rel.tdata[TS_FNS_W +: TS_REL_NS_W] + ts_tod_offset_ns_1_reg);
// pick the correct result
// 2 MSB clear = lower half of range (0-536,870,911)
// 1 MSB clear = upper half of range, but could also be over 1 billion (536,870,912-1,073,741,823)
// 1 MSB set = overflow or underflow
// prefer 2 MSB clear over 1 MSB clear if neither result was overflow or underflow
if (ts_tod_ns_0[30:29] == 0 || (ts_tod_ns_0[30] == 0 && ts_tod_ns_1[30:29] != 0)) begin
output_ts_tod_s_next = ts_tod_s_0_reg;
output_ts_tod_ns_next = ts_tod_ns_0[TS_TOD_NS_W-1:0];
end else begin
output_ts_tod_s_next = ts_tod_s_1_reg;
output_ts_tod_ns_next = ts_tod_ns_1[TS_TOD_NS_W-1:0];
end
output_ts_fns_next = s_axis_ts_rel.tdata[TS_FNS_W-1:0];
output_ts_id_next = s_axis_ts_rel.tid;
output_ts_dest_next = s_axis_ts_rel.tdest;
output_ts_user_next = s_axis_ts_rel.tuser;
output_ts_valid_next = s_axis_ts_rel.tvalid;
end
always_ff @(posedge clk) begin
// extract data
if (dst_td_tvalid_reg) begin
if (dst_td_tid_reg[3:0] == 4'd0) begin
ts_sel_reg <= dst_td_tdata_reg[9];
end
// current
if (dst_td_tid_reg == {4'd1, 4'd1}) begin
if (ts_sel_reg) begin
ts_tod_offset_ns_1_reg[15:0] <= dst_td_tdata_reg;
end else begin
ts_tod_offset_ns_0_reg[15:0] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd1, 4'd2}) begin
if (ts_sel_reg) begin
ts_tod_offset_ns_1_reg[31:16] <= dst_td_tdata_reg;
end else begin
ts_tod_offset_ns_0_reg[31:16] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd0, 4'd3}) begin
if (ts_sel_reg) begin
ts_tod_s_1_reg[15:0] <= dst_td_tdata_reg;
end else begin
ts_tod_s_0_reg[15:0] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd0, 4'd4}) begin
if (ts_sel_reg) begin
ts_tod_s_1_reg[31:16] <= dst_td_tdata_reg;
end else begin
ts_tod_s_0_reg[31:16] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd0, 4'd5}) begin
if (ts_sel_reg) begin
ts_tod_s_1_reg[47:32] <= dst_td_tdata_reg;
end else begin
ts_tod_s_0_reg[47:32] <= dst_td_tdata_reg;
end
end
// alternate
if (dst_td_tid_reg == {4'd2, 4'd1}) begin
if (ts_sel_reg) begin
ts_tod_offset_ns_0_reg[15:0] <= dst_td_tdata_reg;
end else begin
ts_tod_offset_ns_1_reg[15:0] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd2, 4'd2}) begin
if (ts_sel_reg) begin
ts_tod_offset_ns_0_reg[31:16] <= dst_td_tdata_reg;
end else begin
ts_tod_offset_ns_1_reg[31:16] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd2, 4'd3}) begin
if (ts_sel_reg) begin
ts_tod_s_0_reg[15:0] <= dst_td_tdata_reg;
end else begin
ts_tod_s_1_reg[15:0] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd2, 4'd4}) begin
if (ts_sel_reg) begin
ts_tod_s_0_reg[31:16] <= dst_td_tdata_reg;
end else begin
ts_tod_s_1_reg[31:16] <= dst_td_tdata_reg;
end
end
if (dst_td_tid_reg == {4'd2, 4'd5}) begin
if (ts_sel_reg) begin
ts_tod_s_0_reg[47:32] <= dst_td_tdata_reg;
end else begin
ts_tod_s_1_reg[47:32] <= dst_td_tdata_reg;
end
end
end
output_ts_tod_s_reg <= output_ts_tod_s_next;
output_ts_tod_ns_reg <= output_ts_tod_ns_next;
output_ts_fns_reg <= output_ts_fns_next;
output_ts_id_reg <= output_ts_id_next;
output_ts_dest_reg <= output_ts_dest_next;
output_ts_user_reg <= output_ts_user_next;
output_ts_valid_reg <= output_ts_valid_next;
if (rst) begin
output_ts_valid_reg <= 1'b0;
end
end
endmodule
`resetall