From 491fa11569d849e70af40285b6a03b70853e6502 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 6 Nov 2026 16:33:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B0=D0=BF=D0=BF=D0=B0=D1=80=D0=B0=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=BC=D0=BF=D1=83=D0=BB=D1=8C=D1=81=D1=8B=20IGB?= =?UTF-8?q?T=20=D0=B4=D0=BB=D1=8F=20HOPE=20G474?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/CMakeLists.txt | 1 + c/set-protocol/include/set_igbt_realtime.h | 44 +++ c/set-protocol/ports/stm32g474-hope/board.c | 132 +++++++++ .../stm32g474-hope/board_config.template.h | 48 ++++ .../ports/stm32g474-hope/igbt_realtime_port.c | 203 +++++++++++++ .../ports/stm32g474-hope/igbt_realtime_port.h | 6 + c/set-protocol/src/set_igbt_realtime.c | 109 +++++++ python/set_devices/qt_ports/igbt_client.py | 266 ++++++++++-------- 8 files changed, 684 insertions(+), 125 deletions(-) create mode 100644 c/set-protocol/include/set_igbt_realtime.h create mode 100644 c/set-protocol/ports/stm32g474-hope/board.c create mode 100644 c/set-protocol/ports/stm32g474-hope/board_config.template.h create mode 100644 c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.c create mode 100644 c/set-protocol/ports/stm32g474-hope/igbt_realtime_port.h create mode 100644 c/set-protocol/src/set_igbt_realtime.c diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 3e41d91..0b60de6 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -49,6 +49,7 @@ set(SETPROTOCOL_SOURCES src/set_emu_ump.c src/set_emu_tms.c src/set_emu_igbt.c + src/set_igbt_realtime.c # Подключает отдельный наносекундный протокол версии 4 без изменения старой модели и её ABI. src/set_emu_server.c ${SETPROTOCOL_V2_SOURCES} ${SETPROTOCOL_LEGACY_SOURCES} 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/stm32g474-hope/board.c b/c/set-protocol/ports/stm32g474-hope/board.c new file mode 100644 index 0000000..cf3668f --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/board.c @@ -0,0 +1,132 @@ +#include "board.h" /* Подключает публичный интерфейс аппаратного HAL-слоя HOPE. */ +#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 без миллисекундного обслуживания фронтов. */ +#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. */ +uint32_t SystemCoreClock = 16000000u; /* После reset HSI16 работает на 16 МГц до настройки PLL HAL. */ +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 && !EMU_IGBT_REALTIME /* Штатный профиль использует свободные линии J5/J6 по схеме HOPE от 14.04.2025, сохраняя все три CAN. */ +static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_6,GPIO_PIN_7,GPIO_PIN_14}; /* Vin0…7: J5-13/14/15/22/23, J6-7/8, J5-24; PB6/PB7 заменяют занятые CAN2 линии PB12/PB13. */ +#endif /* Без физического порта все команды IGBT остаются доступны через общую карту регистров. */ +void SystemInit(void) /* Startup вызывает функцию до инициализации глобальных переменных C runtime. */ +{ /* До runtime доступны только константы сборки и регистры, поэтому HAL здесь не вызывается. */ + SCB->CPACR |= 0xFu << 20; /* Разрешает CP10/CP11 FPU: четыре бита доступа 20..23 по ARM Cortex-M4 TRM. */ + SCB->VTOR = EMU_FLASH_BASE; /* Переносит векторы на 0x08008000 для CANdle либо 0x08000000 для самостоятельного образа. */ + __DSB(); /* Завершает запись VTOR до потенциальной доставки аппаратного исключения. */ + __ISB(); /* Обновляет контекст инструкций после включения FPU и смены таблицы векторов. */ + SET_BIT(RCC->CR, RCC_CR_HSION); /* Включает HSI16, поскольку CANdle boot передаёт управление с PLL 144 МГц без сброса RCC. */ + while ((RCC->CR & RCC_CR_HSIRDY) == 0u) {} /* Ждёт готовности внутреннего источника до переключения SYSCLK без зависимости от неинициализированного HAL tick. */ + MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI); /* Переводит SYSCLK на HSI16 до изменения PLL, запрещённого при её использовании ядром. */ + while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI) {} /* Подтверждает аппаратное завершение переключения SYSCLK до отключения прежней PLL. */ + CLEAR_BIT(RCC->CR, RCC_CR_PLLON); /* Выключает PLL предыдущего загрузчика, чтобы HAL мог задать новые M/N/R для 160 МГц. */ + while ((RCC->CR & RCC_CR_PLLRDY) != 0u) {} /* Дожидается фактической остановки PLL перед дальнейшей инициализацией C runtime. */ + CLEAR_BIT(RCC->CFGR, RCC_CFGR_HPRE | RCC_CFGR_PPRE1 | RCC_CFGR_PPRE2); /* Возвращает AHB/APB к делению 1: глобальная SystemCoreClock=16 МГц и первый SysTick HAL совпадают с железом. */ +} /* Завершает минимальную настройку процессора без зависимости от BSS. */ +void SystemCoreClockUpdate(void) /* Поддерживает интерфейс CMSIS, используемый HAL при изменении дерева часов. */ +{ /* Частота определяется фактическим RCC, а не только константой желаемой частоты CPU. */ + RCC_ClkInitTypeDef config; uint32_t latency; /* Получает делитель AHB и обязательный выходной параметр задержки Flash. */ + HAL_RCC_GetClockConfig(&config, &latency); /* Читает активную конфигурацию RCC штатным HAL G4. */ + SystemCoreClock = HAL_RCC_GetSysClockFreq() >> AHBPrescTable[config.AHBCLKDivider >> RCC_CFGR_HPRE_Pos]; /* Переводит SYSCLK в HCLK с аппаратным делителем AHB по RM0440. */ +} /* Завершает обновление частоты ядра в герцах. */ +void SysTick_Handler(void) { HAL_IncTick(); } /* Прерывание только увеличивает счётчик миллисекунд; модели выполняются в main. */ +void board_fatal(uint32_t code) { clock_fault = code; for (;;) {} } /* Останавливает дальнейшую настройку после отказа, сохраняя его причину в Watch. */ +static void peripheral_clock(void) /* Создаёт одинаковую частоту таймера DAC для обоих вариантов Flash. */ +{ /* Штатная PLL использует кварц JT1 8 МГц по схеме HOPE от 14.04.2025; HSI16 остаётся резервным профилем сборки. */ + RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; /* Нулевая инициализация отключает неиспользуемые ветви настройки генераторов и шин. */ + RCC_PeriphCLKInitTypeDef peripheral = {0}; RCC_CRSInitTypeDef crs = {0}; /* Готовит независимые источники USB, USART1 и FDCAN. */ + __HAL_RCC_PWR_CLK_ENABLE(); /* Разрешает HAL доступ к регулятору питания перед повышением частоты. */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST) != HAL_OK) board_fatal(3); /* Boost Range1 требуется для CPU 160 МГц по ограничениям ST. */ + osc.OscillatorType = RCC_OSCILLATORTYPE_HSI | RCC_OSCILLATORTYPE_HSI48; /* Включает HSI16 для CPU и отдельный HSI48 для USB FS. */ + osc.HSIState = RCC_HSI_ON; osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; /* Сохраняет заводскую подстройку внутреннего 16-МГц источника. */ + osc.HSI48State = RCC_HSI48_ON; /* USB получает собственный 48-МГц источник, корректируемый USB SOF через CRS. */ + osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSI; /* Задаёт внутренний источник для резервного профиля; штатная ветвь ниже выбирает HSE. */ + osc.PLL.PLLM = RCC_PLLM_DIV4; /* HSI16 / 4 даёт вход PLL 4 МГц, допустимый для PLLN=80. */ +#if EMU_USE_HSE /* Штатный профиль использует подтверждённый схемой кварц JT1 8 МГц для точности CAN и DAC. */ + osc.OscillatorType |= RCC_OSCILLATORTYPE_HSE; /* Добавляет внешний источник, сохраняя HSI48 для USB. */ + osc.HSEState = EMU_HSE_BYPASS ? RCC_HSE_BYPASS : RCC_HSE_ON; /* Различает внешний генератор и пассивный кварц по настройке платы. */ + osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* Переключает источник PLL, от которого зависят CAN и частота генератора. */ +#if EMU_HSE_HZ == 8000000u /* Кварц 8 МГц требует делитель два для того же входа PLL 4 МГц. */ + osc.PLL.PLLM = RCC_PLLM_DIV2; /* Сохраняет итоговые 160 МГц при HSE 8 МГц. */ +#elif EMU_HSE_HZ == 24000000u /* Кварц 24 МГц требует делитель шесть для входа PLL 4 МГц. */ + osc.PLL.PLLM = RCC_PLLM_DIV6; /* Сохраняет итоговые 160 МГц при HSE 24 МГц. */ +#else /* Другие частоты требуют явного расчёта PLL вместо незаметного неверного тайминга. */ +#error Unsupported HOPE HSE frequency /* Допускает только проверяемые варианты внешнего источника 8 и 24 МГц. */ +#endif /* Завершает выбор делителя по физической частоте кварца. */ +#endif /* При EMU_USE_HSE=0 внешний источник вообще не запускается. */ + osc.PLL.PLLN = 80; osc.PLL.PLLP = RCC_PLLP_DIV2; /* Вход 4 МГц умножается до VCO 320 МГц; неиспользуемый PLLP имеет допустимый делитель. */ + osc.PLL.PLLQ = RCC_PLLQ_DIV2; osc.PLL.PLLR = RCC_PLLR_DIV2; /* PLLR / 2 создаёт CPU 160 МГц; USB не использует PLLQ. */ + if (HAL_RCC_OscConfig(&osc) != HAL_OK) board_fatal(3); /* Не продолжает запуск при отказе генератора или захвата PLL. */ + clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; /* Перенастраивает все шины, от которых зависит используемая периферия. */ + clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; /* CPU и AHB получают 160 МГц без дополнительного деления. */ + clk.APB1CLKDivider = RCC_HCLK_DIV2; clk.APB2CLKDivider = RCC_HCLK_DIV1; /* PCLK1=80 МГц, TIM6=160 МГц и USART1 на PCLK2=160 МГц. */ + if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4) != HAL_OK) board_fatal(3); /* Четыре wait-state соответствуют 160 МГц в Boost Range1 по RM0440. */ + peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB | RCC_PERIPHCLK_FDCAN | RCC_PERIPHCLK_USART1; /* Выбирает только реальные блоки HOPE, исключая USART2 профиля Nucleo. */ + peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; /* Подаёт независимые 48 МГц на USB FS. */ + peripheral.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; /* Подаёт PCLK1 80 МГц на FDCAN; общий порт получает 125000 бит/с из EMU_CAN_BITRATE при 16 квантах на бит и делителе 40. */ + peripheral.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; /* HAL вычисляет делитель USART1 из 160 МГц APB2. */ + if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) board_fatal(3); /* Ошибка выбора kernel clock обнаруживается до включения транспорта. */ + __HAL_RCC_CRS_CLK_ENABLE(); /* Разрешает блок автоматической подстройки HSI48 по USB SOF. */ + crs.Prescaler = RCC_CRS_SYNC_DIV1; crs.Source = RCC_CRS_SYNC_SOURCE_USB; /* Использует каждый входящий SOF USB без деления. */ + crs.Polarity = RCC_CRS_SYNC_POLARITY_RISING; /* Сохраняет рекомендованный HAL фронт синхронизации USB. */ + crs.ReloadValue = __HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000u, 1000u); /* Рассчитывает 47999 тактов HSI48 на период SOF 1 мс. */ + crs.ErrorLimitValue = RCC_CRS_ERRORLIMIT_DEFAULT; crs.HSI48CalibrationValue = RCC_CRS_HSI48CALIBRATION_DEFAULT; /* Использует заводские начальные значения допусков и подстройки. */ + HAL_RCCEx_CRSConfig(&crs); usb_clock_ready = 1; /* Разрешает main запуск CDC после подготовки независимого USB clock. */ +} /* Завершает настройку часов без обращения к Flash Option Bytes. */ +void board_init(void) /* Вызывается после создания общих моделей и миллисекундного HAL tick. */ +{ /* Геометрия VE отличается от RB, но оба состояния DBANK при 512 КиБ имеют непрерывную карту Flash. */ + GPIO_InitTypeDef gpio = {0}; /* Не оставляет неинициализированных параметров альтернативных функций GPIO. */ + __HAL_RCC_SYSCFG_CLK_ENABLE(); /* Даёт доступ к признаку фактической перестановки Flash-банков. */ + if (*(const uint16_t *)FLASHSIZE_BASE != 512u || (FLASH->OPTR & FLASH_OPTR_BFB2) || (SYSCFG->MEMRMP & SYSCFG_MEMRMP_FB_MODE)) board_fatal(5); /* Требует реальные 512 КиБ VE и обычное отображение банков; DBANK не изменяет. */ + peripheral_clock(); /* Стабилизирует частоты до запуска GPIO, USART1, FDCAN и USB. */ + __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); /* Порт A обслуживает USB/UART/DAC, порт D — штатный CAN1, порт B оставлен опциональным GPIO. */ + __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_FDCAN_CLK_ENABLE(); /* Подаёт часы только на выбранный USART1 и общий FDCAN kernel. */ + gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode = GPIO_MODE_AF_PP; /* PA9 TX и PA10 RX подключены через JP2 к CH340N по схеме HOPE от 14.04.2025. */ + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Pull = GPIO_PULLUP; gpio.Alternate = GPIO_AF7_USART1; /* AF7 назначает USART1; подтяжка сохраняет idle RX при отсутствии адаптера. */ + 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 && !EMU_IGBT_REALTIME /* Подключает физический IGBT через свободные выводы, не затрагивая CAN1/2/3, USB, UART, DAC и дисплей. */ + __HAL_RCC_GPIOC_CLK_ENABLE(); /* Порт C нужен только для восьми физических выходов Vstat. */ + gpio.Pin = 0; /* Начинает общую маску Vin с пустого набора, чтобы инициализация и опрос использовали одну таблицу каналов. */ + for (unsigned i = 0; i < 8; ++i) gpio.Pin |= input_pins[i]; /* Объединяет восемь свободных выводов GPIOB; индексы массива задают биты Vin0…7. */ + gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB, &gpio); /* Задаёт нулевой Vin на неподключённых входах выбранного ручного профиля. */ + gpio.Pin = 0xFFu; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Биты 0..7 порта C становятся восемью ответами Vstat IGBT. */ + HAL_GPIO_WritePin(GPIOC, 0xFFu, GPIO_PIN_RESET); HAL_GPIO_Init(GPIOC, &gpio); /* Сначала записывает нули в ODR, исключая импульс при включении выходов. */ +#endif /* Без включённого профиля IGBT GPIO сохраняют своё состояние после reset. */ +#if EMU_RS485_DE /* Направление разрешается только для отдельно подключённого внешнего RS-485. */ + 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 до запуска UART/CAN, сохраняя остальные сервисы сервера. */ +#endif /* Прежний профиль продолжает использовать окно миллисекундной модели версии 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_REALTIME /* Аппаратные таймеры обслуживают все фронты независимо от этого шага. */ + emu_tick(); /* Сохраняет миллисекундное время УМП, 2812, DAC, DEIF и применения выбора режима. */ + hope_igbt_realtime_poll(); /* Выключает аппаратные выходы в том же шаге, в котором выбран другой режим платы. */ +#else /* Сборка совместимости сохраняет прежний программный IGBT с шагом 1 мс. */ +#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Считывает восемь физических Vin штатного профиля HOPE с PB6/PB7 вместо CAN2. */ + 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. */ +#else /* Явное отключение GPIO оставляет модель доступной через регистровый источник TX. */ + igbt_emu_set_tx(0); /* При выборе GPIO задаёт нулевой Vin; регистровый источник остаётся независимым. */ +#endif /* Далее все модели продвигаются одинаково независимо от наличия дискретного порта. */ + emu_tick(); /* Продвигает выбранную модель и общие отложенные настройки ровно на 1 мс. */ +#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 new file mode 100644 index 0000000..1230f6c --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/board_config.template.h @@ -0,0 +1,48 @@ +#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. */ +#define USB_CDC_PRODUCT_NAME "SET HOPE G474VE Emulator" /* Отличает виртуальный COM эмулятора от USB–UART CH340N на плате. */ +#define EMU_PRODUCT_MODEL "STM32G474VE-HOPE-EMU" /* Возвращается в DEVICE_INFO и проверяется host-тестом SET v2. */ +#define EMU_BOOT_REQUEST 0x1003u /* Сохраняет прикладную команду входа в загрузчик из USB SET v2. */ +#define EMU_REQUEST_BOOT(now) g474_request_boot(now) /* Адаптер HOPE записывает CANB в SRAM, совместимую с CANdle. */ +#define EMU_RS_ADDRESS 16u /* Узел управления моделями использует прежний адрес SetGUI, отличный от адреса CMD_STD. */ +#define EMU_TMS_ADDRESS 1u /* Отдельный адрес 2812 позволяет отличать CMD_STD от команд настройки эмулятора. */ +#define EMU_CAN_MODE 5u /* Общий протокол эмулятора использует extended RX 0xBA0004 и TX 0xBA0024. */ +#define EMU_UART_BAUD 115200u /* Начальная скорость приложения в бодах; загрузчик CANdle отдельно использует 512000. */ +#define EMU_CAN_BITRATE 125000u /* Скорость CAN приложения HOPE и DEIF по требованию пользователя: 125000 бит/с; при PCLK1 80 МГц и 16 квантах на бит делитель равен 40. */ +#define EMU_FLAGS_DEFAULT 1u /* После сброса разрешает синтетический сценарий УМП; модель по умолчанию остаётся УМП. */ +#define EMU_UART_INSTANCE USART1 /* Использует USART1 PA9/PA10, подключённый через JP2 к CH340N по схеме HOPE от 14.04.2025. */ +#define EMU_UART_IRQn USART1_IRQn /* Направляет RX/TX HAL в аппаратный вектор USART1 вместо USART2 Nucleo. */ +#define SERIAL_UART_IRQ_HANDLER USART1_IRQHandler /* Общий HAL UART публикует правильное имя вектора startup G474. */ +#define EMU_RS485_DE 0 /* Прямой TTL UART PA9/PA10 не требует управления направлением внешнего трансивера. */ +#define SERIAL_DE_PORT GPIOB /* При ручном включении RS-485 используется внешний DE на порту B. */ +#define SERIAL_DE_PIN GPIO_PIN_15 /* PB15 оставлен только опциональному внешнему DE; встроенный RS-485 HOPE не предполагается. */ +#ifndef EMU_USE_HSE /* Позволяет выбрать внешний кварц через настройки компилятора, не меняя переносимый порт. */ +#define EMU_USE_HSE 1 /* Использует кварц JT1 8 МГц, указанный в найденной схеме G474VET6 от 14.04.2025. */ +#endif /* Завершает выбор источника системной PLL. */ +#ifndef EMU_HSE_HZ /* Частота задаётся в герцах и должна соответствовать фактически установленному кварцу. */ +#define EMU_HSE_HZ 8000000u /* Номинал JT1 равен 8 МГц по схеме; HAL и PLL используют одну частоту в герцах. */ +#endif /* Сохраняет явно переданную частоту внешнего источника. */ +#define EMU_HSE_BYPASS 0 /* Внешний режим рассчитан на кварц; для готового генератора нужно отдельно выбрать bypass. */ +#ifndef EMU_IGBT_GPIO_ENABLE /* Позволяет отключить физический IGBT параметром компилятора при использовании только регистрового TX. */ +#define EMU_IGBT_GPIO_ENABLE 1 /* Включает свободные Vin PB0/1/2/10/11/6/7/14 и Vstat PC0…PC7; CAN2 сохраняет PB12/PB13 по схеме HOPE. */ +#endif /* Сохраняет явный выбор потребителя поверх штатного физического профиля. */ +#ifndef EMU_FLASH_BASE /* Адрес задаётся одинаково в профиле Keil и его scatter-файле. */ +#define EMU_FLASH_BASE 0x08008000u /* Резервирует первые 32 КиБ под найденный CAN/UART-загрузчик CANdle. */ +#endif /* Самостоятельный профиль переопределяет адрес на 0x08000000. */ +#define HOPE_BOOT_REQUEST_ADDRESS 0x20013FFCu /* Совпадает с последним словом 80 КиБ SRAM в boot_profile.h CANdle. */ +#define HOPE_BOOT_REQUEST_MAGIC 0x43414E42u /* ASCII CANB запрашивает вход в существующий CAN/UART-загрузчик после reset. */ +#if EMU_RS_ADDRESS < 1 || EMU_RS_ADDRESS > 247 || EMU_TMS_ADDRESS < 1 || EMU_TMS_ADDRESS > 255 || EMU_TMS_ADDRESS == EMU_RS_ADDRESS /* Исключает недопустимые или совпадающие адреса двух обслуживаемых узлов. */ +#error Invalid HOPE emulator addresses /* Останавливает сборку до получения прошивки с неоднозначной маршрутизацией. */ +#endif /* Завершает проверку адресов протокола. */ +#if EMU_CAN_MODE < 1 || EMU_CAN_MODE > 16 /* Номер режима входит в младшую часть extended CAN ID общего протокола. */ +#error Invalid HOPE CAN mode /* Не допускает CAN ID за пределами поддерживаемых шестнадцати режимов. */ +#endif /* Завершает проверку режима CAN. */ +#if EMU_FLASH_BASE != 0x08000000u && EMU_FLASH_BASE != 0x08008000u /* Поддерживаются только два проверяемых варианта размещения Flash. */ +#error Invalid HOPE vector address /* Ошибка адреса VTOR не должна обнаруживаться только после прошивки платы. */ +#endif /* Завершает проверку размещения таблицы векторов. */ +#endif /* Завершает аппаратный профиль HOPE STM32G474VET6. */ 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 index ab544f1..132bdd0 100644 --- a/python/set_devices/qt_ports/igbt_client.py +++ b/python/set_devices/qt_ports/igbt_client.py @@ -1,138 +1,154 @@ -"""Asynchronous IGBT register transactions with probe and readback.""" -import struct +"""Асинхронный обмен IGBT с определением версии, записью и проверкой состояния.""" # Общий клиент последовательно проверяет плату, записывает задания и подтверждает результат полным чтением. +import struct # Распаковывает сетевые 16-битные регистры независимо от порядка байтов компьютера. from .qt_compat import QObject, QTimer, Signal, QSerialPort # Клиент общий для Qt6 и Qt5 и не импортирует приложение-потребитель. -from set_devices.qt_ports.stm_settings import request -from set_devices.tms_terminal import crc16_modbus +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): - finished = Signal(bool, str, object) +class IgbtClient(QObject): # Один Qt-автомат обслуживает версии 3/мс и 4/нс без параллельного захвата COM. + finished = Signal(bool, str, object) # Возвращает успех, русское описание и подтверждённые 112 слов состояния либо None. - def __init__(self, parent=None): - super().__init__(parent) - self.serial = EmulatorSerialPort(self) - self.serial.readyRead.connect(self._receive) - self.serial.errorOccurred.connect(self._error) - self.timer = QTimer(self) - self.timer.setSingleShot(True) - self.timer.timeout.connect(lambda: self._finish(False, 'Тайм-аут ответа IGBT')) - self.active = False - self.rx = bytearray() + 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=()): - if self.active: - return - limits = {2: (0, 1), 6: (0, 255), 7: (0, 1000), 8: (1, 255), - 9: (0, 0), 12: (0, 1), 13: (0, 255), 14: (1, 255)} - channel_limits = [(0, 7), (0, 3), (0, 1000), (1, 1000), (0, 1000), - (1, 1000), (1, 60000), (0, 3), (0, 60000)] - for ch in range(8): - for field, bounds in enumerate(channel_limits): - limits[16 + ch * 12 + field] = bounds - self.writes = list(writes) - if any(off not in limits or not limits[off][0] <= value <= limits[off][1] - for off, value in self.writes): - self.finished.emit(False, 'Недопустимые параметры IGBT', None) - return - self.unit, self.timeout_ms = unit, timeout - self.active, self.write_sent = True, False - self.stage = 'probe' - self.serial.setPortName(port) - self.serial.setBaudRate(baud) - self.serial.setDataBits(QSerialPort.DataBits.Data8) - self.serial.setParity(QSerialPort.Parity.NoParity) - self.serial.setStopBits(QSerialPort.StopBits.OneStop) - self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) - if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite): - self._finish(False, self.serial.errorString()) - return - self._send(3, 0x1400, 16) + 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 _send(self, function, register, value): - self.rx.clear() - self.packet = request(self.unit, function, register, value) - self.write_sent |= function == 6 - self.timer.start(response_timeout(self.serial, self.timeout_ms)) - if self.serial.write(self.packet) != len(self.packet): - self._finish(False, 'Ошибка передачи IGBT') + 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 _receive(self): - chunk = bytes(self.serial.readAll()) - if not self.active: - return - self.rx.extend(chunk) - function = self.packet[1] - while len(self.rx) >= 3: - if self.rx[0] != self.unit or self.rx[1] not in (function, function | 0x80): - del self.rx[0] - continue - size = 5 if self.rx[1] & 0x80 else (5 + self.rx[2] if function == 3 else 8) - if len(self.rx) < size: - return - raw = bytes(self.rx[:size]) - self.timer.stop() - if crc16_modbus(raw): - self._finish(False, 'Неверная CRC ответа IGBT') - return - if raw[1] & 0x80: - self._finish(False, f'Устройство отклонило запрос IGBT, код {raw[2]}') - return - if function == 6: - if raw != self.packet: - self._finish(False, 'Неверное подтверждение записи IGBT') - return - self._next() - return - count = int.from_bytes(self.packet[4:6], 'big') - if len(raw) != 5 + count * 2 or raw[2] != count * 2: - self._finish(False, 'Неверная длина состояния IGBT') - return - words = struct.unpack(f'>{count}H', raw[3:-2]) - if words[:2] != (0x4947, 3) or words[10:12] != (8, 1) or words[15] != 12: - self._finish(False, 'Требуется новая прошивка с IGBT версии 3 и настройкой каналов') - return - if self.stage == 'probe': - self.stage = 'state' - self._send(3, 0x1400, 112) - return - if self.stage == 'state' and self.writes: - self.pending = list(self.writes) - self.stage = 'write' - self._next() - else: - # The edge counter may advance immediately after being reset. - expected = dict(self.writes) - if any(words[off] != value for off, value in expected.items() if off not in (9, 14)): - self._finish(False, 'Записанные параметры IGBT не подтвердились') - else: - self._finish(True, 'IGBT: параметры записаны и проверены' if self.writes - else 'IGBT: состояние прочитано', words) - return + 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 _next(self): - if self.pending: - off, value = self.pending.pop(0) - self._send(6, 0x1400 + off, value) - else: - self.stage = 'verify' - self._send(3, 0x1400, 112) + 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 _error(self, error): - if self.active and error not in (QSerialPort.SerialPortError.NoError, - QSerialPort.SerialPortError.TimeoutError): - self._finish(False, self.serial.errorString()) + 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 cancel(self): - self._finish(False, 'Обмен IGBT отменён') + 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 _finish(self, ok, message, words=None): - if not self.active: - return - self.active = False - self.timer.stop() - self.serial.close() - if not ok and self.write_sent: - message += '. Часть настроек могла примениться; прочитайте состояние повторно.' - self.finished.emit(ok, message, words) + 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.