Files
templates/python/logic_analyzer/analysis.py

390 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Offline analysis of timestamped edges; no resampling or hardware access."""
from __future__ import annotations
from bisect import bisect_left, bisect_right
from dataclasses import dataclass
import heapq
from itertools import groupby
import math
from .files import ImportCancelled
from .decoders.gate_timing import TimingChecker
from .decoders.transistor_pair import PairTimingChecker
from .decoders.set_uart import StreamParser
from .decoders.pm35_uart import PM35Parser
from .decoders.set_can import legacy, Reassembler
@dataclass
class AnalysisEvent:
start: float
end: float
kind: str
text: str
details: str = ''
@dataclass
class AnalysisResult:
events: list
truncated: bool
start: float
end: float
def pulse_measurements(channel, time, start, end):
"""Measure a complete adjacent high/low cycle, without scanning the record."""
edges = channel.edges
index = bisect_right(edges, time)
result = {'level': channel.initial ^ (index & 1)}
if 0 < index < len(edges):
left, right = edges[index - 1], edges[index]
if start <= left < right <= end:
result['width'] = right - left
result['high' if result['level'] else 'low'] = right - left
# Prefer the next complete interval, otherwise the preceding one.
other = None
if index + 1 < len(edges) and edges[index + 1] <= end:
other = edges[index + 1] - right
elif index >= 2 and edges[index - 2] >= start:
other = left - edges[index - 2]
if other is not None:
result['low' if result['level'] else 'high'] = other
result['period'] = result['high'] + result['low']
result['frequency'] = 1 / result['period']
result['duty'] = 100 * result['high'] / result['period']
return result
def interval_edges(channel, a, b):
"""Count rising/falling edges in (min(a,b), max(a,b)]."""
left, right = sorted((a, b))
lo, hi = bisect_right(channel.edges, left), bisect_right(channel.edges, right)
count = hi - lo
first_rising = channel.initial ^ (lo & 1) == 0
rising = count // 2 + int(bool(count & 1) and first_rising)
return rising, count - rising
def _cancel(cancel):
if cancel():
raise ImportCancelled()
def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=False, cancel=lambda: False):
"""Yield (start, end, byte, error), sampling 8-bit UART at bit centres."""
bit = 1.0 / baud
bits = 9 + (parity != 'none') + stops
edges = channel.edges
index = bisect_left(edges, start)
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
if level:
index += 1
continue
finish = time + bits * bit
if finish > end + bit * 1e-6:
yield time, end, None, 'Неполный UART-байт в конце диапазона'
break
read = lambda pos: channel.level_at(time + pos * bit) ^ inverted
if read(0.5):
index += 1 # A glitch shorter than half a start bit.
continue
values = [read(1.5 + i) for i in range(8)]
value = sum(v << i for i, v in enumerate(values))
error = None
if parity != 'none' and read(9.5) != ((sum(values) + (parity == 'odd')) & 1):
error = 'Ошибка чётности UART'
stop_start = 9 + (parity != 'none')
if any(read(stop_start + i + 0.5) != 1 for i in range(stops)):
error = 'Ошибка стопового бита UART / BREAK'
yield time, finish, value, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def _number(bits):
value = 0
for bit in bits:
value = (value << 1) | bit
return value
def can_crc(bits):
crc = 0
for bit in bits:
feedback = ((crc >> 14) & 1) ^ bit
crc = (crc << 1) & 0x7fff
if feedback:
crc ^= 0x4599
return crc
class _CanBits:
def __init__(self, channel, time, end, bitrate, inverted, sample_point):
self.channel, self.time, self.end = channel, time, end
self.bit, self.inverted = 1 / bitrate, inverted
self.sample_point = sample_point
self.position, self.previous, self.run = 0, None, 0
self.bits = []
self.last_sample = time
def raw(self):
# Resynchronise on recessive->dominant edges near the bit boundary.
boundary = self.time + self.position * self.bit
if self.position:
lo = bisect_right(self.channel.edges, max(self.last_sample, boundary - self.bit * .2))
hi = bisect_right(self.channel.edges, boundary + self.bit * .2, lo)
for index in range(lo, hi):
level = self.channel.initial ^ ((index + 1) & 1) ^ self.inverted
if level == 0:
self.time += self.channel.edges[index] - boundary
boundary = self.channel.edges[index]
break
sample = boundary + self.sample_point * self.bit
if sample >= self.end:
raise EOFError('Неполный CAN-кадр в конце диапазона')
self.last_sample = sample
self.position += 1
return self.channel.level_at(sample) ^ self.inverted
def stuffed(self):
if self.run == 5:
value = self.raw()
if value == self.previous:
raise ValueError('CAN: ошибка bit stuffing / error frame')
self.previous, self.run = value, 1
value = self.raw()
self.run = self.run + 1 if value == self.previous else 1
self.previous = value
self.bits.append(value)
return value
def take(self, count):
return _number([self.stuffed() for _ in range(count)])
def can_frames(channel, start, end, bitrate, inverted=False, sample_point=.7, cancel=lambda: False):
"""Classic CAN, strict stuffing/CRC/form validation before application decode."""
edges = channel.edges
index = bisect_left(edges, start)
bit = 1 / bitrate
while index < len(edges) and edges[index] < end:
_cancel(cancel)
time = edges[index]
level = channel.initial ^ ((index + 1) & 1) ^ inverted
previous = edges[index - 1] if index else start
# SOF follows at least three recessive intermission bits.
if level or time - previous < bit * 2.9:
index += 1
continue
reader = _CanBits(channel, time, end, bitrate, inverted, sample_point)
frame, error = None, None
try:
if reader.take(1):
raise ValueError('CAN: неверный SOF')
ident = reader.take(11)
rtr, extended = reader.take(1), reader.take(1)
if extended:
if rtr != 1:
raise ValueError('CAN: неверный SRR')
ident = (ident << 18) | reader.take(18)
rtr = reader.take(1)
if reader.take(2):
raise ValueError('CAN FD / reserved bits не поддерживаются')
elif reader.take(1):
raise ValueError('CAN FD / reserved bit не поддерживается')
dlc = reader.take(4)
if dlc > 8:
raise ValueError('CAN: поддерживается classic DLC 0…8')
data = bytes(reader.take(8) for _ in range(0 if rtr else dlc))
expected = can_crc(reader.bits)
received = reader.take(15)
if reader.run == 5:
if reader.raw() == reader.previous:
raise ValueError('CAN: неверный последний stuff bit')
if reader.raw() != 1:
raise ValueError('CAN: неверный CRC delimiter')
ack = reader.raw() == 0
if reader.raw() != 1 or any(reader.raw() != 1 for _ in range(7)):
raise ValueError('CAN: неверный ACK delimiter / EOF')
if received != expected:
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
frame = dict(ident=ident, extended=bool(extended), remote=bool(rtr),
data=data, dlc=dlc, ack=ack)
except (ValueError, EOFError) as exc:
error = str(exc)
finish = min(end, reader.time + reader.position * bit)
yield time, finish, frame, error
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
def analyze_capture(capture, options, start=None, end=None, progress=lambda n: None, cancel=lambda: False):
start = capture.start if start is None else max(capture.start, start)
end = capture.end if end is None else min(capture.end, end)
if end <= start:
raise ValueError('Выберите непустой интервал анализа.')
mode = options['mode']
channel = capture.channels[options.get('channel', 0)]
events = []
last_progress = -1
limit = options.get('max_events', 50000)
if limit <= 0:
raise ValueError('Лимит результатов должен быть положительным.')
def emit(a, b, kind, text, details=''):
if len(events) >= limit:
raise OverflowError()
events.append(AnalysisEvent(a, b, kind, text, details))
def report(time):
nonlocal last_progress
_cancel(cancel)
value = min(99, int((time - start) / (end - start) * 100))
if value != last_progress:
progress(value)
last_progress = value
def parsed(items):
for a, b, frame, error in items:
emit(a, b, 'error' if error else 'frame', error or frame['summary'],
'' if not frame else bytes(frame.get('raw', frame.get('payload', b''))).hex(' '))
if frame and frame.get('protocol') == 'ProtoCAN bridge' and frame['flags'] & 1 and not frame['flags'] & 10:
try:
application = legacy(frame['can_id'], frame['payload'])
emit(a, b, 'frame', application['summary'])
except ValueError as exc:
emit(a, b, 'error', str(exc))
truncated = False
try:
if mode in ('1SP0635', '1SD536F2'):
other = options.get('status_channel', 1)
if other == options.get('channel', 0):
raise ValueError('Vin1 и Vstat должны быть разными каналами.')
status = capture.channels[other]
if options.get('pair_analysis'):
raise ValueError('Use the Transistor pair analyzer for Vin1/Vin2 timing.')
checker = TimingChecker(1e9, mode, options.get('tolerance_ns', 100),
not options.get('vin_low', False), not options.get('status_low', False))
# The pure checker works in integer ticks. Offset before rounding
# to preserve ns precision for CSV timestamps far from zero.
tick = lambda t: round((t - start) * 1e9)
def timing(items):
for item in items:
text = item['text']
if item['kind'] == 'fault':
text += ' · аварийная обратная связь; превышение тока не подтверждено'
emit(start + item['start'] / 1e9, start + item['end'] / 1e9, item['kind'], text)
def transitions(ch, which):
first = bisect_right(ch.edges, start)
for i in range(first, bisect_right(ch.edges, end)):
yield ch.edges[i], which, ch.initial ^ ((i + 1) & 1)
if bool(status.level_at(start)) == checker.vstat_active_high:
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
streams = [transitions(channel, 0), transitions(status, 1)]
for time, which, level in heapq.merge(*streams):
report(time)
timing(checker.expire(tick(time)))
timing(checker.on_control_edge(tick(time), level) if which == 0
else checker.on_status_edge(tick(time), level))
timing(checker.expire(tick(end)))
if checker.status_start is not None:
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
for pending in checker.pending:
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin1: диапазон закончился до тайм-аута ACK')
elif mode == 'Transistor pair':
second = options.get('vin2_channel', 1)
if second == options.get('channel', 0):
raise ValueError('Vin1 and Vin2 must be different channels.')
vin2 = capture.channels[second]
numeric = {name: options.get(name, 0) for name in (
'vin1_mintime_ns', 'vin2_mintime_ns', 'vin1_minoff_ns', 'vin2_minoff_ns',
'deadtime_12_ns', 'deadtime_21_ns')}
checker = PairTimingChecker(1e9, not options.get('vin_low', False),
not options.get('vin2_low', False), **numeric)
levels = [channel.level_at(start), vin2.level_at(start)]
checker.update(0, *levels)
def edges(ch, which):
for index in range(bisect_right(ch.edges, start), bisect_right(ch.edges, end)):
yield ch.edges[index], which, ch.initial ^ ((index + 1) & 1)
def emit_pair(items):
for item in items:
emit(start + item['start'] / 1e9, start + item['end'] / 1e9,
item['kind'], item['text'])
for time, group in groupby(heapq.merge(edges(channel, 0), edges(vin2, 1)), key=lambda e: e[0]):
report(time)
for _, which, level in group:
levels[which] = level
emit_pair(checker.update(round((time - start) * 1e9), *levels))
emit_pair(checker.finish(round((end - start) * 1e9)))
elif mode in ('UART', 'SET UART', 'PM35 UART'):
baud = options.get('baudrate', 115200)
parity, stops = options.get('parity', 'none'), options.get('stops', 1)
if not math.isfinite(baud) or baud <= 0 or parity not in ('none', 'even', 'odd') or stops not in (1, 2):
raise ValueError('Некорректные настройки UART.')
parser = (StreamParser(options.get('protocol', 'auto')) if mode == 'SET UART' else
PM35Parser(options.get('role', 'response')) if mode == 'PM35 UART' else None)
gap = (3.5 * (9 + (parity != 'none') + stops) / baud if mode == 'PM35 UART'
else options.get('gap_ms', 100) / 1000)
last = None
for a, b, value, error in uart_frames(channel, start, end, baud, parity, stops, options.get('inverted', False), cancel):
report(a)
if parser and last is not None and gap > 0 and a - last >= gap:
parsed(parser.flush())
last = b
if error:
if parser:
parsed(parser.flush())
emit(a, b, 'error', error)
elif parser:
parsed(parser.feed(value, a, b))
else:
emit(a, b, 'byte', 'UART 0x%02X' % value, chr(value) if 32 <= value < 127 else '')
if parser:
parsed(parser.flush())
elif mode in ('CAN', 'SET CAN'):
bitrate = options.get('baudrate', 1000000)
sample_point = options.get('sample_point', 70) / 100
if not math.isfinite(bitrate) or bitrate <= 0 or not .1 <= sample_point <= .95:
raise ValueError('Некорректный битрейт / точка выборки CAN.')
reassembler = Reassembler()
for a, b, frame, error in can_frames(channel, start, end, bitrate, options.get('inverted', False), sample_point, cancel):
report(a)
if error:
emit(a, b, 'error', error)
# Never bridge an invalid/missing physical frame.
for pending in reassembler.pending.values():
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
reassembler.pending.clear()
continue
ident, data = frame['ident'], frame['data']
emit(a, b, 'can', 'CAN %s ID=%08X DLC=%d %s %s' % (
'EXT' if frame['extended'] else 'STD', ident, frame['dlc'],
'RTR' if frame['remote'] else 'DATA', 'ACK' if frame['ack'] else 'NACK'), data.hex(' '))
if mode == 'SET CAN' and frame['extended'] and not frame['remote']:
protocol = options.get('protocol', 'protocan')
if protocol == 'set-v2':
parsed(reassembler.feed(ident, data, a, b, b * 1000))
else:
try:
result = legacy(ident, data, protocol)
if result:
emit(a, b, 'frame', result['summary'], data.hex(' '))
except ValueError as exc:
emit(a, b, 'error', str(exc))
for pending in reassembler.pending.values():
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
else:
raise ValueError('Неизвестный анализатор: ' + mode)
except OverflowError:
truncated = True
_cancel(cancel)
progress(100)
events.sort(key=lambda event: (event.start, event.end))
return AnalysisResult(events, truncated, start, end)