321 lines
14 KiB
Python
321 lines
14 KiB
Python
"""Pure timing checker used by the DSView decoder and unit tests.
|
|
|
|
All time values in profiles are nanoseconds. The ACK delay in the vendor
|
|
data sheets is a typical value only, so the delay window is intentionally an
|
|
engineering warning threshold. ACK pulse width has specified limits and is
|
|
therefore checked as PASS/FAIL.
|
|
"""
|
|
|
|
from collections import deque
|
|
import math
|
|
|
|
|
|
PROFILES = {
|
|
'1SP0635': {
|
|
'ack_delay_typ_ns': 250.0,
|
|
'ack_width_min_ns': 400.0,
|
|
'ack_width_typ_ns': 700.0,
|
|
'ack_width_max_ns': 1050.0,
|
|
'fault_threshold_ns': 1500.0,
|
|
},
|
|
'1SD536F2': {
|
|
'ack_delay_typ_ns': 380.0,
|
|
'ack_width_min_ns': 600.0,
|
|
'ack_width_typ_ns': 900.0,
|
|
'ack_width_max_ns': 1800.0,
|
|
# The application manual calls >1.5 us a fault, while individual
|
|
# data sheets allow ACK pulses up to 1.8 us. A correlated pulse in
|
|
# the specified ACK range wins; otherwise the longer limit is used.
|
|
'fault_threshold_ns': 1800.0,
|
|
},
|
|
}
|
|
|
|
|
|
def format_ns(value_ns):
|
|
if value_ns >= 1000000.0:
|
|
return '%.3f ms' % (value_ns / 1000000.0)
|
|
if value_ns >= 1000.0:
|
|
return '%.3f us' % (value_ns / 1000.0)
|
|
return '%.1f ns' % value_ns
|
|
|
|
|
|
class TimingChecker(object):
|
|
"""Match Vin edges to Vstat pulses and return annotation dictionaries."""
|
|
|
|
def __init__(self, samplerate, profile='1SP0635', delay_tolerance_ns=100.0,
|
|
vin_active_high=True, vstat_active_high=True,
|
|
custom=None, orphan_min_width_ns=0.0, cycle_results=False):
|
|
if not samplerate:
|
|
raise ValueError('samplerate is required')
|
|
if profile == 'custom':
|
|
if not custom:
|
|
raise ValueError('custom profile values are required')
|
|
self.spec = dict(custom)
|
|
else:
|
|
self.spec = dict(PROFILES[profile])
|
|
self.profile = profile
|
|
self.samplerate = float(samplerate)
|
|
self.delay_tolerance_ns = float(delay_tolerance_ns)
|
|
self.orphan_min_width_ns = float(orphan_min_width_ns)
|
|
if not math.isfinite(self.orphan_min_width_ns) or self.orphan_min_width_ns < 0:
|
|
raise ValueError('ORPHAN minimum pulse width must be finite and non-negative')
|
|
self.vin_active_high = bool(vin_active_high)
|
|
self.vstat_active_high = bool(vstat_active_high)
|
|
self.cycle_results = cycle_results
|
|
self.cycle = None
|
|
self.pending = deque()
|
|
self.status_start = None
|
|
self.status_control = None
|
|
|
|
# Long enough to avoid calling a merely late ACK "missing", while
|
|
# still producing a useful annotation during a capture.
|
|
self.ack_timeout_ns = max(
|
|
self.spec['ack_delay_typ_ns'] + 3.0 * self.delay_tolerance_ns,
|
|
self.spec['ack_delay_typ_ns'] + self.spec['ack_width_max_ns'])
|
|
|
|
def samples_to_ns(self, samples):
|
|
return float(samples) * 1000000000.0 / self.samplerate
|
|
|
|
def ns_to_samples(self, value_ns):
|
|
return max(1, int(round(float(value_ns) * self.samplerate / 1000000000.0)))
|
|
|
|
def next_deadline(self):
|
|
if not self.pending:
|
|
return None
|
|
return self.pending[0]['sample'] + self.ns_to_samples(self.ack_timeout_ns)
|
|
|
|
def on_control_edge(self, sample, level):
|
|
state_on = bool(level) == self.vin_active_high
|
|
item = {
|
|
'sample': int(sample),
|
|
'level': int(level),
|
|
'edge': 'ON' if state_on else 'OFF',
|
|
}
|
|
events = []
|
|
if self.cycle_results:
|
|
if state_on:
|
|
if self.cycle is not None and not self.cycle['emitted']:
|
|
events += self._finish_cycle(self.cycle, sample, incomplete=True)
|
|
self.cycle = dict(start=int(sample), severity=0, reasons=[], resolved=set(), emitted=False)
|
|
item['cycle'] = self.cycle
|
|
self.pending.append(item)
|
|
return events + [{
|
|
'kind': 'control', 'start': int(sample), 'end': int(sample),
|
|
'text': 'Vin1 %s' % item['edge'], 'short': item['edge'],
|
|
}]
|
|
|
|
def _finish_cycle(self, cycle, sample, incomplete=False):
|
|
if cycle['emitted']:
|
|
return []
|
|
cycle['emitted'] = True
|
|
if incomplete:
|
|
cycle['severity'] = max(1, cycle['severity'])
|
|
cycle['reasons'].append('incomplete cycle')
|
|
verdict = ('OK', 'WARNING', 'FAULT')[cycle['severity']]
|
|
detail = ', '.join(dict.fromkeys(cycle['reasons']))
|
|
return [dict(kind='cycle_' + verdict.lower(), start=cycle['start'], end=int(sample),
|
|
text=verdict + ': Vin1 ON / ACK / OFF / ACK' + (' - ' + detail if detail else ''),
|
|
short=verdict)]
|
|
|
|
def _cycle_note(self, cycle, kind):
|
|
if cycle is None or cycle['emitted']:
|
|
return
|
|
severity = 2 if kind in ('missing', 'width_fail', 'fault') else 1 if kind in ('delay_warn', 'orphan') else 0
|
|
cycle['severity'] = max(cycle['severity'], severity)
|
|
if severity:
|
|
cycle['reasons'].append(kind)
|
|
|
|
def _resolve_cycle(self, item, sample, kind):
|
|
cycle = item.get('cycle') if item else None
|
|
if cycle is None or cycle['emitted']:
|
|
return []
|
|
self._cycle_note(cycle, kind)
|
|
cycle['resolved'].add(item['edge'])
|
|
if cycle['resolved'] == {'ON', 'OFF'}:
|
|
return self._finish_cycle(cycle, sample)
|
|
return []
|
|
|
|
def finish_cycles(self, sample):
|
|
if self.cycle is not None and not self.cycle['emitted']:
|
|
return self._finish_cycle(self.cycle, sample, incomplete=True)
|
|
return []
|
|
|
|
def expire(self, sample):
|
|
events = []
|
|
timeout_samples = self.ns_to_samples(self.ack_timeout_ns)
|
|
while self.pending and int(sample) >= self.pending[0]['sample'] + timeout_samples:
|
|
item = self.pending.popleft()
|
|
end = item['sample'] + timeout_samples
|
|
events.append({
|
|
'kind': 'missing', 'start': item['sample'], 'end': end,
|
|
'text': 'FAIL: no Vstat ACK after Vin1 %s (timeout %s)' %
|
|
(item['edge'], format_ns(self.ack_timeout_ns)),
|
|
'short': 'NO ACK',
|
|
})
|
|
events.extend(self._resolve_cycle(item, end, 'missing'))
|
|
return events
|
|
|
|
def on_status_edge(self, sample, level):
|
|
sample = int(sample)
|
|
is_active = bool(level) == self.vstat_active_high
|
|
events = self.expire(sample)
|
|
|
|
if is_active:
|
|
# Ignore a second active edge caused by an inconsistent trace.
|
|
if self.status_start is not None:
|
|
return events
|
|
self.status_start = sample
|
|
self.status_control = self.pending.popleft() if self.pending else None
|
|
if self.status_control is not None:
|
|
delay_ns = self.samples_to_ns(sample - self.status_control['sample'])
|
|
typ_ns = self.spec['ack_delay_typ_ns']
|
|
delta_ns = delay_ns - typ_ns
|
|
in_window = abs(delta_ns) <= self.delay_tolerance_ns
|
|
self._cycle_note(self.status_control.get('cycle'), 'delay_ok' if in_window else 'delay_warn')
|
|
events.append({
|
|
'kind': 'delay_ok' if in_window else 'delay_warn',
|
|
'start': self.status_control['sample'], 'end': sample,
|
|
'delay_ns': delay_ns,
|
|
'text': '%s: ACK delay %s (typ %s, delta %+0.1f ns)' %
|
|
('PASS' if in_window else 'WARN', format_ns(delay_ns),
|
|
format_ns(typ_ns), delta_ns),
|
|
'short': '%s %s' % ('OK' if in_window else 'WARN',
|
|
format_ns(delay_ns)),
|
|
})
|
|
return events
|
|
|
|
if self.status_start is None:
|
|
self._cycle_note(self.cycle, 'orphan')
|
|
events.append({
|
|
'kind': 'orphan', 'start': sample, 'end': sample,
|
|
'text': 'Unexpected inactive Vstat edge', 'short': 'Vstat?',
|
|
})
|
|
return events
|
|
|
|
start = self.status_start
|
|
control = self.status_control
|
|
width_ns = self.samples_to_ns(sample - start)
|
|
self.status_start = None
|
|
self.status_control = None
|
|
lo = self.spec['ack_width_min_ns']
|
|
hi = self.spec['ack_width_max_ns']
|
|
|
|
if control is not None and lo <= width_ns <= hi:
|
|
events.append({
|
|
'kind': 'width_ok', 'start': start, 'end': sample,
|
|
'width_ns': width_ns,
|
|
'text': 'PASS: ACK width %s (limit %s...%s)' %
|
|
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
|
'short': 'ACK %s' % format_ns(width_ns),
|
|
})
|
|
elif width_ns > self.spec['fault_threshold_ns']:
|
|
events.append({
|
|
'kind': 'fault', 'start': start, 'end': sample,
|
|
'width_ns': width_ns,
|
|
'text': 'FAULT: Vstat active for %s' % format_ns(width_ns),
|
|
'short': 'FAULT %s' % format_ns(width_ns),
|
|
})
|
|
elif control is None:
|
|
# This is an annotation filter, not a signal debounce: leave ACK
|
|
# matching and fault detection intact. Equality passes the filter.
|
|
if width_ns < self.orphan_min_width_ns:
|
|
return events
|
|
events.append({
|
|
'kind': 'orphan', 'start': start, 'end': sample,
|
|
'width_ns': width_ns,
|
|
'text': 'Unexpected Vstat pulse %s (no Vin1 edge)' % format_ns(width_ns),
|
|
'short': 'ORPHAN %s' % format_ns(width_ns),
|
|
})
|
|
else:
|
|
events.append({
|
|
'kind': 'width_fail', 'start': start, 'end': sample,
|
|
'width_ns': width_ns,
|
|
'text': 'FAIL: ACK width %s outside %s...%s' %
|
|
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
|
'short': 'BAD ACK %s' % format_ns(width_ns),
|
|
})
|
|
kind = events[-1]['kind']
|
|
if control is None:
|
|
self._cycle_note(self.cycle, kind)
|
|
events.extend(self._resolve_cycle(control, sample, kind))
|
|
return events
|
|
|
|
|
|
class InputTimingChecker(object):
|
|
"""Measure complete active pulses and OFF->ON handovers of two inputs.
|
|
|
|
Feed all input levels at a sample together, including the initial sample.
|
|
Unknown pulse starts at the capture boundary are never measured.
|
|
"""
|
|
|
|
def __init__(self, samplerate, vin1_active_high=True, vin2_active_high=True,
|
|
vin1_mintime_ns=0, vin2_mintime_ns=0):
|
|
self.samplerate = float(samplerate)
|
|
self.minimum = (float(vin1_mintime_ns), float(vin2_mintime_ns))
|
|
if not math.isfinite(self.samplerate) or self.samplerate <= 0:
|
|
raise ValueError('samplerate must be finite and positive')
|
|
if any(not math.isfinite(v) or v < 0 for v in self.minimum):
|
|
raise ValueError('Vin mintime must be finite and non-negative')
|
|
self.polarity = (bool(vin1_active_high), bool(vin2_active_high))
|
|
self.levels = None
|
|
self.starts = [None, None]
|
|
self.off = [None, None]
|
|
self.overlap_start = None
|
|
|
|
def _event(self, kind, start, end, label, **values):
|
|
duration = (end - start) * 1e9 / self.samplerate
|
|
text = '%s: %s' % (label, format_ns(duration))
|
|
result = dict(kind=kind, start=start, end=end, text=text, short=text,
|
|
duration_ns=duration)
|
|
result.update(values)
|
|
return result
|
|
|
|
def update(self, sample, vin1, vin2=None):
|
|
sample = int(sample)
|
|
levels = [bool(vin1) == self.polarity[0],
|
|
None if vin2 is None else bool(vin2) == self.polarity[1]]
|
|
if self.levels is None:
|
|
self.levels = levels
|
|
if all(levels):
|
|
self.overlap_start = sample
|
|
return []
|
|
events = []
|
|
previous = self.levels
|
|
# Record OFF edges first so simultaneous handovers measure zero.
|
|
for i in range(2):
|
|
if previous[i] is True and levels[i] is False:
|
|
self.off[i] = sample
|
|
if self.starts[i] is not None:
|
|
duration = (sample - self.starts[i]) * 1e9 / self.samplerate
|
|
failed = duration < self.minimum[i]
|
|
events.append(self._event(
|
|
'mintime_fail' if failed else 'mintime_ok', self.starts[i], sample,
|
|
'%s: Vin%d active (mintime %s)' %
|
|
('FAIL' if failed else 'PASS', i + 1, format_ns(self.minimum[i])),
|
|
channel=i + 1))
|
|
self.starts[i] = None
|
|
for i in range(2):
|
|
if previous[i] is False and levels[i] is True:
|
|
self.starts[i] = sample
|
|
other = 1 - i
|
|
if levels[other] is False and self.off[other] is not None:
|
|
events.append(self._event('deadtime', self.off[other], sample,
|
|
'Deadtime Vin%d -> Vin%d' % (other + 1, i + 1),
|
|
from_channel=other+1, to_channel=i+1))
|
|
# A turn-on consumes the preceding turn-off of either input.
|
|
self.off = [None, None]
|
|
if all(levels) and not all(previous):
|
|
self.overlap_start = sample
|
|
elif all(previous) and not all(levels):
|
|
events.append(self._event('overlap', self.overlap_start, sample,
|
|
'FAIL: Vin1/Vin2 overlap'))
|
|
self.overlap_start = None
|
|
self.levels = levels
|
|
return events
|
|
|
|
def finish(self, sample):
|
|
if self.overlap_start is not None:
|
|
return [self._event('overlap', self.overlap_start, int(sample),
|
|
'FAIL: Vin1/Vin2 overlap (continues at capture end)')]
|
|
return []
|