192 lines
7.4 KiB
Systemverilog
192 lines
7.4 KiB
Systemverilog
`timescale 1ns/1ps
|
|
|
|
module pps_adaptive_tb;
|
|
import pps_pkg::*;
|
|
|
|
localparam int unsigned CLK_FREQ_HZ = 1000;
|
|
localparam logic [63:0] TICK_Q32 = 64'h0000_0001_0000_0000;
|
|
localparam logic [63:0] SECOND_Q32 = CLK_FREQ_HZ * TICK_Q32;
|
|
localparam logic [31:0] ACQUIRE_CONFIG = 32'h0540_0800;
|
|
localparam logic [31:0] PRECISION_CONFIG = 32'h0340_0800;
|
|
localparam logic signed [47:0] ACQUIRE_KP_Q46 = 48'sh0800_0000_0000;
|
|
localparam logic signed [47:0] ACQUIRE_KI_Q46 = 48'sh0400_0000_0000;
|
|
localparam logic signed [47:0] PRECISION_KP_Q46 = 48'sh0200_0000_0000;
|
|
localparam logic signed [47:0] PRECISION_KI_Q46 = 48'sh0100_0000_0000;
|
|
|
|
logic clk = 1'b0;
|
|
logic resetn = 1'b0;
|
|
logic enable = 1'b1;
|
|
logic track_enable = 1'b1;
|
|
logic holdover = 1'b0;
|
|
logic clear_status_toggle = 1'b0;
|
|
logic commit_toggle = 1'b0;
|
|
logic commit_coarse = 1'b0;
|
|
logic [63:0] epoch_counter = '0;
|
|
logic [63:0] gps_seconds = '0;
|
|
logic [31:0] gps_fraction_q32 = '0;
|
|
logic [31:0] epoch_sequence = '0;
|
|
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46 = ACQUIRE_KP_Q46;
|
|
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46 = ACQUIRE_KI_Q46;
|
|
logic [31:0] max_slew_q32 = 32'h7fff_ffff;
|
|
logic [31:0] loop_config = ACQUIRE_CONFIG;
|
|
logic commit_ack_toggle;
|
|
logic [63:0] raw_counter;
|
|
logic pps_out, aligned, tracking, locked, pps_valid;
|
|
logic stale_sequence, stale_age, rate_saturated, phase_saturated;
|
|
logic [31:0] active_sequence, pulse_count;
|
|
logic [63:0] active_seconds, active_subsec_q32;
|
|
logic signed [63:0] phase_error_q32;
|
|
logic signed [63:0] integrator_q32, rate_word_q32;
|
|
int checks = 0;
|
|
|
|
always #4 clk = ~clk;
|
|
|
|
pps_core #(.CLK_FREQ_HZ(CLK_FREQ_HZ)) dut (
|
|
.clk(clk), .resetn(resetn), .enable(enable), .track_enable(track_enable),
|
|
.holdover(holdover), .clear_status_toggle(clear_status_toggle),
|
|
.commit_toggle(commit_toggle), .commit_coarse(commit_coarse),
|
|
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
|
|
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
|
|
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
|
|
.loop_config(loop_config),
|
|
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
|
|
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
|
|
.pps_valid(pps_valid), .stale_sequence(stale_sequence),
|
|
.stale_age(stale_age), .rate_saturated(rate_saturated),
|
|
.phase_saturated(phase_saturated), .active_sequence(active_sequence),
|
|
.active_seconds(active_seconds), .active_subsec_q32(active_subsec_q32),
|
|
.phase_error_q32(phase_error_q32), .integrator_q32(integrator_q32),
|
|
.rate_word_q32(rate_word_q32), .pulse_count(pulse_count)
|
|
);
|
|
|
|
task automatic check_adaptive(input logic condition, input string message);
|
|
begin
|
|
checks = checks + 1;
|
|
if (!condition)
|
|
$fatal(1, "ADAPTIVE CHECK %0d FAILED: %s", checks, message);
|
|
end
|
|
endtask
|
|
|
|
task automatic commit_anchor(
|
|
input logic [63:0] counter_value,
|
|
input logic [63:0] seconds_value,
|
|
input logic [31:0] fraction_value,
|
|
input logic [31:0] sequence_id,
|
|
input logic coarse);
|
|
logic expected_ack;
|
|
int timeout;
|
|
begin
|
|
@(negedge clk);
|
|
epoch_counter = counter_value;
|
|
gps_seconds = seconds_value;
|
|
gps_fraction_q32 = fraction_value;
|
|
epoch_sequence = sequence_id;
|
|
commit_coarse = coarse;
|
|
commit_toggle = ~commit_toggle;
|
|
expected_ack = commit_toggle;
|
|
timeout = 0;
|
|
while (commit_ack_toggle != expected_ack) begin
|
|
@(posedge clk);
|
|
timeout = timeout + 1;
|
|
if (timeout > 40) $fatal(1, "adaptive commit timeout");
|
|
end
|
|
@(negedge clk);
|
|
end
|
|
endtask
|
|
|
|
task automatic commit_current_offset(
|
|
input integer signed offset_ticks,
|
|
input logic [31:0] sequence_id);
|
|
logic [63:0] counter_value;
|
|
logic [63:0] seconds_value;
|
|
logic signed [64:0] subsec_value;
|
|
logic [31:0] fraction_value;
|
|
begin
|
|
@(negedge clk);
|
|
counter_value = raw_counter;
|
|
seconds_value = active_seconds;
|
|
subsec_value = $signed({1'b0, active_subsec_q32}) +
|
|
(offset_ticks * $signed({1'b0, TICK_Q32}));
|
|
if (subsec_value >= $signed({1'b0, SECOND_Q32})) begin
|
|
subsec_value = subsec_value - $signed({1'b0, SECOND_Q32});
|
|
seconds_value = seconds_value + 64'd1;
|
|
end else if (subsec_value < 0) begin
|
|
subsec_value = subsec_value + $signed({1'b0, SECOND_Q32});
|
|
seconds_value = seconds_value - 64'd1;
|
|
end
|
|
fraction_value = subsec_value / CLK_FREQ_HZ;
|
|
commit_anchor(
|
|
counter_value, seconds_value, fraction_value, sequence_id, 1'b0);
|
|
end
|
|
endtask
|
|
|
|
initial begin
|
|
logic [31:0] sequence_before_holdover;
|
|
logic signed [63:0] acquire_rate;
|
|
logic signed [63:0] acquire_integrator;
|
|
|
|
repeat (5) @(posedge clk);
|
|
resetn = 1'b1;
|
|
repeat (5) @(posedge clk);
|
|
|
|
commit_anchor(raw_counter, 64'd100, 32'd0, 32'd1, 1'b1);
|
|
check_adaptive(aligned && !locked && !pps_valid,
|
|
"coarse alignment starts invalid and unlocked");
|
|
|
|
// Lock detection uses the unfiltered PI phase residual. Five coherent
|
|
// updates inside the programmable eight-tick window are required.
|
|
for (int update = 0; update < 4; update = update + 1) begin
|
|
commit_current_offset(0, update + 2);
|
|
check_adaptive(!locked, "lock cannot assert before configured count");
|
|
end
|
|
commit_current_offset(0, 32'd6);
|
|
check_adaptive(locked && pps_valid,
|
|
"raw coherent residuals acquire lock");
|
|
|
|
// Exercise the acquisition gains, then replace Kp, Ki and lock policy
|
|
// while running. The core captures one coherent set per commit and keeps
|
|
// the integrator state across a profile transition.
|
|
commit_current_offset(1, 32'd7);
|
|
acquire_rate = rate_word_q32;
|
|
acquire_integrator = integrator_q32;
|
|
check_adaptive((acquire_rate > 0) && (acquire_integrator > 0),
|
|
"acquisition Kp and Ki both contribute");
|
|
|
|
kp_q46 = PRECISION_KP_Q46;
|
|
ki_q46 = PRECISION_KI_Q46;
|
|
loop_config = PRECISION_CONFIG;
|
|
commit_current_offset(1, 32'd8);
|
|
check_adaptive(locked && pps_valid,
|
|
"on-the-fly PI profile transition preserves lock");
|
|
check_adaptive(integrator_q32 > acquire_integrator,
|
|
"new Ki continues the preserved integrator");
|
|
check_adaptive(rate_word_q32 < acquire_rate,
|
|
"lower precision Kp/Ki reduce loop correction");
|
|
check_adaptive((dut.kp_q46_work == PRECISION_KP_Q46) &&
|
|
(dut.ki_q46_work == PRECISION_KI_Q46),
|
|
"commit atomically captures runtime Kp and Ki");
|
|
|
|
sequence_before_holdover = active_sequence;
|
|
holdover = 1'b1;
|
|
repeat (3) @(posedge clk);
|
|
check_adaptive(locked && !tracking && !pps_valid,
|
|
"holdover retains lock state but marks PPS invalid");
|
|
commit_current_offset(0, 32'd9);
|
|
check_adaptive(active_sequence == sequence_before_holdover,
|
|
"holdover rejects control updates");
|
|
holdover = 1'b0;
|
|
repeat (3) @(posedge clk);
|
|
check_adaptive(pps_valid && tracking,
|
|
"leaving holdover restores validity without a phase step");
|
|
|
|
commit_current_offset(200, 32'd10);
|
|
check_adaptive(!locked && !pps_valid,
|
|
"one raw residual beyond release threshold drops lock");
|
|
|
|
$display("PPS ADAPTIVE TB PASS: %0d checks phase=%0d rate=%0d",
|
|
checks, phase_error_q32, rate_word_q32);
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|