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