Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
114 lines
19 KiB
Python
114 lines
19 KiB
Python
"""Операции PBUS по myXilinx/x_parallel_bus.c прошивки TMS2812 / 22220."""
|
||
|
||
from .exchange_queue import ExchangeDelay # Пауза после запуска остаётся внутри одной атомарной операции общей очереди.
|
||
|
||
PBUS_SETTLE_MS = 100 # Начальная настраиваемая пауза обновления аппаратного буфера после старта, миллисекунды.
|
||
PBUS_COMMAND = 0x200C # Spartan2E_Adr.h: управление запуском циклического опроса параллельной шины.
|
||
PBUS_TABLE_INDEX = 0x200D # Spartan2E_Adr.h: выбор изменяемой строки таблицы опроса FPGA.
|
||
PBUS_TABLE_DATA = 0x200E # Spartan2E_Adr.h: дескриптор строки, содержащий адрес платы, регистр и адрес буфера.
|
||
PBUS_BUFFER = 0x2040 # Spartan2E_Adr.h: ADR_FIRST_FREE, первое слово буфера результатов циклического опроса.
|
||
PBUS_CAPACITY = 64 # Spartan2E_Adr.h: буфер 0x2040–0x207F содержит 64 слова, поэтому ограничиваем число строк таблицы.
|
||
PBUS_ERROR_LIMIT = 0xA # Начальный порог SETGUI соответствует рабочему ручному запуску 0x8Axx; пользователь может задать от 1 до 15 ошибок.
|
||
|
||
|
||
def validate_command(command): # Проверяем все параметры до передачи, чтобы неверный макрос не выполнился частично.
|
||
if not isinstance(command, dict): # Повреждённый JSON не должен приводить к исключению интерфейса вместо понятного отказа валидации.
|
||
raise ValueError("Команда макроса должна содержать именованные параметры") # Останавливаем всю последовательность до передачи первого управляющего запроса.
|
||
bus = command.get("bus", "sbus") # Старые JSON-макросы без поля bus относятся к существовавшей последовательной шине.
|
||
if bus not in ("pbus", "sbus"): # Не допускаем исполнения повреждённого имени шины другим алгоритмом.
|
||
raise ValueError("Неизвестная шина макроса") # Сообщаем причину отказа до передачи управляющих данных.
|
||
operations = ("add", "remove", "poll", "configure", "write", "read", "start", "stop") if bus == "pbus" else ("write", "read") # PBUS управляет общим списком регистров; прежние команды сохраняются для совместимости JSON.
|
||
if command["type"] not in operations: # Направление операции должно явно поддерживаться выбранной шиной.
|
||
raise ValueError("Неизвестная команда макроса") # Не превращаем неизвестную команду в чтение.
|
||
limits = (("tetrad", 15 if bus == "pbus" else 14), ("register", 15), ("data", 0xFFFF)) # PBUS имеет четырёхбитный адрес, прежний SBUS сохраняет диапазон 0–14; данные — 16 бит.
|
||
if bus == "pbus" and command["type"] in ("write", "read", "start", "stop"): # Только прежние команды явно задают индекс и длину аппаратной таблицы.
|
||
limits += (("slot", PBUS_CAPACITY - 1), ("count", PBUS_CAPACITY), ("errors", 15)) # Индекс буфера — 0–63, число строк — 1–64, порог — четыре бита.
|
||
defaults = {"data": 0, "slot": 0, "count": 1, "errors": PBUS_ERROR_LIMIT} # Стабильные значения позволяют сохранять компактные команды PBUS.
|
||
for name, maximum in limits: # Проверяем все значения одним правилом до упаковки битовых полей.
|
||
value = command.get(name, defaults[name]) if name in defaults else command[name] # Адрес и регистр обязательны, параметры таблицы имеют штатные значения.
|
||
minimum = 1 if name in ("count", "errors") else 0 # Пустую таблицу и нулевой порог запрещаем: библиотека считает count_error >= 0 ошибкой.
|
||
if not isinstance(value, int) or not minimum <= value <= maximum: # Исключаем строки, отрицательные числа и переполнение аппаратных полей.
|
||
raise ValueError(f"{name}: допустим диапазон {minimum}…{maximum}") # Указываем конкретный неверный параметр и его пределы.
|
||
if bus == "pbus" and (not isinstance(command.get("errors", PBUS_ERROR_LIMIT), int) or not 1 <= command.get("errors", PBUS_ERROR_LIMIT) <= 15): # Новые команды также сохраняют целочисленный четырёхбитный ненулевой порог ошибки PBUS.
|
||
raise ValueError("errors: допустим диапазон 1…15") # Нулевой порог иначе сделал бы любой статус аппаратной ошибкой.
|
||
if bus == "pbus" and command["type"] in ("write", "read", "start", "stop") and command.get("slot", 0) >= command.get("count", 1): # Проверяем явную адресацию только у прежнего формата команды.
|
||
raise ValueError("PBUS: индекс строки должен быть меньше числа строк таблицы") # Не запускаем опрос, который исключает выбранный результат.
|
||
if bus == "pbus" and "entries" in command: # Явная замена таблицы валидируется целиком до постановки макроса в очередь.
|
||
validated_entries(command["entries"]) # Повреждённая поздняя строка исключает передачу начала перенастройки.
|
||
if bus == "pbus" and "settle_ms" in command and (not isinstance(command["settle_ms"], int) or not 1 <= command["settle_ms"] <= 60000): # Пауза обновления задаётся в положительных миллисекундах.
|
||
raise ValueError("PBUS: пауза после запуска должна быть 1…60000 мс") # Отказываем до любого WriteMemory контроллеру.
|
||
return bus # Возвращаем проверенную шину для выбора алгоритма и адресного отображения результата.
|
||
|
||
|
||
def pbus_result_address(entry, index): # Номер записи таблицы и CD её результата являются разными полями аппаратного протокола.
|
||
cd = entry.get("cd", (PBUS_BUFFER & 0xFF) + index) # При отсутствии явного CD сохраняем обычное размещение результатов с младшего байта 0x40.
|
||
if not isinstance(cd, int) or not 0 <= cd <= 0xFF: # Поле CD занимает ровно восемь бит дескриптора.
|
||
raise ValueError("PBUS: CD должен быть в диапазоне 00…FF") # Не допускаем усечения адреса результата при упаковке дескриптора.
|
||
return 0x2000 | cd # Адрес памяти словный: страница 0x2000 объединяется с CD без умножения на размер слова.
|
||
|
||
|
||
def pbus_descriptor(slave, register, cd): # Формируем дескриптор отдельно от индекса записи, записываемого в 0x200D.
|
||
return (slave << 12) | (register << 8) | cd # Четыре бита платы, четыре бита регистра и восемь бит CD соответствуют заданному формату 0x200E.
|
||
|
||
|
||
def pbus_control(count, errors): # Управляющее слово содержит параметры таблицы без бита запуска.
|
||
if not isinstance(count, int) or not 0 <= count <= PBUS_CAPACITY or not isinstance(errors, int) or not 1 <= errors <= 15: # Проверяем ёмкость таблицы и четырёхбитный аппаратный порог до отправки.
|
||
raise ValueError("PBUS: неверный размер таблицы или порог ошибок") # Неверные параметры не меняют работающую шину.
|
||
return (errors << 8) | (count - 1 if count else 0) # Для пустой таблицы младший байт равен нулю, выражение N-1 не вычисляется с переполнением.
|
||
|
||
|
||
def validated_entries(entries): # Проверяем полный снимок таблицы до её остановки и возвращаем независимые адреса источников.
|
||
if not isinstance(entries, (list, tuple)) or len(entries) > PBUS_CAPACITY: # Аппаратный буфер штатной панели ограничен шестьюдесятью четырьмя источниками.
|
||
raise ValueError("PBUS: в таблице нет места, максимум 64 регистра") # Ошибка поздней строки не должна вызвать частичную перенастройку.
|
||
result = [] # Снимок не содержит считанных значений и временных состояний интерфейса.
|
||
for index, entry in enumerate(entries): # Каждый индекс таблицы проверяется независимо от CD результата.
|
||
validate_command({"bus": "pbus", "type": "add", "tetrad": entry["tetrad"], "register": entry["register"]}) # Плата и регистр должны помещаться в свои четырёхбитные поля.
|
||
pbus_result_address(entry, index) # Явный CD также проверяется до первой аппаратной записи.
|
||
result.append({name: entry[name] for name in ("tetrad", "register", "cd") if name in entry}) # Явное отображение CD сохраняется при добавлении и удалении других строк.
|
||
return result # Полный снимок используется очередью на протяжении всей перенастройки.
|
||
|
||
|
||
def table_entries_after(entries, command): # Вычисляем список регистров до передачи, чтобы целиком проверить общий макрос.
|
||
result = validated_entries(command.get("entries", entries)) # Сохраняем адреса без устаревших считанных значений и промежуточных статусов.
|
||
key = (command["tetrad"], command["register"]) # Одна пара плата/регистр однозначно определяет источник PBUS.
|
||
if command["type"] == "add" and key not in [(entry["tetrad"], entry["register"]) for entry in result]: # Повторное добавление уже наблюдаемого регистра не создаёт дубликат.
|
||
result.append({"tetrad": key[0], "register": key[1], **({"cd": command["cd"]} if "cd" in command else {})}) # Явный CD сохраняется независимо от индекса новой строки, без него буфер выбирается последовательно.
|
||
if command["type"] == "remove": # Удаление по адресам не зависит от смещения строк после предыдущих удалений.
|
||
removed = set(tuple(item) for item in command.get("remove_keys", [key])) # Кнопка и Delete могут удалить сразу несколько выделенных регистров.
|
||
for slave, register in removed: # Проверяем также параметры множественного удаления, приходящие из JSON.
|
||
validate_command({"bus": "pbus", "type": "remove", "tetrad": slave, "register": register}) # Недопустимая пара исключает исполнение всей последовательности заранее.
|
||
result = [entry for entry in result if (entry["tetrad"], entry["register"]) not in removed] # Оставшиеся источники уплотняются, сохраняя порядок отображения.
|
||
if len(result) > PBUS_CAPACITY: # Ограничиваем число строк реальным буфером 0x2040–0x207F.
|
||
raise ValueError("PBUS: в таблице нет места, максимум 64 регистра") # Переполнение не должно отправлять частичную аппаратную конфигурацию.
|
||
return validated_entries(result) # Все CD и адреса новой полной таблицы проверены ещё до постановки аппаратных команд в очередь.
|
||
|
||
|
||
def pbus_table_steps(entries, errors=PBUS_ERROR_LIMIT, *, configure=True, stop_control=None, settle_ms=PBUS_SETTLE_MS, on_control=None): # Перенастройка и ожидание свежего буфера выполняются одной последовательной операцией очереди.
|
||
entries = validated_entries(entries) # Фиксируем все платы, регистры, порядок и CD до первой записи управления.
|
||
control = pbus_control(len(entries), errors) # Для новой таблицы младший байт задаёт её последний используемый индекс.
|
||
if not isinstance(settle_ms, int) or not 1 <= settle_ms <= 60000: # Пауза аппаратного обновления явно задаётся в миллисекундах, от одной до минуты.
|
||
raise ValueError("PBUS: пауза после запуска должна быть 1…60000 мс") # Некорректное время не допускает запуска таблицы с немедленным чтением старого буфера.
|
||
if configure: # Изменение любой строки требует полной остановки и переписи всей новой таблицы.
|
||
stopped = (control if stop_control is None else stop_control) & 0x7FFF # У активной таблицы сохраняем порог и последний индекс, сбрасывая только бит 15.
|
||
yield PBUS_COMMAND, stopped # Ждём подтверждения остановки прежде, чем менять индекс или дескриптор.
|
||
if on_control is not None: # Панель отслеживает только уже подтверждённые управляющие записи.
|
||
on_control(stopped) # После ошибки следующей записи прежняя таблица остаётся известной как остановленная.
|
||
if not entries: # Пустая таблица не содержит последнего индекса и не должна запускаться для очистки.
|
||
return {"entries": [], "rows": [], "control": stopped} # Оставляем PBUS остановленной без 0x80FE, переполнения N-1 и чтения прежнего буфера.
|
||
for index, entry in enumerate(entries): # Переписываем все записи, даже если изменилась только одна из них.
|
||
yield PBUS_TABLE_INDEX, index # Индекс таблицы нумеруется с нуля и не подменяется адресом результата.
|
||
cd = pbus_result_address(entry, index) & 0xFF # CD извлекается из фактического адреса чтения этой строки на странице 0x2000.
|
||
yield PBUS_TABLE_DATA, pbus_descriptor(entry["tetrad"], entry["register"], cd) # Дескриптор хранит плату, регистр и CD выбранной записи в 0x200E.
|
||
yield PBUS_COMMAND, control | 0x8000 # Только подтверждение всех пар 0x200D/0x200E разрешает запуск новой длины N-1.
|
||
if on_control is not None: # Успешный ответ запуска фиксирует реальные параметры работающей таблицы.
|
||
on_control(control | 0x8000) # Последующая остановка сохранит именно этот порог и последний индекс.
|
||
yield ExchangeDelay(settle_ms) # Не читаем старые слова буфера сразу после подтверждения запуска: ждём настроенный аппаратный интервал.
|
||
rows = [] # Результаты соответствуют порядку снимка, но их адреса определяются полем CD.
|
||
for index, entry in enumerate(entries): # Обычное обновление не отправляет ни одной управляющей записи и не перезапускает PBUS.
|
||
address = pbus_result_address(entry, index) # Полный словный адрес равен 0x2000 | CD независимо от номера платы и регистра.
|
||
reply = yield address, None # Существующий ReadMemory возвращает одно шестнадцатибитное слово контроллера TMS.
|
||
if not isinstance(reply, dict) or not isinstance(reply.get("value"), int) or not 0 <= reply["value"] <= 0xFFFF: # Проверяем реальный формат результата чтения, не обращаясь к статусу 0x2012.
|
||
raise ValueError(f"PBUS: некорректный ответ памяти 0x{address:04X}") # Ошибка указывает фактический адрес CD, а не индекс строки.
|
||
rows.append(reply) # HEX и DEC каждого результата будут заполнены только после успешного цикла.
|
||
return {"entries": entries, "rows": rows, "control": control | 0x8000 if entries and configure else stop_control} # Заполненная таблица подтверждается данными после паузы, пустая остаётся остановленной.
|