Files
templates/python/logic_analyzer/decoders/gate_timing.py

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 []