1
0
mirror of https://github.com/corundum/corundum.git synced 2025-01-16 08:12:53 +08:00

Add PTP time distribution components

Signed-off-by: Alex Forencich <alex@alexforencich.com>
This commit is contained in:
Alex Forencich 2023-11-07 13:07:15 -08:00
parent 009560f583
commit bd8e8e5b20
11 changed files with 3539 additions and 0 deletions

View File

@ -307,6 +307,14 @@ PTP clock CDC module with PPS output. Use this module to transfer and deskew a
free-running PTP clock across clock domains. Supports both 64 and 96 bit
timestamp formats.
### `ptp_td_leaf` module
PTP time distribution leaf clock module. Accepts PTP time distribution messages from the `ptp_td_phc` module, and outputs both the 96-bit time-of-day timestamp and 64-bit relative timestamp in the destination clock domain, as well as both single-cycle and stretched PPS outputs. Also supports pipelining the serial data input, automatically compensating for the pipeline delay.
### `ptp_td_phc` module
PTP time distribution master clock module. Generates PTP time distribution messages over a serial interface that can provide PTP time to one or more leaf clocks (`ptp_td_leaf`), as well as both single-cycle and stretched PPS outputs. The fractional nanoseconds portion is shared between the time-of-day and relative timestamps to support reconstruction of the 96-bit time-of-day timestamp from a truncated relative timestamp. The module supports coarse setting of both the ToD and relative timestamps as well as atomically applying offsets to the ToD and relative timestamps and the shared fractional nanoseconds.
### `ptp_ts_extract` module
PTP timestamp extract module. Use this module to extract a PTP timestamp
@ -466,6 +474,8 @@ and data lines.
rtl/oddr.v : Generic DDR output register
rtl/ptp_clock.v : PTP clock
rtl/ptp_clock_cdc.v : PTP clock CDC
rtl/ptp_td_leaf.v : PTP time distribution leaf clock
rtl/ptp_td_phc.v : PTP time distribution master clock
rtl/ptp_ts_extract.v : PTP timestamp extract
rtl/ptp_perout.v : PTP period out
rtl/rgmii_phy_if.v : RGMII PHY interface

1015
rtl/ptp_td_leaf.v Normal file

File diff suppressed because it is too large Load Diff

603
rtl/ptp_td_phc.v Normal file
View File

@ -0,0 +1,603 @@
/*
Copyright (c) 2023 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Language: Verilog 2001
`resetall
`timescale 1ns / 1ps
`default_nettype none
/*
* PTP time distribution PHC
*/
module ptp_td_phc #
(
parameter PERIOD_NS_NUM = 32,
parameter PERIOD_NS_DENOM = 5
)
(
input wire clk,
input wire rst,
/*
* ToD timestamp control
*/
input wire [47:0] input_ts_tod_s,
input wire [29:0] input_ts_tod_ns,
input wire input_ts_tod_valid,
output wire input_ts_tod_ready,
input wire [29:0] input_ts_tod_offset_ns,
input wire input_ts_tod_offset_valid,
output wire input_ts_tod_offset_ready,
/*
* Relative timestamp control
*/
input wire [47:0] input_ts_rel_ns,
input wire input_ts_rel_valid,
output wire input_ts_rel_ready,
input wire [31:0] input_ts_rel_offset_ns,
input wire input_ts_rel_offset_valid,
output wire input_ts_rel_offset_ready,
/*
* Fractional ns control
*/
input wire [31:0] input_ts_offset_fns,
input wire input_ts_offset_valid,
output wire input_ts_offset_ready,
/*
* Period control
*/
input wire [7:0] input_period_ns,
input wire [31:0] input_period_fns,
input wire input_period_valid,
output wire input_period_ready,
input wire [15:0] input_drift_num,
input wire [15:0] input_drift_denom,
input wire input_drift_valid,
output wire input_drift_ready,
/*
* Time distribution serial data output
*/
output wire ptp_td_sdo,
/*
* PPS output
*/
output wire output_pps,
output wire output_pps_str
);
localparam INC_NS_W = 9+8;
localparam FNS_W = 32;
localparam PERIOD_NS = PERIOD_NS_NUM / PERIOD_NS_DENOM;
localparam PERIOD_NS_REM = PERIOD_NS_NUM - PERIOD_NS*PERIOD_NS_DENOM;
localparam PERIOD_FNS = (PERIOD_NS_REM * {32'd1, {FNS_W{1'b0}}}) / PERIOD_NS_DENOM;
localparam PERIOD_FNS_REM = (PERIOD_NS_REM * {32'd1, {FNS_W{1'b0}}}) - PERIOD_FNS*PERIOD_NS_DENOM;
localparam [30:0] NS_PER_S = 31'd1_000_000_000;
reg [7:0] period_ns_reg = PERIOD_NS;
reg [FNS_W-1:0] period_fns_reg = PERIOD_FNS;
reg [15:0] drift_num_reg = PERIOD_FNS_REM;
reg [15:0] drift_denom_reg = PERIOD_NS_DENOM;
reg [15:0] drift_cnt_reg = 0;
reg [15:0] drift_cnt_d1_reg = 0;
reg drift_apply_reg = 1'b0;
reg [23:0] drift_acc_reg = 0;
reg [INC_NS_W-1:0] ts_inc_ns_reg = 0;
reg [FNS_W-1:0] ts_fns_reg = 0;
reg [32:0] ts_rel_ns_inc_reg = 0;
reg [47:0] ts_rel_ns_reg = 0;
reg ts_rel_updated_reg = 1'b0;
reg [47:0] ts_tod_s_reg = 0;
reg [29:0] ts_tod_ns_reg = 0;
reg ts_tod_updated_reg = 1'b0;
reg [31:0] ts_tod_offset_ns_reg = 0;
reg [47:0] ts_tod_alt_s_reg = 0;
reg [31:0] ts_tod_alt_offset_ns_reg = 0;
reg [7:0] td_update_cnt_reg = 0;
reg td_update_reg = 1'b0;
reg [1:0] td_msg_i_reg = 0;
reg input_ts_tod_ready_reg = 1'b0;
reg input_ts_tod_offset_ready_reg = 1'b0;
reg input_ts_rel_ready_reg = 1'b0;
reg input_ts_rel_offset_ready_reg = 1'b0;
reg input_ts_offset_ready_reg = 1'b0;
reg [17*14-1:0] td_shift_reg = {17*14{1'b1}};
reg [15:0] pps_gen_fns_reg = 0;
reg [9:0] pps_gen_ns_inc_reg = 0;
reg [30:0] pps_gen_ns_reg = 31'h40000000;
reg [9:0] pps_delay_reg = 0;
reg pps_reg = 0;
reg pps_str_reg = 0;
reg [3:0] update_state_reg = 0;
reg [47:0] adder_a_reg = 0;
reg [47:0] adder_b_reg = 0;
reg adder_cin_reg = 0;
wire [47:0] adder_sum;
wire adder_cout;
assign {adder_cout, adder_sum} = adder_a_reg + adder_b_reg + adder_cin_reg;
assign input_ts_tod_ready = input_ts_tod_ready_reg;
assign input_ts_tod_offset_ready = input_ts_tod_offset_ready_reg;
assign input_ts_rel_ready = input_ts_rel_ready_reg;
assign input_ts_rel_offset_ready = input_ts_rel_offset_ready_reg;
assign input_ts_offset_ready = input_ts_offset_ready_reg;
assign input_period_ready = 1'b1;
assign input_drift_ready = 1'b1;
assign output_pps = pps_reg;
assign output_pps_str = pps_str_reg;
assign ptp_td_sdo = td_shift_reg[0];
always @(posedge clk) begin
drift_apply_reg <= 1'b0;
input_ts_tod_ready_reg <= 1'b0;
input_ts_tod_offset_ready_reg <= 1'b0;
input_ts_rel_ready_reg <= 1'b0;
input_ts_rel_offset_ready_reg <= 1'b0;
input_ts_offset_ready_reg <= 1'b0;
// update and message generation cadence
{td_update_reg, td_update_cnt_reg} <= td_update_cnt_reg + 1;
// latch drift setting
if (input_drift_valid) begin
drift_num_reg <= input_drift_num;
drift_denom_reg <= input_drift_denom;
end
// drift
if (drift_denom_reg) begin
if (drift_cnt_reg == 0) begin
drift_cnt_reg <= drift_denom_reg - 1;
drift_apply_reg <= 1'b1;
end else begin
drift_cnt_reg <= drift_cnt_reg - 1;
end
end else begin
drift_cnt_reg <= 0;
end
drift_cnt_d1_reg <= drift_cnt_reg;
// drift accumulation
if (drift_apply_reg) begin
drift_acc_reg <= drift_acc_reg + drift_num_reg;
end
// latch period setting
if (input_period_valid) begin
period_ns_reg <= input_period_ns;
period_fns_reg <= input_period_fns;
end
// PPS generation
if (td_update_reg) begin
{pps_gen_ns_inc_reg, pps_gen_fns_reg} <= {period_ns_reg, period_fns_reg[31:16]} + ts_fns_reg[31:16];
end else begin
{pps_gen_ns_inc_reg, pps_gen_fns_reg} <= {period_ns_reg, period_fns_reg[31:16]} + pps_gen_fns_reg;
end
pps_gen_ns_reg <= pps_gen_ns_reg + pps_gen_ns_inc_reg;
if (!pps_gen_ns_reg[30]) begin
// pps_delay_reg <= 14*17 + 32 + 1;
pps_delay_reg <= 14*17 + 32 + 248;
pps_gen_ns_reg[30] <= 1'b1;
end
pps_reg <= 1'b0;
if (ts_tod_ns_reg[29]) begin
pps_str_reg <= 1'b0;
end
if (pps_delay_reg) begin
pps_delay_reg <= pps_delay_reg - 1;
if (pps_delay_reg == 1) begin
pps_reg <= 1'b1;
pps_str_reg <= 1'b1;
end
end
// update state machine
case (update_state_reg)
0: begin
// idle
// set relative timestamp
if (input_ts_rel_valid) begin
ts_rel_ns_reg <= input_ts_rel_ns;
input_ts_rel_ready_reg <= 1'b1;
ts_rel_updated_reg <= 1'b1;
end
// set ToD timestamp
if (input_ts_tod_valid) begin
ts_tod_s_reg <= input_ts_tod_s;
ts_tod_ns_reg <= input_ts_tod_ns;
input_ts_tod_ready_reg <= 1'b1;
ts_tod_updated_reg <= 1'b1;
end
// compute period 1 - add drift and requested offset
if (drift_apply_reg) begin
adder_a_reg <= drift_acc_reg + drift_num_reg;
end else begin
adder_a_reg <= drift_acc_reg;
end
adder_b_reg <= input_ts_offset_valid ? $signed(input_ts_offset_fns) : 0;
adder_cin_reg <= 0;
if (td_update_reg) begin
drift_acc_reg <= 0;
input_ts_offset_ready_reg <= input_ts_offset_valid;
update_state_reg <= 1;
end else begin
update_state_reg <= 0;
end
end
1: begin
// compute period 2 - add drift and offset to period
adder_a_reg <= adder_sum;
adder_b_reg <= {period_ns_reg, period_fns_reg, 8'd0};
adder_cin_reg <= 0;
update_state_reg <= 2;
end
2: begin
// compute next fns
adder_a_reg <= adder_sum;
adder_b_reg <= ts_fns_reg;
adder_cin_reg <= 0;
update_state_reg <= 3;
end
3: begin
// store fns; compute relative timestamp 1 - add previous value and offset
{ts_inc_ns_reg, ts_fns_reg} <= {adder_cout, adder_sum};
adder_a_reg <= ts_rel_ns_reg;
adder_b_reg <= 0;
adder_cin_reg <= 0;
// offset relative timestamp if requested
if (input_ts_rel_offset_valid) begin
adder_b_reg <= $signed(input_ts_rel_offset_ns);
input_ts_rel_offset_ready_reg <= 1'b1;
ts_rel_updated_reg <= 1'b1;
end
update_state_reg <= 4;
end
4: begin
// compute relative timestamp 2 - add increment
adder_a_reg <= adder_sum;
adder_b_reg <= ts_inc_ns_reg;
adder_cin_reg <= 0;
update_state_reg <= 5;
end
5: begin
// store relative timestamp; compute ToD timestamp 1 - add previous value and increment
ts_rel_ns_reg <= adder_sum;
adder_a_reg <= ts_tod_ns_reg;
adder_b_reg <= ts_inc_ns_reg;
adder_cin_reg <= 0;
update_state_reg <= 6;
end
6: begin
// compute ToD timestamp 2 - add offset
adder_a_reg <= adder_sum;
adder_b_reg <= 0;
adder_cin_reg <= 0;
// offset ToD timestamp if requested
if (input_ts_tod_offset_valid) begin
adder_b_reg <= $signed(input_ts_tod_offset_ns);
input_ts_tod_offset_ready_reg <= 1'b1;
ts_tod_updated_reg <= 1'b1;
end
update_state_reg <= 7;
end
7: begin
// compute ToD timestamp 2 - check for underflow/overflow
ts_tod_ns_reg <= adder_sum;
if (adder_b_reg[47] && !adder_cout) begin
// borrowed; add 1 billion
adder_a_reg <= adder_sum;
adder_b_reg <= NS_PER_S;
adder_cin_reg <= 0;
update_state_reg <= 8;
end else begin
// did not borrow; subtract 1 billion to check for overflow
adder_a_reg <= adder_sum;
adder_b_reg <= -NS_PER_S;
adder_cin_reg <= 0;
update_state_reg <= 9;
end
end
8: begin
// seconds decrement
ts_tod_ns_reg <= adder_sum;
pps_gen_ns_reg[30] <= 1'b1;
adder_a_reg <= ts_tod_s_reg;
adder_b_reg <= -1;
adder_cin_reg <= 0;
update_state_reg <= 10;
end
9: begin
// seconds increment
pps_gen_ns_reg <= adder_sum;
if (!adder_cout) begin
// borrowed; leave seconds alone
adder_a_reg <= ts_tod_s_reg;
adder_b_reg <= 0;
adder_cin_reg <= 0;
end else begin
// did not borrow; decrement seconds
ts_tod_ns_reg <= adder_sum;
adder_a_reg <= ts_tod_s_reg;
adder_b_reg <= 1;
adder_cin_reg <= 0;
end
update_state_reg <= 10;
end
10: begin
// store seconds; compute offset
ts_tod_s_reg <= adder_sum;
if (adder_sum == ts_tod_alt_s_reg) begin
// store previous offset as alternate
ts_tod_alt_s_reg <= ts_tod_s_reg;
ts_tod_alt_offset_ns_reg <= ts_tod_offset_ns_reg;
end
adder_a_reg <= ts_tod_ns_reg;
adder_b_reg <= ~ts_rel_ns_reg;
adder_cin_reg <= 1;
update_state_reg <= 11;
end
11: begin
// store offset
ts_tod_offset_ns_reg <= adder_sum;
adder_a_reg <= adder_sum;
adder_b_reg <= -NS_PER_S;
adder_cin_reg <= 0;
if (ts_tod_ns_reg[29]) begin
// latter half of second; compute offset for next second
adder_b_reg <= -NS_PER_S;
update_state_reg <= 12;
end else begin
// former half of second; compute offset for previous second
adder_b_reg <= NS_PER_S;
update_state_reg <= 14;
end
end
12: begin
// store alternate offset for next second
ts_tod_alt_offset_ns_reg <= adder_sum;
adder_a_reg <= ts_tod_s_reg;
adder_b_reg <= 1;
adder_cin_reg <= 0;
update_state_reg <= 13;
end
13: begin
// store alternate second for next second
ts_tod_alt_s_reg <= adder_sum;
update_state_reg <= 0;
end
14: begin
// store alternate offset for previous second
ts_tod_alt_offset_ns_reg <= adder_sum;
adder_a_reg <= ts_tod_s_reg;
adder_b_reg <= -1;
adder_cin_reg <= 0;
update_state_reg <= 15;
end
15: begin
// store alternate second for previous second
ts_tod_alt_s_reg <= adder_sum;
update_state_reg <= 0;
end
default: begin
// invalid state; return to idle
update_state_reg <= 0;
end
endcase
// time distribution message generation
td_shift_reg <= {1'b1, td_shift_reg} >> 1;
if (td_update_reg) begin
// word 0: control
td_shift_reg[17*0+0 +: 1] <= 1'b0;
td_shift_reg[17*0+1 +: 16] <= 0;
td_shift_reg[17*0+1+0 +: 4] <= td_msg_i_reg;
td_shift_reg[17*0+1+8 +: 1] <= ts_rel_updated_reg;
td_shift_reg[17*0+1+9 +: 1] <= ts_tod_s_reg[0];
ts_rel_updated_reg <= 1'b0;
case (td_msg_i_reg)
2'd0: begin
// msg 0 word 1: current ToD ns 15:0
td_shift_reg[17*1+0 +: 1] <= 1'b0;
td_shift_reg[17*1+1 +: 16] <= ts_tod_ns_reg[15:0];
// msg 0 word 2: current ToD ns 29:16
td_shift_reg[17*2+0 +: 1] <= 1'b0;
td_shift_reg[17*2+1+0 +: 15] <= ts_tod_ns_reg[29:16];
td_shift_reg[17*2+1+15 +: 1] <= ts_tod_updated_reg;
ts_tod_updated_reg <= 1'b0;
// msg 0 word 3: current ToD seconds 15:0
td_shift_reg[17*3+0 +: 1] <= 1'b0;
td_shift_reg[17*3+1 +: 16] <= ts_tod_s_reg[15:0];
// msg 0 word 4: current ToD seconds 31:16
td_shift_reg[17*4+0 +: 1] <= 1'b0;
td_shift_reg[17*4+1 +: 16] <= ts_tod_s_reg[31:16];
// msg 0 word 5: current ToD seconds 47:32
td_shift_reg[17*5+0 +: 1] <= 1'b0;
td_shift_reg[17*5+1 +: 16] <= ts_tod_s_reg[47:32];
td_msg_i_reg <= 2'd1;
end
2'd1: begin
// msg 1 word 1: current ToD ns offset 15:0
td_shift_reg[17*1+0 +: 1] <= 1'b0;
td_shift_reg[17*1+1 +: 16] <= ts_tod_offset_ns_reg[15:0];
// msg 1 word 2: current ToD ns offset 31:16
td_shift_reg[17*2+0 +: 1] <= 1'b0;
td_shift_reg[17*2+1 +: 16] <= ts_tod_offset_ns_reg[31:16];
// msg 1 word 3: drift num
td_shift_reg[17*3+0 +: 1] <= 1'b0;
td_shift_reg[17*3+1 +: 16] <= drift_num_reg;
// msg 1 word 4: drift denom
td_shift_reg[17*4+0 +: 1] <= 1'b0;
td_shift_reg[17*4+1 +: 16] <= drift_denom_reg;
// msg 1 word 5: drift state
td_shift_reg[17*5+0 +: 1] <= 1'b0;
td_shift_reg[17*5+1 +: 16] <= drift_cnt_d1_reg;
td_msg_i_reg <= 2'd2;
end
2'd2: begin
// msg 2 word 1: alternate ToD ns offset 15:0
td_shift_reg[17*1+0 +: 1] <= 1'b0;
td_shift_reg[17*1+1 +: 16] <= ts_tod_alt_offset_ns_reg[15:0];
// msg 2 word 2: alternate ToD ns offset 31:16
td_shift_reg[17*2+0 +: 1] <= 1'b0;
td_shift_reg[17*2+1 +: 16] <= ts_tod_alt_offset_ns_reg[31:16];
// msg 2 word 3: alternate ToD seconds 15:0
td_shift_reg[17*3+0 +: 1] <= 1'b0;
td_shift_reg[17*3+1 +: 16] <= ts_tod_alt_s_reg[15:0];
// msg 2 word 4: alternate ToD seconds 31:16
td_shift_reg[17*4+0 +: 1] <= 1'b0;
td_shift_reg[17*4+1 +: 16] <= ts_tod_alt_s_reg[31:16];
// msg 2 word 5: alternate ToD seconds 47:32
td_shift_reg[17*5+0 +: 1] <= 1'b0;
td_shift_reg[17*5+1 +: 16] <= ts_tod_alt_s_reg[47:32];
td_msg_i_reg <= 2'd0;
end
endcase
// word 6: current fns 15:0
td_shift_reg[17*6+0 +: 1] <= 1'b0;
td_shift_reg[17*6+1 +: 16] <= ts_fns_reg[15:0];
// word 7: current fns 31:16
td_shift_reg[17*7+0 +: 1] <= 1'b0;
td_shift_reg[17*7+1 +: 16] <= ts_fns_reg[31:16];
// word 8: current ns 15:0
td_shift_reg[17*8+0 +: 1] <= 1'b0;
td_shift_reg[17*8+1 +: 16] <= ts_rel_ns_reg[15:0];
// word 9: current ns 31:16
td_shift_reg[17*9+0 +: 1] <= 1'b0;
td_shift_reg[17*9+1 +: 16] <= ts_rel_ns_reg[31:16];
// word 10: current ns 47:32
td_shift_reg[17*10+0 +: 1] <= 1'b0;
td_shift_reg[17*10+1 +: 16] <= ts_rel_ns_reg[47:32];
// word 11: current phase increment fns 15:0
td_shift_reg[17*11+0 +: 1] <= 1'b0;
td_shift_reg[17*11+1 +: 16] <= period_fns_reg[15:0];
// word 12: current phase increment fns 31:16
td_shift_reg[17*12+0 +: 1] <= 1'b0;
td_shift_reg[17*12+1 +: 16] <= period_fns_reg[31:16];
// word 13: current phase increment ns 7:0 + crc
td_shift_reg[17*13+0 +: 1] <= 1'b0;
td_shift_reg[17*13+1 +: 16] <= period_ns_reg[7:0];
end
if (rst) begin
period_ns_reg <= PERIOD_NS;
period_fns_reg <= PERIOD_FNS;
drift_num_reg <= PERIOD_FNS_REM;
drift_denom_reg <= PERIOD_NS_DENOM;
drift_cnt_reg <= 0;
drift_acc_reg <= 0;
ts_fns_reg <= 0;
ts_rel_ns_reg <= 0;
ts_rel_updated_reg <= 0;
ts_tod_s_reg <= 0;
ts_tod_ns_reg <= 0;
ts_tod_updated_reg <= 0;
pps_gen_ns_reg[30] <= 1'b1;
pps_delay_reg <= 0;
pps_reg <= 0;
pps_str_reg <= 0;
td_update_cnt_reg <= 0;
td_update_reg <= 1'b0;
td_msg_i_reg <= 0;
td_shift_reg <= {17*14{1'b1}};
end
end
endmodule
`resetall

104
syn/vivado/ptp_td_leaf.tcl Normal file
View File

@ -0,0 +1,104 @@
# Copyright (c) 2019-2023 Alex Forencich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# PTP time distribution leaf module
foreach inst [get_cells -hier -filter {(ORIG_REF_NAME == ptp_td_leaf || REF_NAME == ptp_td_leaf)}] {
puts "Inserting timing constraints for ptp_td_leaf instance $inst"
# get clock periods
set input_clk [get_clocks -of_objects [get_pins "$inst/src_sync_reg_reg/C"]]
set output_clk [get_clocks -of_objects [get_pins "$inst/dst_sync_reg_reg/C"]]
set input_clk_period [if {[llength $input_clk]} {get_property -min PERIOD $input_clk} {expr 1.0}]
set output_clk_period [if {[llength $output_clk]} {get_property -min PERIOD $output_clk} {expr 1.0}]
# TD data sync
set_property ASYNC_REG TRUE [get_cells -hier -regexp ".*/dst_td_(tdata|tid)_reg_reg(\\\[\\d+\\\])?" -filter "PARENT == $inst"]
set_max_delay -from [get_cells "$inst/td_tdata_reg_reg[*]"] -to [get_cells "$inst/dst_td_tdata_reg_reg[*]"] -datapath_only $output_clk_period
set_bus_skew -from [get_cells "$inst/td_tdata_reg_reg[*]"] -to [get_cells "$inst/dst_td_tdata_reg_reg[*]"] $input_clk_period
set_max_delay -from [get_cells "$inst/td_tid_reg_reg[*]"] -to [get_cells "$inst/dst_td_tid_reg_reg[*]"] -datapath_only $output_clk_period
set_bus_skew -from [get_cells "$inst/td_tid_reg_reg[*]"] -to [get_cells "$inst/dst_td_tid_reg_reg[*]"] $input_clk_period
set sync_ffs [get_cells -quiet -hier -regexp ".*/td_sync_sync\[12\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set_max_delay -from [get_cells "$inst/td_sync_reg_reg"] -to [get_cells "$inst/td_sync_sync1_reg_reg"] -datapath_only $input_clk_period
}
# timestamp sync
set_property ASYNC_REG TRUE [get_cells -hier -regexp ".*/src_ns_sync_reg_reg(\\\[\\d+\\\])?" -filter "PARENT == $inst"]
set_max_delay -from [get_cells "$inst/src_ns_reg_reg[*]"] -to [get_cells "$inst/src_ns_sync_reg_reg[*]"] -datapath_only $output_clk_period
set_bus_skew -from [get_cells "$inst/src_ns_reg_reg[*]"] -to [get_cells "$inst/src_ns_sync_reg_reg[*]"] $input_clk_period
# sample clock
set sync_ffs [get_cells -quiet -hier -regexp ".*/src_sync_sample_sync\[12\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set_max_delay -from [get_cells "$inst/src_sync_reg_reg"] -to [get_cells "$inst/src_sync_sample_sync1_reg_reg"] -datapath_only $input_clk_period
}
set sync_ffs [get_cells -quiet -hier -regexp ".*/dst_sync_sample_sync\[12\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set_max_delay -from [get_cells "$inst/dst_sync_reg_reg"] -to [get_cells "$inst/dst_sync_sample_sync1_reg_reg"] -datapath_only $output_clk_period
}
# sample update sync
set sync_ffs [get_cells -quiet -hier -regexp ".*/sample_update_sync\[123\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set src_clk [get_clocks -of_objects [get_pins "$inst/sample_update_reg_reg/C"]]
set src_clk_period [if {[llength $src_clk]} {get_property -min PERIOD $src_clk} {expr 1.0}]
set_max_delay -from [get_cells "$inst/sample_update_reg_reg"] -to [get_cells "$inst/sample_update_sync1_reg_reg"] -datapath_only $src_clk_period
set_max_delay -from [get_cells "$inst/sample_acc_out_reg_reg[*]"] -to [get_cells $inst/sample_acc_sync_reg_reg[*]] -datapath_only $src_clk_period
set_bus_skew -from [get_cells "$inst/sample_acc_out_reg_reg[*]"] -to [get_cells $inst/sample_acc_sync_reg_reg[*]] $output_clk_period
}
# timestamp transfer sync
set sync_ffs [get_cells -quiet -hier -regexp ".*/src_sync_sync\[12\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set_max_delay -from [get_cells "$inst/src_sync_reg_reg"] -to [get_cells "$inst/src_sync_sync1_reg_reg"] -datapath_only $input_clk_period
}
set sync_ffs [get_cells -quiet -hier -regexp ".*/src_marker_sync\[12\]_reg_reg" -filter "PARENT == $inst"]
if {[llength $sync_ffs]} {
set_property ASYNC_REG TRUE $sync_ffs
set_max_delay -from [get_cells "$inst/src_marker_reg_reg"] -to [get_cells "$inst/src_marker_sync1_reg_reg"] -datapath_only $input_clk_period
}
}

604
tb/ptp_td.py Normal file
View File

@ -0,0 +1,604 @@
"""
Copyright (c) 2023 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
from decimal import Decimal, Context
from fractions import Fraction
import cocotb
from cocotb.triggers import RisingEdge, Event
from cocotb.utils import get_sim_time
from cocotbext.eth.reset import Reset
class PtpTdSource(Reset):
def __init__(self,
data=None,
clock=None,
reset=None,
reset_active_level=True,
period_ns=6.4,
td_delay=32,
*args, **kwargs):
self.log = logging.getLogger(f"cocotb.{data._path}")
self.data = data
self.clock = clock
self.reset = reset
self.log.info("PTP time distribution source")
self.log.info("Copyright (c) 2023 Alex Forencich")
self.log.info("https://github.com/alexforencich/verilog-ethernet")
super().__init__(*args, **kwargs)
self.ctx = Context(prec=60)
self.period_ns = 0
self.period_fns = 0
self.drift_num = 0
self.drift_denom = 0
self.drift_cnt = 0
self.set_period_ns(period_ns)
self.ts_fns = 0
self.ts_rel_ns = 0
self.ts_rel_updated = False
self.ts_tod_s = 0
self.ts_tod_ns = 0
self.ts_tod_updated = False
self.ts_tod_offset_ns = 0
self.ts_tod_alt_s = 0
self.ts_tod_alt_offset_ns = 0
self.td_delay = td_delay
self.timestamp_delay = [(0, 0, 0, 0)]
self.data.setimmediatevalue(1)
self.pps = Event()
self._run_cr = None
self._init_reset(reset, reset_active_level)
def set_period(self, ns, fns):
self.period_ns = int(ns)
self.period_fns = int(fns) & 0xffffffff
def set_drift(self, num, denom):
self.drift_num = int(num)
self.drift_denom = int(denom)
def set_period_ns(self, t):
t = Decimal(t)
period, drift = self.ctx.divmod(Decimal(t) * Decimal(2**32), Decimal(1))
period = int(period)
frac = Fraction(drift).limit_denominator(2**16-1)
self.set_period(period >> 32, period & 0xffffffff)
self.set_drift(frac.numerator, frac.denominator)
self.log.info("Set period: %s ns", t)
self.log.info("Period: 0x%x ns 0x%08x fns", self.period_ns, self.period_fns)
self.log.info("Drift: 0x%04x / 0x%04x fns", self.drift_num, self.drift_denom)
def get_period_ns(self):
p = Decimal((self.period_ns << 32) | self.period_fns)
if self.drift_denom:
p += Decimal(self.drift_num) / Decimal(self.drift_denom)
return p / Decimal(2**32)
def set_ts_tod(self, ts_s, ts_ns, ts_fns):
self.ts_tod_s = int(ts_s)
self.ts_tod_ns = int(ts_ns)
self.ts_fns = int(ts_fns)
self.ts_tod_updated = True
def set_ts_tod_64(self, ts):
ts = int(ts)
self.set_ts_tod(ts >> 48, (ts >> 32) & 0x3fffffff, (ts & 0xffff) << 16)
def set_ts_tod_ns(self, t):
ts_s, ts_ns = self.ctx.divmod(Decimal(t), Decimal(1000000000))
ts_s = ts_s.scaleb(-9).to_integral_value()
ts_ns, ts_fns = self.ctx.divmod(ts_ns, Decimal(1))
ts_ns = ts_ns.to_integral_value()
ts_fns = (ts_fns * Decimal(2**32)).to_integral_value()
self.set_ts_tod(ts_s, ts_ns, ts_fns)
def set_ts_tod_s(self, t):
self.set_ts_tod_ns(Decimal(t).scaleb(9, self.ctx))
def set_ts_tod_sim_time(self):
self.set_ts_tod_ns(Decimal(get_sim_time('fs')).scaleb(-6))
def get_ts_tod(self):
ts_tod_s, ts_tod_ns, ts_rel_ns, ts_fns = self.timestamp_delay[0]
return (ts_tod_s, ts_tod_ns, ts_fns)
def get_ts_tod_96(self):
ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod()
return (ts_tod_s << 48) | (ts_tod_ns << 16) | (ts_fns >> 16)
def get_ts_tod_ns(self):
ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod()
ns = Decimal(ts_fns) / Decimal(2**32)
ns = self.ctx.add(ns, Decimal(ts_tod_ns))
return self.ctx.add(ns, Decimal(ts_tod_s).scaleb(9))
def get_ts_tod_s(self):
return self.get_ts_tod_ns().scaleb(-9, self.ctx)
def set_ts_rel(self, ts_ns, ts_fns):
self.ts_rel_ns = int(ts_ns)
self.ts_fns = int(ts_fns)
self.ts_rel_updated = True
def set_ts_rel_64(self, ts):
ts = int(ts)
self.set_ts_rel(ts >> 16, (ts & 0xffff) << 16)
def set_ts_rel_ns(self, t):
ts_ns, ts_fns = self.ctx.divmod(Decimal(t), Decimal(1))
ts_ns = ts_ns.to_integral_value()
ts_fns = (ts_fns * Decimal(2**32)).to_integral_value()
self.set_ts_rel(ts_ns, ts_fns)
def set_ts_rel_s(self, t):
self.set_ts_rel_ns(Decimal(t).scaleb(9, self.ctx))
def set_ts_rel_sim_time(self):
self.set_ts_rel_ns(Decimal(get_sim_time('fs')).scaleb(-6))
def get_ts_rel(self):
ts_tod_s, ts_tod_ns, ts_rel_ns, ts_fns = self.timestamp_delay[0]
return (ts_rel_ns, ts_fns)
def get_ts_rel_64(self):
ts_rel_ns, ts_fns = self.get_ts_rel()
return (ts_rel_ns << 16) | (ts_fns >> 16)
def get_ts_rel_ns(self):
ts_rel_ns, ts_fns = self.get_ts_rel()
return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_rel_ns))
def get_ts_rel_s(self):
return self.get_ts_rel_ns().scaleb(-9, self.ctx)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._run_cr is not None:
self._run_cr.kill()
self._run_cr = None
self.ts_tod_s = 0
self.ts_tod_ns = 0
self.ts_rel_ns = 0
self.ts_fns = 0
self.drift_cnt = 0
self.data.value = 1
else:
self.log.info("Reset de-asserted")
if self._run_cr is None:
self._run_cr = cocotb.start_soon(self._run())
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
msg_index = 0
msg = None
msg_delay = 0
word = None
bit_index = 0
while True:
await clock_edge_event
# delay timestamp
self.timestamp_delay.append((self.ts_tod_s, self.ts_tod_ns, self.ts_rel_ns, self.ts_fns))
while len(self.timestamp_delay) > 14*17+self.td_delay:
self.timestamp_delay.pop(0)
# increment fns portion
self.ts_fns += ((self.period_ns << 32) + self.period_fns)
if self.drift_denom:
if self.drift_cnt > 0:
self.drift_cnt -= 1
else:
self.drift_cnt = self.drift_denom-1
self.ts_fns += self.drift_num
ns_inc = self.ts_fns >> 32
self.ts_fns &= 0xffffffff
# increment relative timestamp
self.ts_rel_ns = (self.ts_rel_ns + ns_inc) & 0xffffffffffff
# increment ToD timestamp
self.ts_tod_ns = self.ts_tod_ns + ns_inc
if self.ts_tod_ns >= 1000000000:
self.log.info("Seconds rollover")
self.pps.set()
self.ts_tod_s += 1
self.ts_tod_ns -= 1000000000
# compute offset for current second
self.ts_tod_offset_ns = (self.ts_tod_ns - self.ts_rel_ns) & 0xffffffff
# compute alternate offset
if self.ts_tod_ns & (1 << 29):
# latter half of second; compute offset for next second
self.ts_tod_alt_s = self.ts_tod_s+1
self.ts_tod_alt_offset_ns = (self.ts_tod_offset_ns - 1000000000) & 0xffffffff
else:
# former half of second; compute offset for previous second
self.ts_tod_alt_s = self.ts_tod_s-1
self.ts_tod_alt_offset_ns = (self.ts_tod_offset_ns + 1000000000) & 0xffffffff
if msg_delay <= 0:
# build message
msg = []
# word 0: control
ctrl = 0
ctrl |= msg_index & 0xf
ctrl |= bool(self.ts_rel_updated) << 8
ctrl |= bool(self.ts_tod_s & 1) << 9
self.ts_rel_updated = False
msg.append(ctrl)
if msg_index == 0:
# msg 0 word 1: current ToD TS ns 15:0
msg.append(self.ts_tod_ns & 0xffff)
# msg 0 word 2: current ToD TS ns 29:16 and flag bit
msg.append(((self.ts_tod_ns >> 16) & 0x3fff) | (0x8000 if self.ts_tod_updated else 0))
self.ts_tod_updated = False
# msg 0 word 3: current ToD TS seconds 15:0
msg.append(self.ts_tod_s & 0xffff)
# msg 0 word 4: current ToD TS seconds 31:16
msg.append((self.ts_tod_s >> 16) & 0xffff)
# msg 0 word 5: current ToD TS seconds 47:32
msg.append((self.ts_tod_s >> 32) & 0xffff)
msg_index = 1
elif msg_index == 1:
# msg 1 word 1: current ToD TS ns offset 15:0
msg.append(self.ts_tod_offset_ns & 0xffff)
# msg 1 word 2: current ToD TS ns offset 31:16
msg.append((self.ts_tod_offset_ns >> 16) & 0xffff)
# msg 1 word 3: drift num
msg.append(self.drift_num)
# msg 1 word 4: drift denom
msg.append(self.drift_denom)
# msg 1 word 5: drift state
msg.append(self.drift_cnt)
msg_index = 2
elif msg_index == 2:
# msg 2 word 1: alternate ToD TS ns offset 15:0
msg.append(self.ts_tod_alt_offset_ns & 0xffff)
# msg 2 word 2: alternate ToD TS ns offset 31:16
msg.append((self.ts_tod_alt_offset_ns >> 16) & 0xffff)
# msg 2 word 3: alternate ToD TS seconds 15:0
msg.append(self.ts_tod_alt_s & 0xffff)
# msg 2 word 4: alternate ToD TS seconds 31:16
msg.append((self.ts_tod_alt_s >> 16) & 0xffff)
# msg 2 word 5: alternate ToD TS seconds 47:32
msg.append((self.ts_tod_alt_s >> 32) & 0xffff)
msg_index = 0
# word 6: current fns 15:0
msg.append(self.ts_fns & 0xffff)
# word 7: current fns 31:16
msg.append((self.ts_fns >> 16) & 0xffff)
# word 8: current relative TS ns 15:0
msg.append(self.ts_rel_ns & 0xffff)
# word 9: current relative TS ns 31:16
msg.append((self.ts_rel_ns >> 16) & 0xffff)
# word 10: current relative TS ns 47:32
msg.append((self.ts_rel_ns >> 32) & 0xffff)
# word 11: current phase increment fns 15:0
msg.append(self.period_fns & 0xffff)
# word 12: current phase increment fns 31:16
msg.append((self.period_fns >> 16) & 0xffff)
# word 13: current phase increment ns 7:0 + crc
msg.append(self.period_ns & 0xff)
msg_delay = 255
else:
msg_delay -= 1
# serialize message
if word is None:
if msg:
word = msg.pop(0)
bit_index = 0
self.data.value = 0
else:
self.data.value = 1
else:
self.data.value = bool((word >> bit_index) & 1)
bit_index += 1
if bit_index == 16:
word = None
class PtpTdSink(Reset):
def __init__(self,
data=None,
clock=None,
reset=None,
reset_active_level=True,
period_ns=6.4,
td_delay=32,
*args, **kwargs):
self.log = logging.getLogger(f"cocotb.{data._path}")
self.data = data
self.clock = clock
self.reset = reset
self.log.info("PTP time distribution sink")
self.log.info("Copyright (c) 2023 Alex Forencich")
self.log.info("https://github.com/alexforencich/verilog-ethernet")
super().__init__(*args, **kwargs)
self.ctx = Context(prec=60)
self.period_ns = 0
self.period_fns = 0
self.drift_num = 0
self.drift_denom = 0
self.ts_fns = 0
self.ts_rel_ns = 0
self.ts_tod_s = 0
self.ts_tod_ns = 0
self.ts_tod_offset_ns = 0
self.ts_tod_alt_s = 0
self.ts_tod_alt_offset_ns = 0
self.td_delay = td_delay
self.drift_cnt = 0
self.pps = Event()
self._run_cr = None
self._init_reset(reset, reset_active_level)
def get_period_ns(self):
p = Decimal((self.period_ns << 32) | self.period_fns)
if self.drift_denom:
return p + Decimal(self.drift_num) / Decimal(self.drift_denom)
return p / Decimal(2**32)
def get_ts_tod(self):
return (self.ts_tod_s, self.ts_tod_ns, self.ts_fns)
def get_ts_tod_96(self):
ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod()
return (ts_tod_s << 48) | (ts_tod_ns << 16) | (ts_fns >> 16)
def get_ts_tod_ns(self):
ts_tod_s, ts_tod_ns, ts_fns = self.get_ts_tod()
ns = Decimal(ts_fns) / Decimal(2**32)
ns = self.ctx.add(ns, Decimal(ts_tod_ns))
return self.ctx.add(ns, Decimal(ts_tod_s).scaleb(9))
def get_ts_tod_s(self):
return self.get_ts_tod_ns().scaleb(-9, self.ctx)
def get_ts_rel(self):
return (self.ts_rel_ns, self.ts_fns)
def get_ts_rel_64(self):
ts_rel_ns, ts_fns = self.get_ts_rel()
return (ts_rel_ns << 16) | (ts_fns >> 16)
def get_ts_rel_ns(self):
ts_rel_ns, ts_fns = self.get_ts_rel()
return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_rel_ns))
def get_ts_rel_s(self):
return self.get_ts_rel_ns().scaleb(-9, self.ctx)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._run_cr is not None:
self._run_cr.kill()
self._run_cr = None
self.ts_tod_s = 0
self.ts_tod_ns = 0
self.ts_rel_ns = 0
self.ts_fns = 0
self.drift_cnt = 0
self.data.value = 1
else:
self.log.info("Reset de-asserted")
if self._run_cr is None:
self._run_cr = cocotb.start_soon(self._run())
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
msg_index = 0
msg = None
msg_delay = 0
cur_msg = []
word = None
bit_index = 0
while True:
await clock_edge_event
sdi_sample = self.data.value.integer
# increment fns portion
self.ts_fns += ((self.period_ns << 32) + self.period_fns)
if self.drift_denom:
if self.drift_cnt > 0:
self.drift_cnt -= 1
else:
self.drift_cnt = self.drift_denom-1
self.ts_fns += self.drift_num
ns_inc = self.ts_fns >> 32
self.ts_fns &= 0xffffffff
# increment relative timestamp
self.ts_rel_ns = (self.ts_rel_ns + ns_inc) & 0xffffffffffff
# increment ToD timestamp
self.ts_tod_ns = self.ts_tod_ns + ns_inc
if self.ts_tod_ns >= 1000000000:
self.log.info("Seconds rollover")
self.pps.set()
self.ts_tod_s += 1
self.ts_tod_ns -= 1000000000
# process messages
if msg_delay > 0:
msg_delay -= 1
if msg_delay == 0 and msg:
self.log.info("process message %r", msg)
# word 0: control
msg_index = msg[0] & 0xf
if msg_index == 0:
# msg 0 word 1: current ToD TS ns 15:0
# msg 0 word 2: current ToD TS ns 29:16
val = ((msg[2] & 0x3fff) << 16) | msg[1]
if self.ts_tod_ns != val:
self.log.info("update ts_tod_ns: old 0x%x, new 0x%x", self.ts_tod_ns, val)
self.ts_tod_ns = val
# msg 0 word 3: current ToD TS seconds 15:0
# msg 0 word 4: current ToD TS seconds 31:16
# msg 0 word 5: current ToD TS seconds 47:32
val = (msg[5] << 32) | (msg[4] << 16) | msg[3]
if self.ts_tod_s != val:
self.log.info("update ts_tod_s: old 0x%x, new 0x%x", self.ts_tod_s, val)
self.ts_tod_s = val
elif msg_index == 1:
# msg 1 word 1: current ToD TS ns offset 15:0
# msg 1 word 2: current ToD TS ns offset 31:16
val = (msg[2] << 16) | msg[1]
if self.ts_tod_offset_ns != val:
self.log.info("update ts_tod_offset_ns: old 0x%x, new 0x%x", self.ts_tod_offset_ns, val)
self.ts_tod_offset_ns = val
# msg 1 word 3: drift num
val = msg[3]
if self.drift_num != val:
self.log.info("update drift_num: old 0x%x, new 0x%x", self.drift_num, val)
self.drift_num = val
# msg 1 word 4: drift denom
val = msg[4]
if self.drift_denom != val:
self.log.info("update drift_denom: old 0x%x, new 0x%x", self.drift_denom, val)
self.drift_denom = val
# msg 1 word 5: drift state
val = msg[5]
if self.drift_cnt != val:
self.log.info("update drift_cnt: old 0x%x, new 0x%x", self.drift_cnt, val)
self.drift_cnt = val
elif msg_index == 2:
# msg 2 word 1: alternate ToD TS ns offset 15:0
# msg 2 word 2: alternate ToD TS ns offset 31:16
val = (msg[2] << 16) | msg[1]
if self.ts_tod_alt_offset_ns != val:
self.log.info("update ts_tod_alt_offset_ns: old 0x%x, new 0x%x", self.ts_tod_alt_offset_ns, val)
self.ts_tod_alt_offset_ns = val
# msg 2 word 3: alternate ToD TS seconds 15:0
# msg 2 word 4: alternate ToD TS seconds 31:16
# msg 2 word 5: alternate ToD TS seconds 47:32
val = (msg[5] << 32) | (msg[4] << 16) | msg[3]
if self.ts_tod_alt_s != val:
self.log.info("update ts_tod_alt_s: old 0x%x, new 0x%x", self.ts_tod_alt_s, val)
self.ts_tod_alt_s = val
# word 6: current fns 15:0
# word 7: current fns 31:16
val = (msg[7] << 16) | msg[6]
if self.ts_fns != val:
self.log.info("update ts_fns: old 0x%x, new 0x%x", self.ts_fns, val)
self.ts_fns = val
# word 8: current relative TS ns 15:0
# word 9: current relative TS ns 31:16
# word 10: current relative TS ns 47:32
val = (msg[10] << 32) | (msg[9] << 16) | msg[8]
if self.ts_rel_ns != val:
self.log.info("update ts_rel_ns: old 0x%x, new 0x%x", self.ts_rel_ns, val)
self.ts_rel_ns = val
# word 11: current phase increment fns 15:0
# word 12: current phase increment fns 31:16
val = (msg[12] << 16) | msg[11]
if self.period_fns != val:
self.log.info("update period_fns: old 0x%x, new 0x%x", self.period_fns, val)
self.period_fns = val
# word 13: current phase increment ns 7:0 + crc
val = msg[13] & 0xff
if self.period_ns != val:
self.log.info("update period_ns: old 0x%x, new 0x%x", self.period_ns, val)
self.period_ns = val
msg = None
# deserialize message
if word is not None:
word = word | (sdi_sample << bit_index)
bit_index += 1
if bit_index == 16:
cur_msg.append(word)
word = None
else:
if not sdi_sample:
# start bit
word = 0
bit_index = 0
elif cur_msg:
# idle
msg = cur_msg
msg_delay = self.td_delay
cur_msg = []

75
tb/ptp_td_leaf/Makefile Normal file
View File

@ -0,0 +1,75 @@
# Copyright (c) 2020 Alex Forencich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
TOPLEVEL_LANG = verilog
SIM ?= icarus
WAVES ?= 0
COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ps
DUT = ptp_td_leaf
TOPLEVEL = $(DUT)
MODULE = test_$(DUT)
VERILOG_SOURCES += ../../rtl/$(DUT).v
# module parameters
export PARAM_TS_REL_EN := 1
export PARAM_TS_TOD_EN := 1
export PARAM_TS_FNS_W := 16
export PARAM_TS_REL_NS_W := 48
export PARAM_TS_TOD_S_W := 48
export PARAM_TS_REL_W := $(shell expr $(PARAM_TS_REL_NS_W) + $(PARAM_TS_FNS_W))
export PARAM_TS_TOD_W := $(shell expr $(PARAM_TS_TOD_S_W) + 32 + $(PARAM_TS_FNS_W))
export PARAM_TD_SDI_PIPELINE := 2
ifeq ($(SIM), icarus)
PLUSARGS += -fst
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
VERILOG_SOURCES += iverilog_dump.v
COMPILE_ARGS += -s iverilog_dump
endif
else ifeq ($(SIM), verilator)
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst
endif
endif
include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v:
echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
echo 'end' >> $@
echo 'endmodule' >> $@
clean::
@rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst

1
tb/ptp_td_leaf/ptp_td.py Symbolic link
View File

@ -0,0 +1 @@
../ptp_td.py

View File

@ -0,0 +1,507 @@
#!/usr/bin/env python
"""
Copyright (c) 2023 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
import os
import sys
from decimal import Decimal
from statistics import mean, stdev
import cocotb_test.simulator
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from cocotb.utils import get_sim_steps, get_sim_time
try:
from ptp_td import PtpTdSource
except ImportError:
# attempt import from current directory
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
try:
from ptp_td import PtpTdSource
finally:
del sys.path[0]
class TB:
def __init__(self, dut):
self.dut = dut
self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG)
cocotb.start_soon(Clock(dut.sample_clk, 9.9, units="ns").start())
self.ptp_td_source = PtpTdSource(
data=dut.ptp_td_sdi,
clock=dut.ptp_clk,
reset=dut.ptp_rst,
period_ns=6.4
)
self.ptp_clock_period = 6.4
dut.ptp_clk.setimmediatevalue(0)
cocotb.start_soon(self._run_ptp_clock())
self.clock_period = 6.4
dut.clk.setimmediatevalue(0)
cocotb.start_soon(self._run_clock())
self.ref_ts_rel = []
self.ref_ts_tod = []
self.output_ts_rel = []
self.output_ts_tod = []
cocotb.start_soon(self._run_collect_ref_ts())
cocotb.start_soon(self._run_collect_output_ts())
async def reset(self):
self.dut.ptp_rst.setimmediatevalue(0)
self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.ptp_clk)
await RisingEdge(self.dut.ptp_clk)
self.dut.ptp_rst.value = 1
self.dut.rst.value = 1
for k in range(10):
await RisingEdge(self.dut.ptp_clk)
self.dut.ptp_rst.value = 0
self.dut.rst.value = 0
for k in range(10):
await RisingEdge(self.dut.ptp_clk)
def set_ptp_clock_period(self, period):
self.ptp_clock_period = period
async def _run_ptp_clock(self):
period = None
steps_per_ns = get_sim_steps(1.0, 'ns')
while True:
if period != self.ptp_clock_period:
period = self.ptp_clock_period
t = Timer(int(steps_per_ns * period / 2.0))
await t
self.dut.ptp_clk.value = 1
await t
self.dut.ptp_clk.value = 0
def set_clock_period(self, period):
self.clock_period = period
def get_output_ts_tod_ns(self):
ts = self.dut.output_ts_tod.value.integer
return Decimal(ts >> 48).scaleb(9) + (Decimal(ts & 0xffffffffffff) / Decimal(2**16))
def get_output_ts_rel_ns(self):
ts = self.dut.output_ts_rel.value.integer
return Decimal(ts) / Decimal(2**16)
async def _run_clock(self):
period = None
steps_per_ns = get_sim_steps(1.0, 'ns')
while True:
if period != self.clock_period:
period = self.clock_period
t = Timer(int(steps_per_ns * period / 2.0))
await t
self.dut.clk.value = 1
await t
self.dut.clk.value = 0
async def _run_collect_ref_ts(self):
clk_event = RisingEdge(self.dut.ptp_clk)
while True:
await clk_event
st = Decimal(get_sim_time('fs')).scaleb(-6)
self.ref_ts_rel.append((st, self.ptp_td_source.get_ts_rel_ns()))
self.ref_ts_tod.append((st, self.ptp_td_source.get_ts_tod_ns()))
async def _run_collect_output_ts(self):
clk_event = RisingEdge(self.dut.clk)
while True:
await clk_event
st = Decimal(get_sim_time('fs')).scaleb(-6)
self.output_ts_rel.append((st, self.get_output_ts_rel_ns()))
self.output_ts_tod.append((st, self.get_output_ts_tod_ns()))
def compute_ts_diff(self, ts_lst_1, ts_lst_2):
ts_lst_1 = [x for x in ts_lst_1]
diffs = []
its1 = ts_lst_1.pop(0)
its2 = ts_lst_1.pop(0)
for ots in ts_lst_2:
while its2[0] < ots[0] and ts_lst_1:
its1 = its2
its2 = ts_lst_1.pop(0)
if its2[0] < ots[0]:
break
dt = its2[0] - its1[0]
dts = its2[1] - its1[1]
its = its1[1]+dts/dt*(ots[0]-its1[0])
# diffs.append(ots[1] - its)
diffs.append(float(ots[1] - its))
return diffs
async def measure_ts_diff(self, N=100):
self.ref_ts_rel = []
self.ref_ts_tod = []
self.output_ts_rel = []
self.output_ts_tod = []
for k in range(N):
await RisingEdge(self.dut.clk)
rel_diffs = self.compute_ts_diff(self.ref_ts_rel, self.output_ts_rel)
tod_diffs = self.compute_ts_diff(self.ref_ts_tod, self.output_ts_tod)
return rel_diffs, tod_diffs
@cocotb.test()
async def run_test(dut):
tb = TB(dut)
await tb.reset()
# set small offset between timestamps
tb.ptp_td_source.set_ts_rel_ns(0)
tb.ptp_td_source.set_ts_tod_ns(10000)
await RisingEdge(dut.clk)
tb.log.info("Same clock speed")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("10 ppm slower")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4*(1+.00001))
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("10 ppm faster")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4*(1-.00001))
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("200 ppm slower")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4*(1+.0002))
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("200 ppm faster")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4*(1-.0002))
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Coherent tracking (+/- 10 ppm)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4)
await RisingEdge(dut.clk)
period = 6.400
step = 0.000002
period_min = 6.4*(1-.00001)
period_max = 6.4*(1+.00001)
for i in range(500):
period += step
if period <= period_min:
step = abs(step)
if period >= period_max:
step = -abs(step)
tb.set_clock_period(period)
for i in range(200):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Coherent tracking (+/- 200 ppm)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.4)
await RisingEdge(dut.clk)
period = 6.400
step = 0.000002
period_min = 6.4*(1-.0002)
period_max = 6.4*(1+.0002)
for i in range(5000):
period += step
if period <= period_min:
step = abs(step)
if period >= period_max:
step = -abs(step)
tb.set_clock_period(period)
for i in range(20):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Slightly faster (6.3 ns)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.3)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Slightly slower (6.5 ns)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(6.5)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Significantly faster (250 MHz)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(4.0)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Significantly slower (100 MHz)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(10.0)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
tb.log.info("Significantly faster (390.625 MHz)")
tb.set_ptp_clock_period(6.4)
tb.set_clock_period(2.56)
await RisingEdge(dut.clk)
for i in range(100000):
await RisingEdge(dut.clk)
assert tb.dut.locked.value.integer
rel_diffs, tod_diffs = await tb.measure_ts_diff()
tb.log.info(f"Difference (rel): {mean(rel_diffs)} ns (stdev: {stdev(rel_diffs)})")
tb.log.info(f"Difference (ToD): {mean(tod_diffs)} ns (stdev: {stdev(tod_diffs)})")
assert abs(mean(rel_diffs)) < 5
assert abs(mean(tod_diffs)) < 5
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
# cocotb-test
tests_dir = os.path.abspath(os.path.dirname(__file__))
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib'))
axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl'))
def test_ptp_td_leaf(request):
dut = "ptp_td_leaf"
module = os.path.splitext(os.path.basename(__file__))[0]
toplevel = dut
verilog_sources = [
os.path.join(rtl_dir, f"{dut}.v"),
]
parameters = {}
parameters['TS_REL_EN'] = 1
parameters['TS_TOD_EN'] = 1
parameters['TS_FNS_W'] = 16
parameters['TS_REL_NS_W'] = 48
parameters['TS_TOD_S_W'] = 48
parameters['TS_REL_W'] = parameters['TS_REL_NS_W'] + parameters['TS_FNS_W']
parameters['TS_TOD_W'] = parameters['TS_TOD_S_W'] + 32 + parameters['TS_FNS_W']
parameters['TD_SDI_PIPELINE'] = 2
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, "sim_build",
request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run(
python_search=[tests_dir],
verilog_sources=verilog_sources,
toplevel=toplevel,
module=module,
parameters=parameters,
sim_build=sim_build,
extra_env=extra_env,
)

69
tb/ptp_td_phc/Makefile Normal file
View File

@ -0,0 +1,69 @@
# Copyright (c) 2020 Alex Forencich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
TOPLEVEL_LANG = verilog
SIM ?= icarus
WAVES ?= 0
COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ps
DUT = ptp_td_phc
TOPLEVEL = $(DUT)
MODULE = test_$(DUT)
VERILOG_SOURCES += ../../rtl/$(DUT).v
# module parameters
export PARAM_PERIOD_NS_NUM := 32
export PARAM_PERIOD_NS_DENOM := 5
ifeq ($(SIM), icarus)
PLUSARGS += -fst
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
VERILOG_SOURCES += iverilog_dump.v
COMPILE_ARGS += -s iverilog_dump
endif
else ifeq ($(SIM), verilator)
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst
endif
endif
include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v:
echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
echo 'end' >> $@
echo 'endmodule' >> $@
clean::
@rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst

1
tb/ptp_td_phc/ptp_td.py Symbolic link
View File

@ -0,0 +1 @@
../ptp_td.py

View File

@ -0,0 +1,550 @@
#!/usr/bin/env python
"""
Copyright (c) 2023 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
import os
import sys
from decimal import Decimal
import cocotb_test.simulator
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
from cocotb.utils import get_sim_time
try:
from ptp_td import PtpTdSink
except ImportError:
# attempt import from current directory
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
try:
from ptp_td import PtpTdSink
finally:
del sys.path[0]
class TB:
def __init__(self, dut):
self.dut = dut
self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG)
cocotb.start_soon(Clock(dut.clk, 6.4, units="ns").start())
self.ptp_td_sink = PtpTdSink(
data=dut.ptp_td_sdo,
clock=dut.clk,
reset=dut.rst,
period_ns=6.4
)
dut.input_ts_rel_ns.setimmediatevalue(0)
dut.input_ts_rel_valid.setimmediatevalue(0)
dut.input_ts_rel_offset_ns.setimmediatevalue(0)
dut.input_ts_rel_offset_valid.setimmediatevalue(0)
dut.input_ts_tod_s.setimmediatevalue(0)
dut.input_ts_tod_ns.setimmediatevalue(0)
dut.input_ts_tod_valid.setimmediatevalue(0)
dut.input_ts_tod_offset_ns.setimmediatevalue(0)
dut.input_ts_tod_offset_valid.setimmediatevalue(0)
dut.input_ts_offset_fns.setimmediatevalue(0)
dut.input_ts_offset_valid.setimmediatevalue(0)
dut.input_period_ns.setimmediatevalue(0)
dut.input_period_fns.setimmediatevalue(0)
dut.input_period_valid.setimmediatevalue(0)
dut.input_drift_num.setimmediatevalue(0)
dut.input_drift_denom.setimmediatevalue(0)
dut.input_drift_valid.setimmediatevalue(0)
async def reset(self):
self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 1
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 0
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
@cocotb.test()
async def run_default_rate(dut):
tb = TB(dut)
await tb.reset()
for k in range(256*6):
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
for k in range(10000):
await RisingEdge(dut.clk)
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta
ts_rel_diff = time_delta - ts_rel_delta
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def run_load_timestamps(dut):
tb = TB(dut)
await tb.reset()
await RisingEdge(dut.clk)
dut.input_ts_tod_s.value = 12
dut.input_ts_tod_ns.value = 123456789
dut.input_ts_tod_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_tod_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_tod_valid.value = 0
dut.input_ts_rel_ns.value = 123456789
dut.input_ts_rel_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_rel_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_rel_valid.value = 0
for k in range(256*6):
await RisingEdge(dut.clk)
# assert tb.ptp_td_sink.get_ts_tod_s() - (12.123456789 + (256*6-(14*17+32)-2)*6.4e-9) < 6.4e-9
# assert tb.ptp_td_sink.get_ts_rel_ns() - (123456789 + (256*6-(14*17+32)-1)*6.4) < 6.4
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
for k in range(10000):
await RisingEdge(dut.clk)
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta
ts_rel_diff = time_delta - ts_rel_delta
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def run_offsets(dut):
tb = TB(dut)
await tb.reset()
for k in range(256*6):
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset FNS (positive)")
await RisingEdge(dut.clk)
dut.input_ts_offset_fns.value = 0x78000000 & 0xffffffff
dut.input_ts_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_offset_valid.value = 0
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset FNS (negative)")
await RisingEdge(dut.clk)
dut.input_ts_offset_fns.value = -0x70000000 & 0xffffffff
dut.input_ts_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_offset_valid.value = 0
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset relative TS (positive)")
dut.input_ts_rel_offset_ns.value = 30000 & 0xffffffff
dut.input_ts_rel_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_rel_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_rel_offset_valid.value = 0
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset relative TS (negative)")
dut.input_ts_rel_offset_ns.value = -10000 & 0xffffffff
dut.input_ts_rel_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_rel_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_rel_offset_valid.value = 0
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset ToD TS (positive)")
dut.input_ts_tod_offset_ns.value = 510000000 & 0x3fffffff
dut.input_ts_tod_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_tod_offset_ready.value:
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
while not dut.input_ts_tod_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_tod_offset_valid.value = 0
for k in range(2000):
await RisingEdge(dut.clk)
tb.log.info("Offset ToD TS (negative)")
dut.input_ts_tod_offset_ns.value = -500000000 & 0x3fffffff
dut.input_ts_tod_offset_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_tod_offset_ready.value:
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
while not dut.input_ts_tod_offset_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_tod_offset_valid.value = 0
for k in range(10000):
await RisingEdge(dut.clk)
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta + Decimal(0.03125) + Decimal(20000000)
ts_rel_diff = time_delta - ts_rel_delta + Decimal(0.03125) + Decimal(20000)
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def run_seconds_increment(dut):
tb = TB(dut)
await tb.reset()
await RisingEdge(dut.clk)
dut.input_ts_tod_s.value = 0
dut.input_ts_tod_ns.value = 999990000
dut.input_ts_tod_valid.value = 1
await RisingEdge(dut.clk)
while not dut.input_ts_tod_ready.value:
await RisingEdge(dut.clk)
dut.input_ts_tod_valid.value = 0
for k in range(256*6):
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
saw_pps = False
for k in range(3000):
await RisingEdge(dut.clk)
if dut.output_pps.value.integer:
saw_pps = True
tb.log.info("Got PPS with sink ToD TS %s", tb.ptp_td_sink.get_ts_tod_ns())
assert (tb.ptp_td_sink.get_ts_tod_s() - 1) < 6.4e-9
assert saw_pps
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta
ts_rel_diff = time_delta - ts_rel_delta
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def run_frequency_adjustment(dut):
tb = TB(dut)
await tb.reset()
await RisingEdge(dut.clk)
dut.input_period_ns.value = 0x6
dut.input_period_fns.value = 0x66240000
dut.input_period_valid.value = 1
await RisingEdge(dut.clk)
dut.input_period_valid.value = 0
for k in range(256*6):
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
for k in range(10000):
await RisingEdge(dut.clk)
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4/(6+(0x66240000+2/5)/2**32))
ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4/(6+(0x66240000+2/5)/2**32))
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
@cocotb.test()
async def run_drift_adjustment(dut):
tb = TB(dut)
await tb.reset()
dut.input_drift_num.value = 20000
dut.input_drift_denom.value = 5
dut.input_drift_valid.value = 1
await RisingEdge(dut.clk)
dut.input_drift_valid.value = 0
for k in range(256*6):
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
start_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
start_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
for k in range(10000):
await RisingEdge(dut.clk)
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
stop_ts_tod = tb.ptp_td_sink.get_ts_tod_ns()
stop_ts_rel = tb.ptp_td_sink.get_ts_rel_ns()
time_delta = stop_time-start_time
ts_tod_delta = stop_ts_tod-start_ts_tod
ts_rel_delta = stop_ts_rel-start_ts_rel
tb.log.info("sim time delta : %s ns", time_delta)
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
tb.log.info("Rel ts delta : %s ns", ts_rel_delta)
ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4/(6+(0x66666666+20000/5)/2**32))
ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4/(6+(0x66666666+20000/5)/2**32))
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
tb.log.info("Rel ts diff : %s ns", ts_rel_diff)
assert abs(ts_tod_diff) < 1e-3
assert abs(ts_rel_diff) < 1e-3
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
# cocotb-test
tests_dir = os.path.abspath(os.path.dirname(__file__))
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib'))
axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl'))
def test_ptp_td_phc(request):
dut = "ptp_td_phc"
module = os.path.splitext(os.path.basename(__file__))[0]
toplevel = dut
verilog_sources = [
os.path.join(rtl_dir, f"{dut}.v"),
]
parameters = {}
parameters['PERIOD_NS_NUM'] = 32
parameters['PERIOD_NS_DENOM'] = 5
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, "sim_build",
request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run(
python_search=[tests_dir],
verilog_sources=verilog_sources,
toplevel=toplevel,
module=module,
parameters=parameters,
sim_build=sim_build,
extra_env=extra_env,
)