feat(pps): add adaptive disciplined timing loop
This commit is contained in:
+49
-11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user