Files
gps/hardware/external/gps/rtl/pps/tb/pps_wrapper_tb.sv
T

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