407 lines
17 KiB
Systemverilog
407 lines
17 KiB
Systemverilog
`timescale 1ns/1ps
|
|
|
|
module pps_wrapper_tb;
|
|
import pps_pkg::*;
|
|
|
|
localparam int unsigned CLK_FREQ_HZ = 1000;
|
|
localparam int unsigned NUM_CHANNELS = 3;
|
|
|
|
logic PCLK = 1'b0;
|
|
logic PRESETN = 1'b0;
|
|
logic [11:0] PADDR = '0;
|
|
logic PSEL = 1'b0;
|
|
logic PENABLE = 1'b0;
|
|
logic PWRITE = 1'b0;
|
|
logic [31:0] PWDATA = '0;
|
|
logic PREADY;
|
|
logic [31:0] PRDATA;
|
|
logic PSLVERR;
|
|
logic pps_clk = 1'b0;
|
|
logic pps_clk_run = 1'b1;
|
|
logic pps_resetn = 1'b0;
|
|
logic [NUM_CHANNELS-1:0] capture_i = '0;
|
|
logic pps_out;
|
|
logic pps_valid;
|
|
|
|
int checks = 0;
|
|
longint unsigned capture_counter;
|
|
longint unsigned pps_edge_counter;
|
|
longint unsigned phase_ticks;
|
|
logic [31:0] read_data;
|
|
logic read_error;
|
|
logic signed [63:0] phase_error_before_holdover;
|
|
|
|
always #5 PCLK = ~PCLK;
|
|
always begin
|
|
#4;
|
|
if (pps_clk_run) pps_clk = ~pps_clk;
|
|
end
|
|
|
|
pps_wrapper #(
|
|
.CLK_FREQ_HZ(CLK_FREQ_HZ),
|
|
.NUM_CAPTURE_CHANNELS(NUM_CHANNELS)
|
|
) dut (
|
|
.PCLK(PCLK), .PRESETN(PRESETN), .PADDR(PADDR), .PSEL(PSEL),
|
|
.PENABLE(PENABLE), .PWRITE(PWRITE), .PWDATA(PWDATA),
|
|
.PREADY(PREADY), .PRDATA(PRDATA), .PSLVERR(PSLVERR),
|
|
.pps_clk(pps_clk), .pps_resetn(pps_resetn), .capture_i(capture_i),
|
|
.pps_out(pps_out), .pps_valid(pps_valid)
|
|
);
|
|
|
|
task automatic check_true(input logic condition, input string message);
|
|
begin
|
|
checks = checks + 1;
|
|
if (!condition) $fatal(1, "CHECK %0d FAILED: %s", checks, message);
|
|
end
|
|
endtask
|
|
|
|
task automatic apb_write(input logic [11:0] address, input logic [31:0] data,
|
|
output logic error);
|
|
begin
|
|
@(negedge PCLK);
|
|
PADDR = address; PWDATA = data; PWRITE = 1'b1; PSEL = 1'b1; PENABLE = 1'b0;
|
|
@(negedge PCLK); PENABLE = 1'b1;
|
|
@(posedge PCLK);
|
|
while (!PREADY) @(posedge PCLK);
|
|
error = PSLVERR;
|
|
@(negedge PCLK);
|
|
PSEL = 1'b0; PENABLE = 1'b0; PWRITE = 1'b0; PADDR = '0; PWDATA = '0;
|
|
repeat (2) @(posedge PCLK);
|
|
end
|
|
endtask
|
|
|
|
task automatic apb_read(input logic [11:0] address, output logic [31:0] data,
|
|
output logic error);
|
|
begin
|
|
@(negedge PCLK);
|
|
PADDR = address; PWRITE = 1'b0; PSEL = 1'b1; PENABLE = 1'b0;
|
|
@(negedge PCLK); PENABLE = 1'b1;
|
|
@(posedge PCLK);
|
|
while (!PREADY) @(posedge PCLK);
|
|
data = PRDATA; error = PSLVERR;
|
|
@(negedge PCLK);
|
|
PSEL = 1'b0; PENABLE = 1'b0; PADDR = '0;
|
|
repeat (2) @(posedge PCLK);
|
|
end
|
|
endtask
|
|
|
|
task automatic write_ok(input logic [11:0] address, input logic [31:0] data);
|
|
logic error;
|
|
begin
|
|
apb_write(address, data, error);
|
|
check_true(!error, $sformatf("APB write error at 0x%03x", address));
|
|
end
|
|
endtask
|
|
|
|
task automatic read_ok(input logic [11:0] address, output logic [31:0] data);
|
|
logic error;
|
|
begin
|
|
apb_read(address, data, error);
|
|
check_true(!error, $sformatf("APB read error at 0x%03x", address));
|
|
end
|
|
endtask
|
|
|
|
task automatic read_capture(input int unsigned channel_index,
|
|
output longint unsigned counter_value,
|
|
output logic [31:0] sequence_value);
|
|
logic [31:0] lo, hi;
|
|
begin
|
|
write_ok(PPS_CAPTURE_SELECT_REG_OFFSET, channel_index);
|
|
read_ok(PPS_CAPTURE_COUNTER_LO_OFFSET, lo);
|
|
read_ok(PPS_CAPTURE_COUNTER_HI_OFFSET, hi);
|
|
read_ok(PPS_CAPTURE_SEQUENCE_OFFSET, sequence_value);
|
|
counter_value = {hi, lo};
|
|
end
|
|
endtask
|
|
|
|
task automatic wait_commit_idle;
|
|
logic [31:0] status;
|
|
int timeout;
|
|
begin
|
|
timeout = 0;
|
|
do begin
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
timeout = timeout + 1;
|
|
if (timeout > 40) $fatal(1, "commit acknowledgement timeout");
|
|
end while (status[PPS_STATUS_COMMIT_BUSY_POS]);
|
|
end
|
|
endtask
|
|
|
|
task automatic pps_sw_snapshot_diagnostics;
|
|
logic [31:0] status;
|
|
int timeout;
|
|
begin
|
|
write_ok(PPS_DIAG_SNAPSHOT_OFFSET, 32'h0000_0001);
|
|
timeout = 0;
|
|
do begin
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
timeout = timeout + 1;
|
|
if (timeout > 40) $fatal(1, "diagnostic snapshot acknowledgement timeout");
|
|
end while (status[PPS_STATUS_DIAG_BUSY_POS]);
|
|
// The acknowledged source mailbox is stable; allow both PCLK data
|
|
// synchronizer stages to settle before multiword reads.
|
|
repeat (3) @(posedge PCLK);
|
|
end
|
|
endtask
|
|
|
|
// This task is the intended baremetal transaction order: all multiword
|
|
// fields are written to shadow registers and become visible to pps_clk only
|
|
// after one final COMMIT write.
|
|
task automatic pps_sw_commit(
|
|
input longint unsigned epoch_count,
|
|
input longint unsigned gps_sec,
|
|
input logic [31:0] gps_frac_q32,
|
|
input logic [31:0] sequence_id,
|
|
input logic coarse
|
|
);
|
|
begin
|
|
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, epoch_count[31:0]);
|
|
write_ok(PPS_EPOCH_COUNTER_HI_OFFSET, epoch_count[63:32]);
|
|
write_ok(PPS_GPS_SECONDS_LO_OFFSET, gps_sec[31:0]);
|
|
write_ok(PPS_GPS_SECONDS_HI_OFFSET, gps_sec[63:32]);
|
|
write_ok(PPS_GPS_FRACTION_Q32_OFFSET, gps_frac_q32);
|
|
write_ok(PPS_EPOCH_SEQUENCE_OFFSET, sequence_id);
|
|
write_ok(PPS_COMMIT_REG_OFFSET, {30'd0, coarse, 1'b1});
|
|
wait_commit_idle();
|
|
end
|
|
endtask
|
|
|
|
function automatic logic [31:0] ticks_to_fraction_q32(input longint unsigned ticks);
|
|
longint unsigned numerator;
|
|
begin
|
|
numerator = (ticks << 32) + (CLK_FREQ_HZ / 2);
|
|
ticks_to_fraction_q32 = numerator / CLK_FREQ_HZ;
|
|
end
|
|
endfunction
|
|
|
|
initial begin
|
|
logic [31:0] capture_sequence_id;
|
|
logic [31:0] status;
|
|
logic signed [63:0] phase_error;
|
|
logic signed [63:0] rate_word;
|
|
longint unsigned second_capture;
|
|
logic [31:0] second_capture_sequence;
|
|
logic [31:0] active_sequence;
|
|
logic [31:0] shadow_before;
|
|
logic [31:0] pulse_count_before;
|
|
logic [31:0] pulse_count_after;
|
|
longint unsigned second_pps_edge_counter;
|
|
longint unsigned first_pps_fall_counter;
|
|
logic [31:0] raw_lo, raw_hi;
|
|
longint unsigned raw_snapshot_value;
|
|
logic [31:0] diag_sub_lo_before, diag_sub_hi_before;
|
|
logic [31:0] diag_sub_lo_after, diag_sub_hi_after;
|
|
longint unsigned lock_ref_counter;
|
|
longint unsigned lock_ref_seconds;
|
|
logic [31:0] lock_ref_fraction;
|
|
integer lock_update;
|
|
|
|
repeat (6) @(posedge PCLK);
|
|
PRESETN = 1'b1;
|
|
pps_resetn = 1'b1;
|
|
repeat (8) @(posedge PCLK);
|
|
|
|
read_ok(PPS_VERSION_REG_OFFSET, read_data);
|
|
check_true(read_data == PPS_VERSION, "version register");
|
|
read_ok(PPS_LOOP_CONFIG_OFFSET, read_data);
|
|
check_true(read_data == PPS_DEFAULT_LOOP_CONFIG,
|
|
"backward-compatible default loop configuration");
|
|
|
|
// Stop the destination clock to force a real mailbox-busy interval. A
|
|
// CPU write to any shadow field during that interval must be rejected;
|
|
// otherwise the supposedly atomic CDC bus could change under the core.
|
|
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, 32'h1122_3344);
|
|
read_ok(PPS_EPOCH_COUNTER_LO_OFFSET, shadow_before);
|
|
@(negedge pps_clk); pps_clk_run = 1'b0;
|
|
write_ok(PPS_COMMIT_REG_OFFSET, 32'h0000_0003);
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(status[PPS_STATUS_COMMIT_BUSY_POS], "commit remains busy while destination clock is stopped");
|
|
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, 32'hdead_beef);
|
|
read_ok(PPS_EPOCH_COUNTER_LO_OFFSET, read_data);
|
|
check_true(read_data == shadow_before, "busy mailbox blocks shadow mutation");
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(status[PPS_STATUS_COMMIT_OVERRUN_POS], "busy mailbox violation is sticky");
|
|
pps_clk_run = 1'b1;
|
|
wait_commit_idle();
|
|
|
|
// Start the functional scenarios from a clean common reset.
|
|
PRESETN = 1'b0; pps_resetn = 1'b0;
|
|
repeat (5) @(posedge PCLK);
|
|
PRESETN = 1'b1; pps_resetn = 1'b1;
|
|
repeat (8) @(posedge PCLK);
|
|
|
|
read_ok(PPS_RAW_COUNTER_LO_REG_OFFSET, raw_lo);
|
|
read_ok(PPS_RAW_COUNTER_HI_REG_OFFSET, raw_hi);
|
|
raw_snapshot_value = {raw_hi, raw_lo};
|
|
check_true(raw_snapshot_value <= dut.i_pps_core.raw_counter,
|
|
"atomic raw-counter read cannot come from the future");
|
|
check_true((dut.i_pps_core.raw_counter - raw_snapshot_value) < 100,
|
|
"raw-counter low-read snapshot keeps the high word coherent");
|
|
|
|
@(negedge PCLK); capture_i[1] = 1'b1;
|
|
@(negedge PCLK); capture_i[1] = 1'b0;
|
|
repeat (3) @(posedge PCLK);
|
|
read_capture(1, capture_counter, capture_sequence_id);
|
|
check_true(capture_sequence_id == 1, "channel capture sequence increments");
|
|
check_true(capture_counter != 0, "channel capture stores the free-running counter");
|
|
apb_write(PPS_CAPTURE_SELECT_REG_OFFSET, NUM_CHANNELS, read_error);
|
|
check_true(read_error, "out-of-range capture channel raises PSLVERR");
|
|
read_ok(PPS_CAPTURE_SELECT_REG_OFFSET, read_data);
|
|
check_true(read_data == 1, "invalid capture selection preserves previous channel");
|
|
|
|
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_0003);
|
|
read_ok(PPS_CONTROL_REG_OFFSET, read_data);
|
|
check_true(read_data[2:0] == 3, "control register readback");
|
|
write_ok(PPS_KP_Q46_LO_OFFSET, 32'h0000_0000);
|
|
write_ok(PPS_KP_Q46_HI_OFFSET, 32'h0000_1000); // 0.25 in signed Q2.46
|
|
write_ok(PPS_KI_Q46_LO_OFFSET, 32'h0000_0000);
|
|
write_ok(PPS_KI_Q46_HI_OFFSET, 32'h0000_0000);
|
|
write_ok(PPS_MAX_SLEW_Q32_OFFSET, 32'h1000_0000);
|
|
write_ok(PPS_LOOP_CONFIG_OFFSET, 32'h0540_0800);
|
|
read_ok(PPS_LOOP_CONFIG_OFFSET, read_data);
|
|
check_true(read_data == 32'h0540_0800,
|
|
"runtime loop configuration readback");
|
|
|
|
// GPS reference says the capture happened at second 100 + 0.2 s.
|
|
// Software and APB deliberately consume time before COMMIT. The core must
|
|
// extrapolate from capture_counter, so the PPS edge stays at capture+800.
|
|
repeat (80) @(posedge pps_clk);
|
|
pps_sw_commit(capture_counter, 100, ticks_to_fraction_q32(200), 1, 1'b1);
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(status[PPS_STATUS_ALIGNED_POS], "coarse commit aligns disciplined time");
|
|
check_true(!status[PPS_STATUS_LOCKED_POS] && !status[PPS_STATUS_VALID_POS],
|
|
"coarse alignment cannot assert output validity before loop lock");
|
|
|
|
// Feed coherent post-coarse references through the same APB mailbox used
|
|
// by software. PPS must remain suppressed until the fifth good residual.
|
|
for (lock_update = 0; lock_update < PPS_LOCK_ACQUIRE_UPDATES; lock_update = lock_update + 1) begin
|
|
@(negedge pps_clk);
|
|
lock_ref_counter = dut.i_pps_core.raw_counter;
|
|
lock_ref_seconds = dut.i_pps_core.active_seconds;
|
|
lock_ref_fraction = dut.i_pps_core.active_subsec_q32 / CLK_FREQ_HZ;
|
|
pps_sw_commit(lock_ref_counter, lock_ref_seconds, lock_ref_fraction,
|
|
lock_update + 2, 1'b0);
|
|
if (lock_update < (PPS_LOCK_ACQUIRE_UPDATES - 1))
|
|
check_true(!pps_valid && !pps_out, "PPS stays low during lock acquisition");
|
|
end
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(status[PPS_STATUS_LOCKED_POS] && status[PPS_STATUS_VALID_POS],
|
|
"read-only status reports acquired PPS lock and validity");
|
|
|
|
@(posedge pps_out);
|
|
#1;
|
|
pps_edge_counter = dut.i_pps_core.raw_counter;
|
|
check_true((pps_edge_counter - capture_counter >= 799) &&
|
|
(pps_edge_counter - capture_counter <= 802),
|
|
"timestamped commit removes software/APB latency from PPS phase");
|
|
|
|
@(negedge pps_out);
|
|
#1;
|
|
first_pps_fall_counter = dut.i_pps_core.raw_counter;
|
|
check_true(first_pps_fall_counter - pps_edge_counter == CLK_FREQ_HZ / 10,
|
|
"external PPS high time is exactly 100 ms");
|
|
|
|
@(posedge pps_out);
|
|
#1;
|
|
second_pps_edge_counter = dut.i_pps_core.raw_counter;
|
|
check_true((second_pps_edge_counter - pps_edge_counter >= CLK_FREQ_HZ - 1) &&
|
|
(second_pps_edge_counter - pps_edge_counter <= CLK_FREQ_HZ + 1),
|
|
"free-running nominal PPS period is one configured second");
|
|
pps_edge_counter = second_pps_edge_counter;
|
|
|
|
// Build the next navigation reference from a new captured hardware epoch.
|
|
repeat (40) @(posedge pps_clk);
|
|
@(negedge PCLK); capture_i[2] = 1'b1;
|
|
@(negedge PCLK); capture_i[2] = 1'b0;
|
|
repeat (3) @(posedge PCLK);
|
|
read_capture(2, second_capture, second_capture_sequence);
|
|
check_true(second_capture_sequence == 1, "independent capture bank per tracking channel");
|
|
phase_ticks = (second_capture - pps_edge_counter) + 2;
|
|
pps_sw_commit(second_capture, 102, ticks_to_fraction_q32(phase_ticks), 7, 1'b0);
|
|
repeat (6) @(posedge PCLK);
|
|
pps_sw_snapshot_diagnostics();
|
|
read_ok(PPS_PHASE_ERROR_LO_OFFSET, read_data);
|
|
phase_error[31:0] = read_data;
|
|
read_ok(PPS_PHASE_ERROR_HI_OFFSET, read_data);
|
|
phase_error[63:32] = read_data;
|
|
read_ok(PPS_RATE_WORD_LO_OFFSET, read_data);
|
|
rate_word[31:0] = read_data;
|
|
read_ok(PPS_RATE_WORD_HI_OFFSET, read_data);
|
|
rate_word[63:32] = read_data;
|
|
check_true(phase_error > $signed(64'h0000_0001_0000_0000), "positive GPS phase residual reaches PI");
|
|
check_true(rate_word > 0, "positive phase residual produces positive rate correction");
|
|
read_ok(PPS_ACTIVE_SUBSEC_LO_OFFSET, diag_sub_lo_before);
|
|
read_ok(PPS_ACTIVE_SUBSEC_HI_OFFSET, diag_sub_hi_before);
|
|
repeat (20) @(posedge pps_clk);
|
|
read_ok(PPS_ACTIVE_SUBSEC_LO_OFFSET, diag_sub_lo_after);
|
|
read_ok(PPS_ACTIVE_SUBSEC_HI_OFFSET, diag_sub_hi_after);
|
|
check_true({diag_sub_hi_after, diag_sub_lo_after} ==
|
|
{diag_sub_hi_before, diag_sub_lo_before},
|
|
"diagnostic words remain coherent until the next explicit snapshot");
|
|
pps_sw_snapshot_diagnostics();
|
|
read_ok(PPS_ACTIVE_SUBSEC_LO_OFFSET, diag_sub_lo_after);
|
|
read_ok(PPS_ACTIVE_SUBSEC_HI_OFFSET, diag_sub_hi_after);
|
|
check_true({diag_sub_hi_after, diag_sub_lo_after} !=
|
|
{diag_sub_hi_before, diag_sub_lo_before},
|
|
"new diagnostic snapshot advances the observed time");
|
|
|
|
// Replaying sequence 7 must acknowledge the mailbox but not update PI.
|
|
@(negedge PCLK); capture_i[2] = 1'b1;
|
|
@(negedge PCLK); capture_i[2] = 1'b0;
|
|
repeat (3) @(posedge PCLK);
|
|
read_capture(2, second_capture, second_capture_sequence);
|
|
check_true(second_capture_sequence == 2, "repeated channel capture advances its sequence");
|
|
pps_sw_commit(second_capture, 102, ticks_to_fraction_q32(phase_ticks), 7, 1'b0);
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(status[PPS_STATUS_STALE_SEQUENCE_POS],
|
|
$sformatf("stale epoch sequence is rejected (status=0x%08x core_stale=%0b active=%0d)",
|
|
status, dut.stale_sequence_pps, dut.active_sequence_pps));
|
|
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_000b); // enable + track + clear sticky status
|
|
repeat (8) @(posedge PCLK);
|
|
read_ok(PPS_STATUS_REG_OFFSET, status);
|
|
check_true(!status[PPS_STATUS_STALE_SEQUENCE_POS], "clear-status command crosses clock domains");
|
|
|
|
phase_error_before_holdover = phase_error;
|
|
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_0007); // enable + track + holdover
|
|
pps_sw_commit(second_capture, 102, ticks_to_fraction_q32(phase_ticks + 10), 8, 1'b0);
|
|
pps_sw_snapshot_diagnostics();
|
|
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
|
|
check_true(active_sequence == 7, "holdover preserves the last accepted control epoch");
|
|
check_true(!pps_valid, "holdover deasserts validity while retaining the disciplined timebase");
|
|
|
|
// Firmware writes every shadow field while held, then releases holdover
|
|
// immediately before the commit toggle. This makes the first post-gap
|
|
// normal update reach the PI instead of receiving a drop-only ACK.
|
|
@(negedge pps_clk);
|
|
lock_ref_counter = dut.i_pps_core.raw_counter;
|
|
lock_ref_seconds = dut.i_pps_core.active_seconds;
|
|
lock_ref_fraction = dut.i_pps_core.active_subsec_q32 / CLK_FREQ_HZ;
|
|
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, lock_ref_counter[31:0]);
|
|
write_ok(PPS_EPOCH_COUNTER_HI_OFFSET, lock_ref_counter[63:32]);
|
|
write_ok(PPS_GPS_SECONDS_LO_OFFSET, lock_ref_seconds[31:0]);
|
|
write_ok(PPS_GPS_SECONDS_HI_OFFSET, lock_ref_seconds[63:32]);
|
|
write_ok(PPS_GPS_FRACTION_Q32_OFFSET, lock_ref_fraction);
|
|
write_ok(PPS_EPOCH_SEQUENCE_OFFSET, 32'd9);
|
|
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_0003); // release after shadows
|
|
write_ok(PPS_COMMIT_REG_OFFSET, 32'h0000_0001);
|
|
wait_commit_idle();
|
|
pps_sw_snapshot_diagnostics();
|
|
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
|
|
check_true(active_sequence == 9,
|
|
"shadow-first holdover release applies first recovery update");
|
|
check_true(pps_valid,
|
|
"recovery update preserves lock and restores validity");
|
|
|
|
apb_read(12'h08c, read_data, read_error);
|
|
check_true(read_error, "unmapped APB register raises PSLVERR");
|
|
apb_write(12'h08c, 32'h1, read_error);
|
|
check_true(read_error, "unmapped APB write raises PSLVERR");
|
|
write_ok(PPS_CAPTURE_SELECT_REG_OFFSET, 0);
|
|
|
|
$display("PPS TB PASS: %0d checks, capture=%0d, first_pps=%0d, phase=%0d, rate=%0d",
|
|
checks, capture_counter, pps_edge_counter, phase_error, rate_word);
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|