"""Операции 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} # Заполненная таблица подтверждается данными после паузы, пустая остаётся остановленной.