mirror of
https://github.com/EttusResearch/uhd.git
synced 2026-10-25 17:36:10 +00:00
fpga: add clock watch circuit
This commit is contained in:
parent
2618b51cb8
commit
33a0dc16bd
@ -70,6 +70,7 @@ map/axis_muxed_kv_map.v \
|
||||
axil_ctrlport_master.v\
|
||||
handshake.v\
|
||||
handshake_latch.sv\
|
||||
clocking_watchdog.sv\
|
||||
ctrlport_to_regport.v \
|
||||
ctrlport_to_wb_i2c.v \
|
||||
ctrlport_to_wb_uart.v \
|
||||
|
||||
186
fpga/usrp3/lib/control/clocking_watchdog.sv
Normal file
186
fpga/usrp3/lib/control/clocking_watchdog.sv
Normal file
@ -0,0 +1,186 @@
|
||||
//
|
||||
// Copyright 2026 Ettus Research, A National Instruments Brand
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
//
|
||||
// Module: clocking_watchdog
|
||||
//
|
||||
// Description:
|
||||
// Monitors clocking-health indicators and asserts clock_fault if any
|
||||
// indicator is out of spec. Two indicators are checked:
|
||||
//
|
||||
// 1. Lock status (clock_locked)
|
||||
// clock_fault is asserted in the monitor_clk domain whenever clock_locked
|
||||
// is not asserted (after synchronization into monitor_clk). This covers
|
||||
// PLL unlock events regardless of the actual frequency of watched_clk.
|
||||
//
|
||||
// 2. Frequency of watched_clk
|
||||
// The rate of watched_clk is measured by dividing it down to ~1 kHz
|
||||
// with clock_div (N = CLK_DIV_1KHZ, chosen to be even), synchronising
|
||||
// the divided clock into the monitor_clk domain, and counting how many
|
||||
// monitor_clk cycles pass between two consecutive rising edges of the
|
||||
// divided clock. A count below (MS_CYCLES_MON_CLK - 1) means the
|
||||
// watched_clk edge arrived sooner than expected, i.e. watched_clk is
|
||||
// running faster than MAX_CLOCK_RATE, and clock_fault_mon is asserted.
|
||||
// The counter saturates at MS_CYCLES_MON_CLK to prevent rollover when
|
||||
// watched_clk is slower than the maximum rate.
|
||||
//
|
||||
// clock_fault_mon (monitor_clk domain) is synchronised back into the
|
||||
// watched_clk domain before being driven out as clock_fault so that
|
||||
// downstream consumers can safely sample it on either clock.
|
||||
//
|
||||
// Clock domains:
|
||||
// monitor_clk : measurement and fault evaluation domain
|
||||
// watched_clk : clock being monitored; only clock_div runs here
|
||||
//
|
||||
// Reset:
|
||||
// rst is synchronous to monitor_clk. For logic in the watched_clk domain
|
||||
// (clock_div and the clock_fault synchronizer), rst is synchronized into the
|
||||
// watched_clk domain before use.
|
||||
//
|
||||
|
||||
module clocking_watchdog #(
|
||||
// Maximum allowed frequency of watched_clk in Hz. A watched_clk rate
|
||||
// strictly above this value causes clock_fault to be asserted.
|
||||
parameter int MAX_CLOCK_RATE = 125_000_000,
|
||||
// Frequency of monitor_clk in Hz. Used to derive the 1 ms measurement
|
||||
// window (MS_CYCLES_MON_CLK = MONITOR_CLK_FREQ / 1000).
|
||||
parameter int MONITOR_CLK_FREQ = 100_000_000
|
||||
) (
|
||||
// Free-running reference clock used for all measurement logic.
|
||||
input logic monitor_clk,
|
||||
// Synchronous reset (monitor_clk domain). Also drives clk_in_rst of
|
||||
// clock_div so the watched_clk-domain divider is reset as well.
|
||||
input logic rst,
|
||||
// PLL/MMCM lock indicator for watched_clk. Active high; when low,
|
||||
// clock_fault is asserted immediately regardless of the measured rate.
|
||||
input logic clock_locked,
|
||||
// Clock whose frequency is being monitored.
|
||||
input logic watched_clk,
|
||||
// Asserted (1) when a clocking fault is detected, deasserted (0) when
|
||||
// clock_locked is high and watched_clk is within [0, MAX_CLOCK_RATE].
|
||||
// Registered in the watched_clk domain.
|
||||
output logic clock_fault
|
||||
);
|
||||
|
||||
// Number of monitor_clk cycles in 1 ms; defines the measurement window.
|
||||
localparam int unsigned MS_CYCLES_MON_CLK = MONITOR_CLK_FREQ / 1000;
|
||||
// Number of watched_clk cycles in 1 ms at MAX_CLOCK_RATE.
|
||||
localparam int unsigned MAX_COUNT_PER_MS = MAX_CLOCK_RATE / 1000;
|
||||
// Worst-case tolerance of each clock source in ppm. The frequency margin
|
||||
// assumes two independent clocks, so their relative error is 2 * CLOCK_PPM.
|
||||
localparam int unsigned CLOCK_PPM = 100;
|
||||
// Divider ratio for clock_div. clock_div requires an even divisor, so
|
||||
// MAX_COUNT_PER_MS is rounded up to the nearest even integer when needed.
|
||||
localparam int unsigned CLK_DIV_1KHZ = ((MAX_COUNT_PER_MS % 2) != 0) ?
|
||||
(MAX_COUNT_PER_MS + 1) : MAX_COUNT_PER_MS;
|
||||
// Worst-case relative error from two independent CLOCK_PPM clocks over a
|
||||
// 1 ms measurement window. Ceiling rounds any fractional cycle up into the
|
||||
// safety margin.
|
||||
localparam int unsigned FREQ_MARGIN_CYCLES =
|
||||
int'($ceil(real'(MS_CYCLES_MON_CLK * (2 * CLOCK_PPM)) / 1_000_000.0));
|
||||
// One extra monitor_clk cycle for synchronizer/edge-detection quantization
|
||||
// when the divided watched clock is sampled into the monitor_clk domain.
|
||||
localparam int unsigned SYNC_MARGIN_CYCLES = 1;
|
||||
|
||||
// Counts monitor_clk cycles since the last rising edge of the divided
|
||||
// clock. One extra bit to hold MS_CYCLES_MON_CLK without wrap-around.
|
||||
logic unsigned [$clog2(MS_CYCLES_MON_CLK+1)-1:0] ms_tick_count;
|
||||
|
||||
logic clock_locked_mon; // clock_locked synchronised into monitor_clk domain
|
||||
logic watched_clk_div1k; // watched_clk divided by CLK_DIV_1KHZ (~1 kHz)
|
||||
logic watched_clk_div1k_mon; // divided clock synchronised into monitor_clk domain
|
||||
logic watched_clk_div1k_mon_prev; // one-cycle delay for rising-edge detection
|
||||
logic clock_fault_mon = 1'b1; // fault flag evaluated in monitor_clk domain
|
||||
logic rst_watched_clk; // rst synchronized into watched_clk domain
|
||||
|
||||
synchronizer #(
|
||||
.WIDTH(1),
|
||||
.STAGES(2),
|
||||
.INITIAL_VAL(0),
|
||||
.FALSE_PATH_TO_IN(1)
|
||||
) sync_data_clock_locked_i (
|
||||
.clk(monitor_clk),
|
||||
.rst(rst),
|
||||
.in(clock_locked),
|
||||
.out(clock_locked_mon)
|
||||
);
|
||||
|
||||
synchronizer #(
|
||||
.WIDTH(1),
|
||||
.STAGES(2),
|
||||
.INITIAL_VAL(1),
|
||||
.FALSE_PATH_TO_IN(1)
|
||||
) sync_rst_watched_clk_i (
|
||||
.clk(watched_clk),
|
||||
.rst(1'b0),
|
||||
.in(rst),
|
||||
.out(rst_watched_clk)
|
||||
);
|
||||
|
||||
clock_div #(
|
||||
.N(CLK_DIV_1KHZ)
|
||||
) clock_div_1khz_i (
|
||||
.clk_in(watched_clk),
|
||||
.clk_in_rst(rst_watched_clk),
|
||||
.clk_out(watched_clk_div1k)
|
||||
);
|
||||
|
||||
synchronizer #(
|
||||
.WIDTH(1),
|
||||
.STAGES(2),
|
||||
.INITIAL_VAL(0),
|
||||
.FALSE_PATH_TO_IN(1)
|
||||
) sync_watched_clk_1khz_i (
|
||||
.clk(monitor_clk),
|
||||
.rst(rst),
|
||||
.in(watched_clk_div1k),
|
||||
.out(watched_clk_div1k_mon)
|
||||
);
|
||||
|
||||
// Measurement and fault evaluation (monitor_clk domain).
|
||||
//
|
||||
// On each monitor_clk cycle ms_tick_count increments, saturating at
|
||||
// MS_CYCLES_MON_CLK to prevent wrap-around for slow watched_clk rates.
|
||||
//
|
||||
// On each rising edge of the synchronised divided clock:
|
||||
// - clock_fault_mon is set if ms_tick_count is below the nominal 1 ms
|
||||
// count minus both the frequency-tolerance margin and the
|
||||
// synchronizer quantization margin, meaning the interval completed
|
||||
// too early and watched_clk is running above MAX_CLOCK_RATE.
|
||||
// - ms_tick_count is reset to start the next measurement window.
|
||||
//
|
||||
// During reset or when clock_locked is not asserted, clock_fault_mon is
|
||||
// held high (fault asserted) and the counter is held at zero.
|
||||
always_ff @(posedge monitor_clk) begin
|
||||
if (rst || !clock_locked_mon) begin
|
||||
ms_tick_count <= '0;
|
||||
watched_clk_div1k_mon_prev <= '0;
|
||||
clock_fault_mon <= 1'b1;
|
||||
end else begin
|
||||
watched_clk_div1k_mon_prev <= watched_clk_div1k_mon;
|
||||
// Saturate to avoid rollover for slower-than-max watched clocks.
|
||||
if (ms_tick_count < MS_CYCLES_MON_CLK) begin
|
||||
ms_tick_count <= ms_tick_count + 1'b1;
|
||||
end
|
||||
if (watched_clk_div1k_mon && !watched_clk_div1k_mon_prev) begin
|
||||
clock_fault_mon <= ms_tick_count
|
||||
< MS_CYCLES_MON_CLK - FREQ_MARGIN_CYCLES - SYNC_MARGIN_CYCLES;
|
||||
ms_tick_count <= '0;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
synchronizer #(
|
||||
.WIDTH(1),
|
||||
.STAGES(2),
|
||||
.INITIAL_VAL(1),
|
||||
.FALSE_PATH_TO_IN(1)
|
||||
) sync_clock_fault_i (
|
||||
.clk(watched_clk),
|
||||
.rst(rst_watched_clk),
|
||||
.in(clock_fault_mon),
|
||||
.out(clock_fault)
|
||||
);
|
||||
|
||||
endmodule
|
||||
39
fpga/usrp3/lib/sim/control/clocking_watchdog/Makefile
Normal file
39
fpga/usrp3/lib/sim/control/clocking_watchdog/Makefile
Normal file
@ -0,0 +1,39 @@
|
||||
#
|
||||
# Copyright 2026 Ettus Research, A National Instruments Brand
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#
|
||||
|
||||
#-------------------------------------------------
|
||||
# Top-of-Makefile
|
||||
#-------------------------------------------------
|
||||
# Define BASE_DIR to point to the "top" dir.
|
||||
BASE_DIR = $(abspath ../../../../top)
|
||||
# Include viv_sim_preamble after defining BASE_DIR
|
||||
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
|
||||
|
||||
#-------------------------------------------------
|
||||
# Design Specific
|
||||
#-------------------------------------------------
|
||||
|
||||
DESIGN_SRCS += $(abspath \
|
||||
$(abspath ../../../control/clocking_watchdog.sv) \
|
||||
$(abspath ../../../control/clock_div.v) \
|
||||
$(abspath ../../../control/synchronizer.v) \
|
||||
$(abspath ../../../control/synchronizer_impl.v) \
|
||||
)
|
||||
|
||||
#-------------------------------------------------
|
||||
# Testbench Specific
|
||||
#-------------------------------------------------
|
||||
SIM_TOP = clocking_watchdog_tb
|
||||
SIM_SRCS = \
|
||||
$(abspath clocking_watchdog_tb.sv) \
|
||||
|
||||
#-------------------------------------------------
|
||||
# Bottom-of-Makefile
|
||||
#-------------------------------------------------
|
||||
# Include all simulator specific makefiles here
|
||||
# Each should define a unique target to simulate
|
||||
# e.g. xsim, vsim, etc and a common "clean" target
|
||||
include $(BASE_DIR)/../tools/make/viv_simulator.mak
|
||||
@ -0,0 +1,170 @@
|
||||
//
|
||||
// Copyright 2026 Ettus Research, A National Instruments Brand
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
//
|
||||
// Module: clocking_watchdog_tb
|
||||
//
|
||||
// Description:
|
||||
// Self-checking testbench for clocking_watchdog.
|
||||
//
|
||||
// Requirements verified:
|
||||
// 1) clock_fault is asserted when clock_locked is deasserted.
|
||||
// 2) clock_fault is asserted when watched_clk is strictly above MAX_CLOCK_RATE.
|
||||
// 3) clock_fault is deasserted when watched_clk is at or below MAX_CLOCK_RATE.
|
||||
//
|
||||
// Clock model:
|
||||
// Both monitor_clk and watched_clk are driven by sim_clock_gen instances.
|
||||
// sim_clock_gen rounds all periods to 1 ps, so the "above threshold" stimulus
|
||||
// must be a frequency whose 1 ps-quantized period is strictly shorter than the
|
||||
// 8.000 ns period of the max-rate clock. ABOVE_CLOCK_RATE is derived so that
|
||||
// 1e12 / ABOVE_CLOCK_RATE rounds to 7.999 ns.
|
||||
//
|
||||
// Settling time:
|
||||
// Each test case waits wait_settle_ms() of monitor_clk time. The watchdog
|
||||
// measures a 1 ms window in the monitor_clk domain, so 2 ms of settle time
|
||||
// is sufficient for the fault to propagate through the synchronizer chain.
|
||||
//
|
||||
|
||||
`default_nettype none
|
||||
|
||||
module clocking_watchdog_tb;
|
||||
|
||||
`include "test_exec.svh"
|
||||
import PkgTestExec::*;
|
||||
|
||||
timeunit 1ns/1ps;
|
||||
// Monitor clock frequency used to evaluate the 1 ms measurement window.
|
||||
localparam int MONITOR_CLK_FREQ = 100_000_000;
|
||||
// Maximum allowed watched_clk rate; matches the DUT parameter.
|
||||
localparam int MAX_CLOCK_RATE = 125_000_000;
|
||||
// One Hz below the limit; the watchdog must remain clear.
|
||||
localparam int BELOW_CLOCK_RATE = MAX_CLOCK_RATE - 1;
|
||||
// First whole-hertz frequency whose 1 ps-quantized period (7.999 ns) is
|
||||
// strictly shorter than the 8.000 ns period of MAX_CLOCK_RATE.
|
||||
// Derivation: smallest f such that floor(1e12 / f) <= 7_999 ps, i.e.
|
||||
// f >= ceil(1e12 / 7_999) = (1e12 + 7_998) / 7_999 using integer ceiling.
|
||||
// The 64-bit literal prevents a 32-bit overflow during elaboration.
|
||||
localparam int ABOVE_CLOCK_RATE =
|
||||
int'(((64'd1000000000000) + 7_998) / 7_999);
|
||||
|
||||
localparam real MONITOR_CLK_PER_NS = 1.0e9 / MONITOR_CLK_FREQ;
|
||||
|
||||
logic monitor_clk = 1'b0;
|
||||
logic monitor_rst;
|
||||
logic rst = 1'b1;
|
||||
logic clock_locked = 1'b0;
|
||||
logic watched_clk = 1'b0;
|
||||
logic watched_rst;
|
||||
logic clock_fault;
|
||||
|
||||
sim_clock_gen #(
|
||||
.PERIOD (MONITOR_CLK_PER_NS),
|
||||
.AUTOSTART (0)
|
||||
) monitor_clk_gen (
|
||||
.clk (monitor_clk),
|
||||
.rst (monitor_rst)
|
||||
);
|
||||
|
||||
sim_clock_gen #(
|
||||
.PERIOD (1.0e9 / MAX_CLOCK_RATE),
|
||||
.AUTOSTART (0)
|
||||
) watched_clk_gen (
|
||||
.clk (watched_clk),
|
||||
.rst (watched_rst)
|
||||
);
|
||||
|
||||
clocking_watchdog #(
|
||||
.MAX_CLOCK_RATE (MAX_CLOCK_RATE),
|
||||
.MONITOR_CLK_FREQ(MONITOR_CLK_FREQ)
|
||||
) dut (
|
||||
.monitor_clk (monitor_clk),
|
||||
.rst (rst),
|
||||
.clock_locked(clock_locked),
|
||||
.watched_clk (watched_clk),
|
||||
.clock_fault (clock_fault)
|
||||
);
|
||||
|
||||
// Set the watched_clk frequency at run time by reprogramming the clock
|
||||
// generator period. freq_hz is in Hz; the generator expects nanoseconds.
|
||||
task automatic set_watched_clk_freq(input int freq_hz);
|
||||
watched_clk_gen.set_period(1.0e9 / freq_hz);
|
||||
$display("[%0t] watched_clk=%0.3f Hz", $time, freq_hz);
|
||||
endtask
|
||||
|
||||
// Sample clock_fault and compare against expected. Uses strict inequality
|
||||
// ("!==" rather than "!=") so X/Z values are treated as failures.
|
||||
task automatic check_fault(input logic expected, input string test_name);
|
||||
test.start_test(test_name, 10ms);
|
||||
|
||||
assert (clock_fault == expected) begin
|
||||
$display("PASS (%s): clock_fault=%0b at time %0t", test_name, clock_fault, $time);
|
||||
test.end_test();
|
||||
end else begin
|
||||
$error("FAIL (%s): clock_fault=%0b expected=%0b at time %0t", test_name, clock_fault, expected, $time);
|
||||
test.end_test(0);
|
||||
end
|
||||
|
||||
endtask
|
||||
|
||||
// Wait settle_ms milliseconds, counted in monitor_clk cycles. Using the
|
||||
// monitor clock rather than $realtime keeps the TB independent of the
|
||||
// watched_clk rate and avoids relying on DUT-internal timing.
|
||||
task automatic wait_settle_ms(input int settle_ms);
|
||||
localparam int MON_CYCLES_PER_MS = MONITOR_CLK_FREQ / 1000;
|
||||
repeat (settle_ms * MON_CYCLES_PER_MS) @(posedge monitor_clk);
|
||||
endtask
|
||||
|
||||
// Assert reset long enough to initialize both clock domains.
|
||||
// The monitor-domain registers reset asynchronously; 8 monitor cycles is
|
||||
// sufficient for those. clock_div resets synchronously to watched_clk, so
|
||||
// rst must remain asserted through at least 2 watched_clk rising edges to
|
||||
// guarantee its internal counter is cleared before measurement begins.
|
||||
task automatic startup_reset();
|
||||
rst <= 1'b1;
|
||||
repeat (8) @(posedge monitor_clk);
|
||||
// clock_div reset is synchronous to watched_clk, so keep rst asserted
|
||||
// through watched_clk edges to initialize its internal counter.
|
||||
repeat (2) @(posedge watched_clk);
|
||||
rst <= 1'b0;
|
||||
repeat (4) @(posedge monitor_clk);
|
||||
endtask
|
||||
|
||||
initial begin : tb_main
|
||||
|
||||
test.start_tb("clocking_watchdog", 100ms);
|
||||
|
||||
monitor_clk_gen.start();
|
||||
watched_clk_gen.start();
|
||||
|
||||
set_watched_clk_freq(MAX_CLOCK_RATE);
|
||||
startup_reset();
|
||||
|
||||
// Requirement 1: If clock_locked is not asserted, clock_fault must be 1.
|
||||
clock_locked <= 1'b0;
|
||||
wait_settle_ms(1);
|
||||
check_fault(1'b1, "clock_locked deasserted");
|
||||
|
||||
// Assert lock, keep watched clock at max rate. Fault must clear.
|
||||
clock_locked <= 1'b1;
|
||||
wait_settle_ms(2);
|
||||
check_fault(1'b0, "watched_clk exactly max_clock_rate");
|
||||
|
||||
// Requirement 2 (lower): At lower than max, fault must remain clear.
|
||||
set_watched_clk_freq(BELOW_CLOCK_RATE);
|
||||
wait_settle_ms(2);
|
||||
check_fault(1'b0, "watched_clk lower than max_clock_rate");
|
||||
|
||||
// Requirement 2 (higher): Use the nearest detectable above-threshold rate.
|
||||
$display("Nearest above-threshold rate used: %0d Hz (offset +%0d Hz)",
|
||||
ABOVE_CLOCK_RATE, ABOVE_CLOCK_RATE - MAX_CLOCK_RATE);
|
||||
set_watched_clk_freq(ABOVE_CLOCK_RATE);
|
||||
wait_settle_ms(2);
|
||||
check_fault(1'b1, "watched_clk above max_clock_rate");
|
||||
|
||||
test.end_tb();
|
||||
end
|
||||
|
||||
endmodule
|
||||
|
||||
`default_nettype wire
|
||||
Loading…
Reference in New Issue
Block a user