diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 04fc604..39a83b5 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -52,6 +52,7 @@ set(SETPROTOCOL_SOURCES src/set_emu_ump.c src/set_emu_tms.c src/set_emu_igbt.c + src/set_igbt_realtime.c # Общий аппаратный IGBT v4 использует нс, сохраняя прежнюю модель v3 и её ABI. src/set_emu_server.c ${SETPROTOCOL_V2_SOURCES} ${SETPROTOCOL_LEGACY_SOURCES} diff --git a/c/set-protocol/docs/DEIF_XDI.md b/c/set-protocol/docs/DEIF_XDI.md index 598cb13..5247930 100644 --- a/c/set-protocol/docs/DEIF_XDI.md +++ b/c/set-protocol/docs/DEIF_XDI.md @@ -94,6 +94,34 @@ fallback протокола отсутствует. Windows source build мож приложение либо стандартный загрузчик находит компонент рядом с основной DLL. Новый автономный EXE использует собственную основную DLL с DEIF. +## Android/JVM: настройки и локальный стенд + +`DeifSettings` хранит профиль SLAVE, независимые параметры MASTER, +выбор пяти отправляемых объектов, калибровки управления, единицу POWER +и ручные привязки. Сериализация не сохраняет запуск симуляции или таймеров. +`readdressed` переносит связанные SAM/DAM-адреса через C, сохраняя отдельные +ручные источники, индексы объектов и масштабы. + +`DeifWorkspace` объединяет пассивное наблюдение, физический CAN-SLAVE +и автономный стенд. Он получает монотонное время от приложения и возвращает +готовые кадры раздельными маршрутами MASTER и SLAVE. Локальная симуляция +не возвращает физические кадры; остановленный локальный SLAVE отклоняет +MASTER и команды экрана вместо переключения на адаптер. +NMT разрешён только работающему локальному SLAVE. Физический сервер +отклоняет запуск встроенного MASTER. + +`DeifXdi.master/make/service/selector/bind/binding/selectorInfo/readdress/validate` +являются тонкими вызовами существующего C ABI. Порты не строят COB-ID, +индексы, little-endian I16, NMT или диммер повторно. Перевод касания +в заданный пользователем масштаб относится к управлению интерфейсом; +интерактивная отрисовка, отдельное подтверждение яркости и палитры, +хранилище настроек, USB и жизненный цикл остаются у приложения. + +`DeifWorkspaceTest` проверяет точные builders, автономный приём интерактивных +заданий, отсутствие физического TX на локальном стенде, ограничения NMT, +независимые периоды MASTER/heartbeat, ошибки JNI, сохранение профиля +и перенос адресов без потери ручных привязок. + ## Проверки `tests/fixtures/deif-xdi-v1.tsv` — единые эталонные байты для C, Python и JNI. diff --git a/c/set-protocol/include/set_igbt_realtime.h b/c/set-protocol/include/set_igbt_realtime.h new file mode 100644 index 0000000..6984c1d --- /dev/null +++ b/c/set-protocol/include/set_igbt_realtime.h @@ -0,0 +1,44 @@ +#ifndef SET_IGBT_REALTIME_H /* Исключает повторное объявление интерфейса аппаратного IGBT версии 4. */ +#define SET_IGBT_REALTIME_H /* Отделяет наносекундный протокол от прежней миллисекундной модели версии 3. */ +#include "set_regmap.h" /* Использует общие коды ошибок и ABI библиотеки регистров. */ +#ifdef __cplusplus /* Сохраняет совместимость общего C-интерфейса с потребителями C++. */ +extern "C" { /* Запрещает изменение имён экспортируемых функций компилятором C++. */ +#endif /* Завершает условное начало C ABI. */ +#define SET_IGBT_RT_CHANNELS 8u /* Число независимых входных и выходных трактов стенда. */ +#define SET_IGBT_RT_WORDS 112u /* Сохраняет размер окна: заголовок 16 слов и восемь каналов по 12 слов. */ +#define SET_IGBT_RT_MIN_NS 50u /* Минимальный программируемый интервал 50 нс оставляет запас относительно трёх тактов HRTIM при 160 МГц. */ +#define SET_IGBT_RT_MAX_NS 60000u /* Максимальные 60000 нс помещаются в 16-битный регистр протокола. */ +typedef struct { /* Задания одного канала; времена всегда выражены целыми наносекундами. */ + uint16_t flags, mode, on_delay, on_width, off_delay, off_width; /* Биты TX/RX/инверсии, режим ответа и времена начала/ширины ACK для двух фронтов. */ + uint16_t tx_width, edges, fault_width, compensation; /* Длительность физического TX, маска ON/OFF, длительность FAULT и измеренная компенсация входного тракта, нс. */ +} set_igbt_rt_channel; /* Независимый аппаратный канал не содержит миллисекундных счётчиков. */ +typedef struct { /* Полное задание применяется одной завершающей командой после записи полей. */ + uint16_t enabled, source, tx, fault; /* Разрешение выходов, 0=внешний Vin/ACK, 1=физический генератор TX, маски удержания TX и неисправностей. */ + set_igbt_rt_channel channel[SET_IGBT_RT_CHANNELS]; /* Восемь одинаковых настроек хранятся отдельно от работающего оборудования. */ +} set_igbt_rt_config; /* Подготовленные настройки не меняют уже работающие таймеры до применения. */ +typedef struct { /* Снимок получает фактические уровни и диагностические счётчики аппаратного порта. */ + uint8_t tx, rx, busy; /* Вход Vin/выход TX, выход ACK/вход Vstat и маска выполняемых одиночных импульсов. */ + uint16_t count[SET_IGBT_RT_CHANNELS], overrun[SET_IGBT_RT_CHANNELS]; /* Счётчики принятых фронтов и потерь захвата по каждому каналу, по модулю 65536. */ +} set_igbt_rt_snapshot; /* Чтение снимка не запускает импульсы и не сбрасывает накопленные события. */ +typedef struct { /* Аппаратный порт выполняет фронты таймерами; общий модуль только проверяет и маршрутизирует команды. */ + unsigned (*selected)(void *); /* Возвращает ненулевое значение только для подтверждённого режима IGBT платы. */ + unsigned (*apply)(void *, const set_igbt_rt_config *); /* Настраивает весь проверенный профиль; ошибка должна оставлять прежнее оборудование без изменения. */ + unsigned (*pulse)(void *, uint8_t); /* Одновременно проверяет и запускает выбранные биты физического TX; длительность отмеряет оборудование. */ + void (*levels)(void *, uint8_t); /* Выставляет постоянные уровни TX только после проверки отсутствия выполняемых импульсов. */ + void (*stop)(void *); /* Немедленно отключает аппаратные триггеры и переводит все восемь выходов в ноль. */ + void (*sample)(void *, set_igbt_rt_snapshot *); /* Снимает уровни, занятость и счётчики без формирования фронтов программными задержками. */ + void (*clear)(void *); /* Сбрасывает только счётчики диагностики, сохраняя настройки и работу таймеров. */ +} set_igbt_rt_port; /* Переносимый слой не зависит от HAL, ОС или частоты опроса GUI. */ +typedef struct { /* Единственный экземпляр сервиса принадлежит вызывающему аппаратному проекту. */ + set_igbt_rt_config staged, active; /* Подготовленный и реально применённый профили предотвращают изменение времён посреди записи регистров. */ + set_igbt_rt_port port; /* Копия таблицы callbacks живёт столько же, сколько сам сервис. */ + void *context; /* Непрозрачное состояние аппаратного порта передаётся каждому callback без владения памятью. */ +} set_igbt_rt; /* Сервис не выделяет динамическую память и не обслуживает наносекундные сроки из основного цикла. */ +PCAN_ABI_API void set_igbt_rt_init(set_igbt_rt *, const set_igbt_rt_port *, void *); /* Создаёт выключенный профиль 1SP: задержка 250 нс, ACK 700 нс, TX 10000 нс. */ +PCAN_ABI_API unsigned set_igbt_rt_validate(const set_igbt_rt_config *); /* Проверяет все восемь каналов до изменения оборудования. */ +PCAN_ABI_API unsigned set_igbt_rt_read(void *, uint16_t, uint16_t, uint16_t *); /* Читает окно версии 4; смещения и число слов заданы в 16-битных регистрах. */ +PCAN_ABI_API unsigned set_igbt_rt_write(void *, uint16_t, uint16_t); /* Записывает задание либо выполняет явную команду пуска, останова или сброса счётчиков. */ +#ifdef __cplusplus /* Закрывает C ABI только при компиляции заголовка как C++. */ +} /* Завершает блок внешних C-объявлений. */ +#endif /* Завершает условную поддержку C++. */ +#endif /* Завершает защиту заголовка аппаратного IGBT. */ diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt new file mode 100644 index 0000000..d2833f7 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt @@ -0,0 +1,134 @@ +package ru.setcorp.setprotocol.deif // Общий контроллер UI находится в templates и использует единственный C-кодек через JNI. +import java.io.StringReader // Текст пользовательского профиля читается без Android Context и доступа к файловой системе. +import java.io.StringWriter // Профиль возвращается вызывающему GUI для сохранения в его собственном хранилище. +import java.util.Properties // Стандартный формат JVM хранит настройки без дополнительной зависимости от JSON-библиотеки. +import kotlin.math.roundToInt // Округление относится к переводу жеста интерфейса в заданный пользователем масштаб управления. + +data class DeifBinding(val field: Int, val selector: String, val scale: Double) // Привязка содержит непрозрачный HEX-селектор и пользовательский множитель индикации. +data class DeifSelectorInfo(val node: Int, val index: Int, val subindex: Int, val dam: Boolean) // Метаданные текущего селектора получены из C и не раскрывают wire-разметку приложению. +data class DeifCommands(val power: Double = 0.0, val rpm: Double = 0.0, val angle: Double = 0.0) // POWER и RPM задаются процентом шкалы, угол — градусами. +data class DeifTraffic(val master: List = emptyList(), val slave: List = emptyList()) // Раздельные маршруты не позволяют командам MASTER выдать себя за ответы SLAVE. +data class DeifSettings( // Сохраняемые параметры независимы от временного запуска сервера и циклической передачи. + val node: Int = 13, val source: Int = 1, val xdi: Boolean = true, val automatic: Boolean = true, // Собственный адрес SLAVE и источник SAM имеют независимый смысл. + val pdo: Int = 2, val group: Int = 1, val monitorId: Long = 0, val bindings: List = emptyList(), // RPDO, группа экрана и ручные привязки передаются непосредственно C-профилю. + val masterNode: Int = 1, val masterId: Long = 0x201, val masterDam: Boolean = false, // Адресация MASTER не изменяется при выборе собственного узла SLAVE. + val periodMs: Int = 100, val heartbeat: Boolean = false, val heartbeatNode: Int = 1, // Период цикла в миллисекундах независим от штатного heartbeat каждые 500 мс. + val selected: List = List(5) { true }, // Порядок соответствует публичному ABI цикла MASTER: угол, RPM, RPM%, POWER и POWER%. + val powerFull: Double = 1000.0, val rpmFull: Double = 1000.0, val angleFactor: Double = 10.0, val powerPercentFactor: Double = 10.0, // Калибровки интерфейса: POWER RAW при 100%, rpm при 100%, RAW/градус и RAW/процент. + val powerUnit: String = "RAW", // Неподтверждённая физическая единица мощности не подменяется автоматически киловаттами. +) { // Конструктор данных не загружает JNI при запуске приложения с другим протоколом. + init { require(selected.size == 5) { "Профиль MASTER должен содержать пять флажков отправки" } } // Повреждённый текст настроек не должен приводить к выходу за границы списка в интерфейсе. + fun readdressed(previousNode: Int, previousSource: Int): DeifSettings { // Изменение адресов переносит только связанные с прежним узлом привязки, сохраняя независимые источники. + return copy(bindings = bindings.map { binding -> // Каждое поле сохраняет собственный индекс, подиндекс и физический масштаб. + val info = DeifXdi.selectorInfo(binding.selector) // Адрес и SAM/DAM сообщает общий C-декодер без масок и endian-копии в Kotlin. + val target = if (info?.dam == true && info.node == previousNode) node else if (info?.dam == false && info.node == previousSource) source else null // DAM следует физическому получателю, SAM — выбранному источнику рассылки. + if (target != null) binding.copy(selector = DeifXdi.readdress(binding.selector, target)) else binding // Независимые ручные источники и отключённые поля остаются прежними. + }) // Применение возвращает один полный профиль для последующей общей проверки. + } // Смена SAM-источника не должна оставлять стандартные поля подписанными на старый Node-ID. + fun model(services: Boolean = false): DeifXdi { // Оба GUI-режима получают профиль единственного переносимого автомата. + val result = DeifXdi(node, source, xdi, automatic, pdo, group, services, monitorId) // Все сетевые диапазоны и исходные привязки проверяет C-конструктор. + try { bindings.forEach { result.bind(it) } } catch (error: Exception) { result.close(); throw error } // Неверная ручная привязка не оставляет частично настроенный сервер. + return result // Владение контекстом и его закрытием получает контроллер сеанса. + } // Создание пассивного профиля ничего не отправляет в CAN. + fun validate() { // Перед применением проверяются и сетевой профиль, и настройки управления интерфейсом. + require(periodMs >= 0 && selected.size == 5 && group in 1..9) { "Проверьте период, группу экрана и выбор объектов DEIF" } // Размер списка закреплён публичным ABI MASTER, номер группы выбирается явно. + require(listOf(powerFull, rpmFull, angleFactor, powerPercentFactor).all { it.isFinite() && it > 0 }) { "Масштабы управления должны быть положительными числами" } // Нулевой либо бесконечный множитель не должен создавать недостоверные задания. + require(bindings.map { it.field }.distinct().size == bindings.size && bindings.all { it.field in 0..4 }) { "Привязки пяти показаний должны быть уникальными" } // Угол задания автоматически обслуживается C-привязкой фактического азимута. + model(true).close() // Самый строгий серверный профиль проверяет RPDO, протокол и оба адреса до изменения UI. + DeifXdi.master(masterNode, IntArray(5), masterId, masterDam) // Общий builder проверяет точный COB-ID и адресацию передатчика. + DeifXdi.service(false, heartbeatNode, 5) // Допустимость адреса heartbeat также проверяет существующий C-builder. + } // Ошибка оставляет прежний работающий профиль без изменений. + fun encode(): String { // Хранилище приложения получает только текст настроек без состояния работающих таймеров. + val p = Properties() // Стандартные ключи JVM сохраняют совместимость Android и host-тестов. + listOf("node" to node, "source" to source, "xdi" to xdi, "automatic" to automatic, "pdo" to pdo, "group" to group, "monitorId" to monitorId, "masterNode" to masterNode, "masterId" to masterId, "masterDam" to masterDam, "periodMs" to periodMs, "heartbeat" to heartbeat, "heartbeatNode" to heartbeatNode, "powerFull" to powerFull, "rpmFull" to rpmFull, "angleFactor" to angleFactor, "powerPercentFactor" to powerPercentFactor, "powerUnit" to powerUnit).forEach { (key, value) -> p.setProperty(key, value.toString()) } // Сохраняются только явные настройки, без флагов запуска SLAVE и MASTER. + p.setProperty("selected", selected.joinToString(",")) // Включение отдельных объектов восстанавливается независимо от значений заданий. + bindings.forEach { p.setProperty("binding.${it.field}", "${it.scale}|${it.selector}") } // HEX хранится непрозрачно, индекс и endian в Kotlin не вычисляются. + return StringWriter().also { p.store(it, "DEIF") }.toString() // Запись в SharedPreferences или файл остаётся задачей платформенного слоя. + } // Сериализация не вызывает CAN-транспорт или системные часы протокола. + companion object { // Загрузка профиля не требует Android API и не запускает нативный сервер. + fun decode(text: String): DeifSettings { // Старый либо отсутствующий профиль получает исходные значения DEIF. + val p = Properties().apply { load(StringReader(text)) } // Properties снимает только текстовое экранирование формата хранения. + fun value(key: String, default: Any) = p.getProperty(key, default.toString()) // Каждый новый параметр имеет явное совместимое исходное значение. + val defaults = DeifSettings() // Единый набор исходных параметров не дублируется в приложении Android. + return DeifSettings(node = value("node", defaults.node).toInt(), source = value("source", defaults.source).toInt(), xdi = value("xdi", true).toBooleanStrict(), automatic = value("automatic", true).toBooleanStrict(), // Адреса и профиль восстанавливаются без объявления узла на CAN-шине. + pdo = value("pdo", 2).toInt(), group = value("group", 1).toInt(), monitorId = value("monitorId", 0).toLong(), bindings = (0..4).mapNotNull { field -> p.getProperty("binding.$field")?.split('|', limit = 2)?.let { DeifBinding(field, it[1], it[0].toDouble()) } }, // Список ручных привязок сохраняет индивидуальные масштабы и отключённые поля. + masterNode = value("masterNode", 1).toInt(), masterId = value("masterId", defaults.masterId).toLong(), masterDam = value("masterDam", false).toBooleanStrict(), // Собственный MASTER остаётся независимым от адреса получателя SLAVE. + periodMs = value("periodMs", 100).toInt(), heartbeat = value("heartbeat", false).toBooleanStrict(), heartbeatNode = value("heartbeatNode", 1).toInt(), selected = value("selected", "true,true,true,true,true").split(',').map { it.toBooleanStrict() }, // Восстанавливаем выбор объектов без автоматического запуска циклической отправки. + powerFull = value("powerFull", 1000).toDouble(), rpmFull = value("rpmFull", 1000).toDouble(), angleFactor = value("angleFactor", 10).toDouble(), powerPercentFactor = value("powerPercentFactor", 10).toDouble(), powerUnit = value("powerUnit", "RAW")) // Калибровки и единицы относятся к интерфейсу конкретной библиотеки прибора. + } // Ошибку повреждённого текста вызывающий GUI показывает либо заменяет безопасными исходными настройками. + } // Конструктор не содержит скрытого алгоритмического fallback вместо C. +} // Один профиль используется локальным стендом, пассивным наблюдателем и физическим сервером. + +class DeifWorkspace(val settings: DeifSettings, val local: Boolean) : AutoCloseable { // Общий контроллер управляет маршрутом, а протокольный автомат остаётся в C. + private var model = settings.model() // Исходный пассивный наблюдатель не производит исходящих кадров. + private val values = IntArray(5) // Значения MASTER хранятся отдельно от принятых измерений модели SLAVE. + var slaveEnabled = false; private set // Запуск сервера всегда требует отдельного явного действия пользователя. + var masterRunning = false; private set // Открытие экрана и восстановление настроек не запускают передачу. + var sent = 0L; private set // Счётчик показывает число переданных маршруту кадров без обещания физической доставки. + var lastService: DeifFrame? = null; private set // Последний служебный кадр помогает диагностировать NMT, heartbeat и SDO. + var lastRpm: DeifFrame? = null; private set // Последний выбранный кадр RPM доступен диагностике без повторной распаковки в GUI. + var cycleGapMs: Long? = null; private set // Интервал между вызовами рабочего цикла измеряется в переданных порту монотонных миллисекундах. + var maxCycleGapMs = 0L; private set // Максимальный интервал выявляет паузы интерфейса, не подтверждая доставку CAN-кадра. + private var lastCycleAt: Long? = null // До первого цикла не показывается выдуманное измерение интервала отправки. + private var nextCycle = 0L; private var nextHeartbeat = 0L // Дедлайны порта используют переданные монотонные миллисекунды без чтения часов в библиотеке. + val commands: DeifCommands get() = DeifCommands(values[3] / settings.powerFull * 100, values[1] / 10.0 / settings.rpmFull * 100, values[0] / settings.angleFactor) // Жёлтые маркеры строятся из фактически ограниченных INT16 заданий. + fun setControl(key: String, value: Double) { // Жест меняет только черновик MASTER и никогда сам не выполняет отправку. + require(value.isFinite()) { "Задание DEIF должно быть конечным числом" } // Некорректная координата интерфейса не попадает в конструктор CAN-кадра. + fun raw(number: Double) = number.coerceIn(-32767.0, 32765.0).roundToInt() // Рабочий диапазон I16 исключает три служебных значения протокола при управлении мышью или касанием. + when (key) { // Ключи описывают визуальные элементы, а не индексы wire-объектов. + "angle" -> values[0] = raw(value * settings.angleFactor) // Градусы переводятся по пользовательской калибровке RAW на градус. + "power" -> { values[3] = raw(value / 100 * settings.powerFull); values[4] = raw(values[3] / settings.powerFull * 100 * settings.powerPercentFactor) } // Процент мощности согласуется с фактически ограниченным абсолютным заданием. + "rpm" -> { values[1] = raw(value / 100 * settings.rpmFull * 10); values[2] = raw(values[1] / 10.0 / settings.rpmFull * 1000) } // Обороты и их процент используют ту же визуальную калибровку, что Desktop. + else -> error("Неизвестный регулятор DEIF") // Ошибка UI не должна молча менять другую физическую величину. + } // Никакие байты CAN здесь не формируются. + } // Последующая разовая либо циклическая отправка использует единый набор значений. + fun startSlave(enabled: Boolean, now: Long) { // Локальный стенд и физический сервер различаются только маршрутом готовых кадров. + stopMaster() // Предыдущий MASTER не должен продолжить отправку после переключения роли. + model.close(); model = settings.model(enabled); slaveEnabled = enabled // Новый профиль не наследует старые измерения и незавершённые служебные события. + if (enabled) model.start(now) // Boot-up, self-start и NMT-состояние создаются C-ядром. + } // События выдаются через tick, а физическую отправку выполняет платформа. + fun startMaster(enabled: Boolean, now: Long) { // Таймер MASTER включается независимо от таймера служебного heartbeat SLAVE. + require(!enabled || !local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Отказ маршрута происходит до изменения флага запуска, а не на первом тике таймера. + require(!enabled || !slaveEnabled || local) { "В режиме CAN-SLAVE передаёт внешний MASTER" } // Физический сервер не конкурирует со встроенным передатчиком приложения. + masterRunning = enabled; nextCycle = now; nextHeartbeat = now; lastCycleAt = null; cycleGapMs = null; maxCycleGapMs = 0 // Новый сеанс начинает отсчёт периодов и статистики с явного запуска. + } // При periodMs=0 порт передаёт не более одного цикла за очередной вызов tick. + fun stopMaster() { masterRunning = false } // Остановка не изменяет принятые физические показания SLAVE. + private fun events(now: Long): List { // Доставка очереди C выполняется также при отсутствии новых входящих кадров. + val frames = model.poll(now); frames.lastOrNull()?.let { lastService = it } // Сохраняем последний готовый сервисный кадр без повторного разбора payload. + return if (local && slaveEnabled) emptyList() else frames // На локальном стенде Boot-up, SDO и heartbeat не выходят в физический адаптер. + } // Один poll извлекает каждый кадр из очереди только один раз. + private fun route(frames: List, now: Long): DeifTraffic { // Решение о локальном маршруте относится к порту, а не к wire-протоколу. + require(!local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Выбранный локальный режим никогда не перенаправляется в физический CAN при остановленном сервере. + sent += frames.size // Счётчик не является подтверждением приёма физическим прибором. + if (local && slaveEnabled) { frames.forEach { model.accept(it.canId, it.data, nowMs = now) }; events(now); return DeifTraffic() } // Внутренний стенд принимает точные C-байты без вызова транспорта. + return DeifTraffic(master = frames) // Внешний маршрут возвращает кадры вызывающему приложению с отдельной проверкой MASTER. + } // Черновик задания не подменяет физический RX-снимок. + fun cycle(now: Long): DeifTraffic { // Один цикл использует C-каталог пяти объектов и отдельного задания азимута. + require(!local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Прямой вызов разового цикла не обходит выбор автономного маршрута пользователя. + require(!slaveEnabled || local) { "В режиме CAN-SLAVE передаёт внешний MASTER" } // Прямой вызов кнопки также не обходит ограничения физического сервера. + val service = events(now) // Сначала учитываем self-start и доставляем ранее накопленные служебные ответы. + val built = DeifXdi.master(settings.masterNode, values, settings.masterId, settings.masterDam) // Проверенный C-builder формирует полный цикл до выбора отправляемых объектов. + cycleGapMs = lastCycleAt?.let { (now - it).coerceAtLeast(0) }; maxCycleGapMs = maxOf(maxCycleGapMs, cycleGapMs ?: 0); lastCycleAt = now // Отмечается интервал вызовов порта, а не сетевой ACK либо срок Data lost. + lastRpm = if (settings.selected[1]) built[1] else null // Семантическая позиция RPM закреплена публичным контрактом цикла MASTER, payload не разбирается. + val frames = built.filterIndexed { index, _ -> settings.selected[if (index < 5) index else 0] } // Отдельный set-point следует выбору общего объекта угла. + return route(frames, now).copy(slave = service) // Все выбранные кадры получают единый локальный либо физический маршрут. + } // Частично собранный цикл не возвращается при ошибке builder ядра. + fun tick(now: Long): DeifTraffic { // Android поставляет время, а сроки self-start и Data lost вычисляет C. + var traffic = DeifTraffic(slave = events(now)) // Служебный таймер обслуживается независимо от передачи MASTER. + if (masterRunning && now >= nextCycle) { val cycle = cycle(now); traffic = DeifTraffic(traffic.master + cycle.master, traffic.slave + cycle.slave); nextCycle = now + settings.periodMs.coerceAtLeast(1) } // Один вызов не создаёт пачку просроченных повторов после паузы GUI. + if (masterRunning && settings.heartbeat && now >= nextHeartbeat) { val heartbeat = route(listOf(DeifXdi.service(false, settings.heartbeatNode, 5)), now); traffic = DeifTraffic(traffic.master + heartbeat.master, traffic.slave + heartbeat.slave); nextHeartbeat = now + 500 } // Штатный Operational heartbeat MASTER имеет независимый период 500 миллисекунд. + return traffic // Порт физического транспорта получает только готовые кадры с сохранёнными ролями. + } // Все действия повторяемы при одних и тех же переданных монотонных часах. + fun accept(frame: DeifFrame, extended: Boolean, remote: Boolean, now: Long) { // Внешний RX не должен изменять внутренний локальный стенд. + if (!local || !slaveEnabled) model.accept(frame.canId, frame.data, extended, remote, now) // STD, RTR, адреса и известные объекты проверяет общий C-код. + } // TX-эхо и ошибки адаптера отфильтровывает платформенный слой. + fun nmt(command: Int, now: Long) { // Локальное управление NMT сохраняет то же ограничение, что Desktop. + require(local && slaveEnabled) { "NMT доступен только в локальной симуляции SLAVE" } // Команды управления сетью не отправляются физическому MASTER. + val frame = DeifXdi.service(true, settings.node, command); model.accept(frame.canId, frame.data, nowMs = now); events(now) // Адресный двухбайтный NMT строится и обрабатывается только C-ядром. + } // Выбор пункта меню сам по себе не вызывает этот метод. + fun display(palette: Boolean, value: Int, now: Long, group: Int = settings.group): DeifTraffic { // Подтверждение фиксирует группу отдельно от ещё не сохранённого черновика остальных настроек. + return route(listOf(DeifXdi.make(if (palette) 7 else 6, settings.masterNode, group, value, settings.masterId, settings.masterDam)), now) // Выбранная группа, U8 яркости и Day/Night/Dusk проверяются общим C-builder. + } // Команды яркости и палитры не включаются в циклическую отправку MASTER. + fun snapshot(now: Long): DeifSnapshot = model.snapshot(now) // Числа, статусы, угол и Data lost поступают из единственного C-автомата. + override fun close() { stopMaster(); model.close(); slaveEnabled = false } // Закрытие владельца прекращает и циклические команды, и служебную очередь SLAVE. +} // Контроллер не содержит Android, Compose, системных часов, USB и второго CAN-кодека. diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt index 77c42b9..e98daa6 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt @@ -10,6 +10,15 @@ internal object NativeDeifBridge { // Только JNI-объявления; з external fun accept(state: ByteBuffer, id: Long, data: ByteArray, flags: Int, nowMs: Long): Boolean // Байты обрабатывает общий C-код. external fun poll(state: ByteBuffer, nowMs: Long): IntArray // Готовые события возвращаются скалярами id, dlc, data[8]. external fun snapshot(state: ByteBuffer, nowMs: Long, expiryMs: Int): DoubleArray // Числа, статусы и сроки уже вычислены в C. + external fun master(node: Int, values: IntArray, id: Long, dam: Boolean): IntArray // Все пять MPDO и отдельное задание строятся общим C-каталогом. + external fun make(field: Int, node: Int, instance: Int, value: Int, id: Long, dam: Boolean): IntArray // Яркость, палитра и измерения возвращаются готовым кадром. + external fun service(nmt: Boolean, node: Int, value: Int): IntArray // C формирует собственный heartbeat либо адресную локальную NMT-команду. + external fun selector(field: Int, node: Int, instance: Int, dam: Boolean): ByteArray // Каталожный селектор возвращается непрозрачным четырёхбайтным результатом. + external fun bind(state: ByteBuffer, field: Int, selector: ByteArray, scale: Double): Boolean // Явные привязки и масштабы проверяет нативный профиль. + external fun binding(state: ByteBuffer, field: Int): DoubleArray // Исходные коэффициенты читаются из C вместо второй таблицы GUI. + external fun validate(id: Long, data: ByteArray, slave: Int): Boolean // Роли MASTER и серверного SLAVE проверяются одним переносимым списком разрешений. + external fun selectorInfo(selector: ByteArray): IntArray // Редактор получает Node, индекс, подиндекс и режим из общего C-декодера. + external fun readdress(selector: ByteArray, node: Int): ByteArray // C меняет только адрес, сохраняя пользовательский объект и режим SAM/DAM. external fun inspect(id: Long, data: ByteArray, flags: Int): IntArray // Журнал получает единый результат декодирования. } // ABI не раскрывает Kotlin layout внутренних структур. data class DeifFrame(val canId: Long, val data: ByteArray) // Готовый STD data frame передаётся существующему CAN-транспорту приложения. @@ -41,8 +50,50 @@ class DeifXdi(node: Int = 13, source: Int = 1, xdi: Boolean = true, automatic: B List(6) { field -> val flags = raw[13 + field * 4].toInt(); if (flags and 2 != 0 && flags and 4 == 0) raw[10 + field * 4] else null }, // Порт отображает valid/stale-флаги, не повторяя таймеры. List(6) { raw[12 + it * 4].toInt() }, (0..5).filter { raw[36].toInt() and (1 shl it) != 0 }.toSet()) // Имена и цвета ошибок остаются ответственностью GUI. } // Снимок можно использовать в UI без обращения к байтам протокола. + @Synchronized fun bind(binding: DeifBinding) { // Пользовательский профиль передаёт селектор без разбора адресного байта в Kotlin. + val bytes = selectorBytes(binding.selector) // Снимается только текстовое HEX-кодирование пользовательского поля. + require(NativeDeifBridge.bind(state, binding.field, bytes, binding.scale)) { "Неверная привязка либо масштаб DEIF" } // Нативный профиль проверяет номер поля и конечность коэффициента. + } // Изменение привязки сбрасывает только связанные с ней измерения по правилам C. + @Synchronized fun binding(field: Int): DeifBinding { // GUI получает текущий каталожный либо пользовательский профиль. + val raw = NativeDeifBridge.binding(state, field); require(raw.size == 6) { "Неизвестное поле DEIF" } // Шесть скаляров являются границей JNI, а не смещениями CAN-пакета. + return DeifBinding(field, if (raw[5] == 0.0) "" else (0..3).joinToString(" ") { "%02X".format(raw[it].toInt()) }, raw[4]) // Непрозрачные байты форматируются для сохранения и ручного редактирования. + } // Исходные масштабы не дублируются в приложении Android. @Synchronized override fun close() { NativeDeifBridge.stop(state) } // Жизненный цикл отменяет heartbeat и очередь до закрытия адаптера. companion object { // Диагностическое представление не содержит второго парсера. + private fun frames(raw: IntArray): List { // Все builders возвращают единый скалярный формат JNI. + require(raw.isNotEmpty() && raw.size % 10 == 0) { "Неверные параметры команды DEIF" } // Пустой результат C означает отказ вместо частично сформированного кадра. + return raw.asList().chunked(10).map { row -> DeifFrame(row[0].toLong(), ByteArray(row[1]) { row[it + 2].toByte() }) } // Копирование готовых байтов не интерпретирует wire-поля. + } // Упаковка JNI одинакова для данных MASTER, диммера и служебных команд. + private fun available() { check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } } // Нативная ошибка не заменяется алгоритмом Kotlin. + private fun selectorBytes(text: String): ByteArray { // Текст HEX является форматом пользовательских настроек, а не вторым кодеком протокола. + val hex = text.filterNot { it.isWhitespace() }; require(hex.isEmpty() || hex.length == 8) { "Селектор DEIF должен содержать четыре HEX-байта либо быть пустым" } // Размер ограничивает доступ JNI к четырёхбайтному селектору. + return hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() // Порядок байтов сохраняется буквально и интерпретируется только C-ядром. + } // Одинаковая текстовая обработка применяется к привязкам и миграции их адресов. + fun selectorInfo(selector: String): DeifSelectorInfo? { // Редактор сравнивает смысл адреса, не извлекая маски CANopen в Kotlin. + available(); val bytes = selectorBytes(selector); if (bytes.isEmpty()) return null // Отключённое поле остаётся без адреса и не участвует в миграции. + val raw = NativeDeifBridge.selectorInfo(bytes); require(raw.size == 4) { "Неверный селектор DEIF" } // Все четыре семантических поля возвращает существующий C-декодер. + return DeifSelectorInfo(raw[0], raw[1], raw[2], raw[3] != 0) // Именованная модель исключает номера ABI-позиций из Compose. + } // Произвольный пользовательский индекс не заменяется стандартным объектом из базы. + fun readdress(selector: String, node: Int): String { // Перенос адреса сохраняет ручные привязки и режим SAM/DAM. + available(); val bytes = selectorBytes(selector); if (bytes.isEmpty()) return "" // Явно отключённое поле не включается при смене адресов. + val result = NativeDeifBridge.readdress(bytes, node); require(result.size == 4) { "Неверный Node-ID DEIF" } // Новый адрес проверяет и применяет только переносимое C-ядро. + return result.joinToString(" ") { "%02X".format(it.toInt() and 255) } // HEX остаётся прозрачным форматом хранения непрозрачных байтов. + } // Сетевая формула адресного байта не дублируется в порту. + fun master(node: Int, values: IntArray, id: Long, dam: Boolean): List { // Порядок пяти объектов и отдельный азимут задаёт только C-каталог. + available(); return frames(NativeDeifBridge.master(node, values, id, dam)) // Адреса и диапазон I16 проверяются до передачи результата. + } // GUI выбирает нужные готовые кадры без повторного построения payload. + fun make(field: Int, node: Int, instance: Int, value: Int, id: Long, dam: Boolean): DeifFrame { // Единый builder поддерживает команды экрана и измерительные данные. + available(); return frames(NativeDeifBridge.make(field, node, instance, value, id, dam)).single() // Ошибка C становится явным отказом API. + } // Формулы индексов, COB-ID и endian остаются в переносимом ядре. + fun service(nmt: Boolean, node: Int, value: Int): DeifFrame { // Локальное NMT отделено от физической валидации MASTER. + available(); return frames(NativeDeifBridge.service(nmt, node, value)).single() // Готовый служебный кадр создаётся существующим C-builder. + } // Порт не собирает двухбайтовую команду вручную. + fun selector(field: Int, node: Int, instance: Int = 1, dam: Boolean = false): ByteArray { // Каталожный редактор может выбирать экземпляр и режим SAM либо DAM. + available(); return NativeDeifBridge.selector(field, node, instance, dam).also { require(it.size == 4) { "Неверный объект, экземпляр или адрес DEIF" } } // Неверный каталог не подменяется произвольным селектором. + } // Преобразование в HEX при отображении остаётся ответственностью GUI. + fun validate(frame: DeifFrame, slave: Int = 0): Boolean { // Перед физической отправкой проверяется роль владельца кадра. + available(); return NativeDeifBridge.validate(frame.canId, frame.data, slave) // MASTER и ответы собственного SLAVE используют тот же C-список разрешений, что Desktop. + } // Произвольные NMT и SDO не открываются через измерительный маршрут MASTER. fun inspect(id: Long, data: ByteArray, extended: Boolean = false, remote: Boolean = false): IntArray { // Приложение передаёт целый транспортный кадр. check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } // Без C-декодера никакого угадывания формата нет. return NativeDeifBridge.inspect(id, data, (if (extended) 1 else 0) or (if (remote) 2 else 0)) // Индексы результата закреплены ABI deif_xdi.h. diff --git a/c/set-protocol/ports/android/set_deif_jni.c b/c/set-protocol/ports/android/set_deif_jni.c index a637be5..f77f6a0 100644 --- a/c/set-protocol/ports/android/set_deif_jni.c +++ b/c/set-protocol/ports/android/set_deif_jni.c @@ -30,3 +30,56 @@ JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_in uint8_t d[8]; int32_t m[12]={0}; jint values[13]; int n=deif_input(e,a,d),code; unsigned i; jintArray result; (void)self; code=n<0 || id<0 || id>0x1FFFFFFF?-1:setp_deif_inspect((uint32_t)id,d,(size_t)n,(unsigned)flags,m); /* Только C знает wire-формат DEIF. */ values[0]=code; for(i=0;i<12;i++) values[i+1]=m[i]; result=(*e)->NewIntArray(e,13); if(result) (*e)->SetIntArrayRegion(e,result,0,13,values); return result; /* Метаданные индексируются по ABI, не по смещениям CAN-data. */ } /* Локализованные подписи формирует Kotlin. */ + +static jintArray deif_frames_java(JNIEnv *e,const setp_deif_frame_t *frames,size_t count) { /* Единая упаковка ABI возвращает готовые кадры, не интерпретируя их payload. */ + jint values[60]; size_t i,j; jintArray result; /* Пять объектов MASTER и отдельное задание азимута занимают не более шести записей по десять скаляров. */ + if(count>6U) count=0; /* Неверное число записей не должно выходить за пределы локального JNI-буфера. */ + for(i=0;iNewIntArray(e,(jsize)(count*10)); if(result && count) (*e)->SetIntArrayRegion(e,result,0,(jsize)(count*10),values); return result; /* Результатом владеет JVM, указатели C не сохраняются. */ +} /* Общий конвертер не является вторым кодеком CANopen. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_master(JNIEnv *e,jobject self,jint node,jintArray input,jlong id,jboolean dam) { /* Порядок объектов и отдельный set-point берутся из публичного C ABI. */ + jint raw[5]; int32_t values[5]; setp_deif_frame_t frames[6]; unsigned i; (void)self; /* Локальные буферы имеют фиксированные размеры контракта MASTER. */ + if(!input || (*e)->GetArrayLength(e,input)!=5 || id<0 || id>0x7FF) return deif_frames_java(e,NULL,0); /* Проверяем размеры Java-массива и отсутствие усечения идентификатора до вызова C. */ + (*e)->GetIntArrayRegion(e,input,0,5,raw); for(i=0;i<5;i++) values[i]=(int32_t)raw[i]; /* JNI преобразует скаляры без извлечения полей протокола. */ + if(setp_deif_master((unsigned)node,values,(uint32_t)id,dam!=0,2,frames)!=5 || !setp_deif_angle_command(frames,frames+5)) return deif_frames_java(e,NULL,0); /* Фактический угол и отдельное задание строит то же ядро, что Desktop. */ + return deif_frames_java(e,frames,6); /* Потребитель выбирает разрешённые объекты, сохраняя готовые байты каждого кадра. */ +} /* Неверная конфигурация не возвращает частично сформированный цикл. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_make(JNIEnv *e,jobject self,jint field,jint node,jint instance,jint value,jlong id,jboolean dam) { /* Общая операция покрывает измерения, яркость и палитру. */ + setp_deif_frame_t frame; (void)self; /* Возвращаемая структура заполняется только проверенным builder ядра. */ + if(id<0 || id>0x7FF || !setp_deif_make((unsigned)field,(unsigned)node,(unsigned)instance,(int32_t)value,(uint32_t)id,dam!=0,&frame)) return deif_frames_java(e,NULL,0); /* Диапазоны параметров и CAN-адресацию проверяет общий C-код. */ + return deif_frames_java(e,&frame,1); /* Kotlin получает один готовый STD data frame. */ +} /* UI не знает индексы диммера и расположение I16 в кадре. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_service(JNIEnv *e,jobject self,jboolean nmt,jint node,jint value) { /* Формирует heartbeat или локальный NMT через существующий C builder. */ + setp_deif_frame_t frame; (void)self; /* Буфер служебной команды ограничен одним Classic CAN кадром. */ + if(!setp_deif_service(nmt!=0,(unsigned)node,(unsigned)value,&frame)) return deif_frames_java(e,NULL,0); /* Адрес, команда и допустимое NMT-состояние проверяются C. */ + return deif_frames_java(e,&frame,1); /* Физический либо локальный маршрут выбирается приложением отдельно. */ +} /* NMT не попадает в физический маршрут MASTER автоматически. */ +JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_selector(JNIEnv *e,jobject self,jint field,jint node,jint instance,jboolean dam) { /* Каталожные привязки формирует единый переносимый справочник. */ + uint8_t selector[4]; jbyteArray result; int ok; (void)self; /* Селектор является непрозрачным четырёхбайтовым результатом C. */ + ok=setp_deif_selector((unsigned)field,(unsigned)node,(unsigned)instance,dam!=0,selector); result=(*e)->NewByteArray(e,ok?4:0); /* Неизвестный объект возвращает пустой результат для явной ошибки Kotlin. */ + if(result && ok) (*e)->SetByteArrayRegion(e,result,0,4,(jbyte *)selector); return result; /* JNI копирует массив без endian-преобразований. */ +} /* Android и Desktop используют один каталог объектов DEIF. */ +JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_bind(JNIEnv *e,jobject self,jobject buffer,jint field,jbyteArray input,jdouble scale) { /* Ручная привязка передаётся ядру как непрозрачные байты и масштаб. */ + uint8_t selector[8]={0}; int count=deif_input(e,input,selector); (void)self; /* Общий копирующий адаптер ограничивает доступ к Java-массиву. */ + if(count!=0 && count!=4) return JNI_FALSE; return setp_deif_bind(deif_context(e,buffer),(unsigned)field,count?selector:NULL,(double)scale)?JNI_TRUE:JNI_FALSE; /* Пустая привязка отключает поле; допустимость масштаба проверяет C. */ +} /* Обновление привязок не требует знания private-layout контекста. */ +JNIEXPORT jdoubleArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_binding(JNIEnv *e,jobject self,jobject buffer,jint field) { /* Чтение текущей привязки исключает вторую таблицу исходных коэффициентов в GUI. */ + uint8_t selector[4]={0}; double scale=0; jdouble values[6]; jdoubleArray result; int bound; unsigned i; (void)self; /* Четыре байта, масштаб и флаг активности составляют скалярный JNI-контракт. */ + if(field<0 || field>5 || !deif_context(e,buffer)) return (*e)->NewDoubleArray(e,0); bound=setp_deif_get_binding(deif_context(e,buffer),(unsigned)field,selector,&scale); /* Недопустимый номер не должен возвращать выдуманную привязку. */ + for(i=0;i<4;i++) values[i]=selector[i]; values[4]=scale; values[5]=bound; result=(*e)->NewDoubleArray(e,6); /* Байты сохраняются скалярами без интерпретации адресного бита или индекса. */ + if(result) (*e)->SetDoubleArrayRegion(e,result,0,6,values); return result; /* JVM владеет неизменяемой копией настроек. */ +} /* Пользовательский HEX является только форматом хранения профиля. */ +JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_validate(JNIEnv *e,jobject self,jlong id,jbyteArray input,jint slave) { /* Последний рубеж различает разрешённые команды MASTER и ответы собственного SLAVE. */ + uint8_t data[8]; int count=deif_input(e,input,data); (void)self; /* Payload остаётся ограниченным размером Classic CAN. */ + return count>=0 && id>=0 && id<=0x7FF && setp_deif_validate((uint32_t)id,data,(size_t)count,0,(unsigned)slave)?JNI_TRUE:JNI_FALSE; /* Все списки допустимых объектов и служебных кадров находятся в C-ядре. */ +} /* NMT и произвольный SDO не могут пройти в физический маршрут MASTER. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_selectorInfo(JNIEnv *e,jobject self,jbyteArray input) { /* Редактор получает семантические поля селектора через существующий C-декодер. */ + uint8_t selector[8]; int32_t metadata[4]; jint values[4]; jintArray result; unsigned i; int ok; (void)self; /* Четыре скаляра описывают Node-ID, индекс, подиндекс и признак DAM. */ + ok=deif_input(e,input,selector)==4 && setp_deif_selector_info(selector,metadata); result=(*e)->NewIntArray(e,ok?4:0); /* Порт не извлекает адресные маски либо little-endian индекс повторно. */ + if(result && ok) { for(i=0;i<4;i++) values[i]=(jint)metadata[i]; (*e)->SetIntArrayRegion(e,result,0,4,values); } return result; /* Контейнер результата принадлежит JVM. */ +} /* Неизвестная ручная библиотечная переменная сохраняет исходный индекс. */ +JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_readdress(JNIEnv *e,jobject self,jbyteArray input,jint node) { /* Смена адреса сохраняет пользовательский объект и режим SAM/DAM. */ + uint8_t selector[8],output[4]; jbyteArray result; int ok; (void)self; /* Размеры ограничены публичным четырёхбайтным контрактом привязки. */ + ok=deif_input(e,input,selector)==4 && setp_deif_readdress(selector,(unsigned)node,2,output); result=(*e)->NewByteArray(e,ok?4:0); /* Режим 2 существующего C ABI оставляет выбранный ранее SAM либо DAM. */ + if(result && ok) (*e)->SetByteArrayRegion(e,result,0,4,(jbyte *)output); return result; /* Kotlin получает непрозрачный новый селектор без самостоятельной сборки адресного байта. */ +} /* Автоматическая миграция адресов использует те же операции, что Desktop. */ diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt new file mode 100644 index 0000000..c56cfbd --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt @@ -0,0 +1,72 @@ +package ru.setcorp.setprotocol.deif // Тесты вызывают тот же общий контроллер и C-библиотеку, что Android GUI. +import org.junit.Assert.* // Проверяются точные кадры, маршруты и принятые физические значения. +import org.junit.Test // Обычные JVM-тесты не требуют запущенного Android-устройства. + +class DeifWorkspaceTest { // Здесь отсутствует резервная реализация кодека или автомата CANopen. + private fun hex(frame: DeifFrame) = frame.data.joinToString("") { "%02X".format(it.toInt() and 255) } // HEX служит только удобным форматом сравнения готового результата. + @Test fun masterAndDisplayBuildersUseSharedBytes() { // Расширенный JNI-порт должен совпадать с известными C- и Python-векторами. + val frames = DeifXdi.master(11, intArrayOf(900, -967, -509, -2098, -300), 0x30B, true) // DAM-получатель Node 11 использует RPDO2. + assertEquals(6, frames.size); assertTrue(frames.all { it.canId == 0x30BL && DeifXdi.validate(it) }) // Пять рабочих объектов и отдельное задание азимута проходят общую проверку MASTER. + assertEquals("8B01300284030000", hex(frames[0])); assertEquals("8B81300239FC0000", hex(frames[1])) // Угол и отрицательные обороты сохраняют опубликованный I16 LE формат. + assertEquals("8B01300784030000", hex(frames[5])) // Отдельная уставка не подменяет фактический объект угла. + assertEquals("8B0335024B000000", hex(DeifXdi.make(6, 11, 3, 75, 0x30B, true))) // Яркость 75% группы 3 остаётся U8 без масштаба RPM. + assertEquals("8B03350301000000", hex(DeifXdi.make(7, 11, 3, 1, 0x30B, true))) // Палитра Night передаётся отдельным объектом той же группы. + assertFalse(DeifXdi.validate(DeifXdi.service(true, 11, 1))) // Физический маршрут MASTER не разрешает локальную NMT-команду. + } // Числовые ожидания здесь являются эталонными тестовыми данными, а не кодеком порта. + @Test fun offlineSimulatorReceivesInteractiveCommandsWithoutTransport() { // Полный автономный сценарий должен работать без адаптера и исходящих физических кадров. + DeifWorkspace(DeifSettings(node = 11), true).use { session -> // Локальный SLAVE использует такой же Node-ID, как исходная desktop-пара. + assertTrue(runCatching { session.cycle(0) }.isFailure); assertTrue(runCatching { session.display(false, 75, 0) }.isFailure) // Выбранный локальный маршрут не возвращает физические кадры даже до запуска либо после остановки внутреннего SLAVE. + assertTrue(runCatching { session.startMaster(true, 0) }.isFailure); assertFalse(session.masterRunning) // Ошибочный локальный запуск не оставляет активный цикл, ожидающий будущего изменения маршрута. + session.startSlave(true, 0); assertTrue(session.tick(0).slave.isEmpty()) // Boot-up остаётся внутри локального стенда. + session.nmt(1, 0); session.setControl("angle", 37.1); session.setControl("rpm", 40.32); session.setControl("power", 63.9) // Касания воспроизводят задания со снимка пользователя. + val traffic = session.cycle(1); val snapshot = session.snapshot(1) // Передача проходит реальный C-builder и автомат приёма SLAVE. + assertTrue(traffic.master.isEmpty() && traffic.slave.isEmpty()); assertEquals(6, snapshot.accepted) // Локальный маршрут не вызывает внешний транспорт даже для heartbeat. + assertEquals(37.1, snapshot.angle!!, 0.001); assertEquals(403.2, snapshot.values[1]!!, 0.001); assertEquals(63.9, snapshot.values[3]!!, 0.001) // Угол, rpm и процент мощности сохраняют правильные масштабы. + session.display(false, 75, 2); session.display(true, 1, 2) // Яркость и Color также доступны без оборудования. + assertEquals(75, session.snapshot(2).brightness); assertEquals(1, session.snapshot(2).palette) // Проверяется применение в нативной модели, а не только изменение UI. + session.nmt(2, 3); session.setControl("rpm", 80.0); session.cycle(4) // NMT Stop прекращает обработку новых PDO. + assertEquals(403.2, session.snapshot(4).values[1]!!, 0.001); assertEquals(4, session.snapshot(4).nmt) // Прежнее принятое значение не подменяется новым жёлтым заданием MASTER. + } // Закрытие владельца прекращает таймеры и очередь C. + } // Тест не содержит USB, COM и Android Context. + @Test fun physicalSlaveCannotBecomeMasterOrSendNmt() { // Собственный узел на физической шине должен иметь отдельный ограниченный маршрут. + DeifWorkspace(DeifSettings(node = 13), false).use { session -> // Контроллер возвращает реальные серверные кадры платформенному передатчику. + session.startSlave(true, 0); val boot = session.tick(0).slave.single() // Boot-up должен быть выдан ровно один раз. + assertEquals(0x70DL, boot.canId); assertEquals("00", hex(boot)); assertTrue(DeifXdi.validate(boot, 13)) // Ответ разрешён только от собственного Node-ID. + assertTrue(runCatching { session.cycle(1) }.isFailure); assertTrue(runCatching { session.startMaster(true, 1) }.isFailure) // Кнопка и прямой программный вызов одинаково запрещают конкурирующий MASTER. + assertTrue(runCatching { session.nmt(1, 1) }.isFailure); assertTrue(session.tick(1).slave.isEmpty()) // Локальный NMT не отправляется физическому узлу ни при каком выборе меню. + } // Сервер закрывается до освобождения транспортного подключения приложения. + } // Внешний MASTER управляет жизненным циклом этого SLAVE через обычный RX. + @Test fun settingsAndManualBindingsSurvivePersistence() { // Сохранение должно охватывать профиль, калибровки, выбор объектов и явные привязки. + val selector = DeifXdi.selector(2, 3).joinToString(" ") { "%02X".format(it.toInt() and 255) } // Тест получает каталожный объект без ручной сборки селектора. + val original = DeifSettings(source = 3, xdi = false, automatic = false, pdo = 4, group = 9, powerFull = 2500.0, selected = listOf(true, false, true, true, false), bindings = listOf(DeifBinding(2, selector, 0.25))) // Несовпадающие значения обнаруживают пропущенные поля сериализации. + val restored = DeifSettings.decode(original.encode()); assertEquals(original, restored); restored.validate() // Профиль восстанавливается без автоматического запуска сервера. + restored.model().use { model -> assertEquals(original.bindings.single(), model.binding(2)) } // Ручной масштаб и объект действительно применяются к C-состоянию. + assertTrue(runCatching { restored.copy(xdi = true, pdo = 1).validate() }.isFailure) // C-профиль запрещает занятый SAM RPDO1 для XDi-net. + assertTrue(runCatching { restored.copy(angleFactor = 0.0).validate() }.isFailure) // Нулевой масштаб не должен давать фиктивный угол управления. + } // Проверяется реальное восстановление общего профиля, а не зеркальная копия реализации тестом. + @Test fun periodicMasterHasIndependentHeartbeatAndNoCatchupBurst() { // Пауза GUI не должна создавать лавину просроченных повторов. + DeifWorkspace(DeifSettings(periodMs = 100, heartbeat = true), false).use { session -> // Два периода принадлежат общему контроллеру порта. + session.startMaster(true, 0); assertEquals(7, session.tick(0).master.size) // На старте выдаются шесть рабочих кадров и один Operational heartbeat. + assertTrue(session.tick(20).master.isEmpty()); assertEquals(6, session.tick(100).master.size) // Ранний тик ничего не отправляет, рабочий цикл наступает через 100 миллисекунд. + assertEquals(7, session.tick(2000).master.size); assertTrue(session.tick(2001).master.isEmpty()) // После двухсекундной паузы выдаётся один цикл вместо очереди устаревших повторов. + session.stopMaster(); assertTrue(session.tick(3000).master.isEmpty()) // Остановка прекращает также независимый heartbeat MASTER. + } // Пассивный наблюдатель не принимает исходящие задания за физические измерения. + } // Монотонное время задаётся явно, без задержек и нестабильных тестовых таймеров. + @Test fun addressChangesPreserveObjectsAndIndependentSources() { // Перенос профиля должен менять только связанный адрес, сохраняя пользовательские объекты и масштабы. + fun selector(field: Int, node: Int, dam: Boolean = false) = DeifXdi.selector(field, node, 2, dam).joinToString(" ") { "%02X".format(it.toInt() and 255) } // Эталонные селекторы создаёт общий каталог с явно выбранным экземпляром 2. + val profile = DeifSettings(node = 13, source = 1, bindings = listOf(DeifBinding(0, selector(0, 1), 0.5), DeifBinding(1, selector(1, 13, true), -0.1), DeifBinding(2, selector(2, 7), 0.25), DeifBinding(3, "", 0.1))) // Включены SAM, собственный DAM, независимый источник и отключённое поле. + val moved = profile.copy(node = 14, source = 3).readdressed(13, 1) // Новый профиль переносит только адреса прежнего собственного узла и выбранного SAM-источника. + assertEquals(selector(0, 3), moved.bindings[0].selector); assertEquals(selector(1, 14, true), moved.bindings[1].selector) // SAM и DAM следуют разным пользовательским адресам. + assertEquals(profile.bindings[2], moved.bindings[2]); assertEquals(profile.bindings[3], moved.bindings[3]) // Независимый источник и явное отключение не меняются. + assertEquals(0.5, moved.bindings[0].scale, 0.0); assertEquals(-0.1, moved.bindings[1].scale, 0.0) // Перенос адреса не сбрасывает индивидуальную калибровку либо инверсию. + assertTrue(DeifXdi.selectorInfo(moved.bindings[1].selector)!!.dam); assertEquals(14, DeifXdi.selectorInfo(moved.bindings[1].selector)!!.node) // Семантический разбор режима и узла выполняется тем же C API. + } // Отдельная привязка азимута продолжает управлять приоритетом факта и set-point в C. + @Test fun invalidNativeCommandsAndProfileShapesFailExplicitly() { // Ошибочные параметры JNI должны давать явный отказ без усечения и Kotlin fallback. + assertTrue(runCatching { DeifXdi.master(1, intArrayOf(0), 0x201, false) }.isFailure) // Неполный цикл не превращается в частичную отправку пяти объектов. + assertTrue(runCatching { DeifXdi.master(1, IntArray(5), 0x100000201L, false) }.isFailure) // Идентификатор не должен молча усекаться до допустимого STD CAN ID. + assertTrue(runCatching { DeifXdi.make(6, 1, 1, 101, 0x201, false) }.isFailure); assertTrue(runCatching { DeifXdi.make(7, 1, 1, 3, 0x201, false) }.isFailure) // Диапазон яркости и палитры проверяет существующий C-builder. + assertTrue(runCatching { DeifXdi.readdress("01 81 30 02", 128) }.isFailure); assertTrue(runCatching { DeifXdi.selectorInfo("01 81") }.isFailure) // Неверный Node-ID и короткий селектор не проходят границу JNI. + assertTrue(runCatching { DeifSettings.decode("selected=true,false") }.isFailure) // Повреждённый сохранённый профиль не должен давать выход за границы списка в Compose. + DeifSettings().model().use { model -> assertTrue(runCatching { model.bind(DeifBinding(1, "01 81 30 02", Double.NaN)) }.isFailure) } // Неконечный масштаб не попадает в показания нативной модели. + } // Ошибки проверяются через публичные методы того же порта, которым пользуется Android GUI. +} // Этот же набор тестов подключается к sourceSets Android из templates. diff --git a/c/set-protocol/ports/stm32g474-hope/IGBT_REALTIME.md b/c/set-protocol/ports/stm32g474-hope/IGBT_REALTIME.md new file mode 100644 index 0000000..b6509d0 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/IGBT_REALTIME.md @@ -0,0 +1,100 @@ +# HOPE STM32G474VET6: аппаратные IGBT-импульсы + +IGBT версии 4 формирует физические сигналы аппаратными таймерами, с заданиями в **наносекундах**. Поддерживаются все восемь независимых каналов и два режима: + +- **Внешний Vin → аппаратный ACK**: фронт внешней команды запускает задержку и ответ Vstat непосредственно в таймере. +- **Физический TX → настоящий драйвер**: выход выдаёт одиночный импульс или удерживаемый логический уровень; вход той же пары можно подключить к Vstat настоящего драйвера для наблюдения уровня и счётчика фронтов. + +Один выходной контакт используется либо как ACK, либо как TX. Это цифровая логическая сторона 3,3 В с общей землёй; сигналы силового затвора не формируются. По умолчанию после загрузки прошивки выбран УМП, а аппаратные выходы IGBT выключены. Работающие импульсы не обслуживаются миллисекундным циклом, USB или GUI. + +## Подключение восьми каналов + +Номера J5/J6 сверены визуально с заводской схемой HOPE от 14.04.2025. Входы и выходы **отличаются от прежней разводки PB… → PC0…PC7**. Отсчёт каналов в GUI начинается с 1, биты протокола — с 0. + +| Канал | Вход Vin / Vstat | Контакт входа | Выход ACK / TX | Контакт выхода | Таймер | +|---|---|---|---|---|---| +| 1 | PB6 | J6-7 | PB14 | J5-26 | HRTIM D1, EEV4 | +| 2 | PB7 | J6-8 | PC8 | J6-21 | HRTIM E1, EEV3 | +| 3 | PB3 | J6-12 | PC6 | J6-23 | HRTIM F1, EEV9 | +| 4 | PD3 | J6-15 | PD4 | J6-16 | TIM2 CH1/CH2 | +| 5 | PB4 | J6-9 | PB5 | J6-10 | TIM3 CH1/CH2 | +| 6 | PD12 | J6-27 | PD13 | J6-28 | TIM4 CH1/CH2 | +| 7 | PA0 | J5-5 | PA1 | J5-6 | TIM5 CH1/CH2 | +| 8 | PA2 | J5-7 | PA3 | J5-8 | TIM15 CH1/CH2 | + +Общая земля — **J5-29 или J6-30**. USB PA11/PA12, штатный USART1/CH340 PA9/PA10 и заводские CAN1/2/3 сохраняются. Перемычки JP2 снимать не требуется. SWD PA13/PA14 сохраняется; PB3 занимает аппаратный вход вместо SWO. На время всего режима IGBT приостанавливается ST7789, поскольку PC8 используется как HRTIM E1; после выхода дисплей восстанавливается, если ранее был включён. + +## Запуск эмулятора ACK + +1. Установите новую прошивку HOPE. Для установленного CANdle boot используйте приложение **с адреса 0x08008000**. Standalone предназначен для отдельного варианта без сохранения загрузчика. +2. Откройте SETGUI: **Терминал → STM32 → HOPE STM32G474VET6 → Эмуляторы → Настройки STM**. Выберите COM, адрес 16, 115200/8N1. Прочитайте STM, выберите IGBT и нажмите **«Применить в STM и проверить»**. +3. Откройте **IGBT / драйвер**, нажмите **«Прочитать состояние»**. В пояснении должно быть **IGBT v4**, у временных полей — **нс**. Старая прошивка остаётся явно обозначенной как v3/мс. +4. Выберите **«Внешний Vin → аппаратный ACK»**, канал и профиль 1SP0635 либо 1SD536F2. Нажмите **«Подготовить профиль с реальными временами»**. +5. Проверьте полярность, разрешения TX/RX и фронты, отметьте **«Аппаратные выходы разрешены»**, нажмите **«Применить канал и общие задания»**. +6. Подайте внешний Vin на вход выбранной пары. Измеряйте Vin и выходной ACK осциллографом либо анализатором с подходящей частотой выборки. Перечитайте счётчики в GUI. + +При подтверждении обоих фронтов ON и OFF должны иметь одинаковые задержки и одинаковые ширины. Для единственного разрешённого фронта используются соответствующие отдельные ON- либо OFF-поля. Фронт во время незавершённого ответа перезапускает аппаратный цикл; для раздельных ACK расстояние между входными фронтами должно превышать задержку плюс ширину ответа. Входной фронт не запускается из ISR. + +Принудительные RX=0/1 задают электрический уровень. «Нет ACK» сохраняет неактивный уровень с учётом полярности. FAULT применяется вместе с профилем: ненулевая длительность заменяет обычную ширину ответа, 0 удерживает активный ответ после фронта до явного изменения профиля или останова. При генерации физического TX маска FAULT не формирует искусственный ответ настоящего драйвера. + +## Запуск физического TX + +1. Подключите **выход** выбранной пары к логическому Vin настоящего драйвера. При необходимости соедините его логический Vstat со **входом** этой пары. +2. Выберите **«Физический TX → настоящий драйвер»**, задайте длительность TX в нс, разрешите канал и аппаратные выходы. +3. Снимите «Задание TX» выбранного канала и нажмите **«Применить канал и общие задания»**. +4. Нажмите **«Импульс TX выбранного канала»**. Профиль по умолчанию выдаёт **10000 нс = 10 мкс**. Реальную длительность отмеряет таймер. + +Кнопка «Отправить TX» устанавливает постоянные уровни. Повторный одиночный импульс при TX=1 либо занятости выбранного канала отвергается. Запись маски 0xFF в команду импульса запускает все восемь генераторов: HRTIM D/E/F запускаются одной записью, обычные TIM — короткой последовательностью записей. Общий синхронный фронт с одинаковой фазой между разными блоками HRTIM/TIM не гарантируется. Непрерывный PWM этой командой не включается. + +**«Выключить эмуляцию»** снимает все аппаратные триггеры и удерживает выходы в нуле. Останов доступен из любого режима и при незавершённой подготовке профиля. «Отменить обмен» прекращает только транзакцию COM и не заменяет физическую команду останова. + +## Времена, разрешение и компенсация + +Интервалы ACK, TX и конечного FAULT задаются в диапазоне **50…60000 нс**. Нулевой FAULT означает удержание. HRTIM автоматически выбирает максимальный множитель, при котором полный цикл помещается в 16-битный счётчик: при коротком профиле ×32 шаг выходной сетки **0,1953125 нс** от 160 МГц. Для больших интервалов множитель уменьшается. У TIM2/3/4/5/15 шаг **6,25 нс**. Значения округляются к ближайшему аппаратному кванту. + +Шаг выходной сетки не равен абсолютной точности Vin→Vstat. Входная синхронизация, GPIO, нагрузка и измерительный тракт добавляют задержку и джиттер. По умолчанию компенсация равна 0: заданная задержка отсчитывается после аппаратного триггера и внешнее измерение будет включать входной тракт. + +Калибровка каждого канала: + +1. Примените одинаковые ON/OFF и измерьте задержку между физическим Vin и началом ACK. +2. Вычислите добавку: измеренная задержка минус заданная задержка. +3. Запишите эту измеренную положительную добавку в **«Измеренная компенсация входа»**, нс, примените и повторите измерение. + +Прошивка вычитает компенсацию из таймерной задержки; оставшийся интервал должен быть не меньше 50 нс. Компенсация ограничена 1000 нс и не сохраняется во Flash. Паспортные 1SP0635 250/700 нс являются исходным заданием; у 1SD536F2 380 нс — справочная задержка, а 900 нс — номинал примера проекта, не заданная паспортом типичная ширина. См. профили и источники в SETGUI/docs/DRIVER_TIMING_RU.md. + +Счётчики фиксируют входные capture-события. Они не измеряют ширину выходного импульса. У обычных TIM отдельно публикуются overcapture; у HRTIM нет гарантии точного подсчёта всех IRQ при чрезмерно частых фронтах. Даже при нагрузке на IRQ времена выходов задаются аппаратными compare. Электрические времена требуют отдельного измерения на плате. + +## Совместимость и карта регистров + +Окно остаётся **0x1400…0x146F, 112 слов**, сигнатура 0x4947, версия **4**, 8 каналов, размер блока канала 12 слов. Код единиц в слове 11 равен **0 = нс**. Версия 3 имеет отдельный код 1 и миллисекундную семантику. GUI заново проверяет версию перед каждым планом записи и не отправляет задания, если единицы изменились. Безусловный останов совместим с обеими версиями. + +| Смещение от 0x1400 | Значение версии 4 | +|---|---| +| 2 | 0: немедленный останов; 1: проверить и применить весь подготовленный профиль, включить выходы | +| 3 / 4 | Фактические Vin/TX и Vstat/RX; в генераторе направления пары обращены | +| 5 | Возможности 0x001F; бит 15: есть неприменённые задания | +| 6 | Подготовленная маска FAULT | +| 7 / 8 | Минимум 50 и максимум 60000 нс | +| 9 | Сумма счётчиков входных фронтов; запись 0 сбрасывает диагностику | +| 10 / 11 | 8 каналов / код единиц 0 | +| 12 | Подготовленный режим: 0 внешний ACK, 1 физический TX | +| 13 | Удерживаемая маска TX; запись действует только в применённом режиме генератора | +| 14 | Чтение: занятые TX; запись ненулевой восьмибитной маски запускает одиночные импульсы | +| 15 | 12 слов на канал | + +База канала с битом `n`: `16 + 12*n`. Поля 0…8 сохраняют порядок флагов, режима RX, задержки ON, ширины ON, задержки OFF, ширины OFF, длительности TX, маски фронтов и длительности FAULT. Все времена здесь в нс. Поле 9 — счётчик входных фронтов; 10 — измеренная компенсация входа в нс; 11 — overcapture обычного TIM. + +Поканальные записи и маски готовятся отдельно от работающих таймеров. Только завершающая запись 1 в смещение 2 применяет согласованный профиль. При изменении одного канала остальные неизменённые каналы продолжают работать; незавершённый ответ изменяемого канала сбрасывается. Смена направления ACK/TX либо включение после останова перенастраивает все каналы. Во время выполняемого физического TX либо удерживаемого TX=1 применение отвергается, чтобы перенастройка GPIO/AF не создала лишний фронт настоящему драйверу. Сначала отправьте нулевой TX либо выполните явный останов. Ошибка проверки или часов обнаруживается до изменения оборудования. + +## Сборка и проверка + +Основной проект: `stm32f407_emulator/G474/HOPE/MDK-ARM/UMP_Emulator_HOPE_G474VE.uvprojx`. Оба target включают переносимый `set_igbt_realtime.c` и аппаратный `igbt_realtime_port.c`. Адреса Flash и существующий startup сохранены. + +Из SETGUI: + +```powershell +.\scripts\build_igbt_realtime.ps1 # Собирает приложение CANdle с VTOR 0x08008000, сохраняя первые 32 КиБ загрузчика. +.\scripts\build_igbt_realtime.ps1 -Standalone # Собирает отдельный образ с VTOR 0x08000000 для варианта без сохранения boot. +``` + +Результаты находятся в `dist/igbt-realtime/hope/`. Тесты `tests/test_igbt_realtime.py` проверяют настоящий протокол C и Qt-клиент, подготовку/применение, отказы, версионную защиту, восемь маршрутов и реальные CMSIS-маски/compare в host-MMIO. Эти проверки и успешная ARM-линковка не заменяют измерение Vin/Vstat на физической плате. diff --git a/c/set-protocol/ports/stm32g474-hope/README.md b/c/set-protocol/ports/stm32g474-hope/README.md index ce14587..3796dc4 100644 --- a/c/set-protocol/ports/stm32g474-hope/README.md +++ b/c/set-protocol/ports/stm32g474-hope/README.md @@ -89,3 +89,7 @@ PB12/PB13 прежнего ручного профиля соединены со Профиль Vin=PB0/1/2/10..14 и Vstat=PC0..PC7 допустим только после физического устранения этого соединения или изменения профиля на свободные выводы. Адреса и алгоритм IGBT остаются общими для всех MCU. + +## Аппаратный IGBT v4 + +Новый HOPE по умолчанию использует `EMU_IGBT_REALTIME=1`, HRTIM D/E/F и TIM2/3/4/5/15. Все восемь каналов работают аппаратно с заданиями в нс; поддержаны внешний Vin→ACK и физический TX. Новая распиновка и порядок проверки: [IGBT_REALTIME.md](IGBT_REALTIME.md). UART, USB и заводские CAN сохранены; PC8 временно используется вместо CS ST7789. Компилируйте также `src/set_igbt_realtime.c` и `igbt_realtime_port.c`. Прежний GPIO-профиль доступен при `EMU_IGBT_REALTIME=0`. diff --git a/c/set-protocol/ports/stm32g474-hope/board.c b/c/set-protocol/ports/stm32g474-hope/board.c index aa49daa..e053dbf 100644 --- a/c/set-protocol/ports/stm32g474-hope/board.c +++ b/c/set-protocol/ports/stm32g474-hope/board.c @@ -2,6 +2,8 @@ #include "board_config.h" /* Выбирает размещение Flash, USART1 и разрешение физических выводов IGBT. */ #include "transport.h" /* Объявляет запуск общего UART/FDCAN-транспорта после настройки GPIO. */ #include "emulator.h" /* Подключает единый шаг моделей УМП, 2812, IGBT и генератора. */ +#include "igbt_realtime_port.h" /* Объявляет аппаратный IGBT версии 4 с наносекундными compare. */ +#include "emulator_port.h" /* Даёт доступ к существующей карте сервера до первого транспортного запроса. */ #include "igbt_emulator.h" /* Передаёт физическую маску Vin и получает маску ответов Vstat. */ #include "stm32g4xx_hal.h" /* Предоставляет HAL RCC/PWR/GPIO и определения регистров Cortex-M4. */ volatile uint32_t clock_fault; /* Хранит диагностический код запуска для просмотра через SWD. */ @@ -9,7 +11,7 @@ uint32_t SystemCoreClock = 16000000u; /* После reset HSI16 работает const uint8_t AHBPrescTable[16] = {0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9}; /* CMSIS-кодирует делители HPRE как степени двойки по RM0440. */ const uint8_t APBPrescTable[8] = {0,0,0,0,1,2,3,4}; /* HAL использует кодировку PPRE для вычисления частоты периферии G4. */ static uint8_t usb_clock_ready; /* До настройки HSI48/CRS предотвращает преждевременный запуск USB. */ -#if EMU_IGBT_GPIO_ENABLE /* Не занимает выводы FLASH/CAN2/CAN3 на неподтверждённой ревизии HOPE по умолчанию. */ +#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Не занимает выводы FLASH/CAN2/CAN3 на неподтверждённой ревизии HOPE по умолчанию. */ static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14}; /* Ручной профиль Vin PB0/1/2/10..14 совпадает с прежним портом G474. */ #endif /* Без физического порта все команды IGBT остаются доступны через общую карту регистров. */ void SystemInit(void) /* Startup вызывает функцию до инициализации глобальных переменных C runtime. */ @@ -89,7 +91,7 @@ void board_init(void) /* Вызывается после создания общ HAL_GPIO_Init(GPIOA, &gpio); /* Применяет HAL-настройку UART к порту A одним вызовом. */ gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Alternate = GPIO_AF9_FDCAN1; /* PD0 RX и PD1 TX соединены с U6 TJA1051T/3; PB8 на этой плате занят BOOT0. */ HAL_GPIO_Init(GPIOD, &gpio); /* Подключает FDCAN1 к штатным CAN1L/CAN1H разъёма J4 по схеме от 14.04.2025. */ -#if EMU_IGBT_GPIO_ENABLE /* Физический профиль включается вручную после проверки занятых выводов FLASH и других разъёмов. */ +#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Физический профиль включается вручную после проверки занятых выводов FLASH и других разъёмов. */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* Порт C нужен только для восьми физических выходов Vstat. */ gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14; /* Сохраняет отключённый ручной профиль Vin; PB12/PB13 соединены с CAN2 и требуют изменения внешних соединений. */ gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB, &gpio); /* Задаёт нулевой Vin на неподключённых входах выбранного ручного профиля. */ @@ -100,12 +102,19 @@ void board_init(void) /* Вызывается после создания общ gpio.Pin = SERIAL_DE_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Настраивает выбранный внешний вывод DE, не меняя UART TX/RX. */ HAL_GPIO_WritePin(SERIAL_DE_PORT, SERIAL_DE_PIN, GPIO_PIN_RESET); HAL_GPIO_Init(SERIAL_DE_PORT, &gpio); /* Начинает в режиме приёма с выключенным передатчиком RS-485. */ #endif /* Штатный TTL UART не занимает вывод DE. */ +#if EMU_IGBT_REALTIME /* Регистрирует аппаратный сервис только в новой сборке HOPE. */ + hope_igbt_realtime_init(emu_server_state()); /* Подменяет окно 0x1400 до первого запроса, сохраняя остальные модели и транспорты. */ +#endif /* Прежний профиль продолжает использовать миллисекундный IGBT версии 3. */ transport_init(); /* Запускает HAL UART IRQ и Classic CAN после полной настройки GPIO/clock. */ } /* Завершает аппаратный запуск HOPE, не запуская генерацию DAC без команды. */ uint8_t board_usb_ready(void) { return usb_clock_ready; } /* Возвращает main готовность источника USB, общую для обоих вариантов Flash. */ void board_tick(void) /* Выполняется главным циклом один раз на каждую обслуженную миллисекунду. */ { /* Протокольная инъекция IGBT работает и при отключённом физическом GPIO. */ -#if EMU_IGBT_GPIO_ENABLE /* Считывает восемь физических Vin только для явно включённого ручного профиля. */ +#if EMU_IGBT_REALTIME /* Выходные сроки обслуживаются аппаратными таймерами независимо от миллисекундного цикла. */ + emu_tick(); /* Сохраняет штатное время моделей и применения выбранного режима MCU. */ + hope_igbt_realtime_poll(); /* Останавливает аппаратный IGBT в том же шаге, в котором выбран другой режим. */ +#else /* Ветка совместимости сохраняет прежние миллисекундные GPIO. */ +#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Считывает восемь физических Vin только для явно включённого ручного профиля. */ uint8_t inputs = 0, outputs; unsigned i; /* Накопленная маска привязана к порядку input_pins, а не к номерам PB. */ for (i = 0; i < 8; ++i) if (HAL_GPIO_ReadPin(GPIOB, input_pins[i]) == GPIO_PIN_SET) inputs |= (uint8_t)(1u << i); /* Переводит восемь уровней в логические Vin0..Vin7. */ igbt_emu_set_tx(inputs); /* Передаёт физическую маску общей модели, сохраняя её выбор источника через регистр 0x140C. */ @@ -113,9 +122,10 @@ void board_tick(void) /* Выполняется главным циклом од igbt_emu_set_tx(0); /* При выборе GPIO задаёт нулевой Vin; регистровый источник остаётся независимым. */ #endif /* Далее все модели продвигаются одинаково независимо от наличия дискретного порта. */ emu_tick(); /* Продвигает выбранную модель и общие отложенные настройки ровно на 1 мс. */ -#if EMU_IGBT_GPIO_ENABLE /* Выдаёт Vstat только после завершения шага модели. */ +#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Выдаёт Vstat только после завершения шага модели. */ outputs = igbt_emu_get_rx(); /* Получает одну согласованную восьмибитную маску ответов IGBT. */ HAL_GPIO_WritePin(GPIOC, (uint16_t)(~outputs & 0xFFu), GPIO_PIN_RESET); /* Сбрасывает только неактивные PC0..PC7 без изменения остальных выводов C. */ HAL_GPIO_WritePin(GPIOC, outputs, GPIO_PIN_SET); /* Устанавливает активные ответы Vstat текущего шага модели. */ #endif /* Завершает опциональное обновление физических ответов. */ +#endif /* Завершает выбор аппаратного либо прежнего логического IGBT. */ } /* Завершает один миллисекундный шаг без обработки моделей из прерываний. */ diff --git a/c/set-protocol/ports/stm32g474-hope/board_config.template.h b/c/set-protocol/ports/stm32g474-hope/board_config.template.h index 679ee14..5729ffd 100644 --- a/c/set-protocol/ports/stm32g474-hope/board_config.template.h +++ b/c/set-protocol/ports/stm32g474-hope/board_config.template.h @@ -1,4 +1,7 @@ #ifndef HOPE_BOARD_CONFIG_H /* Защищает единый профиль платы HOPE от повторного включения. */ +#ifndef EMU_IGBT_REALTIME /* Разрешает явную сборку прежней логической модели для старого GUI. */ +#define EMU_IGBT_REALTIME 1 /* Штатный HOPE использует HRTIM D/E/F и TIM2/3/4/5/15 с окном IGBT версии 4 в наносекундах. */ +#endif /* Сохраняет явное переопределение параметром компилятора. */ #define HOPE_BOARD_CONFIG_H /* Используется всеми аппаратными адаптерами и общей композицией моделей. */ #define EMU_USB_SET_V2 1 /* Нативный USB MCU переносит SET v2; UART сохраняет RTU и SETGUI v1/GAS. */ #define USB_CDC_IRQn USB_LP_IRQn /* G474 обслуживает USB FS через низкоприоритетный вектор USB_LP. */ diff --git a/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.c b/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.c new file mode 100644 index 0000000..98da110 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.c @@ -0,0 +1,203 @@ +#include "igbt_realtime_port.h" /* Получает интерфейс регистрации в уже существующем сервере эмуляторов. */ +#include "set_igbt_realtime.h" /* Использует отдельную карту версии 4 с целыми наносекундами. */ +#include "stm32g4xx_hal.h" /* Предоставляет CMSIS-регистры G474, GPIO и безопасную настройку NVIC. */ +#include /* Обнуляет аппаратный снимок перед публикацией диагностических значений. */ +typedef struct { /* Однозначная привязка входа и выхода каждого из восьми аппаратных каналов. */ + GPIO_TypeDef *input, *output; uint16_t input_pin, output_pin; /* Порты и маски ровно одного физического вывода, сверенные с заводской схемой HOPE. */ + uint8_t input_af, output_af, event, index; TIM_TypeDef *timer; IRQn_Type irq; /* Альтернативные функции, внешний EEV и индекс D/E/F либо обычный TIM с собственным IRQ. */ +} hope_igbt_pin; /* Каждый канал имеет отдельный счётчик времени и не делит задержку с другим каналом. */ +static const hope_igbt_pin pins[8] = { /* Выбирает свободные J5/J6, сохраняя USB, CH340/USART1 и все три заводских CAN. */ + {GPIOB, GPIOB, GPIO_PIN_6, GPIO_PIN_14, 13, 13, 4, 3, 0, HRTIM1_TIMD_IRQn}, /* Канал 1: Vin J6-7/PB6 EEV4 → Vstat/TX J5-26/PB14 HRTIM D1; контакты сверены визуально с заводской схемой. */ + {GPIOB, GPIOC, GPIO_PIN_7, GPIO_PIN_8, 13, 3, 3, 4, 0, HRTIM1_TIME_IRQn}, /* Канал 2: Vin J6-8/PB7 EEV3 → Vstat/TX J6-21/PC8 HRTIM E1; дисплей уступает CS в режиме IGBT. */ + {GPIOB, GPIOC, GPIO_PIN_3, GPIO_PIN_6, 13, 13, 9, 5, 0, HRTIM1_TIMF_IRQn}, /* Канал 3: Vin J6-12/PB3 EEV9 → Vstat/TX J6-23/PC6 HRTIM F1; SWD PA13/14 сохранён, SWO не используется. */ + {GPIOD, GPIOD, GPIO_PIN_3, GPIO_PIN_4, 2, 2, 0, 0, TIM2, TIM2_IRQn}, /* Канал 4: Vin J6-15/PD3 TIM2_CH1 → Vstat/TX J6-16/PD4 TIM2_CH2. */ + {GPIOB, GPIOB, GPIO_PIN_4, GPIO_PIN_5, 2, 2, 0, 0, TIM3, TIM3_IRQn}, /* Канал 5: Vin J6-9/PB4 TIM3_CH1 → Vstat/TX J6-10/PB5 TIM3_CH2. */ + {GPIOD, GPIOD, GPIO_PIN_12, GPIO_PIN_13, 2, 2, 0, 0, TIM4, TIM4_IRQn}, /* Канал 6: Vin J6-27/PD12 TIM4_CH1 → Vstat/TX J6-28/PD13 TIM4_CH2. */ + {GPIOA, GPIOA, GPIO_PIN_0, GPIO_PIN_1, 2, 2, 0, 0, TIM5, TIM5_IRQn}, /* Канал 7: Vin J5-5/PA0 TIM5_CH1 → Vstat/TX J5-6/PA1 TIM5_CH2. */ + {GPIOA, GPIOA, GPIO_PIN_2, GPIO_PIN_3, 9, 9, 0, 0, TIM15, TIM1_BRK_TIM15_IRQn}, /* Канал 8: Vin J5-7/PA2 TIM15_CH1 → Vstat/TX J5-8/PA3 TIM15_CH2; USART1 PA9/10 свободен от таймеров. */ +}; /* Завершает восемь независимых маршрутов с одной общей землёй J5-29/J6-30 и логикой 3,3 В. */ +static const uint32_t reset_events[3] = {HRTIM_RSTR_EXTEVNT4, HRTIM_RSTR_EXTEVNT3, HRTIM_RSTR_EXTEVNT9}; /* Использует маски reset из CMSIS: EEV4/3/9 находятся в битах 12/11/17, а не рядом с capture. */ +static const uint32_t capture_events[3] = {HRTIM_CPT1CR_EXEV4CPT, HRTIM_CPT1CR_EXEV3CPT, HRTIM_CPT1CR_EXEV9CPT}; /* Маски CPT1 берутся из CMSIS независимо от расположения reset-битов тех же внешних событий. */ +static set_igbt_rt service; static set_emu_server *server; /* Хранит один аппаратный сервис и ссылку на текущий режим существующего MCU-сервера. */ +static set_igbt_rt_config applied; static uint8_t owned, pulse_mask, dll_ready; /* Применённые времена, владение выводами, выполняемые TX и результат калибровки HRTIM. */ +static volatile uint16_t edge_count[8], overruns[8]; /* ISR изменяют только диагностику; фронты выходов формирует исключительно оборудование. */ +static uint8_t busy(void); /* Применение профиля предварительно проверяет занятость физического TX без обрыва импульса. */ +static uint32_t ticks(uint32_t ns, uint32_t multiplier) /* Переводит наносекунды в кванты таймера от подтверждённых 160 МГц. */ +{ /* Максимум 60000*160*32 укладывается в uint32_t, поэтому вычисление не требует деления uint64_t на Cortex-M4. */ + return (ns * 160u * multiplier + 500u) / 1000u; /* Округляет к ближайшему кванту: 6,25 нс у TIM и до 0,1953125 нс у HRTIM ×32. */ +} /* Завершает детерминированное преобразование единиц без системных задержек. */ +static void gpio_mode(GPIO_TypeDef *port, uint16_t pin, uint32_t mode, uint32_t af, uint32_t pull) /* Настраивает единственный принадлежащий каналу физический вывод. */ +{ /* Остальные биты GPIO, включая UART/CAN/SWD, остаются без изменения. */ + GPIO_InitTypeDef gpio = {0}; gpio.Pin = pin; gpio.Mode = mode; gpio.Alternate = af; /* Явно задаёт маску, направление и альтернативную функцию выбранного контакта. */ + gpio.Pull = pull; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(port, &gpio); /* Высокая скорость фронтов нужна для импульсов короче микросекунды. */ +} /* Завершает настройку контакта штатным HAL, не включая таймер. */ +static unsigned selected(void *context) /* Общий протокол разрешает аппаратное управление только в выбранном режиме IGBT. */ +{ /* Контекст сервиса статический, режим читает существующий сервер платы. */ + (void)context; return server->config[3] == 2; /* Значение 2 совпадает с каталогом MCU и регистром выбора эмулятора 0x1219. */ +} /* Завершает проверку маршрута без побочного действия. */ +static void stop_channel(unsigned ch) /* Останавливает один независимый аппаратный маршрут перед его перенастройкой. */ +{ /* Не изменяет счётчики и состояние остальных семи каналов. */ + const hope_igbt_pin *p = &pins[ch]; /* Читает единственную каноническую таблицу привязки выбранного канала. */ + if (p->timer) { p->timer->SMCR = 0; p->timer->DIER = 0; p->timer->CR1 = 0; p->timer->CCER = 0; } /* Запрещает внешний пуск, IRQ, счёт и альтернативный выход обычного TIM. */ + else { HRTIM_Timerx_TypeDef *t = &HRTIM1->sTimerxRegs[p->index]; t->RSTxR = 0; t->TIMxDIER = 0; HRTIM1->sMasterRegs.MCR &= ~(1u << (17u + p->index)); HRTIM1->sCommonRegs.ODISR = 1u << (2u * p->index); } /* Отключает EEV, счётчик и физический выход HRTIM D/E/F. */ + HAL_GPIO_WritePin(p->output, p->output_pin, GPIO_PIN_RESET); /* Заранее записывает ноль в ODR, чтобы переход из AF не создавал положительного импульса. */ + gpio_mode(p->output, p->output_pin, GPIO_MODE_OUTPUT_PP, 0, GPIO_NOPULL); /* Явно удерживает выход в нуле даже при ранее инвертированном ACK. */ + NVIC_ClearPendingIRQ(p->irq); /* Убирает отложенное диагностическое прерывание выключенного таймера. */ + pulse_mask &= (uint8_t)~(1u << ch); /* Снимает занятость только остановленного физического TX. */ +} /* Завершает останов одного маршрута без воздействия на остальные таймеры. */ +static void stop(void *context) /* Безусловно снимает аппаратные триггеры и уровни всех принадлежащих IGBT выходов. */ +{ /* Не касается дисплея и чужих GPIO, пока аппаратный IGBT ещё не захватывал выводы. */ + unsigned i; (void)context; if (!owned) return; /* Первое чтение состояния другого режима не меняет аппаратную конфигурацию платы. */ + for (i = 0; i < 8; ++i) stop_channel(i); /* Безусловная команда останова отключает все восемь маршрутов. */ + applied.enabled = 0; /* Следующее применение того же профиля обязано заново вооружить таймеры после явного останова. */ +} /* Завершает останов без ожидания millis или ответа GUI. */ +static void event_config(unsigned event, unsigned edges) /* Выбирает фронты на аппаратном входе EEV без вызова ISR для запуска ACK. */ +{ /* События 1…5 находятся в EECR1, события 6…10 — в EECR2; каждый блок занимает шесть бит по RM0440. */ + volatile uint32_t *reg = event <= 5 ? &HRTIM1->sCommonRegs.EECR1 : &HRTIM1->sCommonRegs.EECR2; /* Выбирает правильный регистр по номеру внешнего события. */ + unsigned shift = ((event - 1u) % 5u) * 6u; uint32_t mask = 63u << shift; /* Выделяет source/polarity/sensitivity/fast одного EEV, сохраняя остальные события. */ + uint32_t sensitivity = edges == 1 ? 1u : edges == 2 ? 2u : 3u; /* Кодирует ON, OFF или оба фронта; source=0 выбирает GPIO соответствующей AF. */ + *reg = (*reg & ~mask) | (sensitivity << (shift + 3u)); /* Использует синхронный путь без цифрового фильтра; задержка входа учитывается измеренной компенсацией. */ +} /* Завершает настройку входного события без программной задержки. */ +static void hrtim_config(unsigned ch, const set_igbt_rt_config *cfg) /* Настраивает один самостоятельный HRTIM D/E/F при отключённом физическом выходе. */ +{ /* Начало и конец каждого ACK либо TX задаются двумя аппаратными compare одного счётчика. */ + const hope_igbt_pin *p = &pins[ch]; const set_igbt_rt_channel *c = &cfg->channel[ch]; HRTIM_Timerx_TypeDef *t = &HRTIM1->sTimerxRegs[p->index]; /* Выбирает канал и его применяемые наносекундные параметры. */ + uint32_t delay = c->edges == 2 ? c->off_delay : c->on_delay, width = c->edges == 2 ? c->off_width : c->on_width; /* При обоих фронтах времена уже проверены на равенство; одиночный OFF использует отдельные задания. */ + uint32_t multiplier = 32, prescaler = 0, start, duration, end, period; unsigned hold = 0; /* Начинает с максимального разрешения HRTIM и при необходимости расширяет диапазон. */ + if (cfg->source) { delay = 50; width = c->tx_width; } /* Физический TX начинает импульс после короткого внутреннего запуска 50 нс; его ширина аппаратно равна заданию. */ + else { delay -= c->compensation; if (cfg->fault & (1u << ch)) { hold = c->fault_width == 0; if (!hold) width = c->fault_width; } } /* Учитывает измеренный входной тракт и выбранный конечный либо удерживаемый FAULT. */ + for (;;) { start = ticks(delay, multiplier); duration = ticks(width, multiplier); end = start + duration; period = end + 3u * multiplier; if (period <= 65503u || multiplier == 1) break; multiplier >>= 1; ++prescaler; } /* Умещает compare и запас три базовых такта в 16-битный счётчик, сохраняя максимально возможное разрешение. */ + t->TIMxCR = prescaler | HRTIM_TIMCR_RETRIG; t->PERxR = period; t->REPxR = 0; /* Выбирает одиночный повторно запускаемый цикл без фоновой периодической генерации. */ + t->CMP1xR = start; t->CMP2xR = end; t->CMP3xR = 0; t->CMP4xR = 0; /* Записывает точные точки начала и конца; неиспользуемые compare не участвуют в выходном crossbar. */ + t->SETx1R = HRTIM_SET1R_CMP1; t->RSTx1R = hold ? 0 : HRTIM_RST1R_CMP2 | HRTIM_RST1R_RESYNC | HRTIM_RST1R_PER; /* Удерживаемый FAULT сохраняет активный уровень до явного снятия, обычный ответ заканчивается compare 2. */ + t->OUTxR = (!cfg->source && (c->flags & 4)) ? HRTIM_OUTR_POL1 : 0; /* Инверсия ACK меняет электрическую полярность, не сдвигая аппаратные времена. */ + t->RSTx1R |= HRTIM_RST1R_SRT; /* Программно задаёт неактивное состояние внутреннего выходного триггера до подключения AF. */ + event_config(p->event, cfg->source ? 3u : c->edges); /* Настраивает вход захвата; в режиме генератора он может принимать фактический Vstat реального драйвера. */ + t->CPT1xCR = (c->flags & 1) ? capture_events[ch] : 0; t->TIMxDIER = (c->flags & 1) ? HRTIM_TIMDIER_CPT1IE : 0; /* Захват CPT1 фиксирует внешние фронты для диагностики; ISR не формирует ACK. */ + t->RSTxR = (!cfg->source && c->edges && c->mode == 0 && (c->flags & 3) == 3) ? reset_events[ch] : 0; /* Разрешает прямой аппаратный пуск только для включённого автоматического ACK. */ + t->TIMxICR = 0xFFFFFFFFu; t->CNTxR = period; /* Начинает на конце периода, чтобы разрешение счётчика не породило ложный ACK при включении. */ + HRTIM1->sMasterRegs.MCR |= 1u << (17u + p->index); HRTIM1->sCommonRegs.OENR = 1u << (2u * p->index); /* Разрешает отдельный счётчик и только его первый физический выход. */ + if (!cfg->source && (!(c->flags & 2) || c->mode == 1 || c->mode == 2 || c->mode == 3 || !c->edges)) { /* Постоянные режимы не должны случайно выдавать импульс по compare при конфигурации. */ + unsigned level = (c->flags & 2) && (c->mode == 2 || ((c->mode == 3 || !c->edges) && (c->flags & 4))); /* Принудительные 0/1 имеют электрический смысл; пропущенный ACK сохраняет полярность неактивного уровня. */ + HAL_GPIO_WritePin(p->output, p->output_pin, level ? GPIO_PIN_SET : GPIO_PIN_RESET); gpio_mode(p->output, p->output_pin, GPIO_MODE_OUTPUT_PP, 0, GPIO_NOPULL); /* Фиксирует постоянный уровень без подмены времён программным переключением. */ + } else gpio_mode(p->output, p->output_pin, GPIO_MODE_AF_PP, p->output_af, GPIO_NOPULL); /* Подключает аппаратный выход только после полного задания таймера и его неактивного состояния. */ +} /* Завершает аппаратную подготовку одного канала HRTIM. */ +static void tim_config(unsigned ch, const set_igbt_rt_config *cfg) /* Настраивает один обычный TIM2/3/4/5/15 с независимым счётчиком 160 МГц. */ +{ /* Канал 1 принимает фронт, канал 2 формирует ответ; IRQ используются только для диагностического счётчика захватов. */ + const hope_igbt_pin *p = &pins[ch]; const set_igbt_rt_channel *c = &cfg->channel[ch]; TIM_TypeDef *t = p->timer; /* Выбирает проверенные альтернативные функции входа и выхода этого TIM. */ + uint32_t delay = c->edges == 2 ? c->off_delay : c->on_delay, width = c->edges == 2 ? c->off_width : c->on_width; unsigned hold = 0; /* Обе полярности используют один аппаратный профиль, если разрешены одновременно. */ + uint32_t start, duration, output_mode; /* Хранит параметры compare в базовых тактах 6,25 нс. */ + if (cfg->source) { delay = 50; width = c->tx_width; } /* В режиме TX аппаратная ширина не зависит от момента доставки команды USB/UART. */ + else { delay -= c->compensation; if (cfg->fault & (1u << ch)) { hold = c->fault_width == 0; if (!hold) width = c->fault_width; } } /* Учитывает фактическую калибровку и сценарий ошибки ACK. */ + start = ticks(delay, 1); duration = ticks(width, 1); output_mode = hold ? 1u : 7u; /* Режим OC set удерживает FAULT; PWM2 выдаёт активный интервал от CCR2 до переполнения ARR. */ + t->CCMR1 = TIM_CCMR1_CC1S_0 | (4u << TIM_CCMR1_OC2M_Pos); /* Сначала принудительно сбрасывает OC2REF: удерживаемый FAULT не должен наследовать активный уровень предыдущего профиля. */ + t->CR1 = TIM_CR1_OPM | TIM_CR1_URS; t->PSC = 0; t->ARR = start + duration - 1u; t->CCR2 = start; /* Записывает одиночный цикл с точной аппаратной шириной duration тактов и без IRQ от программного update. */ + t->CCMR1 = TIM_CCMR1_CC1S_0 | (output_mode << TIM_CCMR1_OC2M_Pos); /* Канал 1 привязан к TI1, канал 2 — к требуемому аппаратному выходному режиму. */ + t->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | ((!cfg->source && (c->flags & 4)) ? TIM_CCER_CC2P : 0); /* Разрешает захват и выход с независимой электрической инверсией ACK. */ + if (cfg->source || c->edges == 3) t->CCER |= TIM_CCER_CC1P | TIM_CCER_CC1NP; else if (c->edges == 2) t->CCER |= TIM_CCER_CC1P; /* Диагностика захватывает оба фронта либо выбранный OFF; ON сохраняет обычную полярность. */ + if (t == TIM15) t->BDTR = TIM_BDTR_MOE; /* TIM15 требует разрешения главного выхода MOE, в отличие от TIM2/3/4/5. */ + t->EGR = TIM_EGR_UG; t->CNT = 0; t->SR = 0; /* Загружает делитель и period при отключённом выходе, затем удаляет технические события конфигурации. */ + t->DIER = (c->flags & 1) ? TIM_DIER_CC1IE : 0; /* Прерывания capture служат только счётчику; окончание ACK всегда выполняется счётчиком и OC. */ + t->SMCR = (!cfg->source && c->edges && c->mode == 0 && (c->flags & 3) == 3) ? TIM_SMCR_SMS_3 | (c->edges == 3 ? TIM_TS_TI1F_ED : TIM_TS_TI1FP1) : 0; /* Combined reset+trigger аппаратно запускает одиночный ответ на каждый разрешённый фронт. */ + if (!cfg->source && (!(c->flags & 2) || c->mode || !c->edges)) { /* Постоянные и пропущенные ответы обходят PWM, сохраняя требуемый электрический уровень. */ + unsigned level = (c->flags & 2) && (c->mode == 2 || ((c->mode == 3 || !c->edges) && (c->flags & 4))); /* ACK активен низким только в автоматическом/пропущенном режиме; принудительный уровень трактуется буквально. */ + HAL_GPIO_WritePin(p->output, p->output_pin, level ? GPIO_PIN_SET : GPIO_PIN_RESET); gpio_mode(p->output, p->output_pin, GPIO_MODE_OUTPUT_PP, 0, GPIO_NOPULL); /* Выставляет постоянный уровень, не создавая периодическую генерацию. */ + } else gpio_mode(p->output, p->output_pin, GPIO_MODE_AF_PP, p->output_af, GPIO_NOPULL); /* Передаёт физический выход полностью подготовленному аппаратному PWM/OC. */ +} /* Завершает конфигурацию одного обычного независимого таймера. */ +static void levels(void *context, uint8_t mask) /* Удерживает ручные уровни физического TX без изменения записанной длительности одиночного импульса. */ +{ /* Вызывается только при остановленных одиночных TX; общая карта предварительно проверяет занятость и разрешения. */ + unsigned i; (void)context; for (i = 0; i < 8; ++i) { /* Обрабатывает каждый физический выход независимо от типа таймера. */ + HAL_GPIO_WritePin(pins[i].output, pins[i].output_pin, (mask & (1u << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET); /* Подготавливает требуемый уровень в ODR до смены AF. */ + gpio_mode(pins[i].output, pins[i].output_pin, GPIO_MODE_OUTPUT_PP, 0, GPIO_NOPULL); /* Удерживаемый уровень не зависит от того, на каком значении остановился счётчик PWM. */ + } /* Завершает запись восьми постоянных уровней. */ +} /* Завершает ручное управление уровнем без busy-wait или задержки HAL. */ +static unsigned apply(void *context, const set_igbt_rt_config *cfg) /* Проверяет аппаратные предпосылки и применяет весь согласованный профиль. */ +{ /* До первого изменения GPIO обнаруживает отказ DLL и неверное дерево часов. */ + unsigned i, full; (void)context; if (SystemCoreClock != 160000000u || HAL_RCC_GetPCLK1Freq() != 80000000u || HAL_RCC_GetPCLK2Freq() != 160000000u || !dll_ready) return 4; /* Пересчёт наносекунд допустим только для подтверждённой конфигурации 160/80/160 МГц и готовой HRTIM DLL. */ + if (owned && applied.source && busy()) return 6; /* Применение конфигурации не обрывает выполняемый аппаратный одиночный TX; явная команда останова остаётся доступной. */ + if (owned && applied.source) for (i = 0; i < 8; ++i) if (pins[i].output->IDR & pins[i].output_pin) return 6; /* Удерживаемый TX=1 также блокирует перенастройку: перевод AF/GPIO не должен создавать лишний фронт настоящему драйверу. */ + full = !owned || !applied.enabled || cfg->source != applied.source; /* Все маршруты перенастраиваются только после останова либо при смене физического направления ACK/TX. */ + if (full) stop(0); /* Смена направления переводит все контакты в ноль до подключения новых функций. */ + owned = 1; /* Последующие команды останова вправе управлять только уже выделенными IGBT выводами. */ + for (i = 0; i < 8; ++i) if (full || memcmp(&cfg->channel[i], &applied.channel[i], sizeof(cfg->channel[i])) || ((cfg->fault ^ applied.fault) & (1u << i))) { /* Поканальное применение оставляет неизменённые семь аппаратных маршрутов работающими. */ + if (!full) stop_channel(i); /* Изменение выбранного профиля сбрасывает только его незавершённый ACK/FAULT, сохраняя остальные каналы. */ + gpio_mode(pins[i].input, pins[i].input_pin, GPIO_MODE_AF_PP, pins[i].input_af, GPIO_PULLDOWN); /* Неподключённый Vin имеет нулевой уровень; внешние сигналы используют только логику 3,3 В. */ + if (pins[i].timer) tim_config(i, cfg); else hrtim_config(i, cfg); /* Выбирает аппаратную реализацию без программного отсчёта наносекундных сроков. */ + NVIC_SetPriority(pins[i].irq, 5); NVIC_ClearPendingIRQ(pins[i].irq); NVIC_EnableIRQ(pins[i].irq); /* Диагностика фронтов ниже критических аппаратных событий; приоритет не влияет на выходные compare. */ + } /* Завершает подготовку всех восьми независимых каналов. */ + applied = *cfg; /* Обновляет аппаратную копию только после настройки всех действительно изменившихся каналов. */ + if (cfg->source) levels(0, (uint8_t)cfg->tx); /* Генератор после применения держит заданные постоянные уровни до отдельной команды импульса. */ + return 0; /* Подтверждает работу оборудования только после полной конфигурации. */ +} /* Завершает аппаратное применение без миллисекундного шага модели. */ +static uint8_t busy(void) /* Проверяет фактическое окончание аппаратных одиночных TX. */ +{ /* HRTIM сохраняет CEN в single-shot, поэтому окончание определяется аппаратным repetition flag, а не этим битом. */ + unsigned i; for (i = 0; i < 8; ++i) if (pulse_mask & (1u << i)) { /* Просматривает только реально запущенные одиночные импульсы генератора. */ + unsigned done = pins[i].timer ? !(pins[i].timer->CR1 & TIM_CR1_CEN) : !!(HRTIM1->sTimerxRegs[pins[i].index].TIMxISR & HRTIM_TIMISR_REP); /* Обычный OPM сбрасывает CEN, HRTIM фиксирует конец единственного периода. */ + if (done) pulse_mask &= (uint8_t)~(1u << i); /* Снимает занятость по аппаратному событию, независимо от задержки USB/UART и частоты GUI. */ + } /* Завершает чтение состояния аппаратных генераторов. */ + return pulse_mask; /* Возвращает оставшиеся активные биты без запуска нового импульса. */ +} /* Завершает обслуживание только статуса, не формы сигнала. */ +static unsigned pulse(void *context, uint8_t mask) /* Запускает одиночные TX на выбранных независимых таймерах. */ +{ /* Все каналы предварительно проверяются до первого физического запуска. */ + unsigned i; uint32_t saved, hrtim_reset = 0; (void)context; if (busy() & mask) return 6; /* Повторный пуск выполняемого канала не обрывает его текущий импульс. */ + for (i = 0; i < 8; ++i) if ((mask & (1u << i)) && (pins[i].output->IDR & pins[i].output_pin)) return 6; /* Требует фактический низкий TX на каждом выбранном контакте, включая удерживаемый вручную уровень. */ + saved = __get_PRIMASK(); __disable_irq(); /* Делает последовательность подготовки и запуска короткой и не прерываемой диагностическими IRQ. */ + for (i = 0; i < 8; ++i) if (mask & (1u << i)) { /* Перезапускает только явно выбранные биты, сохраняя работу остальных каналов. */ + const hope_igbt_pin *p = &pins[i]; /* Выбирает аппаратный генератор данного физического TX. */ + if (p->timer) { p->timer->CR1 &= ~TIM_CR1_CEN; p->timer->CNT = 0; p->timer->SR = 0; } /* Подготавливает начало PWM2 в нуле, удаляя только события выбранного таймера. */ + else { HRTIM_Timerx_TypeDef *t = &HRTIM1->sTimerxRegs[p->index]; t->TIMxICR = HRTIM_TIMICR_REPC; t->RSTx1R |= HRTIM_RST1R_SRT; } /* Сбрасывает прежнее окончание single-shot и внутренний выход до подключения AF. */ + gpio_mode(p->output, p->output_pin, GPIO_MODE_AF_PP, p->output_af, GPIO_NOPULL); /* Подключает неактивный аппаратный выход до начала отсчёта импульса. */ + pulse_mask |= (uint8_t)(1u << i); /* Помечает канал занятым перед аппаратным стартом, исключая повторный запуск. */ + if (!p->timer) hrtim_reset |= 1u << (9u + p->index); /* Собирает общий software-reset HRTIM, не запуская первый канал во время настройки остальных GPIO. */ + } /* Все выбранные выходы уже подключены к неактивным таймерам до первого физического старта. */ + __DSB(); /* Завершает записи конфигурации и GPIO до аппаратного запуска счётчиков. */ + if (hrtim_reset) HRTIM1->sCommonRegs.CR2 = hrtim_reset; /* Каналы HRTIM D/E/F запускаются одной записью без межканального цикла GPIO. */ + for (i = 3; i < 8; ++i) if (mask & (1u << i)) pins[i].timer->CR1 |= TIM_CR1_CEN; /* Независимые TIM стартуют короткой последовательностью записей; общий наносекундный фронт между разными периферийными блоками не обещается. */ + __set_PRIMASK(saved); return 0; /* Восстанавливает прежнее состояние прерываний сразу после запуска аппаратных импульсов. */ +} /* Завершает команду, не ожидая длительность TX на CPU. */ +static void sample(void *context, set_igbt_rt_snapshot *snapshot) /* Снимает реальные уровни контактов и аппаратную занятость. */ +{ /* Короткие ACK могут уже закончиться; накопленные capture IRQ позволяют видеть факт внешних фронтов. */ + unsigned i; uint32_t saved; (void)context; memset(snapshot, 0, sizeof(*snapshot)); /* Начинает согласованный снимок с нулевых масок. */ + snapshot->busy = owned ? busy() : 0; saved = __get_PRIMASK(); __disable_irq(); /* Не допускает изменения поканальных счётчиков во время копирования диагностического снимка. */ + for (i = 0; i < 8; ++i) { /* Преобразует физическую разводку в прежние биты 0…7 общего протокола. */ + unsigned in = !!(pins[i].input->IDR & pins[i].input_pin), out = !!(pins[i].output->IDR & pins[i].output_pin); /* Читает электрические уровни, включая реальную инверсию ACK. */ + if (owned && service.active.enabled) { if (applied.source ? out : in) snapshot->tx |= (uint8_t)(1u << i); if (applied.source ? in : out) snapshot->rx |= (uint8_t)(1u << i); } /* В генераторе выход означает TX, а вход принимает Vstat настоящего драйвера; в эмуляторе направления обратные. */ + snapshot->count[i] = edge_count[i]; snapshot->overrun[i] = overruns[i]; /* Публикует только диагностические счётчики ISR, не выдавая их за измерение ширины импульса. */ + } /* Завершает копирование одного согласованного снимка. */ + __set_PRIMASK(saved); /* Возвращает прежнее разрешение прерываний после короткой критической секции. */ +} /* Завершает чтение без изменения сигналов. */ +static void clear(void *context) /* Очищает диагностические счётчики без остановки аппаратной генерации. */ +{ /* Сброс не меняет compare, разрешение выходов и текущий Vin. */ + unsigned i; uint32_t saved = __get_PRIMASK(); (void)context; __disable_irq(); /* Защищает все восемь пар счётчиков от конкурентного capture IRQ. */ + for (i = 0; i < 8; ++i) { edge_count[i] = 0; overruns[i] = 0; } /* Устанавливает новый нуль отсчёта диагностики. */ + __set_PRIMASK(saved); /* Восстанавливает состояние прерываний вызывающего контекста. */ +} /* Завершает диагностический сброс без формирования фронта. */ +static void capture_irq(unsigned ch) /* Обслуживает только факт захвата внешнего фронта данного аппаратного канала. */ +{ /* Ни задержка ACK, ни ширина импульса здесь не вычисляются и не переключаются. */ + if (pins[ch].timer) { TIM_TypeDef *t = pins[ch].timer; uint32_t status = t->SR; /* Сохраняет флаги до чтения CCR1, которое подтверждает capture. */ + if (status & TIM_SR_CC1IF) { (void)t->CCR1; ++edge_count[ch]; if (status & TIM_SR_CC1OF) ++overruns[ch]; t->SR = ~TIM_SR_CC1OF; } /* Отмечает потерю capture при слишком частых фронтах, не скрывая диагностическую перегрузку CPU. */ + } else { HRTIM_Timerx_TypeDef *t = &HRTIM1->sTimerxRegs[pins[ch].index]; if (t->TIMxISR & HRTIM_TIMISR_CPT1) { t->TIMxICR = HRTIM_TIMICR_CPT1C; ++edge_count[ch]; } } /* Для HRTIM подтверждает CPT1; выходной crossbar уже сработал независимо от времени входа в ISR. */ +} /* Завершает диагностику фронта без программного воздействия на таймерные сроки. */ +void HRTIM1_TIMD_IRQHandler(void) { capture_irq(0); } /* Привязывает вектор HRTIM D к каналу 1, вход PB6. */ +void HRTIM1_TIME_IRQHandler(void) { capture_irq(1); } /* Привязывает вектор HRTIM E к каналу 2, вход PB7. */ +void HRTIM1_TIMF_IRQHandler(void) { capture_irq(2); } /* Привязывает вектор HRTIM F к каналу 3, вход PB3. */ +void TIM2_IRQHandler(void) { capture_irq(3); } /* Привязывает независимый TIM2 к каналу 4, вход PD3. */ +void TIM3_IRQHandler(void) { capture_irq(4); } /* Привязывает независимый TIM3 к каналу 5, вход PB4. */ +void TIM4_IRQHandler(void) { capture_irq(5); } /* Привязывает независимый TIM4 к каналу 6, вход PD12. */ +void TIM5_IRQHandler(void) { capture_irq(6); } /* Привязывает независимый TIM5 к каналу 7, вход PA0. */ +void TIM1_BRK_TIM15_IRQHandler(void) { capture_irq(7); } /* Общий вектор используется только TIM15; TIM1 этот порт не включает. */ +void hope_igbt_realtime_init(set_emu_server *instance) /* Запускается после настройки PLL и до первого обслуживания UART/CAN/USB. */ +{ /* Инициализация таймерных часов не включает физические выходы. */ + static const set_igbt_rt_port port = {selected, apply, pulse, levels, stop, sample, clear}; unsigned i; uint32_t began; /* Связывает переносимый сервис с единственным набором аппаратных callbacks. */ + server = instance; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); /* Открывает только необходимые порты, сохраняя их направления до явного применения. */ + __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_TIM4_CLK_ENABLE(); __HAL_RCC_TIM5_CLK_ENABLE(); __HAL_RCC_TIM15_CLK_ENABLE(); __HAL_RCC_HRTIM1_CLK_ENABLE(); /* Все восемь независимых счётчиков получают своё штатное тактирование от дерева 160 МГц. */ + HRTIM1->sCommonRegs.DLLCR = HRTIM_DLLCR_CAL | HRTIM_DLLCR_CALEN | HRTIM_DLLCR_CALRTE_0; began = HAL_GetTick(); /* Запускает и периодически поддерживает DLL; частота её обслуживания не формирует импульсы на CPU. */ + while (!(HRTIM1->sCommonRegs.ISR & HRTIM_ISR_DLLRDY) && HAL_GetTick() - began < 10u) {} /* Ограничивает ожидание готовности DLL десятью миллисекундами при включённом SysTick. */ + dll_ready = !!(HRTIM1->sCommonRegs.ISR & HRTIM_ISR_DLLRDY); set_igbt_rt_init(&service, &port, 0); /* Отказ DLL оставляет сервис читаемым, но блокирует применение точного профиля кодом аппаратной ошибки. */ + for (i = 0; i < server->map.count; ++i) if (server->regions[i].base == SET_EMU_IGBT_BASE) { /* Заменяет существующий регион до первого транспортного запроса, сохраняя адрес и остальные сервисы. */ + server->regions[i].version = 4; server->regions[i].context = &service; server->regions[i].read = set_igbt_rt_read; server->regions[i].write = set_igbt_rt_write; /* Общие RTU, SET v2 и CAN используют один аппаратный экземпляр без копии протокола. */ + } /* Завершает регистрацию окна IGBT 0x1400…0x146F версии 4. */ +} /* Завершает аппаратную регистрацию без автоматического включения модели. */ +void hope_igbt_realtime_poll(void) /* Вызывается после миллисекундного шага только для контроля владения режимом. */ +{ /* Аппаратная генерация уже идёт независимо от частоты вызова этой функции. */ + if (owned && !selected(0)) { stop(0); service.active.enabled = service.staged.enabled = 0; service.active.tx = service.staged.tx = 0; owned = 0; } /* Смена режима выключает физический IGBT в том же проходе, в котором общий сервер применил новый режим. */ +} /* Завершает контроль режима без программного формирования ACK или TX. */ diff --git a/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.h b/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.h new file mode 100644 index 0000000..502b159 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.h @@ -0,0 +1,6 @@ +#ifndef HOPE_IGBT_REALTIME_PORT_H /* Исключает повторное объявление привязки наносекундного IGBT к HOPE. */ +#define HOPE_IGBT_REALTIME_PORT_H /* Отделяет аппаратные таймеры от общего миллисекундного сервера. */ +#include "set_emu_server.h" /* Передаёт существующую карту регистров без нового транспорта или копии сервера. */ +void hope_igbt_realtime_init(set_emu_server *); /* Подменяет окно IGBT до первого обращения к карте; выходы остаются выключенными. */ +void hope_igbt_realtime_poll(void); /* Останавливает таймеры при выходе из режима IGBT; формы импульсов основной цикл не обслуживает. */ +#endif /* Завершает интерфейс аппаратного порта HOPE. */ diff --git a/c/set-protocol/src/set_igbt_realtime.c b/c/set-protocol/src/set_igbt_realtime.c new file mode 100644 index 0000000..830a93f --- /dev/null +++ b/c/set-protocol/src/set_igbt_realtime.c @@ -0,0 +1,109 @@ +#include "set_igbt_realtime.h" /* Подключает независимый протокол аппаратных импульсов с временами в наносекундах. */ +#include /* Использует определённое обнуление состояния и сравнение подготовленных конфигураций. */ +void set_igbt_rt_init(set_igbt_rt *s, const set_igbt_rt_port *port, void *context) /* Инициализация выполняется только при остановленном аппаратном порте. */ +{ /* Начинает создание полностью выключенного сервиса версии 4. */ + unsigned i; /* Обходит восемь каналов в порядке битов протокола. */ + memset(s, 0, sizeof(*s)); s->port = *port; s->context = context; /* Сбрасывает также padding, чтобы последующее сравнение конфигураций было воспроизводимым. */ + for (i = 0; i < SET_IGBT_RT_CHANNELS; ++i) { /* Назначает профиль 1SP0635 каждому независимому каналу. */ + set_igbt_rt_channel *c = &s->staged.channel[i]; /* Изменяет только подготовленные задания, не включая выходы. */ + c->flags = 3; c->edges = 3; /* Разрешает обработку Vin и выдачу ACK на оба фронта после явного применения. */ + c->on_delay = c->off_delay = 250; c->on_width = c->off_width = 700; /* Использует типичные задержку и ширину из паспорта 1SP0635x2x1-33, нс. */ + c->tx_width = 10000; /* Одиночный физический TX по умолчанию длится 10 мкс, заданные как 10000 нс. */ + } /* Завершает подготовку восьми одинаковых, но независимо изменяемых каналов. */ + s->active = s->staged; /* Начальный применённый профиль также выключен, поэтому инициализация не создаёт фронтов. */ +} /* Завершает инициализацию без вызова аппаратных callbacks. */ +unsigned set_igbt_rt_validate(const set_igbt_rt_config *cfg) /* Проверяет полный профиль перед атомарным применением оборудования. */ +{ /* Все проверки выполняются до остановки или перенастройки работающих таймеров. */ + unsigned i; /* Номер проверяемого независимого канала. */ + if (!cfg || cfg->enabled > 1 || cfg->source > 1 || cfg->tx > 255 || cfg->fault > 255) return 3; /* Отклоняет неверные переключатели и маски шире восьми каналов кодом Illegal Value. */ + for (i = 0; i < SET_IGBT_RT_CHANNELS; ++i) { /* Проверяет даже выключенные каналы, чтобы последующее разрешение имело однозначные параметры. */ + const set_igbt_rt_channel *c = &cfg->channel[i]; /* Читает подготовленные поля без изменения профиля. */ + if (c->flags > 7 || c->mode > 3 || c->edges > 3 || c->compensation > 1000) return 3; /* Ограничивает битовые поля и измеренную компенсацию величиной 1000 нс. */ + if (c->on_delay < SET_IGBT_RT_MIN_NS || c->on_delay > SET_IGBT_RT_MAX_NS || c->off_delay < SET_IGBT_RT_MIN_NS || c->off_delay > SET_IGBT_RT_MAX_NS) return 3; /* Задержки находятся в аппаратном диапазоне 50…60000 нс. */ + if (c->on_width < SET_IGBT_RT_MIN_NS || c->on_width > SET_IGBT_RT_MAX_NS || c->off_width < SET_IGBT_RT_MIN_NS || c->off_width > SET_IGBT_RT_MAX_NS) return 3; /* Любая ширина ACK также должна выдерживать ограничения аппаратных compare. */ + if (c->tx_width < SET_IGBT_RT_MIN_NS || c->tx_width > SET_IGBT_RT_MAX_NS || c->fault_width > SET_IGBT_RT_MAX_NS) return 3; /* TX длится 50…60000 нс; нулевой FAULT означает удержание ответа. */ + if (c->fault_width && c->fault_width < SET_IGBT_RT_MIN_NS) return 3; /* Запрещает конечный FAULT короче минимального аппаратного импульса. */ + if ((c->edges & 1) && c->on_delay < c->compensation + SET_IGBT_RT_MIN_NS) return 3; /* После вычитания измеренной задержки входа для ON остаётся минимум 50 нс таймерного интервала. */ + if ((c->edges & 2) && c->off_delay < c->compensation + SET_IGBT_RT_MIN_NS) return 3; /* Применяет то же ограничение к реально разрешённому OFF-фронту. */ + if (!cfg->source && c->edges == 3 && (c->on_delay != c->off_delay || c->on_width != c->off_width)) return 3; /* Один аппаратный триггер обоих фронтов требует одинаковых ON/OFF; обработчик ISR не подменяет точные времена. */ + if (cfg->source && (cfg->tx & (1u << i)) && !(c->flags & 1)) return 3; /* Удержание физического TX допускается только у разрешённого передающего канала. */ + } /* Завершает предварительную проверку без побочных эффектов. */ + return 0; /* Разрешает аппаратному порту применить весь согласованный профиль. */ +} /* Завершает проверку протокольных и общих аппаратных ограничений. */ +unsigned set_igbt_rt_read(void *context, uint16_t offset, uint16_t count, uint16_t *out) /* Возвращает задания и один согласованный аппаратный снимок. */ +{ /* Чтение одинаково доступно в выбранном IGBT и в остальных режимах платы. */ + set_igbt_rt *s = (set_igbt_rt *)context; set_igbt_rt_snapshot snap = {0}; /* Снимок по умолчанию нулевой, включая отсутствующие аппаратные счётчики. */ + uint16_t words[SET_IGBT_RT_WORDS] = {0}; unsigned i; /* Формирует прежние 112 слов без увеличения максимального кадра RTU. */ + if (!s || !out || !count || (uint32_t)offset + count > SET_IGBT_RT_WORDS) return 2; /* Проверяет весь диапазон до обращения к аппаратному порту. */ + s->port.sample(s->context, &snap); /* Читает GPIO и счётчики, не формируя импульсы и не сбрасывая события. */ + words[0] = 0x4947; words[1] = 4; words[2] = s->active.enabled; /* Сигнатура IG и версия 4 запрещают старому GUI трактовать наносекунды как миллисекунды. */ + words[3] = snap.tx; words[4] = snap.rx; words[5] = (uint16_t)(31u | (memcmp(&s->staged, &s->active, sizeof(s->active)) ? 0x8000u : 0u)); /* Возможности дополняет старший бит наличия неприменённых заданий. */ + words[6] = s->staged.fault; words[7] = SET_IGBT_RT_MIN_NS; words[8] = SET_IGBT_RT_MAX_NS; /* Публикует подготовленную маску FAULT и диапазон времён в наносекундах. */ + words[10] = SET_IGBT_RT_CHANNELS; words[11] = 0; words[12] = s->staged.source; /* Восемь каналов; код единиц 0 означает наносекунды только для версии 4. */ + words[13] = s->active.tx; words[14] = snap.busy; words[15] = 12; /* Возвращает удерживаемый TX, занятость одиночных импульсов и размер блока канала. */ + for (i = 0; i < SET_IGBT_RT_CHANNELS; ++i) { /* Упаковывает каждый канал без зависимости от выравнивания структуры C. */ + const set_igbt_rt_channel *c = &s->staged.channel[i]; unsigned b = 16 + 12 * i; /* Вычисляет протокольное смещение канала в 16-битных словах. */ + words[b] = c->flags; words[b + 1] = c->mode; words[b + 2] = c->on_delay; words[b + 3] = c->on_width; /* Сохраняет прежние смещения флагов, режима и ON-времён. */ + words[b + 4] = c->off_delay; words[b + 5] = c->off_width; words[b + 6] = c->tx_width; /* OFF и физический TX передаются целыми наносекундами. */ + words[b + 7] = c->edges; words[b + 8] = c->fault_width; words[b + 9] = snap.count[i]; /* Маска фронтов, FAULT и счётчик фактически захваченных событий. */ + words[b + 10] = c->compensation; words[b + 11] = snap.overrun[i]; /* Последние слова версии 4 означают компенсацию, нс, и потери захвата, а не остатки миллисекунд. */ + words[9] = (uint16_t)(words[9] + snap.count[i]); /* Общий счётчик складывает события восьми каналов по модулю 65536. */ + } /* Завершает сборку одного снимка регистрами. */ + for (i = 0; i < count; ++i) out[i] = words[offset + i]; /* Копирует только предварительно проверенный диапазон в буфер вызывающего транспорта. */ + return 0; /* Подтверждает чтение без изменения заданий или выхода. */ +} /* Завершает обработчик FC03 для аппаратного IGBT. */ +unsigned set_igbt_rt_write(void *context, uint16_t offset, uint16_t value) /* Разделяет подготовку конфигурации и команды с физическим действием. */ +{ /* Вызывается последовательно из общего сервера, аппаратные ISR меняют только счётчики. */ + set_igbt_rt *s = (set_igbt_rt *)context; unsigned error, i; /* Общий код ошибки не скрывает аппаратный отказ настройки. */ + if (!s) return 3; /* Не обращается к callbacks отсутствующего экземпляра. */ + if (offset == 2 && value == 0) { s->port.stop(s->context); s->active.enabled = s->staged.enabled = 0; s->active.tx = s->staged.tx = 0; if (!set_igbt_rt_validate(&s->staged)) s->active = s->staged; return 0; } /* Явный останов всегда доступен; корректные выключенные задания сохраняются без включения оборудования. */ + if (!s->port.selected(s->context)) return 6; /* Запрещает запуск и подготовку аппаратного IGBT из другого режима платы. */ + if (offset >= 16) { /* Поканальные записи изменяют только подготовленную копию до команды применения. */ + unsigned ch = (offset - 16) / 12, field = (offset - 16) % 12; uint16_t *target = 0; /* Раскладывает смещение на номер канала и поле, не полагаясь на layout структуры. */ + set_igbt_rt_channel *c; /* Выбирается только после проверки границ массива каналов. */ + if (ch >= SET_IGBT_RT_CHANNELS) return 2; /* Область за восьмым каналом не должна изменять память сервиса. */ + c = &s->staged.channel[ch]; /* Выбирает подготовленный канал только после проверки границ массива. */ + switch (field) { /* Явно сопоставляет доступные на запись поля протоколу версии 4. */ + case 0: if (value > 7) return 3; target = &c->flags; break; /* Разрешает только три флага TX/RX/инверсии ACK. */ + case 1: if (value > 3) return 3; target = &c->mode; break; /* Ограничивает режимы автоматическим ACK, 0, 1 и пропуском ответа. */ + case 2: target = &c->on_delay; break; /* Подготавливает задержку ACK после фронта ON, нс. */ + case 3: target = &c->on_width; break; /* Подготавливает ширину ACK после фронта ON, нс. */ + case 4: target = &c->off_delay; break; /* Подготавливает задержку ACK после фронта OFF, нс. */ + case 5: target = &c->off_width; break; /* Подготавливает ширину ACK после фронта OFF, нс. */ + case 6: target = &c->tx_width; break; /* Подготавливает длительность одиночного физического TX, нс. */ + case 7: if (value > 3) return 3; target = &c->edges; break; /* Выбирает ни одного, ON, OFF либо оба фронта внешнего Vin. */ + case 8: target = &c->fault_width; break; /* Нуль означает удержание FAULT; остальные допустимые значения задают длительность в нс. */ + case 10: if (value > 1000) return 3; target = &c->compensation; break; /* Измеренная компенсация входного тракта не превышает 1000 нс. */ + default: return 2; /* Счётчики и диагностика потерь доступны только на чтение. */ + } /* Завершает выбор единственного изменяемого подготовленного поля. */ + if ((field >= 2 && field <= 6) && (value < SET_IGBT_RT_MIN_NS || value > SET_IGBT_RT_MAX_NS)) return 3; /* Проверяет диапазон наносекунд до изменения задания. */ + if (field == 8 && (value > SET_IGBT_RT_MAX_NS || (value && value < SET_IGBT_RT_MIN_NS))) return 3; /* Разрешает только удерживаемый либо воспроизводимый аппаратный FAULT. */ + *target = value; return 0; /* Сохраняет подготовленное значение, не перенастраивая ни один работающий таймер. */ + } /* Завершает обработку поканальных заданий. */ + switch (offset) { /* Общие регистры отделяют подготовку масок от физического пуска. */ + case 2: /* Единица в регистре включения является завершающей командой применения всего профиля. */ + if (value != 1) return 3; /* Отклоняет неизвестную команду включения до изменения подготовленного профиля. */ + s->staged.enabled = 1; /* Подготавливает включение только после проверки значения команды. */ + error = set_igbt_rt_validate(&s->staged); if (error) return error; /* Проверяет весь профиль до передачи его аппаратуре. */ + error = s->port.apply(s->context, &s->staged); if (error) return error; /* Аппаратный порт также проверяет выполнимость перед изменением выходов. */ + s->active = s->staged; return 0; /* Подтверждает включение только после успешной настройки всех восьми каналов. */ + case 6: if (value > 255) return 3; s->staged.fault = value; return 0; /* Маска неисправностей применяется вместе с последней командой включения. */ + case 9: if (value) return 3; s->port.clear(s->context); return 0; /* Нулевая запись очищает счётчики без изменения формы сигнала. */ + case 12: if (value > 1) return 3; s->staged.source = value; return 0; /* Выбор внешнего Vin либо физического TX вступает в силу при применении. */ + case 13: { /* Постоянные уровни TX доступны только в уже применённом режиме физического генератора. */ + set_igbt_rt_snapshot snap = {0}; /* Получает занятость аппаратных импульсов перед ручным изменением уровней. */ + if (value > 255) return 3; /* Маска ручных уровней не может выходить за восемь физических каналов. */ + if (!s->active.enabled || !s->active.source) return 6; /* Исключает ручное управление ACK и выключенными выходами. */ + s->port.sample(s->context, &snap); if (snap.busy) return 6; /* Не обрывает аппаратный импульс командой удержания уровня. */ + for (i = 0; i < SET_IGBT_RT_CHANNELS; ++i) if ((value & (1u << i)) && !(s->active.channel[i].flags & 1)) return 3; /* Удерживаемые высокие уровни требуют разрешённого TX. */ + s->port.levels(s->context, (uint8_t)value); s->active.tx = s->staged.tx = value; return 0; /* Изменяет только постоянные уровни, сохраняя применённые длительности. */ + } /* Завершает команду ручных уровней генератора. */ + case 14: /* Одиночный импульс запускается только из согласованного применённого профиля. */ + if (!value || value > 255) return 3; /* Требует хотя бы один из восьми физических каналов без старших битов. */ + if (!s->active.enabled || !s->active.source) return 6; /* Импульс возможен только у включённого аппаратного генератора. */ + if (memcmp(&s->staged, &s->active, sizeof(s->active)) != 0 || (value & s->active.tx)) return 6; /* Запрещает пуск с неприменёнными заданиями и повторный фронт при удерживаемом TX=1. */ + for (i = 0; i < SET_IGBT_RT_CHANNELS; ++i) if ((value & (1u << i)) && !(s->active.channel[i].flags & 1)) return 6; /* Предварительно проверяет все выбранные каналы до первого физического пуска. */ + return s->port.pulse(s->context, (uint8_t)value); /* Старт разрешённых каналов выполняется аппаратным портом, GUI не отмеряет длительность. */ + default: return 2; /* Версия, возможности, снимки и прежние глобальные миллисекундные поля недоступны на запись. */ + } /* Завершает маршрутизацию команды общего регистра. */ +} /* Завершает обработчик FC06 аппаратного IGBT. */ diff --git a/python/set_devices/qt_ports/igbt_client.py b/python/set_devices/qt_ports/igbt_client.py new file mode 100644 index 0000000..132bdd0 --- /dev/null +++ b/python/set_devices/qt_ports/igbt_client.py @@ -0,0 +1,154 @@ +"""Асинхронный обмен IGBT с определением версии, записью и проверкой состояния.""" # Общий клиент последовательно проверяет плату, записывает задания и подтверждает результат полным чтением. +import struct # Распаковывает сетевые 16-битные регистры независимо от порядка байтов компьютера. + +from .qt_compat import QObject, QTimer, Signal, QSerialPort # Клиент общий для Qt6 и Qt5 и не импортирует приложение-потребитель. +from set_devices.qt_ports.stm_settings import request # Использует штатный формирователь кадров RTU управления STM. +from set_devices.tms_terminal import crc16_modbus # Проверяет CRC тем же общим алгоритмом, что остальные последовательные клиенты. +from .emulator_serial import EmulatorSerialPort, response_timeout # Тот же транспорт SET v2/RTU обслуживает эмуляторы всех поддержанных MCU. + + +class IgbtClient(QObject): # Один Qt-автомат обслуживает версии 3/мс и 4/нс без параллельного захвата COM. + finished = Signal(bool, str, object) # Возвращает успех, русское описание и подтверждённые 112 слов состояния либо None. + + def __init__(self, parent=None): # Родитель QObject связывает время жизни клиента и панели приложения. + super().__init__(parent) # Регистрирует объект в дереве Qt без открытия подключения к плате. + self.serial = EmulatorSerialPort(self) # Создаёт общий транспорт MCU с определением SET v2/RTU и проверкой DEVICE_INFO. + self.serial.readyRead.connect(self._receive) # Направляет произвольные порции принятых байтов в единственный приёмный автомат. + self.serial.errorOccurred.connect(self._error) # Обрабатывает физические ошибки COM отдельно от протокольных исключений. + self.timer = QTimer(self) # Создаёт таймер ожидания ответа транспорта, который не отмеряет длительности импульсов. + self.timer.setSingleShot(True) # Один запрос имеет один предельный срок ожидания до явного перезапуска. + self.timer.timeout.connect(lambda: self._finish(False, 'Тайм-аут ответа IGBT')) # По истечении срока освобождает COM и сообщает об отсутствии ответа платы. + self.active = False # До явного start клиент не удерживает COM и не принимает ответы как действующие. + self.rx = bytearray() # Накапливает фрагментированный RTU-кадр без предположения о границах readyRead. + + def start(self, port, unit, baud, timeout, writes=(), *, expected_version=None): # Ожидаемая версия фиксирует единицы времени, которые пользователь видел перед записью. + if self.active: # Повторный start не должен смешивать адреса и ответы двух выполняемых планов. + return # Прекращает текущую ветвь автомата после завершения действия либо отказа. + self.writes = list(writes) # Сохраняет план, но допускает его к отправке только после чтения версии платы. + self.expected_version = expected_version if expected_version is not None else 3 # Прежний публичный API записи по умолчанию использует миллисекунды версии 3. + if any(not isinstance(off, int) or not isinstance(value, int) or not 0 <= off < 112 or not 0 <= value <= 65535 for off, value in self.writes): # До открытия COM проверяет только тип и размер протокольного слова; единицы определит probe. + self.finished.emit(False, 'Недопустимые параметры IGBT', None) # Неверный план отвергается до открытия порта и не имеет физических действий. + return # Не продолжает start после отказа предварительной проверки. + self.unit, self.timeout_ms = unit, timeout # Сохраняет адрес устройства и пользовательский тайм-аут в миллисекундах ожидания связи. + self.active, self.write_sent = True, False # Отмечает активный сеанс и сбрасывает признак уже отправленных записей. + self.stage = 'probe' # Начинает новый сеанс чтением версии, чтобы не переносить единицы времени с прежней платы. + self.serial.setPortName(port) # Применяет выбранный пользователем COM к общему транспорту MCU. + self.serial.setBaudRate(baud) # Сохраняет выбранную скорость UART в бодах; для USB CDC это не скорость USB. + self.serial.setDataBits(QSerialPort.DataBits.Data8) # Устанавливает восемь бит данных штатного формата UART 8N1. + self.serial.setParity(QSerialPort.Parity.NoParity) # Отключает бит чётности согласно формату UART эмулятора. + self.serial.setStopBits(QSerialPort.StopBits.OneStop) # Задаёт один стоп-бит штатного 8N1. + self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Эмулятор не требует аппаратных RTS/CTS либо программного управления потоком. + if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite): # Не начинает запрос, если COM занят либо не может быть открыт для чтения и записи. + self._finish(False, self.serial.errorString()) # Передаёт точную аппаратную ошибку Qt и освобождает состояние автомата. + return # Не отправляет запрос после отказа открытия COM. + self._send(3, 0x1400, 16) # FC03 читает 16 слов заголовка по адресу 0x1400 до первой возможной FC06-записи. + + def _valid_writes(self, version): # Проверяет диапазоны в единицах уже опознанной прошивки, сохраняя прежние ограничения версии 3. + limits = {2: (0, 1), 6: (0, 255), 9: (0, 0), 12: (0, 1), 13: (0, 255), 14: (1, 255)} # Общие команды имеют одинаковые адреса, но их физический смысл определяется версией. + if version == 3: # Старая логическая модель продолжает принимать только миллисекундные значения. + limits.update({7: (0, 1000), 8: (1, 255)}) # Прежние общие задержка и ширина существуют только у модели версии 3. + timing = [(50, 60000)] * 5 if version == 4 else [(0, 1000), (1, 1000), (0, 1000), (1, 1000), (1, 60000)] # Версия 4 принимает реальные интервалы 50…60000 нс, без преобразования в мс. + fields = [(0, 7), (0, 3), *timing, (0, 3), (0, 60000)] # Формирует поканальные ограничения флагов, режима, времён, фронтов и FAULT. + for ch in range(8): # Проверяет все восемь независимых блоков по 12 регистров. + limits.update({16 + ch * 12 + field: bounds for field, bounds in enumerate(fields)}) # Сохраняет прежние смещения без зависимости от выбранного MCU. + if version == 4: # Измеренная компенсация входного тракта отсутствует в миллисекундной модели. + limits[26 + ch * 12] = (0, 1000) # Поле 10 блока версии 4 задаёт компенсацию в наносекундах. + return all(off in limits and limits[off][0] <= value <= limits[off][1] and not (version == 4 and off >= 16 and (off - 16) % 12 == 8 and 0 < value < 50) for off, value in self.writes) # Ненулевой FAULT также должен воспроизводиться аппаратным таймером. + + def _send(self, function, register, value): # Отправляет один кадр и устанавливает ожидание соответствующего ответа. + self.rx.clear() # Удаляет остаток прежнего кадра перед отдельным новым запросом. + self.packet = request(self.unit, function, register, value) # Сохраняет точные байты запроса для проверки адреса, длины и echo записи. + self.write_sent |= function == 6 # FC06 помечает возможность частично изменённых либо подготовленных заданий при дальнейшей потере связи. + self.timer.start(response_timeout(self.serial, self.timeout_ms)) # Добавляет транспортное время определения SET v2/RTU к ожиданию ответа IGBT. + if self.serial.write(self.packet) != len(self.packet): # Неполная передача кадра не может подтверждаться как записанная команда. + self._finish(False, 'Ошибка передачи IGBT') # Завершает сеанс при отказе записи байтов и не продолжает план. + + def _receive(self): # Собирает ответы COM независимо от фрагментации, проверяя CRC, длину и прочитанную версию. + chunk = bytes(self.serial.readAll()) # Получает текущую порцию байтов до проверки активного состояния сеанса. + if not self.active: # Поздние байты завершённого обмена не могут изменить его уже сообщённый результат. + return # Не анализирует байты вне активной транзакции. + self.rx.extend(chunk) # Сохраняет неполный ответ до получения оставшихся байтов. + function = self.packet[1] # Ожидает ту же FC03 либо FC06, которая реально отправлена текущим запросом. + while len(self.rx) >= 3: # Минимальные три байта определяют адрес, функцию и длину ответа. + if self.rx[0] != self.unit or self.rx[1] not in (function, function | 0x80): # Ищет ответ нужного узла и ожидаемой функции, включая код исключения с битом 0x80. + del self.rx[0] # Пропускает один чужой байт, сохраняя возможность найти правильный заголовок далее. + continue # Продолжает синхронизацию кадра после удаления постороннего байта. + size = 5 if self.rx[1] & 0x80 else (5 + self.rx[2] if function == 3 else 8) # Исключение занимает 5 байт, FC03 содержит byte-count плюс 5 служебных байт, echo FC06 занимает 8 байт. + if len(self.rx) < size: # Незавершённый кадр остаётся в буфере до следующего readyRead. + return # Ожидает недостающие байты, не объявляя неполный ответ ошибочным. + raw = bytes(self.rx[:size]) # Проверяет ровно ожидаемый кадр без включения следующих байтов потока. + self.timer.stop() # После полного кадра прекращает ожидание текущего запроса до его проверки. + if crc16_modbus(raw): # Ненулевой остаток Modbus CRC означает повреждённый ответ. + self._finish(False, 'Неверная CRC ответа IGBT') # Не разрешает дальнейшие записи после ошибки целостности ответа. + return # Прекращает обработку повреждённого кадра. + if raw[1] & 0x80: # Старший бит функции обозначает протокольное исключение устройства. + self._finish(False, f'Устройство отклонило запрос IGBT, код {raw[2]}') # Показывает реальный код отказа платы вместо ложного подтверждения задания. + return # Прекращает транзакцию после исключения устройства. + if function == 6: # Запись FC06 требует буквального совпадения echo с адресом и значением запроса. + if raw != self.packet: # Иное echo не может продвинуть последовательность подготовленных команд. + self._finish(False, 'Неверное подтверждение записи IGBT') # Сообщает отказ подтверждения записи и освобождает COM. + return # Не продолжает план после неверного echo. + self._next() # Переходит к следующей записи либо полному чтению подтверждённого результата. + return # Один вызов обработчика подтверждает только один ожидаемый кадр. + count = int.from_bytes(self.packet[4:6], 'big') # Число слов извлекается из реально переданного FC03 в сетевом порядке байтов. + if len(raw) != 5 + count * 2 or raw[2] != count * 2: # Проверяет и общую длину кадра, и byte-count, где каждое слово занимает два байта. + self._finish(False, 'Неверная длина состояния IGBT') # Не распаковывает короткий либо чрезмерный снимок состояния. + return # Прекращает обработку кадра неверной длины. + words = struct.unpack(f'>{count}H', raw[3:-2]) # Распаковывает ровно count беззнаковых 16-битных регистров big-endian, исключая адрес, заголовок и CRC. + version = words[1] # Версия задаёт единицы времени и проверяется заново при каждом открытии COM. + if words[0] != 0x4947 or version not in (3, 4) or words[10:12] != (8, 0 if version == 4 else 1) or words[15] != 12: # Принимает только известные структуры и явный код наносекунд у аппаратной версии 4. + self._finish(False, 'Требуется IGBT версии 3 (мс) или версии 4 (аппаратные нс) с 8 каналами') # Не допускает управление неизвестным форматом регистров. + return # Не управляет неизвестной структурой либо кодом единиц времени. + if self.stage == 'probe': # Заголовок должен подтвердить версию и единицы до чтения полного состояния и FC06. + self.interface_version = version # Сохраняет результат probe для проверки полного снимка и запрета смены единиц посреди обмена. + if self.writes and any(item != (2, 0) for item in self.writes) and version != self.expected_version: # Запись времён требует совпадения единиц; безусловный останов одинаков у обеих версий и остаётся доступным. + self._finish(False, 'Версия IGBT и единицы времени изменились; прочитайте состояние и примените задания заново') # Завершает обмен до первой записи, сохраняя выходы платы. + return # Не отправляет подготовленные в других единицах значения. + if not self._valid_writes(version): # Проверяет все записи до первой команды FC06. + self._finish(False, 'Недопустимые параметры IGBT для прочитанной версии прошивки') # Ошибка диапазона также не оставляет частично отправленного задания. + return # Не начинает запись неверного профиля. + self.stage = 'state' # После probe начинает этап полного чтения известных поканальных заданий. + self._send(3, 0x1400, 112) # Полный FC03 запрашивает 112 слов окна IGBT, включая все восемь каналов. + return # Ожидает полный снимок отдельным следующим ответом. + if version != self.interface_version: # Не допускает подмену протокола между заголовком и полным чтением. + self._finish(False, 'Версия IGBT изменилась во время обмена; повторите чтение') # Прерывает транзакцию до дальнейших записей или ложного подтверждения. + return # Требует нового определения единиц времени. + if self.stage == 'state' and self.writes: # Записи выполняются только после успешного полного чтения известной версии. + self.pending = list(self.writes) # Копирует проверенный план, сохраняя исходный список для сравнения чтением. + self.stage = 'write' # Отмечает этап последовательного выполнения FC06. + self._next() # Отправляет первую подготовленную команду после завершения всех предварительных чтений. + else: # Ветка без новых записей либо после их выполнения возвращает фактический снимок. + # Счётчик может измениться сразу после сброса из-за нового внешнего фронта. + expected = dict(self.writes) # При повторной записи одного смещения проверяется последнее отправленное значение. + if any(words[off] != value for off, value in expected.items() if off not in (9, 14)): # Команда импульса 14 и изменяемый фронтами счётчик 9 исключены из буквального readback; остальные задания должны совпасть. + self._finish(False, 'Записанные параметры IGBT не подтвердились') # Отклоняет успешное echo, если последующее чтение не подтвердило задания. + else: # Подтверждение выдаётся только после сравнения фактических регистров с планом. + self._finish(True, 'IGBT: параметры записаны и проверены' if self.writes # Отличает успешную запись с проверкой от обычного чтения без физических действий. + else 'IGBT: состояние прочитано', words) # Передаёт панели полный подтверждённый снимок после чтения либо readback. + return # Завершает обработку одного ожидаемого ответа текущего шага. + + def _next(self): # Продвигает проверенный план без параллельных запросов на общий COM. + if self.pending: # Следующая запись разрешается только после echo предыдущей команды. + off, value = self.pending.pop(0) # Извлекает одно относительное смещение и соответствующее 16-битное значение. + self._send(6, 0x1400 + off, value) # FC06 использует прежнюю абсолютную базу 0x1400 плюс смещение регистра. + else: # После последнего echo начинается подтверждение всего результата чтением. + self.stage = 'verify' # Отмечает этап проверки, чтобы полный ответ уже не запускал новый план записи. + self._send(3, 0x1400, 112) # Подтверждает все восемь каналов одним FC03 на 112 слов. + + def _error(self, error): # Значимые физические ошибки Qt завершают обмен, тайм-ауты обрабатываются собственным таймером. + if self.active and error not in (QSerialPort.SerialPortError.NoError, # Не выдаёт NoError за отказ и не завершает неактивный сеанс поздним сигналом. + QSerialPort.SerialPortError.TimeoutError): # Qt TimeoutError не дублирует собственный контролируемый тайм-аут ответа IGBT. + self._finish(False, self.serial.errorString()) # Показывает точную аппаратную причину отказа COM и освобождает транспорт. + + def cancel(self): # Отмена прерывает текущую транзакцию, не заменяя команду физического останова платы. + self._finish(False, 'Обмен IGBT отменён') # Запрещает дальнейшую отправку плана и сообщает пользователю отмену обмена. + + def _finish(self, ok, message, words=None): # Единая точка завершения освобождает COM, таймер и блокировку UI при любом исходе. + if not self.active: # Поздний timeout либо error не должен отправлять повторный результат завершённого обмена. + return # Не изменяет состояние и не повторяет finished для уже завершённого сеанса. + self.active = False # Снимает блокировку последовательного автомата до уведомления панели. + self.timer.stop() # Прекращает транспортное ожидание, которое уже не относится к активному запросу. + self.serial.close() # Освобождает общий COM для настроек STM и других разрешённых инструментов. + if not ok and self.write_sent: # После отправленной FC06 нельзя считать прежнее состояние платы подтверждённым при потере связи. + message += '. Часть настроек могла примениться; прочитайте состояние повторно.' # Напоминает перечитать действующие и подготовленные задания после частичного обмена. + self.finished.emit(ok, message, words) # Передаёт панели успех, причину отказа и только подтверждённый снимок либо None.