feat(pps): add adaptive disciplined timing loop

This commit is contained in:
2026-07-25 17:56:41 +03:00
parent 0f6b3482c5
commit ecbb8d0b68
39 changed files with 4505 additions and 96 deletions
+49 -11
View File
@@ -24,6 +24,15 @@ Q2.30 LSB. Phase, integrator, and rate diagnostics
are signed Q32 counter-tick values. `MAX_SLEW_Q32` is the absolute rate limit
in Q32 ticks per 125 MHz clock; the reset value is approximately 20 ppm.
The PI controller is the hardware loop filter: `Kp` supplies proportional
phase correction and `Ki` is the retained frequency/integral state. There is
no additional phase IIR and no derivative term. An extra phase IIR was
modelled and rejected because it duplicated the small navigation pre-filter
and added acquisition/profile-transition delay. Software may change `Kp`,
`Ki`, maximum slew and lock policy while running; the core captures the whole
mailbox coherently at the next commit and preserves the integrator across a
normal profile transition.
Each rising PPS edge is held high for 100 ms (12,500,000 tracking-clock cycles
at 125 MHz), making the physical output easy to observe without changing the
disciplined edge timing. The width is derived from `CLK_FREQ_HZ` and the shared
@@ -46,7 +55,17 @@ The `capture_i` vector is synchronous to `PCLK`. Each pulse stores the
Gray-synchronized 125 MHz counter and increments that channel's capture
sequence. The two-stage crossing adds a small fixed timestamp pipeline offset;
the SoC-level connection must use the exact PCLK event that updates
`ABS_SAMPLE/REM_CODE_PHASE`, and that offset must be measured once on hardware.
`ABS_SAMPLE/REM_CODE_PHASE`. `CAPTURE_COUNTER` initially refers to the integer
RF-sample event. Before writing `EPOCH_COUNTER`, navigation software subtracts
```
round((REM_CODE_PHASE / 1.023e6) * (125e6 / RF_HZ))
```
counter ticks, so the counter and corrected WLS observation refer to the same
fractional code boundary. This removes an otherwise variable 0--61 ns term.
The remaining PCLK/Gray pipeline delay is fixed and must be measured once on
hardware.
## APB register map
@@ -68,15 +87,29 @@ the SoC-level connection must use the exact PCLK event that updates
| `0x44` | `COMMIT` | WO | bit 0 commit, bit 1 coarse-align commit |
| `0x48..0x74` | diagnostics | RO | sequence, GPS time, phase, integrator, rate and pulse count |
| `0x78` | `DIAG_SNAPSHOT` | WO | bit 0 atomically snapshots all multiword diagnostics |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020100`) |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020200`) |
| `0x88` | `LOOP_CONFIG` | RW shadow | bits 15:8 acquire ticks, 23:16 release ticks, 31:24 consecutive acquire updates; bits 7:0 reserved |
Coarse alignment does not assert lock or drive the pin. Five consecutive PI
updates with at most eight tracking-clock ticks (64 ns at 125 MHz) phase error
acquire lock; an error above 32 ticks (256 ns) releases it. This hysteresis
allows realistic WLS epoch jitter without edge chatter. Holdover
retains an already acquired lock, while a newly committed bad reference drops
lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the physical PPS output
therefore distinguish an aligned timebase from a converged one.
Coarse alignment does not assert lock or drive the pin. The reset policy
requires five consecutive raw PI residuals within eight tracking-clock ticks
(64 ns at 125 MHz); a residual above 32 ticks (256 ns) releases lock. Runtime
profiles may widen the release threshold or require more good updates without
filtering the phase seen by the controller. Holdover retains the internal
frequency/lock state but deasserts `STATUS.VALID`; a newly committed bad
reference drops lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the
physical PPS output therefore distinguish a held/aligned timebase from a
currently qualified one.
Firmware uses a two-second no-reference watchdog to enter holdover but keeps
the Doppler-predicted bias and PI state for bounded same-datum gaps. On
recovery it fills all mailbox shadow words, releases holdover immediately
before the normal commit toggle, and therefore applies the first coherent
post-gap reference (v2.2 rejects normal PI commits that arrive while holdover
is still asserted). The physical PPS already runs from the held rate word and
does not phase-step; `STATUS.VALID` may precede the PI apply only by the
bounded synchronizer/commit-pipeline latency. A datum change or an observation
gap beyond the prediction horizon uses a coarse commit and remains invalid
until lock is reacquired.
Diagnostic words do not change until another `DIAG_SNAPSHOT` request completes;
software polls `STATUS.DIAG_BUSY` before reading the group. The disciplined
@@ -104,19 +137,24 @@ Reading `ACTIVE_SEQUENCE` or `PULSE_COUNT` without this request can return the
previous snapshot, including reset zero, even while the core is accepting
commits and generating pulses.
Run lint plus both unit-level Questa tests from this directory:
Run lint plus all five Questa tests from this directory:
```
make -C tb check
```
`pps_wrapper_tb` uses the same ordered MMIO writes as `gps_system/src/gps_pps.c` and
injects CPU/APB latency. `pps_core_tb` independently stresses signed PI math,
injects CPU/APB latency; it separately verifies that a held normal commit is
not applied and that the firmware's shadow-first release sequence applies the
first recovery update. `pps_core_tb` independently stresses signed PI math,
slew/anti-windup saturation, stale/future epochs, phase saturation, sequence
wrap, holdover and output disable. `pps_convergence_tb` supplies an independent
GPS truth clock, a +15 ppm oscillator error followed by a +8 ppm temperature-like
step, deterministic measurement noise, varying 3--27 cycle software latency,
and 5 Hz navigation commits. It checks phase and rate convergence numerically.
`pps_adaptive_tb` changes `Kp`, `Ki`, maximum slew and lock policy while the
loop is live, checks integrator preservation, validates raw-residual lock
acquisition/release, and verifies that holdover suppresses validity.
`CLK_FREQ_HZ` is reduced only to shorten simulation; tick/Q-format behavior is
unchanged.
+23 -7
View File
@@ -16,6 +16,7 @@ module pps_core #(
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] kp_q46,
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] ki_q46,
input logic [pps_pkg::PPS_FRACTION_WIDTH-1:0] max_slew_q32,
input logic [31:0] loop_config,
output logic commit_ack_toggle,
output logic [pps_pkg::PPS_COUNTER_WIDTH-1:0] raw_counter,
output logic pps_out,
@@ -50,8 +51,7 @@ module pps_core #(
PPS_MILLISECONDS_PER_SECOND - 1) / PPS_MILLISECONDS_PER_SECOND);
localparam int unsigned PPS_PULSE_COUNTER_WIDTH =
(PPS_PULSE_WIDTH_TICKS <= 1) ? 1 : $clog2(PPS_PULSE_WIDTH_TICKS);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH =
(PPS_LOCK_ACQUIRE_UPDATES <= 1) ? 1 : $clog2(PPS_LOCK_ACQUIRE_UPDATES);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH = 8;
// A navigation commit is rare (normally 5 Hz), while the disciplined clock
// runs at 125 MHz. Keep the per-clock time accumulator short and process a
@@ -101,6 +101,9 @@ module pps_core #(
logic [PPS_SEQUENCE_WIDTH-1:0] epoch_sequence_work;
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_work, ki_q46_work;
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_work;
logic [7:0] lock_acquire_ticks_work;
logic [7:0] lock_release_ticks_work;
logic [7:0] lock_acquire_updates_work;
logic signed [PPS_INCREMENT_WIDTH-1:0] reference_increment_work;
// Registered arithmetic stages. No multiplier feeds another multiplier,
@@ -135,13 +138,15 @@ module pps_core #(
logic signed [PPS_PRODUCT_WIDTH-1:0] max_slew_wide;
logic signed [PPS_STATE_WIDTH-1:0] max_slew_signed;
logic [PPS_STATE_WIDTH-1:0] phase_abs_work;
logic [PPS_STATE_WIDTH-1:0] lock_acquire_phase_q32_work;
logic [PPS_STATE_WIDTH-1:0] lock_release_phase_q32_work;
assign raw_counter = raw_counter_p;
assign pps_out = pps_out_p;
assign aligned = aligned_p;
assign tracking = aligned_p && track_enable && !holdover;
assign locked = locked_p;
assign pps_valid = enable && aligned_p && locked_p;
assign pps_valid = enable && aligned_p && locked_p && !holdover;
assign stale_sequence = stale_sequence_p;
assign stale_age = stale_age_p;
assign rate_saturated = rate_saturated_p;
@@ -213,6 +218,10 @@ module pps_core #(
max_slew_signed = $signed({32'd0, max_slew_q32_work});
phase_abs_work = phase_work[PPS_STATE_WIDTH-1] ? $unsigned(-phase_work) :
$unsigned(phase_work);
lock_acquire_phase_q32_work =
{24'd0, lock_acquire_ticks_work, 32'd0};
lock_release_phase_q32_work =
{24'd0, lock_release_ticks_work, 32'd0};
end
always_ff @(posedge clk or negedge resetn) begin
@@ -250,6 +259,9 @@ module pps_core #(
kp_q46_work <= '0;
ki_q46_work <= '0;
max_slew_q32_work <= '0;
lock_acquire_ticks_work <= '0;
lock_release_ticks_work <= '0;
lock_acquire_updates_work <= '0;
reference_increment_work <= '0;
fraction_ticks_work <= '0;
commit_age_ticks_work <= '0;
@@ -310,6 +322,9 @@ module pps_core #(
kp_q46_work <= kp_q46;
ki_q46_work <= ki_q46;
max_slew_q32_work <= max_slew_q32;
lock_acquire_ticks_work <= loop_config[15:8];
lock_release_ticks_work <= loop_config[23:16];
lock_acquire_updates_work <= loop_config[31:24];
if (aligned_p)
reference_increment_work <= $signed({1'b0, NOMINAL_INCREMENT_Q32}) +
$signed(rate_word_q32_p);
@@ -445,15 +460,16 @@ module pps_core #(
// A wider release threshold keeps small measurement noise from
// chattering an already valid physical PPS output.
if (phase_overflow_work ||
(phase_abs_work > PPS_LOCK_RELEASE_PHASE_Q32)) begin
(phase_abs_work > lock_release_phase_q32_work)) begin
locked_p <= 1'b0;
lock_good_updates_p <= '0;
pps_out_p <= 1'b0;
pulse_ticks_remaining_p <= '0;
end else if (!locked_p) begin
if (phase_abs_work <= PPS_LOCK_ACQUIRE_PHASE_Q32) begin
if (lock_good_updates_p ==
PPS_LOCK_COUNTER_WIDTH'(PPS_LOCK_ACQUIRE_UPDATES - 1)) begin
if (phase_abs_work <= lock_acquire_phase_q32_work) begin
if ((lock_acquire_updates_work <= 8'd1) ||
(lock_good_updates_p >=
(lock_acquire_updates_work - 1'b1))) begin
locked_p <= 1'b1;
lock_good_updates_p <= '0;
end else begin
+9 -2
View File
@@ -28,6 +28,12 @@ package pps_pkg;
localparam logic [PPS_STATE_WIDTH-1:0] PPS_LOCK_RELEASE_PHASE_Q32 =
64'h0000_0020_0000_0000; // 32 tracking-clock ticks (256 ns at 125 MHz)
localparam logic [PPS_FRACTION_WIDTH-1:0] PPS_DEFAULT_MAX_SLEW_Q32 = 32'd85899;
localparam logic [31:0] PPS_DEFAULT_LOOP_CONFIG = {
8'(PPS_LOCK_ACQUIRE_UPDATES),
PPS_LOCK_RELEASE_PHASE_Q32[39:32],
PPS_LOCK_ACQUIRE_PHASE_Q32[39:32],
8'd0 // reserved
};
localparam logic [PPS_STATE_WIDTH-1:0] PPS_ONE_TICK_Q32 = 64'h0000_0001_0000_0000;
typedef enum logic [31:0] {
@@ -64,7 +70,8 @@ package pps_pkg;
PPS_DIAG_SNAPSHOT_OFFSET = 32'h0000_0078,
PPS_VERSION_REG_OFFSET = 32'h0000_007c,
PPS_KP_Q46_HI_OFFSET = 32'h0000_0080,
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084,
PPS_LOOP_CONFIG_OFFSET = 32'h0000_0088
} pps_wrapper_reg_offsets_e;
typedef enum int unsigned {
@@ -104,7 +111,7 @@ package pps_pkg;
PPS_APB_STATE_FINISH
} pps_apb_state_e;
localparam logic [31:0] PPS_VERSION = 32'h0002_0100;
localparam logic [31:0] PPS_VERSION = 32'h0002_0200;
function automatic logic [PPS_COUNTER_WIDTH-1:0] pps_gray_to_binary(
input logic [PPS_COUNTER_WIDTH-1:0] gray);
logic [PPS_COUNTER_WIDTH-1:0] binary;
+14 -2
View File
@@ -39,6 +39,7 @@ module pps_wrapper #(
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_p, kp_q46_n;
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46_p, ki_q46_n;
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_p, max_slew_q32_n;
logic [31:0] loop_config_p, loop_config_n;
logic commit_coarse_p, commit_coarse_n;
logic commit_req_toggle_p, commit_req_toggle_n;
logic clear_status_toggle_p, clear_status_toggle_n;
@@ -65,7 +66,8 @@ module pps_wrapper #(
logic [PPS_SEQUENCE_WIDTH-1:0] active_sequence_pps, pulse_count_pps;
logic [PPS_TIME_WIDTH-1:0] active_seconds_pps;
logic [PPS_STATE_WIDTH-1:0] active_subsec_pps;
logic signed [PPS_STATE_WIDTH-1:0] phase_error_pps, integrator_pps, rate_word_pps;
logic signed [PPS_STATE_WIDTH-1:0] phase_error_pps;
logic signed [PPS_STATE_WIDTH-1:0] integrator_pps, rate_word_pps;
(* ASYNC_REG = "TRUE" *) logic [11:0] status_sync1_p, status_sync2_p;
logic [PPS_SEQUENCE_WIDTH-1:0] active_sequence_sync1_p, active_sequence_sync2_p;
@@ -83,7 +85,8 @@ module pps_wrapper #(
logic [PPS_SEQUENCE_WIDTH-1:0] diag_active_sequence_pps, diag_pulse_count_pps;
logic [PPS_TIME_WIDTH-1:0] diag_active_seconds_pps;
logic [PPS_STATE_WIDTH-1:0] diag_active_subsec_pps;
logic [PPS_STATE_WIDTH-1:0] diag_phase_error_pps, diag_integrator_pps, diag_rate_word_pps;
logic [PPS_STATE_WIDTH-1:0] diag_phase_error_pps;
logic [PPS_STATE_WIDTH-1:0] diag_integrator_pps, diag_rate_word_pps;
logic commit_busy;
logic diag_busy;
@@ -147,6 +150,7 @@ module pps_wrapper #(
kp_q46_p <= '0;
ki_q46_p <= '0;
max_slew_q32_p <= PPS_DEFAULT_MAX_SLEW_Q32;
loop_config_p <= PPS_DEFAULT_LOOP_CONFIG;
commit_coarse_p <= 1'b0;
commit_req_toggle_p <= 1'b0;
clear_status_toggle_p <= 1'b0;
@@ -190,6 +194,7 @@ module pps_wrapper #(
kp_q46_p <= kp_q46_n;
ki_q46_p <= ki_q46_n;
max_slew_q32_p <= max_slew_q32_n;
loop_config_p <= loop_config_n;
commit_coarse_p <= commit_coarse_n;
commit_req_toggle_p <= commit_req_toggle_n;
clear_status_toggle_p <= clear_status_toggle_n;
@@ -254,6 +259,7 @@ module pps_wrapper #(
kp_q46_n = kp_q46_p;
ki_q46_n = ki_q46_p;
max_slew_q32_n = max_slew_q32_p;
loop_config_n = loop_config_p;
commit_coarse_n = commit_coarse_p;
commit_req_toggle_n = commit_req_toggle_p;
clear_status_toggle_n = clear_status_toggle_p;
@@ -318,6 +324,9 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET): begin
if (commit_busy) commit_overrun_n = 1'b1; else max_slew_q32_n = PWDATA;
end
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET): begin
if (commit_busy) commit_overrun_n = 1'b1; else loop_config_n = PWDATA;
end
(LOW_ADDRESS + PPS_COMMIT_REG_OFFSET): begin
if (PWDATA[PPS_COMMIT_VALID_POS]) begin
if (!commit_busy) begin
@@ -357,6 +366,7 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_KP_Q46_HI_OFFSET),
(LOW_ADDRESS + PPS_KI_Q46_HI_OFFSET),
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET),
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET),
(LOW_ADDRESS + PPS_COMMIT_REG_OFFSET),
(LOW_ADDRESS + PPS_DIAG_SNAPSHOT_OFFSET): begin end
default: PSLVERR = 1'b1;
@@ -392,6 +402,7 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_KP_Q46_HI_OFFSET): PRDATA = {{16{kp_q46_p[47]}}, kp_q46_p[47:32]};
(LOW_ADDRESS + PPS_KI_Q46_HI_OFFSET): PRDATA = {{16{ki_q46_p[47]}}, ki_q46_p[47:32]};
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET): PRDATA = max_slew_q32_p;
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET): PRDATA = loop_config_p;
(LOW_ADDRESS + PPS_ACTIVE_SEQUENCE_OFFSET): PRDATA = active_sequence_sync2_p;
(LOW_ADDRESS + PPS_ACTIVE_SECONDS_LO_OFFSET): PRDATA = active_seconds_sync2_p[31:0];
(LOW_ADDRESS + PPS_ACTIVE_SECONDS_HI_OFFSET): PRDATA = active_seconds_sync2_p[63:32];
@@ -421,6 +432,7 @@ module pps_wrapper #(
.epoch_counter(epoch_counter_shadow_p), .gps_seconds(gps_seconds_shadow_p),
.gps_fraction_q32(gps_fraction_shadow_p), .epoch_sequence(epoch_sequence_shadow_p),
.kp_q46(kp_q46_p), .ki_q46(ki_q46_p), .max_slew_q32(max_slew_q32_p),
.loop_config(loop_config_p),
.commit_ack_toggle(commit_ack_pps), .raw_counter(raw_counter_pps),
.pps_out(pps_out), .aligned(aligned_pps),
.tracking(tracking_pps), .locked(locked_pps), .pps_valid(pps_valid),
+1
View File
@@ -14,6 +14,7 @@ test:
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_wrapper_tb $(QUESTA_VSIM) -c -l wrapper.log -wlf wrapper.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_core_tb $(QUESTA_VSIM) -c -l core.log -wlf core.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_convergence_tb $(QUESTA_VSIM) -c -l convergence.log -wlf convergence.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_adaptive_tb $(QUESTA_VSIM) -c -l adaptive.log -wlf adaptive.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_precision_tb $(QUESTA_VSIM) -c -l precision.log -wlf precision.wlf -do $(CURDIR)/run.do
clean:
+191
View File
@@ -0,0 +1,191 @@
`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
@@ -75,6 +75,7 @@ module pps_convergence_tb;
.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(PPS_DEFAULT_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),
+1
View File
@@ -40,6 +40,7 @@ module pps_core_tb;
.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(PPS_DEFAULT_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),
+33 -2
View File
@@ -203,6 +203,9 @@ module pps_wrapper_tb;
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;
@@ -254,6 +257,10 @@ module pps_wrapper_tb;
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
@@ -360,10 +367,34 @@ module pps_wrapper_tb;
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");
apb_read(12'h088, read_data, read_error);
// 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'h088, 32'h1, read_error);
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);
+1
View File
@@ -9,6 +9,7 @@ vlog -sv \
[file join $tb_dir pps_wrapper_tb.sv] \
[file join $tb_dir pps_core_tb.sv] \
[file join $tb_dir pps_convergence_tb.sv] \
[file join $tb_dir pps_adaptive_tb.sv] \
[file join $tb_dir pps_precision_tb.sv]
vsim -c -voptargs=+acc work.$::env(PPS_TB_TOP)
run -all