fpga: add clock watch circuit

This commit is contained in:
Lars Amsel 2026-07-31 10:23:14 -05:00 committed by mkoop-ni
parent 2618b51cb8
commit 33a0dc16bd
4 changed files with 396 additions and 0 deletions

View File

@ -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 \

View 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

View 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

View File

@ -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