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

182 lines
7.1 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
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):
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.vin_active_high = bool(vin_active_high)
self.vstat_active_high = bool(vstat_active_high)
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',
}
self.pending.append(item)
return [{
'kind': 'control', 'start': int(sample), 'end': int(sample),
'text': 'Vin %s' % item['edge'], 'short': item['edge'],
}]
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 Vin %s (timeout %s)' %
(item['edge'], format_ns(self.ack_timeout_ns)),
'short': 'NO ACK',
})
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
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:
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:
events.append({
'kind': 'orphan', 'start': start, 'end': sample,
'width_ns': width_ns,
'text': 'Unexpected Vstat pulse %s (no Vin 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),
})
return events