Files
templates/tools/dsview/decoders/plates_sbus/pd.py
T
Andrey a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00

173 lines
23 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.
"""SBus по SerialBusSlave_ver_2_0.vhd; Python 3.6+, sigrok API 3.""" # Формат берётся из HDL, без зависимости от приложения SETGUI.
import math # Округление длительностей вверх исключает преждевременное срабатывание таймеров.
import sigrokdecode as srd # Среда DSView предоставляет входные отсчёты и выходные аннотации.
# CRC повторяет параллельные присваивания HDL, включая четыре завершающие единицы.
def frame_crc(direction, address, value): # В расчёт входят направление, восьмибитный адрес и 16 бит данных.
word = (direction << 24) | (address << 16) | value # Поля образуют 25 бит в порядке передачи от старшего к младшему.
crc = 0 # Начальное состояние регистра CRC равно x"0" в SerialBusSlave_ver_2_0.vhd.
for bit in [((word >> shift) & 1) for shift in range(24, -1, -1)] + [1] * 4: # После 25 бит HDL подаёт четыре единицы.
c3, c2, c1, c0 = [(crc >> shift) & 1 for shift in (3, 2, 1, 0)] # Все новые разряды используют прежнее состояние CRC.
crc = ((c2 ^ c3) << 3) | ((c1 ^ c0) << 2) | (c0 << 1) | (bit ^ c1) # Это точная обратная связь HDL, а не стандартный CRC-4.
return crc # На провод передаются четыре бита результата, начиная с бита 3.
# Декодер разделяет кадры по паузе любого уровня CLK и выбирает линию данных по направлению.
class Decoder(srd.Decoder): # Базовый класс реализован встроенным модулем DSView.
api_version = 3 # API 3 позволяет ждать фронтов и временных интервалов без перебора всех отсчётов.
id = 'plates_sbus' # Уникальное имя не пересекается со штатными декодерами.
name = 'Plates SBus' # Короткое название отображается при выборе протокола.
longname = 'Plates 96-pin SBus' # Уточнение отличает шину от других последовательных протоколов.
desc = 'SBus: direction, device, register, data, CRC and write ACK.' # Краткое описание доступно в диалоге DSView.
license = 'gplv3+' # Это допустимый идентификатор лицензии для libsigrokdecode.
inputs = ['logic'] # Декодер получает цифровые сигналы анализатора.
outputs = ['plates_sbus'] # Python-пакеты можно использовать в следующем уровне декодирования.
tags = ['Embedded/industrial'] # Категория соответствует промышленной шине плат.
channels = ( # Направления линий указаны относительно мастера ТМС.
{'id': 'clk', 'name': 'SBCLK', 'desc': 'Serial clock'}, # CLK задаёт границы битов и паузы между кадрами.
{'id': 'tx', 'name': 'SBDATAIN', 'desc': 'TMS -> plates'}, # На этой линии находятся запрос и данные записи.
{'id': 'rx', 'name': 'SBDATAOUT', 'desc': 'Plates -> TMS'}, # На этой линии находятся данные чтения и активный низкий ACK записи.
) # Все три линии обязательны для проверки обоих направлений.
options = ( # Времена задаются в микросекундах и пересчитываются по частоте захвата.
{'id': 'idle_us', 'desc': 'Frame gap (us)', 'default': 3.6}, # В текущем HDL COUNT_RESET=180 при 50 МГц: 180/50=3,6 мкс.
{'id': 'ack_us', 'desc': 'Write ACK sampling delay (us)', 'default': 0.5}, # Полмикросекунды после CRC оставляют запас для логики проверки до сброса по паузе.
) # Параметры можно изменить для реального захвата другой аппаратуры.
annotations = ( # Первое поле задаёт числовой тип DSView; индекс кортежа используется в put().
('1', 'direction', 'Direction'), # Индекс 0: чтение или запись со стороны ТМС.
('2', 'device', 'Device'), # Индекс 1: адрес устройства 0…15.
('3', 'register', 'Register'), # Индекс 2: адрес регистра 0…15.
('4', 'data', 'Data'), # Индекс 3: сырое 16-битное значение без выдуманных единиц.
('5', 'crc', 'CRC'), # Индекс 4: принятая и рассчитанная контрольные суммы.
('6', 'ack', 'Write ACK'), # Индекс 5: подтверждение записи, отдельное от проверки CRC.
('7', 'frame', 'Transaction'), # Индекс 6: итог операции целиком.
('1000', 'warning', 'Warnings'), # Индекс 7: CRC, отказ и незавершённые кадры.
) # Числовой тип 1000 соответствует предупреждениям в DSView.
annotation_rows = ( # Параллельные строки позволяют видеть поля и итог кадра одновременно.
('fields', 'Fields', (0, 1, 2, 3)), # Неперекрывающиеся поля запроса и данных.
('checks', 'CRC / ACK', (4, 5)), # Независимые проверки целостности и ответа платы.
('frames', 'Transactions', (6,)), # Сводная строка удобна для поиска обращений.
('warnings', 'Warnings', (7,)), # Ошибки видны независимо от масштаба остальных строк.
) # Индексы соответствуют annotations выше.
# Новый экземпляр должен начинаться без синхронизации с границей кадра.
def __init__(self): # Загрузчик создаёт экземпляр при добавлении декодера.
self.reset() # Единый сброс используется также при повторном декодировании.
# Сбрасываем только состояние захвата, не значения обязательных входных каналов.
def reset(self): # Вызывается средой при начале нового прохода.
self.samplerate = None # Частота захвата будет передана через metadata().
self.bits = [] # Элементы содержат номер отсчёта и значение бита текущего кадра.
self.pending = None # Завершённая запись ждёт отдельного момента выборки ACK.
self.ack_at = None # Номер отсчёта выборки ACK существует только при pending.
# Выходы регистрируются после сброса до обработки первых отсчётов.
def start(self): # API DSView вызывает этот метод перед decode().
self.out_ann = self.register(srd.OUTPUT_ANN) # Текстовые поля и предупреждения рисуются над захватом.
self.out_python = self.register(srd.OUTPUT_PYTHON) # Структурированные кадры сохраняют независимые CRC и ACK.
# Частота нужна для отделения кадров по реальному времени, а не количеству битов.
def metadata(self, key, value): # Среда передаёт метаданные захвата независимо от фронтов.
if key == srd.SRD_CONF_SAMPLERATE: # Другие метаданные не влияют на разбор шины.
self.samplerate = value # Единица измерения — отсчёты в секунду.
# Обёртка сохраняет допустимый порядок границ даже для одноточечной аннотации.
def annotate(self, start, end, kind, text): # kind — индекс annotations, text — содержимое над сигналом.
self.put(start, max(start, end), self.out_ann, [kind, [text]]) # Одноточечные поля направления допустимы API.
# Поля передаются старшим битом вперёд; срез не включает правую границу.
def field(self, start, end): # Параметры задают позиции битов внутри 29-битного кадра.
value = 0 # Аккумулятор формируется заново для каждого поля.
for _, bit in self.bits[start:end]: # Номер отсчёта не влияет на числовое значение.
value = (value << 1) | bit # Следующий бит занимает младший разряд после сдвига.
return value # Значение сохраняется без знакового преобразования.
# Итог записи отделяет хороший CRC от ACK; ACK не доказывает применение команды CPU1.
def emit_frame(self, packet, end): # packet уже содержит все поля и результаты доступных проверок.
ack = packet['ack'] # Для чтения и необследованного ответа записи значение остаётся None.
suffix = '' if packet['operation'] == 'read' else ' ACK ' + ('?' if ack is None else ('OK' if ack else 'HIGH')) # Высокий уровень не различает NACK и отсутствие платы.
status = 'OK' if packet['crc_ok'] else 'BAD' # Целостность CRC показывается независимо от подтверждения.
text = '%s %X:%X = 0x%04X CRC %s%s' % (packet['operation'].upper(), packet['dev'], packet['reg'], packet['value'], status, suffix) # Адреса и данные удобны для сверки с регистрами проекта.
self.annotate(self.bits[0][0], end, 6, text) # Сводная аннотация покрывает весь кадр и ожидание ACK.
self.put(self.bits[0][0], end, self.out_python, ['FRAME', packet]) # Структурированный выход не теряет повторные одинаковые команды.
# Бит принимается только по нарастающему фронту, когда ответ ведомого уже установлен после спада.
def receive_bit(self, pins): # pins имеет порядок CLK, запрос ТМС, ответ платы.
index = len(self.bits) # Первый бит — направление, следующие восемь — адрес.
if index >= 29: # Кадр содержит 1+8+16+4 бита; лишние такты до паузы не становятся новым запросом.
return # Синхронизация нового кадра требует паузы на CLK.
source = 1 if index < 9 or self.bits[0][1] else 2 # Заголовок и запись идут от ТМС; данные чтения и CRC — от платы.
self.bits.append((self.samplenum, pins[source])) # Сохраняем точное положение каждого принятого бита.
count = len(self.bits) # Поле выводится сразу после последнего составляющего бита.
if count == 1: # Направление 0 означает чтение, 1 означает запись в HDL.
self.annotate(self.samplenum, self.samplenum, 0, 'WRITE' if self.bits[0][1] else 'READ') # Операция указана относительно ТМС.
for size, begin, kind, label, width in ((5, 1, 1, 'DEV', 1), (9, 5, 2, 'REG', 1), (25, 9, 3, 'DATA', 4)): # Границы определены ширинами адреса и данных HDL.
if count == size: # Одно поле выводится только один раз за кадр.
self.annotate(self.bits[begin][0], self.samplenum, kind, '%s 0x%0*X' % (label, width, self.field(begin, size))) # Нулевые старшие разряды данных сохраняются.
if count != 29: # Проверка CRC возможна только при полном кадре.
return # Неполные кадры будут отмечены по паузе или концу захвата.
direction, address, value = self.field(0, 1), self.field(1, 9), self.field(9, 25) # Восьмибитный адрес делится на устройство и регистр.
actual, expected = self.field(25, 29), frame_crc(direction, address, value) # CRC рассчитывается над теми же данными, которые видны на проводе.
packet = {'bus': 'SBus', 'operation': 'write' if direction else 'read', 'dev': address >> 4, 'reg': address & 15, 'value': value, 'crc': actual, 'crc_expected': expected, 'crc_ok': actual == expected, 'ack': None} # Неизвестное подтверждение не подменяется успешным.
self.annotate(self.bits[25][0], self.samplenum, 4, 'CRC %X / %X %s' % (actual, expected, 'OK' if actual == expected else 'BAD')) # Оба значения помогают диагностировать ошибку захвата.
if actual != expected: # Ошибка видна и в отдельной строке предупреждений.
self.annotate(self.bits[25][0], self.samplenum, 7, 'CRC mismatch') # Повреждённый кадр всё равно выводится для диагностики.
if direction: # Запись подтверждается после проверки CRC ведомым.
self.pending = packet # Выход кадра откладывается до выборки ACK.
self.ack_at = self.samplenum + self.ack_samples # Задержка отсчитывается от последнего бита CRC.
else: # У чтения после CRC нет отдельного подтверждения ведомого.
self.emit_frame(packet, self.samplenum) # Полное чтение доступно без ожидания межкадровой паузы.
# Частичный заголовок не выдаётся за достоверную операцию.
def close_partial(self, end): # Вызывается на паузе и при завершении захвата.
if self.pending is not None: # Захват может закончиться до заданного времени выборки ACK.
self.annotate(self.bits[-1][0], end, 7, 'Write ACK not sampled: capture ended') # Наблюдений недостаточно для вывода о подтверждении.
self.emit_frame(self.pending, end) # Полные данные сохраняются с ack=None.
self.pending = None # Повторный end() не должен дублировать кадр.
if 0 < len(self.bits) < 29: # Полный кадр уже выведен и не считается обрывом.
self.annotate(self.bits[0][0], end, 7, 'Incomplete SBus frame: %d/29 bits' % len(self.bits)) # Число бит помогает найти место потери синхронизации.
self.bits = [] # Следующая операция начинает собственный независимый буфер.
# end() поддерживает диагностику обрыва даже без следующего фронта или полной паузы.
def end(self): # DSView передаёт last_samplenum после последнего блока захвата.
self.close_partial(getattr(self, 'last_samplenum', getattr(self, 'samplenum', 0))) # Резервная граница полезна в средах без last_samplenum.
# Ожидаем фронты и ближайший таймер; тихий участок захвата не требует перебора отсчётов.
def decode(self): # Бесконечное ожидание завершается самой средой libsigrokdecode.
if not self.samplerate or not all(self.has_channel(i) for i in range(3)): # Для времени и обоих направлений нужны частота захвата и три сигнала.
raise ValueError('SBus requires samplerate, SBCLK, SBDATAIN and SBDATAOUT') # Ошибка настройки отличается от повреждённого кадра.
idle_us, ack_us = self.options['idle_us'], self.options['ack_us'] # Настройки относятся к временной шкале захвата.
if not math.isfinite(idle_us) or not math.isfinite(ack_us) or not 0 < ack_us < idle_us: # ACK должен наблюдаться раньше сброса при паузе.
raise ValueError('Require finite 0 < ACK delay < frame gap') # Неверные интервалы не должны создавать ложную диагностику.
gap = max(1, int(math.ceil(self.samplerate * idle_us / 1000000.0))) # Миллион переводит микросекунды в секунды.
self.ack_samples = max(1, int(math.ceil(self.samplerate * ack_us / 1000000.0))) # Округление вверх обеспечивает минимум один отсчёт ожидания.
if self.ack_samples >= gap: # При грубой дискретизации разные времена могут попасть в один отсчёт.
raise ValueError('Samplerate too low to separate ACK sampling and frame gap') # Такой захват не позволяет выполнить заданную временную проверку.
pins = self.wait({}) # Первый отсчёт задаёт уровень CLK без выдуманного фронта.
previous, last_edge, armed = pins[0], self.samplenum, False # Начало посреди кадра игнорируется до наблюдаемой паузы.
while True: # Каждый проход обрабатывает фронт или один ближайший таймер.
deadlines = [] if armed else [last_edge + gap] # После подтверждённой паузы достаточно дождаться следующего фронта.
if self.pending is not None: # Отдельный таймер ACK не зависит от дальнейших тактов CLK.
deadlines.append(self.ack_at) # Последний CRC может быть последним тактом в захвате.
conditions = [{0: 'e'}] # Учитываются оба фронта, поскольку сброс HDL отсчитывается от любого изменения CLK.
if deadlines: # Таймер задаётся относительно текущего положения декодера.
conditions.append({'skip': max(1, min(deadlines) - self.samplenum)}) # Минимум один отсчёт исключает ожидание в той же точке.
pins = self.wait(conditions) # Значения линий доступны и при срабатывании временного условия.
now = self.samplenum # Номер отсчёта общий для фронтов и таймеров.
if self.pending is not None and now >= self.ack_at: # ACK измеряется после CRC, а не на его последнем фронте.
self.pending['ack'] = pins[2] == 0 # В HDL низкий SBDATAOUT подтверждает запись; высокий означает отказ либо отсутствие ответа.
self.annotate(self.bits[-1][0], now, 5, 'ACK' if self.pending['ack'] else 'ACK HIGH (NACK / no response)') # Уровень ответа не скрывается даже при плохом CRC.
if not self.pending['ack']: # Отсутствие низкого ACK является отдельной проблемой обмена.
self.annotate(self.bits[-1][0], now, 7, 'No low write ACK') # Это не доказательство конкретной причины отказа.
self.emit_frame(self.pending, now) # Повторные записи выводятся отдельными кадрами.
self.pending = None # Таймер подтверждения больше не нужен.
if not armed and now - last_edge >= gap: # Пауза допустима как при высоком, так и при низком CLK.
self.close_partial(now) # Незавершённый предыдущий кадр отмечается до повторной синхронизации.
armed = True # Первый следующий нарастающий фронт начинает заголовок.
if pins[0] != previous: # Срабатывание одного таймера не должно создавать фиктивный бит.
if pins[0] and (armed or self.bits): # После паузы начинаем кадр; внутри кадра продолжаем сбор.
armed = False # До следующей длинной паузы новый заголовок запрещён.
self.receive_bit(pins) # Бит выбирается с нужной линии с учётом направления.
last_edge = now # Любой фронт перезапускает измерение неподвижного CLK.
previous = pins[0] # Следующий таймер сравнивается с последним реальным уровнем.