///|
/// Configuration for clock-quality analysis.
pub struct ClockConfig {
signal_id : String
expected_period : Int64?
tolerance : Int64
active_rising : Bool
minimum_edges : Int
} derive(Debug, Eq)
///|
/// One measured clock cycle.
pub struct ClockCycle {
index : Int
rising_time : Int64
falling_time : Int64?
next_rising_time : Int64?
period : Int64?
high_time : Int64?
low_time : Int64?
duty_cycle : Double?
} derive(Debug, Eq)
///|
/// Aggregate clock-quality results.
pub struct ClockReport {
signal_id : String
edge_count : Int
cycle_count : Int
missing_falling_edges : Int
missing_next_edges : Int
periods : Array[Int64]
minimum_period : Int64?
maximum_period : Int64?
mean_period : Double?
period_jitter : Double?
mean_duty_cycle : Double?
out_of_tolerance : Int
} derive(Debug)
///|
/// A cross-clock-domain sampling relationship.
pub struct CdcObservation {
source_id : String
destination_id : String
destination_time : Int64
nearest_source_edge : Int64?
distance : Int64?
inside_guard_band : Bool
source_value : Value?
destination_value : Value?
} derive(Debug)
///|
/// Return a basic clock configuration.
pub fn clock_config(signal_id : String) -> ClockConfig {
{
signal_id,
expected_period: None,
tolerance: 0L,
active_rising: true,
minimum_edges: 2,
}
}
///|
/// Attach an expected period and absolute tolerance.
pub fn ClockConfig::with_period(
self : ClockConfig,
period : Int64,
tolerance : Int64,
) -> ClockConfig {
{
..self,
expected_period: Some(Int64::max(1L, period)),
tolerance: Int64::max(0L, tolerance),
}
}
///|
/// Require a minimum number of measured edges.
pub fn ClockConfig::with_minimum_edges(
self : ClockConfig,
edges : Int,
) -> ClockConfig {
{ ..self, minimum_edges: Int::max(2, edges) }
}
///|
/// Extract rising-edge timestamps for a configured clock.
pub fn clock_edge_times(trace : Trace, config : ClockConfig) -> Array[Int64] {
let kind = if config.active_rising { Rising } else { Falling }
edges(trace, config.signal_id)
.filter(fn(item) { item.kind == kind })
.map(fn(item) { item.time })
}
///|
/// Measure cycles by matching each active edge to the next edge.
pub fn clock_cycles(trace : Trace, config : ClockConfig) -> Array[ClockCycle] {
let active = clock_edge_times(trace, config)
let opposite_kind = if config.active_rising { Falling } else { Rising }
let opposite = edges(trace, config.signal_id).filter(fn(item) {
item.kind == opposite_kind
})
let cycles : Array[ClockCycle] = []
for index, time in active {
let next = if index + 1 < active.length() {
Some(active[index + 1])
} else {
None
}
let fall = closest_edge_after(opposite, time, next)
let period = next.map(fn(value) { value - time })
let high = interval_to_edge(time, fall)
let low = match (period, high) {
(Some(total), Some(high_time)) => Some(Int64::max(0L, total - high_time))
_ => None
}
cycles.push({
index,
rising_time: time,
falling_time: fall,
next_rising_time: next,
period,
high_time: high,
low_time: low,
duty_cycle: match (high, period) {
(Some(a), Some(b)) if b > 0L => Some(a.to_double() / b.to_double())
_ => None
},
})
}
cycles
}
///|
/// Produce an aggregate clock report.
pub fn clock_report(trace : Trace, config : ClockConfig) -> ClockReport {
let cycles = clock_cycles(trace, config)
let periods = cycles.filter_map(fn(item) { item.period })
let duties = cycles.filter_map(fn(item) { item.duty_cycle })
let missing_falling = cycles.count_if(fn(item) { item.falling_time is None })
let missing_next = cycles.count_if(fn(item) { item.next_rising_time is None })
let out_of_tolerance = match config.expected_period {
Some(expected) =>
periods.count_if(fn(period) {
Int64::abs(period - expected) > config.tolerance
})
None => 0
}
let mean_period = mean_i64(periods)
{
signal_id: config.signal_id,
edge_count: clock_edge_times(trace, config).length(),
cycle_count: cycles.length(),
missing_falling_edges: missing_falling,
missing_next_edges: missing_next,
periods,
minimum_period: optional_minimum_i64(
cycles.filter_map(fn(item) { item.period }),
),
maximum_period: optional_maximum_i64(
cycles.filter_map(fn(item) { item.period }),
),
mean_period,
period_jitter: period_jitter(cycles),
mean_duty_cycle: mean_double(duties),
out_of_tolerance,
}
}
///|
/// Compare two clocks by nearest active-edge distance.
pub fn clock_phase_offsets(
trace : Trace,
left_id : String,
right_id : String,
guard_band : Int64,
) -> Array[Int64] {
let left = edges(trace, left_id)
.filter(fn(item) { item.kind == Rising })
.map(fn(item) { item.time })
let right = edges(trace, right_id)
.filter(fn(item) { item.kind == Rising })
.map(fn(item) { item.time })
let offsets : Array[Int64] = []
for time in left {
match nearest_time(right, time) {
Some(value) => offsets.push(value - time)
None => ()
}
}
offsets.filter(fn(value) {
Int64::abs(value) <= Int64::max(0L, guard_band) || guard_band < 0L
})
}
///|
/// Detect destination samples that occur inside a source guard band.
pub fn cdc_observations(
trace : Trace,
source_id : String,
destination_id : String,
guard_band : Int64,
) -> Array[CdcObservation] {
let source_edges = edges(trace, source_id)
.filter(fn(item) { item.kind == Rising })
.map(fn(item) { item.time })
let destination_edges = edges(trace, destination_id)
.filter(fn(item) { item.kind == Rising })
.map(fn(item) { item.time })
let observations : Array[CdcObservation] = []
for destination_time in destination_edges {
let nearest = nearest_time(source_edges, destination_time)
let distance = nearest.map(fn(value) {
Int64::abs(value - destination_time)
})
observations.push({
source_id,
destination_id,
destination_time,
nearest_source_edge: nearest,
distance,
inside_guard_band: match distance {
Some(value) => value <= Int64::max(0L, guard_band)
None => false
},
source_value: value_at(trace, source_id, destination_time),
destination_value: value_at(trace, destination_id, destination_time),
})
}
observations
}
///|
/// Render a clock report as text.
pub fn ClockReport::to_text(self : ClockReport) -> String {
"clock=" +
self.signal_id +
" edges=" +
self.edge_count.to_string() +
" cycles=" +
self.cycle_count.to_string() +
" mean_period=" +
optional_double_text(self.mean_period) +
" jitter=" +
optional_double_text(self.period_jitter) +
" duty=" +
optional_double_text(self.mean_duty_cycle) +
" out_of_tolerance=" +
self.out_of_tolerance.to_string() +
" missing_falling=" +
self.missing_falling_edges.to_string()
}
///|
/// Render cycle samples as CSV.
pub fn clock_cycles_csv(cycles : Array[ClockCycle]) -> String {
let rows : Array[String] = [
"index,rising,falling,next,period,high,low,duty_cycle",
]
for cycle in cycles {
rows.push(
cycle.index.to_string() +
"," +
cycle.rising_time.to_string() +
"," +
optional_int64_text(cycle.falling_time) +
"," +
optional_int64_text(cycle.next_rising_time) +
"," +
optional_int64_text(cycle.period) +
"," +
optional_int64_text(cycle.high_time) +
"," +
optional_int64_text(cycle.low_time) +
"," +
optional_double_text(cycle.duty_cycle),
)
}
rows.join("\n")
}
///|
/// Render a clock report as JSON.
pub fn ClockReport::to_json(self : ClockReport) -> String {
"{\"signal\":\"" +
csv_cell(self.signal_id) +
"\",\"edge_count\":" +
self.edge_count.to_string() +
",\"cycle_count\":" +
self.cycle_count.to_string() +
",\"missing_falling_edges\":" +
self.missing_falling_edges.to_string() +
",\"missing_next_edges\":" +
self.missing_next_edges.to_string() +
",\"minimum_period\":" +
optional_int64_json(self.minimum_period) +
",\"maximum_period\":" +
optional_int64_json(self.maximum_period) +
",\"mean_period\":" +
optional_double_json(self.mean_period) +
",\"period_jitter\":" +
optional_double_json(self.period_jitter) +
",\"mean_duty_cycle\":" +
optional_double_json(self.mean_duty_cycle) +
",\"out_of_tolerance\":" +
self.out_of_tolerance.to_string() +
"}"
}
///|
/// Find the first opposite edge after an active edge and before the next cycle.
fn closest_edge_after(
edges : Array[EdgeEvent],
start : Int64,
next : Int64?,
) -> Int64? {
let before_next = fn(time : Int64) {
match next {
Some(value) => time < value
None => true
}
}
for edge in edges {
if edge.time >= start && before_next(edge.time) {
return Some(edge.time)
}
}
None
}
///|
/// Return the distance to an edge or None.
fn interval_to_edge(start : Int64, edge : Int64?) -> Int64? {
edge.map(fn(value) { Int64::max(0L, value - start) })
}
///|
/// Return the closest timestamp in a sorted array.
fn nearest_time(values : Array[Int64], target : Int64) -> Int64? {
if values.length() == 0 {
return None
}
let mut best = values[0]
let mut distance = Int64::abs(best - target)
for value in values[1:] {
let current = Int64::abs(value - target)
if current < distance {
best = value
distance = current
}
}
Some(best)
}
///|
/// Calculate the mean of integer measurements.
fn mean_i64(values : Array[Int64]) -> Double? {
if values.length() == 0 {
None
} else {
Some(
values.fold(init=0L, fn(total, item) { total + item }).to_double() /
values.length().to_double(),
)
}
}
///|
/// Calculate the mean of floating-point measurements.
fn mean_double(values : Array[Double]) -> Double? {
if values.length() == 0 {
None
} else {
Some(
values.fold(init=0.0, fn(total, item) { total + item }) /
values.length().to_double(),
)
}
}
///|
/// Estimate period jitter as mean absolute deviation from the mean.
fn period_jitter(cycles : Array[ClockCycle]) -> Double? {
let periods = cycles.filter_map(fn(item) { item.period })
match mean_i64(periods) {
Some(mean) =>
Some(
periods.fold(init=0.0, fn(total, item) {
let delta = item.to_double() - mean
let deviation = if delta < 0.0 { 0.0 - delta } else { delta }
total + deviation
}) /
periods.length().to_double(),
)
None => None
}
}