Merge origin/ump-v4 into main
Combine Flash CAN updates, signal processing and HOPE pin fixes with current Modbus/GAS, STM32 ports and CCS release exports. Resolve protocol, documentation and Qt conflicts; deduplicate Android JNI sources and include gauge/IGBT sources in host builds.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/kdiff3"]
|
||||
path = third_party/kdiff3
|
||||
url = https://invent.kde.org/sdk/kdiff3.git
|
||||
@@ -63,6 +63,10 @@ templates/
|
||||
|
||||
### Инструменты
|
||||
|
||||
Исходники внешних приложений находятся в [`third_party`](third_party/README.md):
|
||||
[`third_party/kdiff3`](third_party/kdiff3) содержит официальный репозиторий KDiff3
|
||||
как вложенный Git submodule для развития сравнения файлов и каталогов в SETGUI.
|
||||
|
||||
| Инструмент | Что делает |
|
||||
|---|---|
|
||||
| [`tools/firmware-build/fpga`](tools/firmware-build/fpga/README.md) | общий итератор версии и HDL-паспорт для Xilinx ISE, Vivado и Altera/Intel Quartus; контроль `.bit`/`.sof`, manifest, SHA-256 и архив |
|
||||
|
||||
@@ -9,5 +9,5 @@ if(NOT WIN32)
|
||||
endif()
|
||||
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
|
||||
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
|
||||
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
|
||||
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32 cfgmgr32) # Configuration Manager сопоставляет интерфейсы CAN и DFU по родительскому USB-узлу.
|
||||
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
+149
-4
@@ -34,6 +34,86 @@
|
||||
#include "candle_defs.h"
|
||||
#include "candle_ctrl_req.h"
|
||||
#include "ch_9.h"
|
||||
#include <cfgmgr32.h> /* Сопоставляет интерфейсы CAN и DFU по общему родительскому USB-устройству Windows. */
|
||||
|
||||
static DWORD candle_dfu_parent(const wchar_t *path, DEVINST *parent) /* Возвращает родителя интерфейса, чтобы команда не попала в другой подключённый адаптер. */
|
||||
{ /* Начало поиска физического USB-устройства по пути выбранного CAN-интерфейса. */
|
||||
HDEVINFO devices = SetupDiCreateDeviceInfoList(NULL, NULL); /* Создаёт временный набор SetupAPI без перечисления посторонних устройств. */
|
||||
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Сохраняет причину отказа создания набора Windows. */
|
||||
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Подготавливает описание интерфейса для открытия по точному пути. */
|
||||
iface.cbSize = sizeof(iface); /* Передаёт SetupAPI размер структуры текущей разрядности процесса. */
|
||||
SP_DEVINFO_DATA info = {0}; /* Получает идентификатор узла PnP выбранного интерфейса. */
|
||||
info.cbSize = sizeof(info); /* Указывает размер структуры устройства согласно контракту SetupAPI. */
|
||||
DWORD size = 0, error = ERROR_SUCCESS; /* Хранит требуемое число байтов и итоговый код Windows. */
|
||||
if (!SetupDiOpenDeviceInterfaceW(devices, path, 0, &iface)) error = GetLastError(); /* Открывает только заданный путь, сохраняя ошибку исчезнувшего устройства. */
|
||||
if (!error) SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Узнаёт объём буфера переменной длины для пути USB-интерфейса. */
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = size ? malloc(size) : NULL; /* Выделяет буфер только после успешного определения его размера. */
|
||||
if (!error && !detail) error = size ? ERROR_NOT_ENOUGH_MEMORY : GetLastError(); /* Отличает нехватку памяти от ошибки поиска описания. */
|
||||
if (detail) { /* Обрабатывает описание только при наличии выделенного буфера. */
|
||||
detail->cbSize = sizeof(*detail); /* Согласует размер заголовка с архитектурой Win32 или Win64. */
|
||||
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, &info)) error = GetLastError(); /* Получает узел интерфейса из SetupAPI. */
|
||||
else if (CM_Get_Parent(parent, info.DevInst, 0) != CR_SUCCESS) error = ERROR_NOT_FOUND; /* Получает общий родительский узел составного USB-адаптера. */
|
||||
free(detail); /* Освобождает описание после получения идентификатора родителя. */
|
||||
} /* Завершает работу с описанием интерфейса. */
|
||||
SetupDiDestroyDeviceInfoList(devices); /* Освобождает набор SetupAPI при любом результате поиска. */
|
||||
return error; /* Возвращает ноль при успехе либо причину отказа Windows. */
|
||||
} /* Завершает сопоставление интерфейса с физическим USB-устройством. */
|
||||
|
||||
uint32_t __stdcall candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]) /* Возвращает код Windows и шестибайтовый DFU_GETSTATUS согласно DFU 1.1. */
|
||||
{ /* Начало отдельного сеанса управляющего интерфейса обновления. */
|
||||
if (!can_path || !status) return ERROR_INVALID_PARAMETER; /* Запрещает отсутствие точного пути адаптера или выходного буфера статуса. */
|
||||
memset(status, 0, 6); /* Исключает использование неинициализированных полей ответа при ошибке USB. */
|
||||
DEVINST wanted_parent = 0; /* Запоминает физический адаптер, выбранный пользователем до остановки CAN. */
|
||||
DWORD error = candle_dfu_parent(can_path, &wanted_parent); /* Ищет родителя по сохранённому пути CAN, не заменяя его первым найденным устройством. */
|
||||
if (error) return error; /* Прерывает переход, если выбранный адаптер уже исчез из PnP. */
|
||||
GUID guid = {0xc25b4308, 0x04d3, 0x11e6, {0xb3, 0xea, 0x60, 0x57, 0x18, 0x9e, 0x64, 0x43}}; /* GUID интерфейса обновления из дескриптора CANable 2.5. */
|
||||
HDEVINFO devices = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); /* Перечисляет только подключённые интерфейсы обновления CANable 2.5. */
|
||||
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Возвращает отказ SetupAPI без попытки послать команду. */
|
||||
error = ERROR_NOT_FOUND; /* Отсутствие интерфейса обновления не считается успешным входом в DFU. */
|
||||
for (DWORD index = 0; ; ++index) { /* Проверяет интерфейсы до точного совпадения физического устройства. */
|
||||
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Создаёт независимое описание очередного DFU-интерфейса. */
|
||||
iface.cbSize = sizeof(iface); /* Задаёт обязательный размер структуры SetupAPI. */
|
||||
if (!SetupDiEnumDeviceInterfaces(devices, NULL, &guid, index, &iface)) { /* Завершает обход либо сообщает ошибку перечисления. */
|
||||
if (GetLastError() != ERROR_NO_MORE_ITEMS) error = GetLastError(); /* Отличает штатный конец списка от отказа Windows. */
|
||||
break; /* Выходит из обхода после исчерпания интерфейсов или ошибки. */
|
||||
} /* Завершает проверку результата перечисления. */
|
||||
DWORD size = 0; /* Хранит размер описания очередного интерфейса в байтах. */
|
||||
SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Определяет буфер для полного пути интерфейса обновления. */
|
||||
if (!size) { error = GetLastError(); break; } /* Не продолжает работу с неполным результатом SetupAPI. */
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = malloc(size); /* Выделяет описание с достаточным местом для USB-пути. */
|
||||
if (!detail) { error = ERROR_NOT_ENOUGH_MEMORY; break; } /* Не допускает обращения к отсутствующему буферу. */
|
||||
detail->cbSize = sizeof(*detail); /* Сохраняет ABI описания интерфейса Windows. */
|
||||
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, NULL)) { /* Получает путь, который можно передать CreateFileW. */
|
||||
error = GetLastError(); free(detail); break; /* Сохраняет ошибку и освобождает буфер перед выходом. */
|
||||
} /* Завершает чтение пути интерфейса. */
|
||||
DEVINST parent = 0; /* Хранит родителя текущего кандидата на обновление. */
|
||||
if (candle_dfu_parent(detail->DevicePath, &parent) || parent != wanted_parent) { /* Исключает каждый интерфейс другого физического адаптера. */
|
||||
free(detail); continue; /* Освобождает описание неподходящего кандидата и продолжает поиск. */
|
||||
} /* Завершает проверку принадлежности выбранному USB-адаптеру. */
|
||||
HANDLE file = CreateFileW(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); /* Эксклюзивно открывает интерфейс обновления выбранного устройства для WinUSB. */
|
||||
free(detail); /* Путь больше не нужен после открытия системного дескриптора. */
|
||||
if (file == INVALID_HANDLE_VALUE) { error = GetLastError(); break; } /* Не скрывает отсутствие доступа или занятость интерфейса. */
|
||||
WINUSB_INTERFACE_HANDLE usb = NULL; /* Хранит отдельный WinUSB-сеанс интерфейса обновления без CAN bulk endpoint. */
|
||||
error = ERROR_SUCCESS; /* Сбрасывает исходное отсутствие интерфейса после точного совпадения устройства. */
|
||||
if (!WinUsb_Initialize(file, &usb)) error = GetLastError(); /* Проверяет наличие рабочего WinUSB-драйвера DFU runtime. */
|
||||
USB_INTERFACE_DESCRIPTOR descriptor = {0}; /* Получает реальный номер и класс открытого интерфейса. */
|
||||
if (!error && !WinUsb_QueryInterfaceSettings(usb, 0, &descriptor)) error = GetLastError(); /* Не предполагает, что произвольный WinUSB-интерфейс поддерживает DFU. */
|
||||
if (!error && (descriptor.bInterfaceClass != 0xFE || descriptor.bInterfaceSubClass != 1 || descriptor.bInterfaceProtocol != 1)) error = ERROR_NOT_SUPPORTED; /* Проверяет класс приложения, подкласс DFU и runtime-протокол по DFU 1.1. */
|
||||
ULONG timeout = 500, transferred = 0; /* Ограничивает управляющий USB-запрос 500 миллисекундами и учитывает длину ответа. */
|
||||
if (!error && !WinUsb_SetPipePolicy(usb, 0, PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout)) error = GetLastError(); /* Устанавливает тайм-аут endpoint 0, чтобы отсутствующий ответ не блокировал программу. */
|
||||
WINUSB_SETUP_PACKET packet = {0}; /* Обнуляет поля USB SETUP перед запросом перехода в загрузчик. */
|
||||
packet.RequestType = 0x21; packet.Request = 0; packet.Value = 1000; packet.Index = descriptor.bInterfaceNumber; /* DFU_DETACH: class/interface OUT, запрос 0, ожидаемое время отключения 1000 мс. */
|
||||
if (!error && !WinUsb_ControlTransfer(usb, packet, NULL, 0, &transferred, NULL)) error = GetLastError(); /* Отправляет DETACH и требует подтверждённой USB-транзакции. */
|
||||
packet.RequestType = 0xA1; packet.Request = 3; packet.Value = 0; packet.Length = 6; /* DFU_GETSTATUS: class/interface IN, запрос 3, ответ ровно 6 байт по DFU 1.1. */
|
||||
if (!error && !WinUsb_ControlTransfer(usb, packet, status, 6, &transferred, NULL)) error = GetLastError(); /* Проверяет результат до отложенного перехода прошивки через 300 мс. */
|
||||
if (!error && transferred != 6) error = ERROR_INVALID_DATA; /* Не трактует укороченный ответ как подтверждение входа в загрузчик. */
|
||||
if (usb) WinUsb_Free(usb); /* Освобождает WinUSB до появления загрузчика с другим VID/PID. */
|
||||
CloseHandle(file); /* Освобождает файловый дескриптор после успеха и при любой ошибке USB. */
|
||||
break; /* Не отправляет DETACH повторно и не переключается на другой адаптер при отказе. */
|
||||
} /* Завершает поиск и обработку единственного выбранного адаптера. */
|
||||
SetupDiDestroyDeviceInfoList(devices); /* Освобождает перечень интерфейсов при любом результате. */
|
||||
return error; /* Статус прошивки возвращается отдельно от системной ошибки USB. */
|
||||
} /* Завершает управление DFU runtime без записи flash или изменения образа. */
|
||||
|
||||
static bool candle_dev_interal_open(candle_handle hdev);
|
||||
|
||||
@@ -656,6 +736,30 @@ bool __stdcall DLL candle_dev_open(candle_handle hdev)
|
||||
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity) /* Читает USB Product String открытого адаптера в буфер UTF-16; capacity измеряется в wchar_t, включая завершающий ноль. */
|
||||
{ /* Начало чтения отображаемого имени без изменения режима CAN. */
|
||||
if (!product || !capacity) return false; /* Не пишет в отсутствующий либо пустой выходной буфер. */
|
||||
product[0] = 0; /* Сохраняет пустую строку при любом отказе чтения дескриптора. */
|
||||
candle_device_t *dev = (candle_device_t*)hdev; /* Получает уже открытый WinUSB-сеанс выбранного физического адаптера. */
|
||||
if (!dev || !dev->winUSBHandle) return false; /* Не открывает устройство заново и не обращается к закрытому интерфейсу. */
|
||||
USB_DEVICE_DESCRIPTOR descriptor = {0}; /* Получает индекс строки продукта из стандартного дескриптора устройства. */
|
||||
ULONG received = 0; /* Хранит фактически полученное количество байтов управляющего ответа. */
|
||||
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 1, 0, 0, (PUCHAR)&descriptor, sizeof(descriptor), &received) || received != sizeof(descriptor)) return false; /* Тип 1 обозначает Device Descriptor по USB 2.0; неполный ответ не используется. */
|
||||
UCHAR string_index = descriptor.iProduct; /* Для обычных адаптеров использует объявленный устройством индекс USB Product String. */
|
||||
if (descriptor.idVendor == 0x1D50 && descriptor.idProduct == 0x606F) string_index = 2; /* В CANable Candlelight имя продукта имеет индекс 2; WinUSB может подменять iProduct индексом имени интерфейса, как описано в штатном примере проекта. */
|
||||
if (!string_index) return false; /* Нулевой iProduct означает, что устройство не предоставляет имя продукта. */
|
||||
UCHAR raw[256] = {0}; /* Буфер вмещает строковый дескриптор USB, чей однобайтовый bLength ограничен 255 байтами. */
|
||||
USHORT language = 0x0409; /* Использует английский идентификатор USB как резервный вариант для устройств без списка языков. */
|
||||
if (WinUsb_GetDescriptor(dev->winUSBHandle, 3, 0, 0, raw, sizeof(raw), &received) && received >= 4 && raw[0] >= 4 && raw[1] == 3) language = (USHORT)(raw[2] | ((USHORT)raw[3] << 8)); /* Тип 3 и индекс 0 возвращают список LANGID; выбирает первый язык, предоставленный адаптером. */
|
||||
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 3, string_index, language, raw, sizeof(raw), &received)) return false; /* Читает именно строку продукта на поддерживаемом устройством языке. */
|
||||
if (received < 2 || raw[1] != 3 || raw[0] < 2 || raw[0] > received || (raw[0] & 1)) return false; /* Проверяет тип, размер и чётное число байтов UTF-16 перед копированием в выходной буфер. */
|
||||
uint32_t count = (raw[0] - 2) / 2; /* Исключает двухбайтовый заголовок и переводит длину UTF-16LE из байтов в число кодовых единиц. */
|
||||
if (count >= capacity) return false; /* Не обрезает индивидуальный номер в конце имени и оставляет место для завершающего нуля. */
|
||||
for (uint32_t index = 0; index < count; ++index) product[index] = (wchar_t)(raw[2 + index * 2] | ((USHORT)raw[3 + index * 2] << 8)); /* Явно собирает кодовые единицы UTF-16LE без предположений о выравнивании массива байтов. */
|
||||
product[count] = 0; /* Завершает строку для безопасного чтения через ctypes и отображения в Qt. */
|
||||
return count > 0; /* Пустое имя оставляет интерфейсу стандартную резервную подпись адаптера. */
|
||||
} /* Завершает получение настоящего имени из прошивки USB-адаптера. */
|
||||
|
||||
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
|
||||
{
|
||||
return candle_ctrl_get_timestamp(hdev, timestamp_us);
|
||||
@@ -823,6 +927,38 @@ bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
|
||||
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags) /* Новый вход сохраняет прежний ABI start и позволяет отключать HW_TIMESTAMP. */
|
||||
{ /* Явно выбранные Python-флаги должны попасть в прошивку без автоматического дополнения. */
|
||||
return candle_ctrl_set_device_mode((candle_device_t*)hdev, ch, CANDLE_DEVMODE_START, flags); /* Передаём точную маску после проверки возможностей на уровне транспорта. */
|
||||
} /* Завершение запуска с явно заданными параметрами. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap) /* Экспортируем чтение FD-пределов для расчёта тайминга на любой частоте CAN. */
|
||||
{ /* Общая форма capability позволяет повторно использовать валидатор номинального тайминга. */
|
||||
return candle_ctrl_data_capability((candle_device_t*)hdev, ch, cap); /* USB-запрос возвращает реальные пределы выбранного канала. */
|
||||
} /* Завершение получения возможностей быстрой фазы. */
|
||||
|
||||
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state) /* Значения 0 и 1 соответствуют OFF и ON стандарта gs_usb. */
|
||||
{ /* Проверка исключает отправку неопределённого состояния в прошивку. */
|
||||
return state <= 1 && candle_ctrl_termination((candle_device_t*)hdev, ch, &state, true); /* Выполняем запись только для допустимого состояния. */
|
||||
} /* Завершение переключения терминатора. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state) /* Чтение состояния не меняет топологию CAN. */
|
||||
{ /* Используем тот же формат 32-битного значения, что и при записи. */
|
||||
return candle_ctrl_termination((candle_device_t*)hdev, ch, state, false); /* false выбирает USB_DIR_IN и GET_TERMINATION. */
|
||||
} /* Завершение чтения терминатора. */
|
||||
|
||||
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr) /* Сохраняем счётчики, которые старый get_state отбрасывал. */
|
||||
{ /* Диагностика выполняется только для уже открытого устройства. */
|
||||
candle_device_state_t data = {0}; /* Полная структура протокола содержит три слова: состояние, RX и TX. */
|
||||
if (!candle_ctrl_get_state((candle_device_t*)hdev, ch, &data)) { /* Не возвращаем нулевые счётчики как будто запрос был успешным. */
|
||||
return false; /* Причина USB-отказа уже записана управляющим запросом. */
|
||||
} /* Дальнейшее копирование относится только к подтверждённому ответу. */
|
||||
*state = data.state; /* Сохраняем состояние Error-active, Warning, Passive, Bus-off, Stopped или Sleeping. */
|
||||
*rxerr = data.rxerr; /* Счётчик ошибок приёма измеряется в единицах CAN-контроллера. */
|
||||
*txerr = data.txerr; /* Счётчик ошибок передачи помогает отличать отсутствие ACK от ошибок приёма. */
|
||||
return true; /* Все три выходных значения относятся к одному USB-ответу. */
|
||||
} /* Завершение расширенной диагностики канала. */
|
||||
|
||||
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
|
||||
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
|
||||
* is never accessed by two threads simultaneously. */
|
||||
@@ -1017,14 +1153,13 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
|
||||
}
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
|
||||
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
|
||||
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
|
||||
if (bytes_transfered < classic_min) {
|
||||
const DWORD header_size = 12; /* Заголовок gs_usb занимает 12 байт в Classic и FD; короткий FD может не содержать восьми байтов данных. */
|
||||
if (bytes_transfered < header_size) { /* До чтения flags и DLC проверяем наличие полного заголовка. */
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
classic_min);
|
||||
header_size); /* Минимальная длина здесь относится к заголовку, а payload проверяется после определения формата. */
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
@@ -1047,6 +1182,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
|
||||
if (is_fd_frame) {
|
||||
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
|
||||
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
|
||||
if (raw_dlc > 15) { /* FD DLC является четырёхбитным кодом и не может содержать значения выше 15. */
|
||||
candle_prepare_read(dev, urb_num); /* Повреждённый USB-пакет не должен останавливать последующий приём. */
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE; /* Неверный DLC считается ошибкой формата входного кадра. */
|
||||
return false; /* Не превращаем повреждённый DLC в пустой успешный FD-кадр. */
|
||||
} /* Далее длина безопасно вычисляется по таблице CAN FD. */
|
||||
const DWORD data_len = candle_dlc_to_len(raw_dlc);
|
||||
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
|
||||
|
||||
@@ -1077,6 +1217,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
|
||||
}
|
||||
/* else: timestamp stays zero from memset above */
|
||||
} else {
|
||||
if (bytes_transfered < sizeof(candle_frame_t) - sizeof(uint32_t)) { /* Classic-пакет обязан содержать весь восьмибайтовый буфер данных, даже при меньшем DLC. */
|
||||
candle_prepare_read(dev, urb_num); /* Возобновляем соответствующий URB после отклонения неполного пакета. */
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE; /* Не допускаем чтение неполученных данных из локальной структуры. */
|
||||
return false; /* Повреждённый Classic-пакет не попадает в Python через FD API. */
|
||||
} /* Минимальный размер Classic теперь проверен отдельно от коротких FD. */
|
||||
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
|
||||
candle_frame_t classic;
|
||||
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
; прикладные регистры SETProtocol.
|
||||
|
||||
EXPORTS
|
||||
candle_dev_get_product ; Имя продукта из USB-дескриптора прошивки для списка адаптеров SETGUI.
|
||||
candle_dev_enter_dfu ; Переход через отдельный DFU runtime-интерфейс того же физического USB-адаптера.
|
||||
candle_list_scan
|
||||
candle_list_free
|
||||
candle_list_length
|
||||
@@ -31,3 +33,13 @@ EXPORTS
|
||||
candle_frame_data
|
||||
candle_frame_timestamp_us
|
||||
candle_dev_last_error
|
||||
candle_channel_start_configured ; Точная маска запуска с управляемыми аппаратными метками.
|
||||
candle_channel_get_data_capabilities ; Пределы тайминга быстрой фазы CAN FD.
|
||||
candle_channel_set_termination ; Переключение встроенного терминатора.
|
||||
candle_channel_get_termination ; Чтение состояния встроенного терминатора.
|
||||
candle_channel_get_diagnostics ; Состояние и счётчики ошибок CAN.
|
||||
candle_channel_set_data_timing ; Тайминг фазы данных CAN FD.
|
||||
candle_fd_frame_send ; Отправка полного кадра CAN FD до 64 байт.
|
||||
candle_fd_frame_read ; Приём FD и Classic без обрезания данных.
|
||||
candle_fd_frame_type ; Различение RX, TX echo и ошибок FD.
|
||||
candle_fd_frame_id ; Получение CAN ID из структуры FD.
|
||||
|
||||
@@ -246,7 +246,9 @@ bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
|
||||
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
|
||||
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
|
||||
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
|
||||
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity); /* Возвращает USB Product String в UTF-16; capacity включает завершающий ноль и измеряется в wchar_t. */
|
||||
bool __stdcall DLL candle_dev_open(candle_handle hdev);
|
||||
uint32_t __stdcall DLL candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]); /* Переводит выбранный физический адаптер в DFU и возвращает Win32-код плюс ответ DFU_GETSTATUS. */
|
||||
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
|
||||
bool __stdcall DLL candle_dev_close(candle_handle hdev);
|
||||
bool __stdcall DLL candle_dev_free(candle_handle hdev);
|
||||
@@ -259,6 +261,11 @@ bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, can
|
||||
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
|
||||
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
|
||||
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
|
||||
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags); /* Запускаем с точной маской без принудительного включения аппаратных меток. */
|
||||
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap); /* Возвращаем пределы тайминга быстрой фазы FD. */
|
||||
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state); /* Состояние терминатора: 0 — выключен, 1 — включён. */
|
||||
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state); /* Чтение не меняет подключение встроенного резистора. */
|
||||
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr); /* Передаём состояние CAN и оба счётчика ошибок без потери данных ответа. */
|
||||
|
||||
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
|
||||
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
|
||||
|
||||
@@ -39,8 +39,10 @@ enum {
|
||||
CANDLE_TIMESTAMP_GET = 6,
|
||||
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
|
||||
CANDLE_BREQ_DATA_BITTIMING = 10,
|
||||
/* 11: SET_TERMINATION, not used here */
|
||||
CANDLE_BREQ_GET_STATE = 12,
|
||||
CANDLE_BREQ_BT_CONST_EXT = 11, /* Расширенные пределы номинальной и быстрой фазы согласно gs_usb. */
|
||||
CANDLE_BREQ_SET_TERMINATION = 12, /* Запрос gs_usb 12 переключает встроенный резистор окончания шины. */
|
||||
CANDLE_BREQ_GET_TERMINATION = 13, /* Запрос gs_usb 13 читает текущее состояние терминатора. */
|
||||
CANDLE_BREQ_GET_STATE = 14, /* Стандартный GET_STATE имеет номер 14; номер 12 принадлежит терминатору. */
|
||||
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
|
||||
};
|
||||
|
||||
@@ -237,3 +239,22 @@ bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap) /* Структура gs_usb BT_CONST_EXT содержит десять общих и восемь быстрых полей uint32. */
|
||||
{ /* Используем массив, поскольку протокол содержит только однородные 32-битные значения без отступов. */
|
||||
uint32_t fields[18] = {0}; /* 18 полей по 4 байта образуют стандартный ответ размером 72 байта. */
|
||||
bool rc = usb_control_msg(dev->winUSBHandle, CANDLE_BREQ_BT_CONST_EXT, USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, fields, sizeof(fields)); /* Запрашиваем пределы именно выбранного CAN-канала. */
|
||||
if (rc) { /* Выходная структура должна меняться только после успешного USB-ответа. */
|
||||
memcpy(cap, fields, 2 * sizeof(uint32_t)); /* Маска функций и частота CAN общие для обеих фаз. */
|
||||
memcpy(&cap->tseg1_min, &fields[10], 8 * sizeof(uint32_t)); /* Поля DTSEG1…DBRP_INC заменяют номинальные пределы структуры capability. */
|
||||
} /* При отказе вызывающий код получит false и не использует нулевые пределы. */
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST; /* Сохраняем диагностический код чтения аппаратных ограничений. */
|
||||
return rc; /* Успех означает наличие полной структуры пределов быстрой фазы. */
|
||||
} /* Завершение запроса расширенных возможностей FD. */
|
||||
|
||||
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write) /* Переключение терминатора использует стандартные SET=12 и GET=13. */
|
||||
{ /* Состояние передаётся одним 32-битным словом согласно протоколу gs_usb. */
|
||||
bool rc = usb_control_msg(dev->winUSBHandle, write ? CANDLE_BREQ_SET_TERMINATION : CANDLE_BREQ_GET_TERMINATION, (write ? USB_DIR_OUT : USB_DIR_IN)|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, state, sizeof(*state)); /* Номер CAN-канала передаётся в wValue, а wIndex равен нулю по спецификации. */
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE; /* Ошибка настройки доступна Python через candle_dev_last_error. */
|
||||
return rc; /* Не выдаём успешное переключение при отказе USB-контроллера. */
|
||||
} /* Завершение управления встроенным окончанием шины. */
|
||||
|
||||
@@ -51,4 +51,6 @@ bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candl
|
||||
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
|
||||
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
|
||||
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
|
||||
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap); /* Читаем пределы быстрой фазы CAN FD отдельным запросом расширенной структуры. */
|
||||
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write); /* Передаём состояние терминатора: 0 — выключен, 1 — включён. */
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166.
|
||||
|
||||
|
||||
def git_state(repository: Path) -> dict:
|
||||
|
||||
@@ -52,7 +52,8 @@ typedef enum {
|
||||
PCAN_BOOT_CMD_PING = 0x0CU,
|
||||
PCAN_BOOT_CMD_CAPABILITIES = 0x0DU,
|
||||
PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU,
|
||||
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU
|
||||
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU, /* Сохраняет существующий номер постраничного паспорта памяти. */
|
||||
PCAN_BOOT_CMD_SELECT_MEMORY = 0x10U /* Выбирает 0=штатное хранилище, 1=внутренняя Flash до передачи метаданных. */
|
||||
} pcan_boot_command_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -127,6 +128,7 @@ typedef struct {
|
||||
/** Optional precise result, replacing legacy boolean verify_image. */
|
||||
pcan_boot_status_t (*verify_status)(void *user, pcan_boot_byte_t slot,
|
||||
const pcan_boot_manifest_t *manifest);
|
||||
pcan_boot_status_t (*select_memory)(void *user, pcan_boot_byte_t memory, pcan_boot_config_t *config); /* Необязательный порт явно переключает назначение образа до ERASE; старые порты возвращают INVALID_COMMAND. */
|
||||
} pcan_boot_port_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -50,6 +50,7 @@ RS Flash должен стирать только B–H, публиковать
|
||||
Старая самостоятельная Flash-сборка с codestart `0x33FFF6` несовместима с этим
|
||||
размещением и не должна передаваться как обновление приложения.
|
||||
|
||||
|
||||
Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно.
|
||||
Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное
|
||||
слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный,
|
||||
@@ -154,7 +155,31 @@ SPI-драйвер загрузчика имеет ограничение ожи
|
||||
Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует
|
||||
0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации,
|
||||
0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует
|
||||
NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash.
|
||||
NOINIT handoff в обоих проектах. TI Flash API временно использует RAM
|
||||
`0x8000..0x97FF` до запуска приложения. В обновлённом постоянном загрузчике
|
||||
CAN также записывает внутреннюю Flash приложения B–H; сектор A защищён.
|
||||
|
||||
## Внутренняя Flash по CAN
|
||||
|
||||
После ENTER_BOOT клиент явно отправляет SELECT_MEMORY (`0x10`, DLC=1,
|
||||
DATA[0]=1) до BEGIN_IMAGE/BEGIN_COMPAT. Новая сессия по умолчанию использует
|
||||
EEPROM A/B, поэтому старые клиенты сохраняют прежнее поведение. Выбор памяти
|
||||
после метаданных запрещён; старый boot отвечает INVALID_COMMAND до ERASE.
|
||||
CAPABILITIES bit 12 обозначает поддержку записи внутренней Flash.
|
||||
|
||||
Flash SCI8 с FAB1 передаётся в неактивный EEPROM-слот как staging. VERIFY
|
||||
проверяет CRC32 EEPROM, таблицу, непересекающиеся адреса только B–H и FAB1/CRC
|
||||
будущей Flash. COMMIT повторяет проверку staging, вызывает официальный TI API
|
||||
со стиранием `0x00FE`, пишет и проверяет данные порциями до 128 слов, публикует
|
||||
FAB1 последним и проверяет CRC физической Flash. ACK COMMIT приходит после
|
||||
записи; клиент даёт до 180 секунд и затем отправляет отдельный REBOOT.
|
||||
EEPROM active и параметры сохраняются; прежний неактивный образ заменяется staging.
|
||||
|
||||
В SETGUI: «Прошивка → CAN / ProtoCAN Boot → ProtoCAN: память прошивки →
|
||||
Внутренняя Flash F28335», файл `*_flash_rs.bin` существующего выпуска.
|
||||
Сначала требуется обновить постоянный `F28335_CAN_AB.out` через JTAG с маской
|
||||
только сектора A. В Debug/Release подключён установленный TI Flash API v2.10;
|
||||
его код и таблицы копируются в RAM перед занятостью общего банка Flash A–H.
|
||||
|
||||
## Первичная установка
|
||||
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
* Сброс сессии не удаляет active/pending из EEPROM. */
|
||||
static pcan_boot_t boot;
|
||||
static f28335_boot_store_t store;
|
||||
static unsigned int healthy; /* Результат открытия EEPROM-журнала известен также функции выбора назначения Flash. */
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Flash API доступен только постоянному загрузчику с явной поддержкой обновления B–H. */
|
||||
#include "f28335_flash_update.h" /* Подключает проверку Flash SCI8 и аппаратный writer с сохранением сектора A. */
|
||||
#include "f28335_flash_update.c" /* Общий алгоритм компилируется с тем же pcan_boot и разметкой, что этот порт. */
|
||||
static f28335_flash_update_t flash_update; /* Индекс и кеш занимают ограниченную внутреннюю RAM без загрузки приложения поверх boot. */
|
||||
static unsigned int flash_selected, flash_staging; /* Явная цель Flash использует только неактивный EEPROM-слот как временное хранилище. */
|
||||
static pcan_boot_status_t select_memory(void *u, pcan_boot_byte_t memory, pcan_boot_config_t *cfg) /* Выбирает назначение до BEGIN_IMAGE, сохраняя прежний EEPROM-путь по умолчанию. */
|
||||
{ /* Изменения конфигурации разрешены только при исправном EEPROM-журнале. */
|
||||
(void)u; if (!healthy) return PCAN_BOOT_STATUS_FLASH_ERROR; /* Ошибка чтения журнала запрещает выбор staging поверх неизвестного active. */
|
||||
if (memory > 1U) return PCAN_BOOT_STATUS_INVALID_COMMAND; /* Единственные определённые цели: 0=EEPROM A/B и 1=внутренняя Flash. */
|
||||
flash_selected = memory; flash_update.valid = false; /* Новая цель отменяет прежнее разрешение программирования. */
|
||||
cfg->slot_count = memory ? 1U : 2U; cfg->active_slot = memory ? 0U : store.active; /* CAN Flash использует один логический слот A, EEPROM сохраняет выбор неактивного A/B. */
|
||||
return PCAN_BOOT_STATUS_OK; /* Физический staging будет инвалидирован лишь при последующей команде ERASE. */
|
||||
} /* Никакие данные Flash или EEPROM при выборе цели не изменяются. */
|
||||
#endif /* Приложение и старые host-модели не подключают Flash API. */
|
||||
/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов
|
||||
* используется 31, чтобы head==tail однозначно означало пустую очередь.
|
||||
* Долгие операции SPI запрещены внутри обработчика CAN. */
|
||||
@@ -16,7 +31,7 @@ static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue
|
||||
/* reset_requested только планирует reset; Service ждёт завершения TX.
|
||||
* ready сообщает об окончании старта приложения, confirmed — о сохранённом
|
||||
* подтверждении слота. healthy отражает результат открытия журнала при Init. */
|
||||
static unsigned int reset_requested,reset_session,ready,healthy,confirmed;
|
||||
static unsigned int reset_requested,reset_session,ready,confirmed; /* Состояние перехода и подтверждения приложения отделено от доступности EEPROM. */
|
||||
static uint32_t sent_at,ready_at;
|
||||
static unsigned int main_passes;
|
||||
static unsigned int addressed; /* Разрешает обслуживание только после проверки номера PERIPH и установки согласованных фильтров CAN. */
|
||||
@@ -49,9 +64,19 @@ static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_
|
||||
* эти callbacks возвращают отказ, даже если им удастся передать команду записи.
|
||||
* ERASE для EEPROM означает инвалидирование manifest выбранного слота. */
|
||||
static bool erase(void *u,pcan_boot_byte_t slot)
|
||||
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); }
|
||||
{ /* ERASE подготавливает staging; внутренняя Flash остаётся неизменной до VERIFY и COMMIT. */
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Новая цель не меняет прежний формат EEPROM A/B. */
|
||||
if (flash_selected) { flash_update.valid = false; flash_staging = store.active == 0U ? 1U : 0U; return healthy && f28335_boot_prepare(&store, flash_staging); } /* Инвалидирует только неактивный слот, сохраняя подтверждённый резерв и параметры. */
|
||||
#endif /* В остальных сборках остаётся штатная подготовка EEPROM-слота. */
|
||||
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); /* Обычный CAN-клиент сохраняет прежний путь обновления RAM-приложения. */
|
||||
} /* Подготовка не вызывает TI Flash_Erase. */
|
||||
static bool write_block(void *u,pcan_boot_byte_t slot,uint32_t offset,const pcan_boot_byte_t *d,pcan_boot_byte_t n)
|
||||
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); }
|
||||
{ /* CAN ISR только ставит данные в очередь; запись staging выполняет main. */
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Запись Flash-файла использует тот же проверенный EEPROM writer. */
|
||||
if (flash_selected) return healthy && f28335_boot_write(&store, flash_staging, offset, d, n); /* Логический DATA_A направляется в выбранный неактивный физический слот. */
|
||||
#endif /* Обычные A/B-данные сохраняют исходную адресацию. */
|
||||
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); /* Не допускает записи из прикладной сборки. */
|
||||
} /* Внутренняя Flash ещё не затронута. */
|
||||
/* Проверка включает CRC EEPROM и структуру SCI8. Сейчас отказ этого слоя
|
||||
* кодируется CRC_ERROR как общий отказ проверки образа; причины внутри
|
||||
* парсера SCI8 отдельными статусами протокола не различаются. */
|
||||
@@ -59,10 +84,21 @@ static pcan_boot_status_t verify(void *u,pcan_boot_byte_t slot,const pcan_boot_m
|
||||
{
|
||||
(void)u;
|
||||
if(!F28335_BOOT_LOADER || !healthy) return PCAN_BOOT_STATUS_INVALID_STATE;
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Для внутренней Flash нужен собственный допустимый диапазон SCI8 и обязательный FAB1. */
|
||||
if (flash_selected) return f28335_flash_update_validate(&flash_update, &store.io, flash_staging ? F28335_AB_SLOT_B : F28335_AB_SLOT_A, m) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_CRC_ERROR; /* Повторно читает EEPROM и проверяет транспортную CRC, таблицу и CRC будущей Flash до erase. */
|
||||
#endif /* RAM SCI8 проходит прежнюю проверку приложения EEPROM. */
|
||||
return f28335_boot_verify(&store,slot,m,0)?PCAN_BOOT_STATUS_OK:PCAN_BOOT_STATUS_CRC_ERROR;
|
||||
}
|
||||
static bool commit(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
|
||||
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); }
|
||||
{ /* COMMIT выполняет фактическую запись только после подтверждённой проверки образа. */
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Постоянный boot остаётся вне обновляемых секторов B–H. */
|
||||
if (flash_selected) { /* Синхронный TI API может работать дольше обычного интервала CAN, не создавая pending EEPROM. */
|
||||
bool result = healthy && F28335Boot_FlashProgram(&flash_update); /* Итог зависит от erase, каждой порции readback и полной CRC физической Flash. */
|
||||
boot.now_ms = F28335Boot_Now(); boot.last_activity_ms = boot.now_ms; return result; /* Обновляет срок сессии после аппаратной операции, чтобы штатный REBOOT не был отклонён из-за времени записи. */
|
||||
} /* Ошибка записи оставляет постоянный boot доступным для нового сеанса. */
|
||||
#endif /* Обычное обновление EEPROM сохраняет trial/CONFIRM. */
|
||||
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); /* Публикует только RAM-образ в исходном журнале A/B. */
|
||||
} /* Итоговый статус и отдельный REBOOT отправляет существующее ядро ProtoCAN. */
|
||||
/* Подтверждение принадлежит приложению после READY. Загрузчик не может
|
||||
* подтвердить кандидата сам: иначе исчезнет смысл проверки пробного запуска.
|
||||
* Разрешение кормления watchdog из ISR открываем только после записи журнала. */
|
||||
@@ -83,7 +119,12 @@ static unsigned int enter(void *u,pcan_boot_byte_t session)
|
||||
{
|
||||
(void)u;
|
||||
if(!healthy) return 2;
|
||||
if(F28335_BOOT_LOADER) return 0;
|
||||
if(F28335_BOOT_LOADER) { /* Постоянный загрузчик открывает сессию без дополнительного reset. */
|
||||
#if F28335_CAN_FLASH_UPDATE /* Каждая новая сессия должна начинаться со штатного EEPROM-назначения. */
|
||||
if (boot.state == PCAN_BOOT_STATE_IDLE) (void)select_memory(0, 0U, &boot.config); /* Повторный ENTER текущей сессии не меняет уже выбранную Flash. */
|
||||
#endif /* Старые сборки не используют переключение назначения. */
|
||||
return 0; /* Ядро отправит адресный ACK готовности. */
|
||||
} /* Прикладной handoff проверяется ниже. */
|
||||
/* No destructive fallback on boards still starting through the SPI ROM. */
|
||||
if((F28335Boot_RunningSlot()>1 && F28335Boot_RunningSlot()!=F28335_BOOT_FLASH_SLOT) || !confirmed) return 2; /* Только handoff общего загрузчика разрешает переход из EEPROM либо защищённой внутренней Flash; старый SPI-ROM старт остаётся запрещён. */
|
||||
reset_session=session; reset_requested=1; return 1;
|
||||
@@ -102,7 +143,10 @@ static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boo
|
||||
/* bits: ping, discovery, identity, name, memory, A/B, confirm, rollback,
|
||||
* separate loader, session timeout, readback, SCI8 validation. */
|
||||
info32(out,(F28335_BOOT_LOADER || F28335Boot_RunningSlot()<2 || F28335Boot_RunningSlot()==F28335_BOOT_FLASH_SLOT)?0xFFFUL:0x1FUL); /* Flash-приложение с достоверным handoff поддерживает вход в постоянный CAN Boot так же, как подтверждённый EEPROM-образ. */
|
||||
info32(out+4,F28335_AB_SLOT_BYTES); return true;
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Дополнительный бит сообщает клиенту поддержку явной внутренней Flash. */
|
||||
out[1] |= 0x10U; /* Бит 12 общего little-endian capability word означает SELECT_MEMORY и запись B–H. */
|
||||
#endif /* Старые приложения продолжают сообщать прежние возможности. */
|
||||
info32(out+4,F28335_AB_SLOT_BYTES); return true; /* Максимальная длина одинакова для staging Flash и EEPROM A/B. */
|
||||
}
|
||||
if(command==PCAN_BOOT_CMD_DEVICE_NAME) {
|
||||
if((unsigned int)page*8U>=name_size) return false; /* Страница после конца выбранного имени не является допустимой частью паспорта. */
|
||||
@@ -146,6 +190,10 @@ void F28335CanBoot_Init(unsigned int periph_id) /* Один номер плат
|
||||
cfg.active_slot=store.active;
|
||||
cfg.slot_count=2; cfg.max_image_size=F28335_AB_SLOT_BYTES; cfg.ack_window=16;
|
||||
cfg.session_timeout_ms=30000UL;
|
||||
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* После reset никакое случайное содержимое RAM не выбирает внутреннюю Flash. */
|
||||
flash_selected = 0U; flash_staging = F28335_AB_NONE; flash_update.valid = false; /* Возвращает исходную цель и отменяет разрешение записи предыдущей сессии. */
|
||||
port.select_memory = select_memory; /* Новая команда доступна только сборке с реально подключённым TI API. */
|
||||
#endif /* Приложение отвечает только через постоянный загрузчик после handoff. */
|
||||
port.send=transmit; port.erase_slot=erase; port.write_slot=write_block;
|
||||
port.verify_status=verify; port.set_pending_slot=commit; port.reboot=reboot;
|
||||
port.confirm_running_slot=confirm; port.enter_boot=enter;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "f28335_flash_update.h" /* Разделяет проверяемый алгоритм и аппаратные вызовы TI Flash API. */
|
||||
#include <string.h> /* Явно очищает состояние источника перед разбором нового SCI8. */
|
||||
static unsigned int octet(f28335_flash_update_t *s, uint32_t offset) /* Читает октет через страничный кеш, исключая SPI-транзакцию на каждый байт CRC. */
|
||||
{ /* Все смещения ограничены фактической длиной принятого файла. */
|
||||
if (offset >= s->length) { s->fault = true; return 0U; } /* Обрезанное поле не превращается в данные за концом staging. */
|
||||
if (!s->cache_size || offset < s->cache_at || offset - s->cache_at >= s->cache_size) { /* Загружает новую страницу только при выходе за текущую. */
|
||||
s->cache_at = offset & ~255UL; /* Выравнивает смещение на страницу из 256 физических октетов. */
|
||||
s->cache_size = s->length - s->cache_at > 256UL ? 256U : (unsigned int)(s->length - s->cache_at); /* Последнее чтение не включает паддинг CAN за концом образа. */
|
||||
if (!s->io.read(s->io.user, s->base + s->cache_at, s->cache, s->cache_size)) { s->fault = true; s->cache_size = 0U; return 0U; } /* Любой неполный аппаратный обмен запрещает erase. */
|
||||
} /* После заполнения кеша извлечение октета не обращается к SPI. */
|
||||
return s->cache[offset - s->cache_at] & 255U; /* Маска сохраняет восьмибитный wire-формат на C28x. */
|
||||
} /* Завершает ограниченное чтение октета. */
|
||||
static uint16_t word(f28335_flash_update_t *s, uint32_t offset) /* SCI8 передаёт младший октет каждого 16-битного слова первым. */
|
||||
{ /* Объединяет два октета только через проверяемый источник. */
|
||||
unsigned int low = octet(s, offset); /* Явный порядок вызовов не зависит от порядка вычисления операндов компилятором. */
|
||||
return (uint16_t)(low | (octet(s, offset + 1UL) << 8)); /* Восстанавливает одно слово CPU из двух октетов SCI8. */
|
||||
} /* Завершает декодирование слова. */
|
||||
static uint32_t address_at(f28335_flash_update_t *s, uint32_t offset) /* Адрес таблицы TI содержит старшее слово перед младшим. */
|
||||
{ /* Порядок слов адреса отличается от порядка октетов внутри слова. */
|
||||
uint32_t high = word(s, offset); /* Читает старшие 16 бит адреса раньше младших. */
|
||||
return (high << 16) | word(s, offset + 2UL); /* Получает CPU word-address без перевода в байты. */
|
||||
} /* Завершает восстановление адреса SCI8. */
|
||||
static uint16_t image_word(void *user, uint32_t address) /* Предоставляет общему валидатору будущий вид Flash, включая стёртые промежутки. */
|
||||
{ /* Отсортированный индекс позволяет проверять весь CRC-диапазон последовательно. */
|
||||
f28335_flash_update_t *s = user; f28335_flash_section_t *p; /* Контекст содержит только предварительно проверенные секции. */
|
||||
if (s->cursor >= s->count || address < s->section[s->cursor].address) s->cursor = 0U; /* Возврат от заголовка к началу диапазона корректно перематывает индекс. */
|
||||
while (s->cursor < s->count) { /* Пропускает только секции, целиком лежащие перед запрошенным словом. */
|
||||
p = &s->section[s->cursor]; /* Обращается к существующему элементу проверенного индекса. */
|
||||
if (address < p->address) return 0xFFFFU; /* Flash_Erase оставляет незагружаемый промежуток заполненным единицами. */
|
||||
if (address - p->address < p->words) return word(s, p->offset + 2UL * (address - p->address)); /* Переводит смещение слов секции в октеты SCI8. */
|
||||
++s->cursor; /* Следующий запрос не должен повторно сканировать уже пройденную секцию. */
|
||||
} /* После последней секции остаётся только стёртая Flash. */
|
||||
return 0xFFFFU; /* CRC учитывает также завершающий промежуток диапазона FAB1. */
|
||||
} /* Сам этот callback никогда не читает аппаратную Flash. */
|
||||
bool f28335_flash_update_validate(f28335_flash_update_t *s, const f28335_boot_io_t *io, uint32_t base, const pcan_boot_manifest_t *m) /* Проверяет транспорт, таблицу, допустимые адреса и FAB1 перед erase. */
|
||||
{ /* Любая ошибка оставляет объект непригодным для программирования. */
|
||||
uint32_t offset = 22UL, crc = 0UL, entry, checked_entry, bytes; unsigned int i, j, headers = 0U, have_entry = 0U; /* Заголовок SCI8 занимает 11 слов, то есть 22 октета. */
|
||||
f28335_flash_section_t p; bool ended = false; /* Временная секция нужна для вставки в отсортированный индекс. */
|
||||
if (!s || !io || !io->read || !m) return false; /* Не разыменовывает отсутствующие callbacks и manifest. */
|
||||
memset(s, 0, sizeof(*s)); s->io = *io; s->base = base; s->length = m->image_size; s->crc = m->image_crc32; /* Отбрасывает кеш и разрешение записи предыдущего образа. */
|
||||
if (s->length < 24UL || (s->length & 1UL) || s->length > F28335_AB_SLOT_BYTES || word(s, 0UL) != 0x08AAU) return false; /* Принимает только полный чётный SCI8 в пределах staging-слота. */
|
||||
for (offset = 0UL; offset < s->length; offset += bytes) { /* Независимо проверяет фактически записанную EEPROM, а не только принятые CAN-кадры. */
|
||||
bytes = s->length - offset > 256UL ? 256UL : s->length - offset; /* Делит чтение на ограниченные страницы без выхода за файл. */
|
||||
if (!s->io.read(s->io.user, base + offset, s->cache, (unsigned int)bytes)) return false; /* Аппаратная ошибка запрещает последующий erase. */
|
||||
crc = pcan_boot_crc32_update(crc, s->cache, (size_t)bytes); /* Использует общий CRC32 IEEE, совпадающий с клиентом SETGUI. */
|
||||
} /* Все октеты staging включены в транспортную CRC32. */
|
||||
s->cache_size = 0U; if (crc != s->crc) return false; /* Сбрасывает кеш после прямого чтения и отклоняет повреждённую EEPROM. */
|
||||
entry = address_at(s, 18UL); offset = 22UL; /* Точка входа следует после ключа и восьми резервных слов SCI8. */
|
||||
if (entry < F28335_FLASH_APP_DATA || entry >= F28335_FLASH_APP_STOP) return false; /* Запуск разрешён только из приложения ниже постоянного сектора A. */
|
||||
while (offset + 2UL <= s->length) { /* Для каждой секции сначала проверяет наличие поля длины. */
|
||||
p.words = word(s, offset); offset += 2UL; /* Длина секции выражена адресуемыми словами C28x. */
|
||||
if (!p.words) { ended = offset == s->length; break; } /* Нулевой терминатор обязан завершать файл без лишнего хвоста. */
|
||||
if (s->count == F28335_AB_MAX_SECTIONS || offset + 4UL > s->length) return false; /* Не допускает переполнение индекса либо обрезанный адрес секции. */
|
||||
p.address = address_at(s, offset); offset += 4UL; p.offset = offset; bytes = 2UL * p.words; /* Хранит CPU-адрес и байтовое смещение полезных данных отдельно. */
|
||||
if (bytes > s->length - offset || p.address < F28335_FLASH_APP_START || p.address >= F28335_FLASH_APP_STOP || p.words > F28335_FLASH_APP_STOP - p.address) return false; /* Исключает RAM, сектор A, CSM, OTP и выход за конец staging. */
|
||||
if (p.address == F28335_FLASH_APP_START) { if (p.words < F28335_FLASH_APP_HEADER_WORDS) return false; ++headers; } /* Полные 16 слов FAB1 должны публиковаться одной заключительной операцией. */
|
||||
if (entry >= p.address && entry - p.address < p.words) have_entry = 1U; /* Код entry обязан присутствовать именно в принятой таблице. */
|
||||
for (i = 0U; i < s->count; ++i) if (p.address < s->section[i].address + s->section[i].words && s->section[i].address < p.address + p.words) return false; /* Повторное программирование пересекающихся диапазонов запрещено. */
|
||||
for (j = s->count; j && s->section[j - 1U].address > p.address; --j) s->section[j] = s->section[j - 1U]; /* Сортирует индекс по адресу независимо от порядка секций в SCI8. */
|
||||
s->section[j] = p; ++s->count; offset += bytes; /* Запоминает непересекающуюся секцию и переходит к следующему заголовку. */
|
||||
} /* Структура файла полностью разобрана без записи Flash. */
|
||||
if (!ended || headers != 1U || !have_entry || s->fault) return false; /* Требует терминатор, единственный FAB1 и фактический исполняемый entry. */
|
||||
s->cursor = 0U; if (!f28335_flash_app_validate(image_word, s, &checked_entry) || s->fault || checked_entry != entry) return false; /* Общий валидатор проверяет magic, границы, инверсию entry и CRC будущей Flash. */
|
||||
s->entry = entry; s->valid = true; return true; /* Разрешает программирование только после всех независимых проверок. */
|
||||
} /* Ошибка проверки не меняет текущую Flash и журнал активной EEPROM. */
|
||||
bool f28335_flash_update_program(f28335_flash_update_t *s, const f28335_flash_ops_t *ops) /* Выполняет запись B–H только из полностью проверенного staging. */
|
||||
{ /* Заголовок FAB1 фиксируется после успешной проверки каждой порции данных. */
|
||||
uint16_t buffer[128]; unsigned int i, j, n; uint32_t at, left, entry; pcan_boot_manifest_t m; f28335_boot_io_t io; /* Одна порция содержит не более 128 слов, то есть 256 физических октетов. */
|
||||
if (!s || !s->valid || !ops || !ops->erase || !ops->program || !ops->read) return false; /* Неполный аппаратный контракт не должен начать стирание. */
|
||||
memset(&m, 0, sizeof(m)); m.image_size = s->length; m.image_crc32 = s->crc; io = s->io; at = s->base; /* Сохраняет исходные параметры перед полной повторной проверкой EEPROM. */
|
||||
if (!f28335_flash_update_validate(s, &io, at, &m)) return false; /* Изменение staging между VERIFY и COMMIT обнаруживается до первого erase. */
|
||||
s->valid = false; if (!ops->erase(ops->user)) return false; /* Одно разрешение записи расходуется до изменения Flash; повтор после отказа требует нового VERIFY. */
|
||||
for (i = 0U; i < s->count; ++i) { /* Программирует обычные данные каждой непересекающейся секции. */
|
||||
at = s->section[i].address; left = s->section[i].words; /* CPU-курсор и остаток выражены словами. */
|
||||
if (at == F28335_FLASH_APP_START) { at += F28335_FLASH_APP_HEADER_WORDS; left -= F28335_FLASH_APP_HEADER_WORDS; } /* Пропускает FAB1 до подтверждения всех остальных данных. */
|
||||
while (left) { /* Большие секции разбивает на порции рабочего буфера. */
|
||||
n = left > 128UL ? 128U : (unsigned int)left; /* Последняя порция может быть короче 128 слов. */
|
||||
for (j = 0U; j < n; ++j) buffer[j] = word(s, s->section[i].offset + 2UL * (at + j - s->section[i].address)); /* Распаковывает слова непосредственно из staging EEPROM. */
|
||||
if (s->fault || !ops->program(ops->user, at, buffer, n)) return false; /* Ошибка чтения, записи или readback оставляет FAB1 неопубликованным. */
|
||||
at += n; left -= n; /* Продвигает курсоры только после подтверждённой порции. */
|
||||
} /* Завершает запись данных секции. */
|
||||
} /* Все обычные слова уже записаны и проверены аппаратным портом. */
|
||||
for (i = 0U; i < F28335_FLASH_APP_HEADER_WORDS; ++i) buffer[i] = image_word(s, F28335_FLASH_APP_START + i); /* Собирает единственное проверенное описание FAB1 для заключительной записи. */
|
||||
if (s->fault || !ops->program(ops->user, F28335_FLASH_APP_START, buffer, F28335_FLASH_APP_HEADER_WORDS)) return false; /* Публикует описание только после успешной записи остальных секций. */
|
||||
return f28335_flash_app_validate(ops->read, ops->user, &entry) && entry == s->entry; /* Итоговый успех требует общей CRC уже физической Flash, а не только поблочного сравнения. */
|
||||
} /* Решение о CAN-подтверждении и перезапуске остаётся существующему протоколу. */
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef F28335_FLASH_UPDATE_H /* Один контракт объединяет проверку CAN-образа и аппаратную запись внутренней Flash. */
|
||||
#define F28335_FLASH_UPDATE_H /* Исключает повторное объявление структур при подключении аппаратного порта. */
|
||||
#include "f28335_boot_store.h" /* Использует существующий побайтовый доступ к staging-слоту EEPROM и manifest ProtoCAN. */
|
||||
#include "f28335_flash_app.h" /* Получает границы приложения B–H и проверку завершающего описания FAB1. */
|
||||
typedef struct { uint32_t address, offset; uint16_t words; } f28335_flash_section_t; /* Адрес CPU считается словами, смещение SCI8 — октетами, длина секции — словами. */
|
||||
typedef struct { /* Хранит проверенный индекс SCI8, не размещая целый образ во внутренней RAM загрузчика. */
|
||||
f28335_boot_io_t io; uint32_t base, length, crc, entry; /* Источник EEPROM, начало staging, точная длина, транспортная CRC32 и подтверждённый entry. */
|
||||
f28335_flash_section_t section[F28335_AB_MAX_SECTIONS]; /* Лимит 64 совпадает с ограничением выпуска SCI8 и ограничивает расход RAM. */
|
||||
unsigned int count, cursor, cache_size; bool fault, valid; /* Курсор ускоряет последовательную CRC; fault запрещает принятие неполного чтения EEPROM. */
|
||||
uint32_t cache_at; pcan_boot_byte_t cache[256]; /* Одна страница M95M02 содержит 256 физических октетов, даже при 16-битном char C28x. */
|
||||
} f28335_flash_update_t; /* Объект принадлежит только main постоянного загрузчика, а не CAN ISR. */
|
||||
typedef struct { /* Аппаратные операции отделены от формата SCI8 для испытаний обрыва и ошибок TI API. */
|
||||
bool (*erase)(void *user); /* Стирает только секторы B–H, сохраняя сектор A с постоянным загрузчиком. */
|
||||
bool (*program)(void *user, uint32_t address, uint16_t *data, unsigned int words); /* Записывает и проверяет одну порцию слов по допустимому CPU-адресу. */
|
||||
f28335_flash_read_word_t read; void *user; /* Чтение физической Flash подтверждает общий FAB1/CRC после последней записи. */
|
||||
} f28335_flash_ops_t; /* Никаких неявных операций reset или изменения EEPROM-журнала здесь нет. */
|
||||
bool f28335_flash_update_validate(f28335_flash_update_t *s, const f28335_boot_io_t *io, uint32_t base, const pcan_boot_manifest_t *m); /* Полностью проверяет staging до первого стирания Flash. */
|
||||
bool f28335_flash_update_program(f28335_flash_update_t *s, const f28335_flash_ops_t *ops); /* Повторяет проверку, пишет данные и публикует FAB1 последним. */
|
||||
bool F28335Boot_FlashProgram(f28335_flash_update_t *s); /* Аппаратная реализация подключает официальный TI Flash API только в постоянном загрузчике. */
|
||||
#endif /* Завершает защиту интерфейса CAN-обновления внутренней Flash. */
|
||||
@@ -80,6 +80,7 @@
|
||||
<listOptionValue builtIn="false" value="${PROJECT_ROOT}/../../../vendor/ti/f28335/common/include" />
|
||||
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/include. -->
|
||||
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/include" />
|
||||
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/include" /> <!-- Подключает официальный TI Flash API v2.10 F28335 для обновления секторов B–H по CAN. -->
|
||||
</option>
|
||||
<!-- Задаёт параметр сборки: Pre-define NAME (- -define, -D). -->
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE.409484878.f28335" name="Pre-define NAME (--define, -D)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE" valueType="definedSymbols">
|
||||
@@ -89,6 +90,7 @@
|
||||
<listOptionValue builtIn="false" value="EEPROM_M95M02=1" />
|
||||
<!-- Передаёт значение параметра: F28335_BOOT_LOADER=1. -->
|
||||
<listOptionValue builtIn="false" value="F28335_BOOT_LOADER=1" />
|
||||
<listOptionValue builtIn="false" value="F28335_CAN_FLASH_UPDATE=1" /> <!-- Разрешает выбор внутренней Flash только при реально подключённом TI API. -->
|
||||
<!-- Передаёт значение параметра: _DEBUG. -->
|
||||
<listOptionValue builtIn="false" value="_DEBUG" />
|
||||
</option>
|
||||
@@ -119,11 +121,13 @@
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY.1967188356.f28335" name="Include library file or command file as input (--library, -l)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY" valueType="libs">
|
||||
<!-- Передаёт значение параметра: rts2800_fpu32.lib. -->
|
||||
<listOptionValue builtIn="false" value="rts2800_fpu32.lib" />
|
||||
<listOptionValue builtIn="false" value="Flash28335_API_V210.lib" /> <!-- Официальные erase/program/verify копируются в RAM перед занятостью общего банка Flash. -->
|
||||
</option>
|
||||
<!-- Задаёт параметр сборки: Add <dir> to library search path (- -search_path, -i). -->
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH.1097407993.f28335" name="Add <dir> to library search path (--search_path, -i)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH" valueType="libPaths">
|
||||
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/lib. -->
|
||||
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/lib" />
|
||||
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/lib" /> <!-- Находит установленную библиотеку TI без добавления её двоичного содержимого в проект. -->
|
||||
<!-- Передаёт значение параметра: ${PROJECT_ROOT}. -->
|
||||
<listOptionValue builtIn="false" value="${PROJECT_ROOT}" />
|
||||
</option>
|
||||
@@ -234,6 +238,7 @@
|
||||
<listOptionValue builtIn="false" value="${PROJECT_ROOT}/../../../vendor/ti/f28335/common/include" />
|
||||
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/include. -->
|
||||
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/include" />
|
||||
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/include" /> <!-- Подключает официальный TI Flash API v2.10 F28335 для обновления секторов B–H по CAN. -->
|
||||
</option>
|
||||
<!-- Задаёт параметр сборки: Pre-define NAME (- -define, -D). -->
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE.409484878.f28335" name="Pre-define NAME (--define, -D)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE" valueType="definedSymbols">
|
||||
@@ -243,6 +248,7 @@
|
||||
<listOptionValue builtIn="false" value="EEPROM_M95M02=1" />
|
||||
<!-- Передаёт значение параметра: F28335_BOOT_LOADER=1. -->
|
||||
<listOptionValue builtIn="false" value="F28335_BOOT_LOADER=1" />
|
||||
<listOptionValue builtIn="false" value="F28335_CAN_FLASH_UPDATE=1" /> <!-- Разрешает выбор внутренней Flash только при реально подключённом TI API. -->
|
||||
<!-- Передаёт значение параметра: _DEBUG. -->
|
||||
</option>
|
||||
<!-- Задаёт параметр сборки: Releaseging model. -->
|
||||
@@ -271,11 +277,13 @@
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY.1967188356.f28335" name="Include library file or command file as input (--library, -l)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY" valueType="libs">
|
||||
<!-- Передаёт значение параметра: rts2800_fpu32.lib. -->
|
||||
<listOptionValue builtIn="false" value="rts2800_fpu32.lib" />
|
||||
<listOptionValue builtIn="false" value="Flash28335_API_V210.lib" /> <!-- Официальные erase/program/verify копируются в RAM перед занятостью общего банка Flash. -->
|
||||
</option>
|
||||
<!-- Задаёт параметр сборки: Add <dir> to library search path (- -search_path, -i). -->
|
||||
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH.1097407993.f28335" name="Add <dir> to library search path (--search_path, -i)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH" valueType="libPaths">
|
||||
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/lib. -->
|
||||
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/lib" />
|
||||
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/lib" /> <!-- Находит установленную библиотеку TI без добавления её двоичного содержимого в проект. -->
|
||||
<!-- Передаёт значение параметра: ${PROJECT_ROOT}. -->
|
||||
<listOptionValue builtIn="false" value="${PROJECT_ROOT}" />
|
||||
</option>
|
||||
|
||||
@@ -4,6 +4,7 @@ PAGE 0: /* Исполняемые секции и их Flash LOAD-адреса.
|
||||
FLASHA : origin = 0x338000, length = 0x007F80 /* Сектор A до зарезервированной CSM-области содержит постоянный загрузчик. */
|
||||
BEGIN : origin = 0x33FFF6, length = 0x000002 /* Два слова аппаратного Flash boot перехода; пароли CSM не выделяются. */
|
||||
RAMFUNCS : origin = 0x00F800, length = 0x000800 /* Верхние 2048 слов RAM загрузчика нужны InitFlash и не входят в SCI8 payload приложения. */
|
||||
RAMAPI : origin = 0x008000, length = 0x001800 /* 6144 слова временно принадлежат TI Flash API; приложение использует эту RAM только после выхода загрузчика. */
|
||||
ADC_CAL : origin = 0x380080, length = 0x000009 /* Заводская калибровка TI используется без записи/стирания. */
|
||||
RESET : origin = 0x3FFFC0, length = 0x000002 /* Аппаратный reset находится в ROM и не программируется. */
|
||||
PAGE 1: /* Рабочие данные не пересекают код RAM-приложения ниже E000. */
|
||||
@@ -15,6 +16,7 @@ PAGE 1: /* Рабочие данные не пересекают код RAM-пр
|
||||
SECTIONS /* Не выделяет ни одного загружаемого слова в RAM приложения. */
|
||||
{ /* Инициализированные Flash-секции остаются в секторе A. */
|
||||
codestart : > BEGIN, PAGE = 0 /* Аппаратный переход передаёт управление watchdog-safe C startup. */
|
||||
flash_api : { -lFlash28335_API_V210.lib(.text) -lFlash28335_API_V210.lib(.econst) } LOAD = FLASHA, RUN = RAMAPI, LOAD_START(_FlashApiLoadStart), LOAD_END(_FlashApiLoadEnd), RUN_START(_FlashApiRunStart), PAGE = 0 /* Код и таблицы TI API выполняются из RAM, пока общий банк Flash занят изменением B–H. */
|
||||
.text : > FLASHA, PAGE = 0 /* Весь постоянный алгоритм и библиотеки исполняются из Flash A. */
|
||||
.cinit : > FLASHA, PAGE = 0 /* Таблицы ROM-модели подготавливают данные загрузчика до main. */
|
||||
.pinit : > FLASHA, PAGE = 0 /* Сохраняет стандартный порядок C runtime, если компилятор создаст эту секцию. */
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "DSP2833x_Device.h" /* Предоставляет реальные типы C28x, частоту SYSCLKOUT, CSM и маскирование IRQ. */
|
||||
#include "../../../ports/tms320f28335/f28335_flash_update.h" /* Подключает проверяемый алгоритм staging SCI8 и аппаратный контракт записи. */
|
||||
#include "Flash2833x_API_Library.h" /* Использует официальный TI Flash API v2.10 для F28335, а не запись Flash обычным указателем. */
|
||||
#include <string.h> /* Копирует код и таблицы TI API из постоянного сектора A во внутреннюю RAM до операции Flash. */
|
||||
extern Uint16 FlashApiLoadStart, FlashApiLoadEnd, FlashApiRunStart; /* Символы LOAD/RUN задаёт linker в адресуемых словах CPU. */
|
||||
static FLASH_ST flash_status; /* Диагностика TI хранится во внутренней RAM во время стирания и программирования. */
|
||||
static bool erase_flash(void *user) /* Стирает только область приложения, сохраняя постоянный загрузчик. */
|
||||
{ /* Даже IRQ из сектора A запрещены, поскольку F28335 имеет общий банк Flash для A–H. */
|
||||
Uint16 saved, status; (void)user; /* Сохраняет прежнее состояние IRQ и точный результат TI API. */
|
||||
saved = __disable_interrupts(); status = Flash_Erase(0x00FEU, &flash_status); __restore_interrupts(saved); /* Бит 0 — сектор A; маска FE выбирает только B–H согласно TI API v2.10. */
|
||||
return status == STATUS_SUCCESS; /* Ошибка erase запрещает любую последующую публикацию FAB1. */
|
||||
} /* После возврата TI API банк Flash снова доступен коду постоянного загрузчика. */
|
||||
static bool program_flash(void *user, uint32_t address, uint16_t *data, unsigned int words) /* Записывает и проверяет одну ограниченную порцию Flash. */
|
||||
{ /* Аппаратный слой повторно защищает сектор A независимо от валидатора SCI8. */
|
||||
Uint16 saved, status; (void)user; /* Результат API и маска IRQ не размещаются в обновляемой Flash. */
|
||||
if (!words || address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || words > F28335_FLASH_APP_STOP - address) return false; /* Не допускает даже частичного выхода за B–H, включая CSM и аппаратный boot. */
|
||||
saved = __disable_interrupts(); status = Flash_Program((Uint16 *)address, data, words, &flash_status); /* Во время программирования процессор выполняет только скопированный в RAM TI API. */
|
||||
if (!status) status = Flash_Verify((Uint16 *)address, data, words, &flash_status); /* Проверяет фактические слова до подтверждения этой порции. */
|
||||
__restore_interrupts(saved); return status == STATUS_SUCCESS; /* Возвращает исходное состояние IRQ даже при отказе записи или readback. */
|
||||
} /* Общий writer опубликует FAB1 лишь после успешной записи остальных порций. */
|
||||
static uint16_t read_flash(void *user, uint32_t address) /* Читает физическую Flash для итоговой проверки FAB1/CRC. */
|
||||
{ /* Этот callback вызывается только после окончания аппаратных операций TI API. */
|
||||
(void)user; return *(volatile Uint16 *)address; /* Адрес уже ограничен общим валидатором диапазоном приложения B–H. */
|
||||
} /* Программирование не подменяется чтением staging EEPROM. */
|
||||
bool F28335Boot_FlashProgram(f28335_flash_update_t *s) /* Единственная точка фактической записи внутренней Flash из CAN COMMIT. */
|
||||
{ /* Постоянный загрузчик может вернуться к CAN recovery при любой ошибке. */
|
||||
f28335_flash_ops_t ops; /* Аппаратные операции передаются проверяемому транспортно-независимому writer. */
|
||||
if (CsmRegs.CSMSCR.bit.SECURE) return false; /* Защищённый CSM запрещает стирание до любого вызова TI API. */
|
||||
memcpy(&FlashApiRunStart, &FlashApiLoadStart, &FlashApiLoadEnd - &FlashApiLoadStart); /* API и его константы должны быть в RAM даже при сохранении сектора A. */
|
||||
Flash_CPUScaleFactor = (Uint32)(1048576.0L * (200.0L * (long double)SYSCLKOUT / 1000000000.0L)); /* Формула TI IQ20 использует реальную частоту SYSCLKOUT в герцах. */
|
||||
Flash_CallbackPtr = 0; ops.erase = erase_flash; ops.program = program_flash; ops.read = read_flash; ops.user = 0; /* Исключает callback во Flash во время занятости общего банка A–H. */
|
||||
return f28335_flash_update_program(s, &ops); /* Итоговый ACK COMMIT появляется только после CRC физической Flash; REBOOT остаётся отдельной командой клиента. */
|
||||
} /* Постоянный boot, handoff и EEPROM-параметры не программируются. */
|
||||
@@ -216,7 +216,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
|
||||
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
|
||||
|
||||
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
|
||||
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
|
||||
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO || command == PCAN_BOOT_CMD_SELECT_MEMORY) ? 1U : 0U)) { /* Выбор памяти имеет ровно один октет и не может читать отсутствующий payload. */
|
||||
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
|
||||
}
|
||||
if (command == PCAN_BOOT_CMD_PING) {
|
||||
@@ -269,6 +269,13 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
|
||||
|
||||
boot->last_activity_ms=boot->now_ms;
|
||||
switch (command) {
|
||||
case PCAN_BOOT_CMD_SELECT_MEMORY: { /* Явное назначение Flash не меняет поведение прежних клиентов без этой команды. */
|
||||
pcan_boot_status_t status; /* Точный отказ порта передаётся клиенту без подмены успешным ACK. */
|
||||
if (boot->state != PCAN_BOOT_STATE_METADATA || boot->have_image || boot->have_compat) return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); /* После начала метаданных, приёма либо проверки запрещает менять цель записи. */
|
||||
if (!boot->port.select_memory) return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_COMMAND); /* Старый загрузчик отклоняет новую цель до ERASE и передачи образа. */
|
||||
status = boot->port.select_memory(boot->port_user, data[0], &boot->config); /* Порт проверяет поддержку выбранной физической памяти и задаёт её режим. */
|
||||
return send_status(boot, command, status); /* Ответ защищён исходной адресацией и текущей ненулевой сессией. */
|
||||
} /* Остальные команды используют выбранную конфигурацию слотов. */
|
||||
case PCAN_BOOT_CMD_BEGIN_IMAGE:
|
||||
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) &&
|
||||
boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
|
||||
|
||||
@@ -48,6 +48,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
|
||||
set(SETPROTOCOL_SOURCES
|
||||
src/deif_xdi.c # Общее C99-ядро DEIF для всех платформ.
|
||||
src/set_invertor_gas.c # Подключает две проекции одной карты STM Invertor: GAS 0x3 и масочный GAS SLOT 0x4.
|
||||
src/set_deif_gauge.c
|
||||
src/set_regmap.c
|
||||
src/set_emu_ump.c
|
||||
src/set_emu_tms.c
|
||||
|
||||
@@ -11,6 +11,16 @@
|
||||
`include/balsam167_eeprom_layout.h`; PEEK, CRC и порядок байтов реализованы
|
||||
в `src/periph28335.c`. Проверка полного диапазона — `test_periph28335_eeprom`.
|
||||
|
||||
Декодер `balsam_can` знает служебные слова heartbeat периферии Бальзам-167:
|
||||
`0x0100` — счётчик секунд по модулю 65536, `0x0101` — аппаратный Mode,
|
||||
`0x0102` — код типа платы Desk. Назначения соответствуют `CAN_HEARTBEAT_ADDR`
|
||||
и ветке `CAN_send` в `Balsam_167_periph/Source/Internal/ecan.c`.
|
||||
Python-адаптер `set_devices.balsam_can` выделяет ответ с адресом `0x0100`
|
||||
и маской `111` как `BALZAM Heartbeat`, сохраняя исходные регистры и маршрут.
|
||||
Пример: `0x00BA0017`, `00 08 00 04 E1 00 00 5A` — heartbeat задатчика,
|
||||
90 секунд, Mode=8, Desk=4. Счётчик отображается без знака, в том числе
|
||||
при значениях `0x8000–0xFFFF`; обычные технологические int16 сохраняют знак.
|
||||
|
||||
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
|
||||
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
|
||||
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Универсальное адресное пространство эмуляторов
|
||||
|
||||
## Подключение панелей к конкретному MCU
|
||||
|
||||
Каталог `python/set_devices/mcu_profiles.py` связывает платы с общими сервисами,
|
||||
моделью `DEVICE_INFO`, настройками подключения и аппаратным таймером.
|
||||
`python/set_devices/qt_ports/emulator_workspace.py` принимает готовые панели
|
||||
потребителя по ключам `settings`, `ump`, `tms2812`, `igbt`, `wavegen` и выбирает
|
||||
их состав методом `select_mcu`. Общие алгоритмы УМП, 2812, IGBT и ЦАП остаются
|
||||
в описанном ниже C-ядре; новая панель не создаёт вторую модель или соединение.
|
||||
|
||||
SETGUI подключает эти инструменты из страниц «Терминал → STM32 → MCU».
|
||||
Поддержаны эмулятор STM32F407VET6 с TIM6 72 МГц и порт STM32G474RB с TIM6
|
||||
160 МГц. Параметры COM хранятся отдельно; расчёт частот ЦАП, подписи Vin
|
||||
и проверяемая модель SET v2 соответствуют выбранной плате. Профиль MCU
|
||||
нельзя переключать во время обмена. Сам выбор профиля не отправляет команды.
|
||||
|
||||
Общий автомат обнаружения находится в `python/set_devices/emulator_protocol.py`,
|
||||
COM-адаптер и клиент IGBT — в `python/set_devices/qt_ports/emulator_serial.py`
|
||||
и `igbt_client.py`. Они не импортируют SETGUI. Старые пути SETGUI только
|
||||
реэкспортируют эти реализации для совместимости.
|
||||
|
||||
STM32G474VET6 с прошивкой CANdle имеет отдельный профиль BOOT: приложение
|
||||
`0x08008000`, размер до 480 КиБ, CAN 500 кбит/с, Type 7 / Device 15 /
|
||||
Product 0, либо USART1 PA9/PA10 на 512000 бод, 8N1. Это другая прошивка,
|
||||
чем эмулятор STM32G474RB. Профиль BOOT готовит форму обновления и проверяет
|
||||
векторы приложения до начала CAN-передачи.
|
||||
|
||||
Общий код находится в `templates/c/set-protocol`. Прошивка платы задаёт память,
|
||||
GPIO и аппаратные callbacks; протокол и модели не включают STM32/HAL/USB/ОС,
|
||||
не выделяют heap и не читают системное время. Все вызовы одного экземпляра
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef SET_DEIF_GAUGE_H /* Переносимая геометрия прибора подключается один раз. */
|
||||
#define SET_DEIF_GAUGE_H /* Макет повторяет прибор SETGUI в логических пикселях 600×510. */
|
||||
#include <stdint.h> /* Цвет передаётся как RGB888 независимо от framebuffer и Qt. */
|
||||
#include "pcan_abi.h" /* Общий макрос экспорта делает графику доступной также из shared-библиотеки. */
|
||||
#ifdef __cplusplus /* Qt/C++ использует те же немодифицированные имена функций C. */
|
||||
extern "C" { /* Открывает переносимый ABI без привязки к конкретному GUI. */
|
||||
#endif /* Начало совместимости с C++. */
|
||||
typedef struct { /* Снимок содержит физические значения, а не wire-байты CAN. */
|
||||
double value[6], angle, command[3]; /* Порядок value совпадает с deif_xdi.h; command: POWER%, RPM%, градусы. */
|
||||
unsigned valid, angle_kind, commands, role, node, source; /* Маска свежести, тип угла, черновики и подписи роли/узлов; role=2 обозначает MASTER, остальные роли показывают принятые данные. */
|
||||
} setp_deif_gauge_values; /* Выбор свежести и фактического угла выполняет общее протокольное ядро. */
|
||||
typedef struct { /* Потребитель преобразует логические пиксели в свой холст. */
|
||||
void *user; /* Непрозрачный контекст рисующего порта. */
|
||||
void (*line)(void *,double,double,double,double,uint32_t,double); /* Отрезок x1/y1/x2/y2, RGB888 и толщина в пикселях эталона; порт сохраняет сглаживание при масштабировании. */
|
||||
void (*rect)(void *,double,double,double,double,uint32_t); /* Прямоугольник x/y/w/h; ненулевой старший байт задаёт alpha, RGB888 без старшего байта означает непрозрачный цвет. */
|
||||
void (*text)(void *,double,double,double,double,const char *,uint32_t,double,unsigned); /* UTF-8, RGB888, размер Qt в пунктах; style: 1 bold, 2 stretch80%, 4 left, 8 right. */
|
||||
void (*triangle)(void *,const double *,uint32_t); /* Три пары логических координат задают заполненные указатели движителя без копирования геометрии в порт. */
|
||||
void (*azimuth)(void *,unsigned); /* Значение 1 включает одинаковый масштаб X/Y вокруг центра компаса; 0 возвращает растяжение остальных элементов макета. */
|
||||
} setp_deif_gauge_painter; /* Ни Qt, ни Linux, ни выделение памяти не входят в контракт. */
|
||||
PCAN_ABI_API void setp_deif_gauge_draw(const setp_deif_gauge_values *v,const setp_deif_gauge_painter *p); /* Рисует шкалы и движитель; ноль азимута направлен вверх. */
|
||||
PCAN_ABI_API int setp_deif_gauge_hit(double x,double y,double *value); /* Возвращает 1=POWER%, 2=RPM%, 3=угол либо 0 вне активных зон. */
|
||||
#ifdef __cplusplus /* Закрывает единый ABI при включении заголовка из Qt/C++. */
|
||||
} /* Конец extern C. */
|
||||
#endif /* Чистая C99-сборка не получает лишних конструкций C++. */
|
||||
#endif /* Конец общего интерфейса графики DEIF. */
|
||||
@@ -31,7 +31,8 @@ enum set_plot_operation {
|
||||
PCAN_ABI_API uint32_t set_plot_abi_version(void);
|
||||
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
|
||||
* TRANSFORM rejects malformed viewports; invalid gesture values return the
|
||||
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
|
||||
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
|
||||
* with focus anchoring.
|
||||
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
|
||||
*/
|
||||
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
|
||||
|
||||
@@ -13,6 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
|
||||
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
|
||||
* Bounds: 2..100000 input points, 2..1000000 output points.
|
||||
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
|
||||
* meta[0..2] = original count, unique count, RMSE at original measurements.
|
||||
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).
|
||||
@@ -26,6 +27,15 @@ enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SP
|
||||
PCAN_ABI_API int set_signal_reconstruct(const double *x, const double *y, size_t count,
|
||||
int method, unsigned degree, size_t output_count, int endpoint,
|
||||
double *out_x, double *out_y, double *meta, double *work, size_t work_count);
|
||||
/* Дополнительный решатель получает уникальные нормированные X/Y и пишет degree+1
|
||||
* коэффициентов по возрастанию степени для аргумента 2*x-1. Код 0 означает успех,
|
||||
* 3 — недостаточный ранг, 5 — ошибку памяти/решателя. NULL сохраняет прежний МНК.
|
||||
* C99-подготовка, интерполяция, RMSE и размещение work одинаковы для всех решателей. */
|
||||
typedef int (*set_signal_fit_fn)(const double *, const double *, size_t, unsigned, double *); /* Обратный вызов отделяет необязательный C++-порт от переносимого ядра. */
|
||||
PCAN_ABI_API int set_signal_reconstruct_with_fit( /* Расширение ABI не меняет сигнатуру существующей функции для прошивок и GUI. */
|
||||
const double *x, const double *y, size_t count, int method, unsigned degree, /* Входные точки и степень имеют те же ограничения, что у исходного API. */
|
||||
size_t output_count, int endpoint, double *out_x, double *out_y, /* Результат остаётся в памяти вызывающего и сохраняет выбор конечной точки. */
|
||||
double *meta, double *work, size_t work_count, set_signal_fit_fn fit); /* Решатель используется только для полинома; рабочая память сохраняет прежний формат. */
|
||||
/* Reject out-of-range voltages, never silently clip. vref must be positive.
|
||||
* All input is validated before any output is written. */
|
||||
PCAN_ABI_API int set_signal_dac12(const double *volts, size_t count, double vref,
|
||||
|
||||
@@ -30,8 +30,12 @@ typedef struct {
|
||||
uint16_t count, received, ready, running, rate_high;
|
||||
uint32_t rate;
|
||||
set_wave_port port;
|
||||
uint32_t *xy_samples; /* Упакованные пары DHR12RD: X в битах 0…11, Y в 16…27; память предоставляет плата. */
|
||||
int (*start_xy)(void *, const uint32_t *, size_t, uint32_t); /* Запускает обе координаты общим таймером и одним DMA без сдвига между каналами. */
|
||||
uint16_t channels, selected, received_other; /* Режим 1/2, выбранная координата 0/1 и число отсчётов второй координаты для общего commit. */
|
||||
} set_wave_state;
|
||||
void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
|
||||
void set_wave_enable_xy(set_wave_state *, uint32_t *, int (*)(void *, const uint32_t *, size_t, uint32_t)); /* Подключает необязательный аппаратный порт XY после обычной инициализации. */
|
||||
/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
|
||||
* At most 120 words: request <=249 bytes, response <=245 bytes. */
|
||||
PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
|
||||
|
||||
@@ -33,6 +33,6 @@ LOCAL_SRC_FILES += ../../src/set_gas_watch.c # Включает общий GAS_W
|
||||
LOCAL_SRC_FILES += ../../src/pcan_gas_slot.c gas_slots_jni.c # Android собирает общий слотный кодек и тонкую JNI-границу без второго алгоритма.
|
||||
LOCAL_SRC_FILES += ../../src/set_invertor_gas.c # Android получает ту же C-реализацию нового профиля без копирования алгоритмов.
|
||||
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
|
||||
LOCAL_SRC_FILES += ../../src/deif_xdi.c set_deif_jni.c # Единственные DEIF-правила компилируются вместе с тонким JNI-портом.
|
||||
LOCAL_LDLIBS := -llog -lm
|
||||
LOCAL_SRC_FILES += ../../src/deif_xdi.c set_deif_jni.c # Общий DEIF и тонкий JNI без второй реализации.
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
||||
+5
-2
@@ -152,14 +152,17 @@ object BalsamCanProtocol {
|
||||
fun deviceName(device: Int): String = if (NativeSetProtocol.available) {
|
||||
NativeSetProtocol.nativeBalsamDeviceName(device)
|
||||
} else listOf(
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2",
|
||||
"УМП 1", "УМП 2", "Двигатель", "ВЭП", "Задатчик", "Узел 10", "Узел 11",
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой 1", "Силовой 2", // Имена узлов 1..4 повторяют таблицу адресов 167 и общее C-ядро.
|
||||
"УМП 1", "УМП 2", "Двигатель", "Задатчик", "ВЭП", "ПМУ", "Узел 11", // Узлы 5..10 соответствуют таблице, включая ПМУ на паре 0xBA0009/0xBA0019; узел 11 остаётся служебным.
|
||||
"Узел 12", "Терминал",
|
||||
).getOrElse(device - 1) { "Неизвестный узел" }
|
||||
|
||||
fun registerName(device: Int, address: Int): String =
|
||||
if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address)
|
||||
else when {
|
||||
address == 0x0100 -> "Heartbeat: счётчик, с" // CAN_HEARTBEAT_ADDR из ecan.c содержит беззнаковый секундный счётчик по модулю 65536.
|
||||
address == 0x0101 -> "Heartbeat: Mode" // Второе слово служебного пакета передаёт аппаратный номер Mode платы.
|
||||
address == 0x0102 -> "Heartbeat: тип платы (Desk)" // Третье слово передаёт Desk, чтобы Android без JNI использовал названия общего C-ядра.
|
||||
device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}"
|
||||
device in 3..4 && address in 0x18..0x27 -> "Показания T° ${address - 0x17}"
|
||||
device == 7 && address in 0x18..0x1F -> "Показания T° ${address - 0x17}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "board_config.h" /* Выбирает размещение Flash, USART1 и разрешение физических выводов IGBT. */
|
||||
#include "transport.h" /* Объявляет запуск общего UART/FDCAN-транспорта после настройки GPIO. */
|
||||
#include "emulator.h" /* Подключает единый шаг моделей УМП, 2812, IGBT и генератора. */
|
||||
#include "igbt_realtime_port.h" /* Объявляет аппаратный IGBT версии 4 с наносекундными compare. */
|
||||
#include "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. */
|
||||
@@ -11,8 +11,8 @@ uint32_t SystemCoreClock = 16000000u; /* После reset HSI16 работает
|
||||
const uint8_t AHBPrescTable[16] = {0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9}; /* CMSIS-кодирует делители HPRE как степени двойки по RM0440. */
|
||||
const uint8_t APBPrescTable[8] = {0,0,0,0,1,2,3,4}; /* HAL использует кодировку PPRE для вычисления частоты периферии G4. */
|
||||
static uint8_t usb_clock_ready; /* До настройки HSI48/CRS предотвращает преждевременный запуск USB. */
|
||||
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Не занимает выводы FLASH/CAN2/CAN3 на неподтверждённой ревизии HOPE по умолчанию. */
|
||||
static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14}; /* Ручной профиль Vin PB0/1/2/10..14 совпадает с прежним портом G474. */
|
||||
#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 здесь не вызывается. */
|
||||
@@ -68,7 +68,7 @@ static void peripheral_clock(void) /* Создаёт одинаковую час
|
||||
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; /* Общий CAN-порт рассчитывает 250 кбит/с из фактических 80 МГц PCLK1. */
|
||||
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. */
|
||||
@@ -91,9 +91,10 @@ void board_init(void) /* Вызывается после создания общ
|
||||
HAL_GPIO_Init(GPIOA, &gpio); /* Применяет HAL-настройку UART к порту A одним вызовом. */
|
||||
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Alternate = GPIO_AF9_FDCAN1; /* PD0 RX и PD1 TX соединены с U6 TJA1051T/3; PB8 на этой плате занят BOOT0. */
|
||||
HAL_GPIO_Init(GPIOD, &gpio); /* Подключает FDCAN1 к штатным CAN1L/CAN1H разъёма J4 по схеме от 14.04.2025. */
|
||||
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Физический профиль включается вручную после проверки занятых выводов FLASH и других разъёмов. */
|
||||
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Подключает физический IGBT через свободные выводы, не затрагивая CAN1/2/3, USB, UART, DAC и дисплей. */
|
||||
__HAL_RCC_GPIOC_CLK_ENABLE(); /* Порт C нужен только для восьми физических выходов Vstat. */
|
||||
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14; /* Сохраняет отключённый ручной профиль Vin; PB12/PB13 соединены с CAN2 и требуют изменения внешних соединений. */
|
||||
gpio.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, исключая импульс при включении выходов. */
|
||||
@@ -102,23 +103,23 @@ void board_init(void) /* Вызывается после создания общ
|
||||
gpio.Pin = SERIAL_DE_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Настраивает выбранный внешний вывод DE, не меняя UART TX/RX. */
|
||||
HAL_GPIO_WritePin(SERIAL_DE_PORT, SERIAL_DE_PIN, GPIO_PIN_RESET); HAL_GPIO_Init(SERIAL_DE_PORT, &gpio); /* Начинает в режиме приёма с выключенным передатчиком RS-485. */
|
||||
#endif /* Штатный TTL UART не занимает вывод DE. */
|
||||
#if EMU_IGBT_REALTIME /* Регистрирует аппаратный сервис только в новой сборке HOPE. */
|
||||
hope_igbt_realtime_init(emu_server_state()); /* Подменяет окно 0x1400 до первого запроса, сохраняя остальные модели и транспорты. */
|
||||
#endif /* Прежний профиль продолжает использовать миллисекундный IGBT версии 3. */
|
||||
#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(); /* Сохраняет штатное время моделей и применения выбранного режима MCU. */
|
||||
hope_igbt_realtime_poll(); /* Останавливает аппаратный IGBT в том же шаге, в котором выбран другой режим. */
|
||||
#else /* Ветка совместимости сохраняет прежние миллисекундные GPIO. */
|
||||
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Считывает восемь физических Vin только для явно включённого ручного профиля. */
|
||||
#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 /* Штатный профиль HOPE не использует неподтверждённые внешние дискретные линии. */
|
||||
#else /* Явное отключение GPIO оставляет модель доступной через регистровый источник TX. */
|
||||
igbt_emu_set_tx(0); /* При выборе GPIO задаёт нулевой Vin; регистровый источник остаётся независимым. */
|
||||
#endif /* Далее все модели продвигаются одинаково независимо от наличия дискретного порта. */
|
||||
emu_tick(); /* Продвигает выбранную модель и общие отложенные настройки ровно на 1 мс. */
|
||||
@@ -127,5 +128,5 @@ void board_tick(void) /* Выполняется главным циклом од
|
||||
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. */
|
||||
#endif /* Завершает выбор между аппаратным IGBT и прежней миллисекундной моделью. */
|
||||
} /* Завершает один миллисекундный шаг без обработки моделей из прерываний. */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef HOPE_BOARD_CONFIG_H /* Защищает единый профиль платы HOPE от повторного включения. */
|
||||
#ifndef EMU_IGBT_REALTIME /* Разрешает явную сборку прежней логической модели для старого GUI. */
|
||||
#define EMU_IGBT_REALTIME 1 /* Штатный HOPE использует HRTIM D/E/F и TIM2/3/4/5/15 с окном IGBT версии 4 в наносекундах. */
|
||||
#endif /* Сохраняет явное переопределение параметром компилятора. */
|
||||
#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. */
|
||||
@@ -13,7 +13,7 @@
|
||||
#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 250000u /* Начальная скорость приложения в бит/с; CAN-загрузчик отдельно использует 500000. */
|
||||
#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. */
|
||||
@@ -28,9 +28,9 @@
|
||||
#define EMU_HSE_HZ 8000000u /* Номинал JT1 равен 8 МГц по схеме; HAL и PLL используют одну частоту в герцах. */
|
||||
#endif /* Сохраняет явно переданную частоту внешнего источника. */
|
||||
#define EMU_HSE_BYPASS 0 /* Внешний режим рассчитан на кварц; для готового генератора нужно отдельно выбрать bypass. */
|
||||
#ifndef EMU_IGBT_GPIO_ENABLE /* Ручной профиль Vin требует устранить подтверждённое схемой пересечение PB12/PB13 с CAN2. */
|
||||
#define EMU_IGBT_GPIO_ENABLE 0 /* Модель IGBT доступна через регистры; штатные линии CAN2 не становятся дискретными Vin. */
|
||||
#endif /* Позволяет включить физические Vin/Vstat параметром компилятора. */
|
||||
#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. */
|
||||
|
||||
@@ -61,7 +61,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
relative = can_id - BALSAM_CAN_BASE_ID;
|
||||
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
|
||||
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
|
||||
output->device = (uint8_t)((relative & 0x0FU) + 1U);
|
||||
output->device = (uint8_t)((relative & 0x0FU) + 1U); /* В ecan.c перед сборкой CAN ID выполняется DevNum--; маска 0x0F выделяет индекс 0..12, а +1 восстанавливает номер Mode. */
|
||||
if (can_id == 0x00745009UL || can_id == 0x00745019UL) {
|
||||
output->device = 10U;
|
||||
output->direction = can_id == 0x00745019UL
|
||||
@@ -80,9 +80,9 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
const char *balsam_can_device_name(uint8_t device)
|
||||
{
|
||||
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
|
||||
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
|
||||
"Задатчик", "Пульт (МПУ)", "Узел 11", "Узел 12", "Терминал"
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой 1", /* Первые три узла соответствуют строкам 1..3 предоставленной таблицы адресов 167. */
|
||||
"Силовой 2", "УМП 1", "УМП 2", "Двигатель", "Задатчик", /* Строки 4..8 таблицы; VPU1_CAN_DEVICE=8 соответствует задатчику, а не ВЭП. */
|
||||
"ВЭП", "ПМУ", "Узел 11", "Узел 12", "Терминал" /* Строка 9 — ВЭП, пара 0xBA0009/0xBA0019 — ПМУ; служебные узлы 11..13 сохраняют прежние назначения. */
|
||||
};
|
||||
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
|
||||
? names[device - 1U] : "Неизвестный узел";
|
||||
@@ -91,6 +91,13 @@ const char *balsam_can_device_name(uint8_t device)
|
||||
size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||
char *output, size_t output_size)
|
||||
{
|
||||
if (address == 0x0100U) /* CAN_HEARTBEAT_ADDR в ecan.c периферии 167 задаёт служебный адрес вне технологической карты R0..R127. */
|
||||
return copy_name("Heartbeat: счётчик, с", output, output_size); /* Первое слово hiword содержит секунды после запуска по модулю 65536. */
|
||||
if (address == 0x0101U) /* Следующее слово heartbeat соответствует старшим 16 битам loword в CAN_send. */
|
||||
return copy_name("Heartbeat: Mode", output, output_size); /* Прошивка передаёт аппаратный Mode без уменьшения на единицу, применяемого только к CAN ID. */
|
||||
if (address == 0x0102U) /* Третье слово heartbeat соответствует младшим 16 битам loword. */
|
||||
return copy_name("Heartbeat: тип платы (Desk)", output, output_size); /* Значение Desk задаёт тип платы и отличается от номера Mode и номера CAN-узла. */
|
||||
|
||||
if ((device == 1U || device == 2U) && address < 20U)
|
||||
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||
if ((device == 1U || device == 2U) && address >= 0x18U && address <= 0x2BU)
|
||||
@@ -139,7 +146,7 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||
if (device == 7U && address >= 0x18U && address <= 0x1FU)
|
||||
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||
|
||||
if (device == 8U) {
|
||||
if (device == 9U) { /* Карта напряжений принадлежит ВЭП с adr_SHKF=9; задатчик с номером 8 использует другую карту. */
|
||||
static const char *const vep_names[] = {
|
||||
"Диагностика 380В Ф1", "Диагностика 380В Ф2",
|
||||
"Диагностика 31В Ф1", "Диагностика 31В Ф2",
|
||||
@@ -164,15 +171,15 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||
return copy_name("Дискретные датчики (14 бит)", output, output_size);
|
||||
}
|
||||
|
||||
if (device == 9U && address == 0U)
|
||||
if (device == 8U && address == 0U) /* У VPU1_CAN_DEVICE=8 слово 0 передаёт обороты ГЭД, а не диагностику ВЭП. */
|
||||
return copy_name("Обороты ГЭД, об/мин", output, output_size);
|
||||
if (device == 9U && address == 1U)
|
||||
if (device == 8U && address == 1U) /* Слово 1 той же платы задатчика содержит обороты гребного вала. */
|
||||
return copy_name("Обороты ГВ, об/мин", output, output_size);
|
||||
if (device == 9U && address == 2U)
|
||||
if (device == 8U && address == 2U) /* ADR_CAN_LAMPS_ON_VPU=2 задаёт регистр первой группы ламп задатчика. */
|
||||
return copy_name("Лампы (8 бит)", output, output_size);
|
||||
if (device == 9U && address == 3U)
|
||||
if (device == 8U && address == 3U) /* ADR_CAN_LAMPS_2_ON_VPU=3 задаёт регистр второй группы индикаторов задатчика. */
|
||||
return copy_name("Диоды (16 бит)", output, output_size);
|
||||
if (device == 9U && address == 0x10U)
|
||||
if (device == 8U && address == 0x10U) /* ADR_CAN_KEY_ON_VPU=16, то есть 0x10, задаёт слово кнопок VPU; адрес не перенумеровывается. */
|
||||
return copy_name("Кнопки (12 бит)", output, output_size);
|
||||
|
||||
if (address == 0x16U)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,6 +7,8 @@
|
||||
#include "set_plot.h"
|
||||
#include <math.h>
|
||||
|
||||
#define MIN_VIEW_SPAN 1e-12 /* Минимальная безразмерная доля полного диапазона по X и Y задаёт увеличение до 10^12 раз; запас относительно точности double сохраняет различимые границы нормированной области. */
|
||||
|
||||
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
|
||||
static int finite_values(const double *v, size_t n) {
|
||||
size_t i;
|
||||
@@ -23,13 +25,13 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
|
||||
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
|
||||
if (op == SET_PLOT_TRANSFORM) {
|
||||
double w, h, fx, fy;
|
||||
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
|
||||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
|
||||
if (!finite_values(v, 4) || v[2] < MIN_VIEW_SPAN || v[2] > 1 || /* Проверяем конечность смещений и размеров, разрешая ширину до триллионной доли исходного диапазона X. */
|
||||
v[3] < MIN_VIEW_SPAN || v[3] > 1 || v[0] < 0 || v[1] < 0 || /* Такой же предел для Y сохраняет возможность дальнейшего перемещения и увеличения уже приближённой области. */
|
||||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
|
||||
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
|
||||
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
|
||||
w = clamp(v[2] / v[4], 1.0/128, 1);
|
||||
h = clamp(v[3] / v[5], 1.0/128, 1);
|
||||
w = clamp(v[2] / v[4], MIN_VIEW_SPAN, 1); /* Делим текущую долю X на положительный коэффициент приближения; ограничиваем результат между новым пределом и полным диапазоном. */
|
||||
h = clamp(v[3] / v[5], MIN_VIEW_SPAN, 1); /* Аналогично масштабируем Y, снимая прежнее ограничение 128 раз и сохраняя ненулевую высоту для координатных преобразований. */
|
||||
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
|
||||
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
|
||||
out[1] = clamp(v[1] + (v[3] - h) * fy - v[7] * h, 0, 1 - h);
|
||||
|
||||
@@ -33,14 +33,14 @@ static double evaluate(double t,const double *x,const double *y,size_t n,
|
||||
return (2*u*u*u-3*u*u+1)*y[i]+(u*u*u-2*u*u+u)*h*d[i]
|
||||
+(-2*u*u*u+3*u*u)*y[i+1]+(u*u*u-u*u)*h*d[i+1];
|
||||
}
|
||||
int set_signal_reconstruct(const double *input_x,const double *input_y,size_t count,
|
||||
int method,unsigned degree,size_t output_count,int endpoint,double *out_x,
|
||||
double *out_y,double *meta,double *work,size_t work_count) {
|
||||
int set_signal_reconstruct_with_fit(const double *input_x,const double *input_y,size_t count, /* Общая подготовка данных обслуживает исходный и подключаемый решатели. */
|
||||
int method,unsigned degree,size_t output_count,int endpoint,double *out_x, /* Метод, степень и временная сетка сохраняют контракт существующего API. */
|
||||
double *out_y,double *meta,double *work,size_t work_count,set_signal_fit_fn fit) { /* NULL выбирает прежний МНК; другой callback получает уже нормированные данные. */
|
||||
point *pairs; double *x,*y,*d,*h,*s,*q,*column;
|
||||
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
|
||||
size_t i,j,k,n=0; unsigned pass;
|
||||
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
|
||||
||output_count<2||output_count>10000||work_count<14*count+128
|
||||
||output_count<2||output_count>1000000||work_count<14*count+128
|
||||
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
|
||||
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
|
||||
s=h+count; q=s+count; column=q+6*count;
|
||||
@@ -65,6 +65,10 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
|
||||
}
|
||||
if(method==SET_SIGNAL_POLYNOMIAL) {
|
||||
if(degree<1||degree>5||n<=degree) return 1;
|
||||
if(fit) { /* Подключаемый решатель заменяет только вычисление коэффициентов МНК. */
|
||||
int status=fit(x,y,n,degree,coef); /* Передаём уникальные нормированные точки и массив до шести коэффициентов. */
|
||||
if(status) return status; /* Ошибка ранга или памяти возвращается явно, без скрытого перехода на другой решатель. */
|
||||
} else { /* Исходный алгоритм остаётся неизменным вариантом для сравнения и прошивок. */
|
||||
for(j=0;j<=degree;++j) {
|
||||
double norm=0;
|
||||
for(i=0;i<n;++i) column[i]=pow(2*x[i]-1,(double)j);
|
||||
@@ -81,6 +85,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
|
||||
j=i-1; for(k=j+1;k<=degree;++k) coef[j]-=r[j][k]*coef[k];
|
||||
coef[j]/=r[j][j];
|
||||
}
|
||||
} /* Оба решателя возвращают коэффициенты в одном базисе для общего evaluate. */
|
||||
} else if(method==SET_SIGNAL_PCHIP) {
|
||||
d[0]=d[n-1]=s[0];
|
||||
if(n>2) {
|
||||
@@ -112,6 +117,11 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
|
||||
meta[0]=(double)count; meta[1]=(double)n; meta[2]=sqrt(rmse)*scale;
|
||||
return isfinite(meta[2])?0:2;
|
||||
}
|
||||
int set_signal_reconstruct(const double *input_x,const double *input_y,size_t count, /* Сохраняем прежний экспорт и поведение всех существующих клиентов. */
|
||||
int method,unsigned degree,size_t output_count,int endpoint,double *out_x, /* Параметры передаются без изменения единиц времени и амплитуды. */
|
||||
double *out_y,double *meta,double *work,size_t work_count) { /* Владение выходами и рабочим буфером по-прежнему остаётся у вызывающего. */
|
||||
return set_signal_reconstruct_with_fit(input_x,input_y,count,method,degree,output_count,endpoint,out_x,out_y,meta,work,work_count,NULL); /* NULL выбирает исходный двухпроходный МНК без зависимости от C++. */
|
||||
} /* Завершаем совместимую обёртку исходного C ABI. */
|
||||
int set_signal_dac12(const double *volts,size_t count,double vref,uint16_t *codes) {
|
||||
size_t i;
|
||||
if(!volts||!codes||count<2||count>10000||!isfinite(vref)||vref<=0) return 1;
|
||||
|
||||
@@ -13,18 +13,41 @@ static size_t seal(uint8_t *p,size_t n) {
|
||||
uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);return n+2;
|
||||
}
|
||||
void set_wave_init(set_wave_state *s,uint16_t *samples,size_t capacity,const set_wave_port *port) {
|
||||
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;
|
||||
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;s->channels=1; /* После reset сохраняет совместимость с прежним одноканальным клиентом. */
|
||||
if(port) s->port=*port;
|
||||
}
|
||||
void set_wave_enable_xy(set_wave_state *s,uint32_t *pairs,int (*start)(void *,const uint32_t *,size_t,uint32_t)) { /* Плата явно объявляет доступность второго выхода и его SRAM. */
|
||||
s->xy_samples=pairs;s->start_xy=start; /* Нулевой callback оставляет устройство одноканальным, включая прежние F103 и G474RB. */
|
||||
} /* Подключение порта не запускает периферию и не меняет таблицу. */
|
||||
static uint16_t sample_get(const set_wave_state *s,unsigned index) { /* Читает выбранную координату без перестановки готовых пар перед DMA. */
|
||||
return s->channels==2?(uint16_t)((s->xy_samples[index]>>(16u*s->selected))&4095u):s->samples[index]; /* Полуслова DHR12RD разделены 16 битами; код каждого DAC занимает 12 бит. */
|
||||
} /* Одноканальный формат uint16_t остаётся прежним. */
|
||||
static void sample_set(set_wave_state *s,unsigned index,uint16_t value) { /* Изменяет только выбранную координату, сохраняя парный отсчёт другого канала. */
|
||||
if(s->channels==2) { /* Двухканальная таблица уже хранится в формате единственной 32-битной передачи DMA. */
|
||||
unsigned shift=16u*s->selected; /* Сдвиг 0 выбирает X/PA4, сдвиг 16 выбирает Y/PA5 по DHR12RD из RM0090/RM0440. */
|
||||
s->xy_samples[index]=(s->xy_samples[index]&~(0xffffu<<shift))|((uint32_t)value<<shift); /* Очищает полуслово вместе с резервными битами и записывает проверенный 12-битный код. */
|
||||
} else s->samples[index]=value; /* Сохраняет прежнюю компактную таблицу одиночного канала. */
|
||||
} /* Проверки диапазона выполняет вызывающий обработчик до изменения памяти. */
|
||||
static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
|
||||
if(reg==SET_WAVE_BASE+2 && value==0) {
|
||||
if(s->port.stop) s->port.stop(s->port.context);
|
||||
s->running=0; return 0;
|
||||
}
|
||||
if(reg==SET_WAVE_BASE+10) { /* Выбор X/Y меняет только окно загрузки и чтения, поэтому доступен и при воспроизведении. */
|
||||
uint16_t previous; /* Временно сохраняет счётчик прежней координаты при переключении окна. */
|
||||
if(value>=s->channels)return 3; /* Одноканальная плата отвергает Y; произвольные индексы не меняют состояние. */
|
||||
if(value!=s->selected) {previous=s->received;s->received=s->received_other;s->received_other=previous;s->selected=(uint16_t)value;} /* Меняет местами счётчики, сохраняя оба независимо от порядка загрузки. */
|
||||
return 0; /* Выбор окна не снимает commit и не меняет фазу аппаратного вывода. */
|
||||
} /* Остальные изменения таблицы запрещены во время running. */
|
||||
if(s->running) return 6;
|
||||
if(reg==SET_WAVE_BASE+9) { /* Число одновременно воспроизводимых каналов задаётся до начала новой загрузки. */
|
||||
if(value<1||value>2||(value==2&&(!s->xy_samples||!s->start_xy)))return 3; /* Двойной режим допустим только с буфером пар и аппаратным исполнителем. */
|
||||
s->channels=(uint16_t)value;s->selected=0;s->count=s->received=s->received_other=s->ready=0; /* Смена формата инвалидирует обе координаты, исключая запуск смешанной старой таблицы. */
|
||||
return 0; /* Пуск остаётся отдельной командой после полного commit. */
|
||||
} /* Прежние регистры частоты и длины сохраняют адреса. */
|
||||
if(reg==SET_WAVE_BASE+2 && value==1) {
|
||||
if(!s->ready||!s->port.start) return 3;
|
||||
if(s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4;
|
||||
if(s->channels==2?s->start_xy(s->port.context,s->xy_samples,s->count,s->rate):s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4; /* XY запускается одной аппаратной транзакцией; ошибка порта не выставляет running. */
|
||||
s->running=1;return 0;
|
||||
}
|
||||
if(reg==SET_WAVE_BASE+3) {
|
||||
@@ -38,37 +61,38 @@ static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
|
||||
}
|
||||
if(reg==SET_WAVE_BASE+4) {
|
||||
if(value<2||value>SET_WAVE_MAX||value>s->capacity) return 3;
|
||||
s->count=(uint16_t)value;s->received=s->ready=0;return 0;
|
||||
s->count=(uint16_t)value;s->received=s->received_other=s->ready=0; /* Новая длина начинает загрузку обеих координат с нуля. */return 0;
|
||||
}
|
||||
if(reg>=SET_WAVE_DATA&®<SET_WAVE_DATA+SET_WAVE_MAX) {
|
||||
unsigned index=reg-SET_WAVE_DATA;
|
||||
if(index>=s->count||index>s->received||value>4095||s->ready) return 3;
|
||||
if(index<s->received) return s->samples[index]==value?0:3;
|
||||
s->samples[index]=(uint16_t)value;++s->received;return 0;
|
||||
if(index<s->received) return sample_get(s,index)==value?0:3; /* Повтор подтверждается только при совпадении выбранной координаты; соседнее полуслово пары не учитывается. */
|
||||
sample_set(s,index,(uint16_t)value); /* Готовит 16-битный отсчёт либо соответствующее полуслово XY. */++s->received;return 0;
|
||||
}
|
||||
if(reg==SET_WAVE_BASE+6) {
|
||||
if(value!=0xA55A||s->count<2||s->received!=s->count) return 3;
|
||||
if(value!=0xA55A||s->count<2||s->received!=s->count||(s->channels==2&&s->received_other!=s->count)) return 3; /* Общий commit требует полные X и Y одинаковой длины. */
|
||||
s->ready=1;return 0;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
unsigned set_wave_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
|
||||
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[9];unsigned i;
|
||||
if(!s||!s->samples||!out||!count||(uint32_t)off+count>9)return 2;
|
||||
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[13]; /* Девять прежних слов дополнены режимом, селектором, возможностями и вторым счётчиком XY. */unsigned i;
|
||||
if(!s||!s->samples||!out||!count||(uint32_t)off+count>13)return 2; /* Проверяет весь диапазон расширенного статуса до чтения четырёх дополнительных слов XY. */
|
||||
w[0]=0x5747;w[1]=3;w[2]=s->running;w[3]=(uint16_t)s->rate;
|
||||
w[4]=s->count;w[5]=s->received;w[6]=s->ready;w[7]=(uint16_t)s->capacity;w[8]=(uint16_t)(s->rate>>16);
|
||||
w[9]=s->channels;w[10]=s->selected;w[11]=s->start_xy?2:1;w[12]=s->received_other; /* Расширение 0x1309…0x130C не меняет WG v3 и прежний ответ из восьми слов. */
|
||||
for(i=0;i<count;++i)out[i]=w[off+i];
|
||||
return 0;
|
||||
}
|
||||
unsigned set_wave_write(void *ctx,uint16_t off,uint16_t value) {
|
||||
set_wave_state *s=(set_wave_state *)ctx;
|
||||
if(!s||!s->samples||off>=9)return 2;
|
||||
if(!s||!s->samples||off>=11)return 2; /* Запись разрешена до селектора 0x130A включительно; возможности и второй счётчик доступны только для чтения. */
|
||||
return write_register(s,SET_WAVE_BASE+off,value);
|
||||
}
|
||||
unsigned set_wave_samples_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
|
||||
set_wave_state *s=(set_wave_state *)ctx;unsigned i;
|
||||
if(!s||!s->samples||!out||!count||(uint32_t)off+count>s->received)return 2;
|
||||
for(i=0;i<count;++i)out[i]=s->samples[off+i];
|
||||
for(i=0;i<count;++i)out[i]=sample_get(s,off+i) /* Возвращает выбранный канал из общей таблицы пар. */;
|
||||
return 0;
|
||||
}
|
||||
unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
|
||||
@@ -77,21 +101,22 @@ unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
|
||||
return write_register(s,SET_WAVE_DATA+off,value);
|
||||
}
|
||||
size_t set_wave_rtu(set_wave_state *s,unsigned device,const uint8_t *req,size_t n,uint8_t *out,size_t cap) {
|
||||
unsigned reg,value,error=0,i;uint16_t status[8];size_t length=0;
|
||||
unsigned reg,value,error=0,i;uint16_t status[13]; /* Вмещает прежний статус и дополнительные регистры XY без изменения старого запроса. */size_t length=0;
|
||||
if(!s||!s->samples||!req||!out||cap<21||n!=8||device<1||device>247
|
||||
||req[0]!=device||set_crc16_modbus(req,n)) return 0;
|
||||
reg=read16(req+2);value=read16(req+4);
|
||||
if(!((reg>=SET_WAVE_BASE&®<SET_WAVE_BASE+9)||(reg>=SET_WAVE_DATA&®<SET_WAVE_DATA+SET_WAVE_MAX))) return 0;
|
||||
if(!((reg>=SET_WAVE_BASE&®<SET_WAVE_BASE+13)||(reg>=SET_WAVE_DATA&®<SET_WAVE_DATA+SET_WAVE_MAX))) return 0; /* Общий RTU-кодек узнаёт расширенное окно XY и прежнюю таблицу без захвата чужих адресов. */
|
||||
out[0]=(uint8_t)device;out[1]=req[1];
|
||||
if(req[1]==6) {error=write_register(s,reg,value);memcpy(out,req,6);length=6;}
|
||||
else if(req[1]==3) {
|
||||
(void)set_wave_read(s,0,8,status);
|
||||
if(reg==SET_WAVE_BASE&&value==8) {
|
||||
out[2]=16;for(i=0;i<8;++i) write16(out+3+2*i,status[i]);length=19;
|
||||
} else if(reg==SET_WAVE_BASE+8&&value==1) {
|
||||
out[2]=2;write16(out+3,s->rate>>16);length=5;
|
||||
} else if(reg>=SET_WAVE_DATA&&value==1&®-SET_WAVE_DATA<s->received) {
|
||||
out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5;
|
||||
if(reg>=SET_WAVE_BASE&®<SET_WAVE_BASE+13&&value&&value<=13) { /* Отдельный RTU-кодек читает те же регистры, что общий сервер SET v2. */
|
||||
error=set_wave_read(s,(uint16_t)(reg-SET_WAVE_BASE),(uint16_t)value,status); /* Проверяет весь диапазон до формирования ответа. */
|
||||
if(!error) { /* Ошибочный диапазон возвращает исключение без чтения за пределами статуса. */
|
||||
if(cap<5u+2u*value)return 0; /* Не пишет ответ расширенного статуса в прежний короткий буфер. */
|
||||
out[2]=(uint8_t)(2u*value);for(i=0;i<value;++i)write16(out+3+2*i,status[i]);length=3u+2u*value; /* Отдаёт точное запрошенное число слов с прежним порядком байтов Modbus. */
|
||||
} /* CRC добавляется общим seal после формирования тела. */
|
||||
} else if(reg>=SET_WAVE_DATA&&value==1&®-SET_WAVE_DATA<s->received) { /* Чтение одного отсчёта сохраняет старый wire-контракт. */
|
||||
out[2]=2;write16(out+3,sample_get(s,reg-SET_WAVE_DATA));length=5; /* XY возвращает выбранную координату, mono возвращает прежний uint16_t. */
|
||||
} else error=2;
|
||||
} else error=1;
|
||||
if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
|
||||
@@ -111,6 +136,9 @@ size_t set_wave_request(unsigned device,unsigned op,unsigned index,unsigned valu
|
||||
case 7: if(index>=SET_WAVE_MAX)return 0;reg=SET_WAVE_DATA+index;fn=3;value=1;break;
|
||||
case 8: reg+=8;fn=3;value=1;break;
|
||||
case 9: reg+=8;if(value>(SET_WAVE_MAX_RATE>>16))return 0;break;
|
||||
case 10: reg+=9;if(value<1||value>2)return 0;break; /* Выбирает один или два синхронных канала через существующий RTU/SET v2 транспорт. */
|
||||
case 11: reg+=10;if(value>1)return 0;break; /* Выбирает X=0 либо Y=1 для прежних команд sample/block/readback. */
|
||||
case 12: reg+=9;fn=3;value=4;break; /* Читает режим, селектор, возможности порта и количество отсчётов второй координаты. */
|
||||
default:return 0;
|
||||
}
|
||||
out[0]=(uint8_t)device;out[1]=(uint8_t)fn;write16(out+2,reg);write16(out+4,value);return seal(out,6);
|
||||
@@ -131,7 +159,7 @@ int set_wave_response(const uint8_t *req,const uint8_t *reply,size_t size,uint16
|
||||
if(req[1]==6) {if(memcmp(req,reply,8)||cap<1)return -1;words[0]=read16(req+4);return 1;}
|
||||
if(reply[2]!=expected-5||cap<(expected-5)/2)return -1;
|
||||
for(i=0;i<(expected-5)/2;++i)words[i]=read16(reply+3+2*i);
|
||||
if(read16(req+2)==SET_WAVE_BASE&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2;
|
||||
if(read16(req+2)==SET_WAVE_BASE&&read16(req+4)>=2&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2; /* Версия проверяется только при наличии двух слов, исключая чтение за границей короткого ответа. */
|
||||
return (int)((expected-5)/2);
|
||||
}
|
||||
|
||||
@@ -168,13 +196,13 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
|
||||
/* Preflight the whole block, including replayed prefix, before writing. */
|
||||
for(i=0;i<count;++i) {
|
||||
unsigned value=read16(req+7+2*i);
|
||||
if(value>4095||(index+i<s->received&&s->samples[index+i]!=value)) {
|
||||
if(value>4095||(index+i<s->received&&sample_get(s,index+i)!=value)) {
|
||||
error=3;break;
|
||||
}
|
||||
}
|
||||
if(!error) {
|
||||
if(cap<8)return 0;
|
||||
for(i=0;i<count;++i)s->samples[index+i]=read16(req+7+2*i);
|
||||
for(i=0;i<count;++i)sample_set(s,index+i,read16(req+7+2*i)); /* После preflight записывает весь блок только выбранной координаты. */
|
||||
if(index+count>s->received)s->received=(uint16_t)(index+count);
|
||||
memcpy(out,req,6);length=6;
|
||||
}
|
||||
@@ -184,7 +212,7 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
|
||||
else {
|
||||
if(cap<5+2*(size_t)count)return 0;
|
||||
out[2]=(uint8_t)(2*count);
|
||||
for(i=0;i<count;++i)write16(out+3+2*i,s->samples[index+i]);
|
||||
for(i=0;i<count;++i)write16(out+3+2*i,sample_get(s,index+i)); /* Чтение блоков использует тот же селектор, что и запись. */
|
||||
length=3+2*(size_t)count;
|
||||
}
|
||||
} else error=1;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
|
||||
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
|
||||
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
|
||||
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
|
||||
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1e15,1e15,0,0,0.5,0.5],"output":[0.4999999999995,0.4999999999995,1e-12,1e-12]},
|
||||
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
|
||||
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
|
||||
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},
|
||||
|
||||
@@ -26,6 +26,7 @@ SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
|
||||
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
|
||||
"set_deif_gauge.c", "set_igbt_realtime.c",
|
||||
"set_regmap.c", "set_emu_ump.c", "set_emu_tms.c", "set_emu_igbt.c", "set_emu_server.c", "set_boot.c",
|
||||
"firmware_image.c", "altera_logic.c",
|
||||
"altera_stream.c",
|
||||
@@ -65,7 +66,7 @@ def _build_msvc(output: Path, build_dir: Path) -> None:
|
||||
import_library = build_dir / "setprotocol.lib"
|
||||
jni_flags = " ".join(f'/I"{path}"' for path in JNI_INCLUDES)
|
||||
command = (
|
||||
f'call "{vcvars}" && cl /nologo /W4 /std:c11 '
|
||||
f'call "{vcvars}" && cl /nologo /W4 /O2 /std:c11 ' # Оптимизируем исходный C-решатель, чтобы сравнение с оптимизированным Eigen не измеряло различие режимов компиляции.
|
||||
f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" {jni_flags} /LD {quoted_sources} '
|
||||
f'/Fe"{output}" /link /IMPLIB:"{import_library}"'
|
||||
)
|
||||
@@ -78,7 +79,7 @@ def _build_cc(output: Path, build_dir: Path) -> None:
|
||||
compiler = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc")
|
||||
if compiler is None:
|
||||
raise SystemExit("C compiler was not found")
|
||||
command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Wpedantic",
|
||||
command = [compiler, "-O2", "-std=c99", "-Wall", "-Wextra", "-Wpedantic", # Одинаковая оптимизация C99 и C++ делает время настольных вариантов сопоставимым.
|
||||
"-fPIC", "-shared", f"-I{INCLUDE}",
|
||||
*(f"-I{path}" for path in JNI_INCLUDES), *map(str, SOURCES),
|
||||
"-o", str(output), "-lm"]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Альтернативный МНК Eigen QR
|
||||
|
||||
Необязательный настольный модуль выполняет полиномиальный МНК через
|
||||
`Eigen::ColPivHouseholderQR`. Исходный двухпроходный решатель остаётся вариантом
|
||||
по умолчанию. Прошивки и основная SETProtocol DLL продолжают собираться как C99.
|
||||
|
||||
Путь вызова: GUI → Python/ctypes → `setsignal_eigen` → общая подготовка
|
||||
`set_signal.c` → callback Eigen → общие вычисление кривой и RMSE.
|
||||
Сортировка, усреднение повторов X, нормировка, сетка, рабочий буфер и аннотации
|
||||
формул совпадают у обоих вариантов. Порог ранга Eigen относительный: `1e-12`;
|
||||
он может иначе классифицировать пограничные данные, чем исходная ортогонализация.
|
||||
Модуль не зависит от Qt и не участвует в обмене с устройствами.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
| --- | --- |
|
||||
| `signal_eigen.cpp` | Решатель QR и экспорт `set_signal_reconstruct_eigen`; C++14, Eigen, `set_signal.h` |
|
||||
| `build.py` | Сборка DLL/SO, проверка и распаковка локального архива; Python 3.8+ и C/C++ компиляторы |
|
||||
| `vendor/eigen-3.4.0.tar.gz` | Оригинальный архив зависимости, без изменений |
|
||||
| `../../python/set_devices/signal_eigen.py` | Ленивый ctypes-загрузчик отдельного компонента |
|
||||
|
||||
Зависимость: [Eigen 3.4.0, официальный архив](https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz).
|
||||
SHA-256: `8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72`.
|
||||
Заголовки компилируются с `EIGEN_MPL2_ONLY`; `Eigen-COPYING.MPL2` копируется
|
||||
рядом с библиотекой и включается в пакет SETGUI. Сборка не требует интернета.
|
||||
|
||||
Из каталога SETGUI:
|
||||
|
||||
```powershell
|
||||
.venv/Scripts/python.exe third_party/templates/cpp/signal-eigen/build.py --output src/gui_desktop/native/setsignal_eigen.dll # Собираем 64-битный настольный компонент рядом с основной DLL.
|
||||
```
|
||||
|
||||
Для 32-битного приложения добавьте `--arch x86`. Компилятор C обрабатывает
|
||||
общий алгоритм отдельно от C++14-решателя. Windows-модуль использует статический
|
||||
CRT и PE-подсистему 6.01; работоспособность на самой Windows 7 требует проверки
|
||||
на соответствующей машине. Для Linux/macOS укажите имя `libsetsignal_eigen.so`
|
||||
или `libsetsignal_eigen.dylib`. `scripts/build_exe.py` собирает и упаковывает
|
||||
компонент автоматически, включая вариант Windows 7 x86.
|
||||
|
||||
Контракт `set_signal_fit_fn` описан в `c/set-protocol/include/set_signal.h`:
|
||||
callback получает уникальные нормированные X/Y, количество точек и степень 1…5,
|
||||
возвращает коэффициенты по возрастанию степеней аргумента `2*x-1`.
|
||||
Владение выходами остаётся у вызывающего; Eigen выделяет временные матрицы
|
||||
только в настольном порте. Коды ошибок: 0 — успех, 1 — параметры, 2 — нечисловой
|
||||
результат, 3 — недостаточный ранг, 5 — память/ошибка C++-решателя.
|
||||
Исключения не пересекают C ABI. Общих изменяемых рабочих буферов нет.
|
||||
|
||||
Загрузчик ищет компонент рядом с выбранной SETProtocol DLL. Переменная
|
||||
`SETSIGNAL_EIGEN_LIBRARY` позволяет явно задать путь для тестов. Отсутствующая
|
||||
библиотека вызывает понятную ошибку после выбора Eigen; скрытой замены
|
||||
на исходный решатель нет. Используется в обработке графиков и генераторе SETGUI.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
||||
#define EIGEN_MPL2_ONLY // Ограничиваем подключаемые части Eigen лицензией MPL-2.0.
|
||||
#include <Eigen/QR> // Подключаем QR с выбором ведущего столбца для полиномиального МНК.
|
||||
#include <cmath> // Проверяем конечность вычисленных коэффициентов перед передачей в C.
|
||||
#include <new> // Перехватываем ошибку выделения памяти внутри границы C ABI.
|
||||
#include "set_signal.h" // Используем общую подготовку точек, вычисление кривой и стабильный C ABI.
|
||||
|
||||
static int eigen_fit(const double *x,const double *y,size_t count,unsigned degree,double *coefficients) { // Вычисляем только коэффициенты по уже нормированным уникальным точкам.
|
||||
try { // Исключения C++ не должны пересекать границу ctypes или C-ядра.
|
||||
Eigen::MatrixXd design(count,degree+1); // Матрица имеет одну строку на точку и до шести степеней, включая свободный член.
|
||||
for(size_t i=0;i<count;++i) { // Заполняем нормированную матрицу Вандермонда в том же базисе, что исходный МНК.
|
||||
double power=1.0; // Нулевая степень задаёт столбец свободного члена без вызова pow.
|
||||
for(unsigned j=0;j<=degree;++j) { // Последовательно строим степени от нулевой до выбранной пользователем.
|
||||
design(i,j)=power; // Сохраняем коэффициент при соответствующей степени в строке измерения.
|
||||
power*=2.0*x[i]-1.0; // Переводим нормированное время из [0,1] в [-1,1], сохраняя обусловленность исходного алгоритма.
|
||||
} // Заканчиваем заполнение строки матрицы по одной временной точке.
|
||||
} // Все точки участвуют в МНК с одинаковым весом после общего усреднения повторов X.
|
||||
Eigen::ColPivHouseholderQR<Eigen::MatrixXd> qr(design); // Используем отражения Хаусхолдера с перестановкой столбцов вместо собственной ортогонализации.
|
||||
qr.setThreshold(1e-12); // Относительный порог ранга соответствует порядку допуска исходного решателя; он не является абсолютной погрешностью сигнала.
|
||||
if(qr.rank()!=static_cast<Eigen::Index>(degree+1)) return 3; // Отклоняем вырожденный базис, не выдавая неоднозначные коэффициенты за корректную кривую.
|
||||
Eigen::Map<const Eigen::VectorXd> values(y,count); // Читаем нормированные амплитуды из памяти C без дополнительного копирования входного вектора.
|
||||
Eigen::VectorXd solution=qr.solve(values); // Находим минимум суммы квадратов без формирования нормальных уравнений AᵀA.
|
||||
for(unsigned j=0;j<=degree;++j) { // Возвращаем степени в исходном порядке: Eigen отменяет перестановку столбцов при solve.
|
||||
if(!std::isfinite(solution[j])) return 2; // Не разрешаем NaN или бесконечности попасть в оценку кривой и RMSE.
|
||||
coefficients[j]=solution[j]; // Передаём коэффициент в буфер общего C-алгоритма вычисления полинома.
|
||||
} // Весь выходной полином проверен до передачи управления C-ядру.
|
||||
return 0; // Сообщаем общий для двух реализаций код успешного расчёта.
|
||||
} catch(const std::bad_alloc &) { // Преобразуем нехватку памяти необязательного настольного решателя в детерминированный код.
|
||||
return 5; // Код 5 обозначает невозможность выполнить расчёт, включая выделение матриц.
|
||||
} catch(...) { // Защищаем потребителей C ABI от остальных исключений библиотеки C++.
|
||||
return 5; // Возвращаем явную ошибку без подмены выбранного пользователем решателя.
|
||||
} // Завершаем защиту границы между C++ и C.
|
||||
} // Решатель не хранит глобальных данных и допускает независимые параллельные вызовы.
|
||||
|
||||
extern "C" PCAN_ABI_API int set_signal_reconstruct_eigen( // Отдельный экспорт сохраняет возможность загрузить модуль без изменения основной DLL.
|
||||
const double *x,const double *y,size_t count,int method,unsigned degree, // Формат точек и предел степени совпадают с исходным API.
|
||||
size_t output_count,int endpoint,double *out_x,double *out_y, // Временная сетка и включение конечной точки рассчитываются общим C-ядром.
|
||||
double *meta,double *work,size_t work_count) { // Метаданные и рабочая память сохраняют формат аннотаций формулы GUI.
|
||||
if(method!=SET_SIGNAL_POLYNOMIAL) return 1; // Eigen доступен только для МНК; остальные методы не маскируются под альтернативный решатель.
|
||||
return set_signal_reconstruct_with_fit(x,y,count,method,degree,output_count,endpoint,out_x,out_y,meta,work,work_count,eigen_fit); // Повторно используем C99-подготовку, обработку повторов, вычисление кривой и RMSE.
|
||||
} // Завершаем экспорт с соглашением вызова C для x86 и x64.
|
||||
BIN
Binary file not shown.
@@ -11,6 +11,7 @@ Device Name — отображаемое имя адреса, а не допол
|
||||
| `0x2` | `0x0…0xF` | PERIPH 16–31 | Вторая группа; номер PERIPH = 16 + DeviceID |
|
||||
| `0x1` | `0x5` | **УМП1** | PERIPH 5, собственный адрес обычного обмена ProtoCAN и загрузчика |
|
||||
| `0x1` | `0x6` | УМП2 | PERIPH 6, GAS 0x3 — регистры; GAS SLOT 0x4 — слоты 0x0600…0x0642 |
|
||||
| `0x1` | `0x8` | **Задатчик** | PERIPH 8; для ПМ35 с M95M02 сохраняется ProductType `0x2833` |
|
||||
| `0x6` | `0xE` | Altera Logic | Отдельный существующий потоковый профиль FE00/FF00 |
|
||||
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
|
||||
| `0x7` | `0xE` | **STM Invertor** | Самостоятельный ПЧ STM32G474VET6; отдельный GAS-профиль на 250 слотов 0x4, 750 значений; Word0 — маска |
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -147,3 +147,78 @@ CAN ID = 0x13590702
|
||||
|
||||
Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
|
||||
находятся в [examples/test-vectors.json](examples/test-vectors.json).
|
||||
|
||||
## GAS: сквозная карта УКСС и профиль 167/v1
|
||||
|
||||
УКСС назначены десятичные ID **0…11999**, блоки по 1000 слов:
|
||||
`GAS_ID = номер_узла × 1000 + смещение`. Блок 0 — общий резерв;
|
||||
узлы 1…11 и все границы описаны в
|
||||
[GAS_ADDRESS_ALLOCATION_167.md](GAS_ADDRESS_ALLOCATION_167.md).
|
||||
Диапазон 12000…65535 свободен для последующего распределения.
|
||||
|
||||
УМП 1 (узел 5) назначены ID 5000…5127, УМП 2 (узел 6) —
|
||||
6000…6127. Смещения 128…999 зарезервированы. Полный словарь 128 слов,
|
||||
ограничения LOAD, биты Commands/LOAD_STATUS и соответствие Modbus приведены в
|
||||
[GAS_MODBUS_MAP_167.md](GAS_MODBUS_MAP_167.md).
|
||||
|
||||
Новые ID пока назначены только в документации. Действующий профиль 167/v1
|
||||
использует Modbus 0x0000…0x007F и окно GAS 0x5000…0x507F;
|
||||
например, LOAD_CAN_TIMEOUT: Modbus 0x0052, GAS 0x5052,
|
||||
новые ID 5082/6082. Десятичный ID 5000 не равен hex-адресу 0x5000.
|
||||
Служебный CAN УМП с маркером 0x1F00 — отдельный транспорт;
|
||||
его пакет нельзя интерпретировать как обычный payload ProtoCAN GAS.
|
||||
|
||||
Словарь и lookup находятся в `python/protocan/gas_167.py` и
|
||||
`gas_167_registers.json` проекта templates. Парсер обычных кадров GAS
|
||||
показывает назначенные имена, узел, старые адреса и статус для запросов
|
||||
и каждого слова ответа; неизвестные и резервные адреса сохраняются raw.
|
||||
Общая сводка `Decoded.summary` содержит имя каждого назначенного параметра,
|
||||
УМП 1/2, десятичный ID, hex-адрес и значение в dec/hex. Поэтому имена видны
|
||||
непосредственно в колонке «Расшифровка» всех клиентов общего декодера:
|
||||
онлайн-монитор, трассировка, фильтр кадров, ручной разбор и DevBoard_V1.
|
||||
Полный текст длинной строки доступен во всплывающей подсказке.
|
||||
Тип сообщения отображается как `GAS (General Address Space)`.
|
||||
Обычные демо-потоки шины и DevBoard также включают назначенные адреса УМП.
|
||||
Низкие Modbus-адреса и старое окно не определяют профиль прибора автоматически.
|
||||
DATA обычного ProtoCAN GAS — до четырёх u16 little-endian; Modbus RTU
|
||||
передаёт каждое слово старшим байтом вперёд. GUI использует этот же словарь
|
||||
через совместимый импорт, без собственной реализации карты.
|
||||
|
||||
### Демо УМП в SETGUI
|
||||
|
||||
Общий список «Протокол» позволяет выбрать `ProtoCAN / GAS`,
|
||||
`УМП / Balsam 167 (legacy CAN)`, карту `GAS 167/v1` отдельно для УМП 1/2
|
||||
или `УМП / ProtoCAN GAS — новые ID 5000/6000`.
|
||||
Выбор перерисовывает уже полученные кадры. Для старой карты номер УМП
|
||||
выбирается оператором и обозначается «выбран вручную»; низкие адреса
|
||||
0…127 и окно 0x5000…0x507F сами по себе номер узла не кодируют.
|
||||
Этот режим накладывает словарь на слова ProtoCAN GAS/Modbus Holding;
|
||||
он не является декодером служебных CAN-пакетов с маркером 0x1F00.
|
||||
Профиль Balsam принимает только собственный формат и показывает
|
||||
несоответствие для чужих кадров. В сводке Commands показаны активные
|
||||
биты с названиями команд; это не подтверждение их выполнения.
|
||||
|
||||
В режиме «Авто» legacy-ID Balsam проверяются первыми. Для корректного
|
||||
ProtoCAN GAS с целыми словами все адреса кадра сверяются с назначенными
|
||||
диапазонами УМП. Если все они принадлежат одному узлу, поля Device Name
|
||||
и Msg Type показывают УМП 1/2; основание выбора записывается в подробности
|
||||
«Автоопределение». Это распознавание назначенной карты, а не подтверждение
|
||||
её поддержки прошивкой. Для старых/неизвестных адресов, смешанных диапазонов
|
||||
и некорректного payload автоматический выбор УМП не выполняется;
|
||||
сводка сообщает «профиль/узел не определены». Номер Device в ProtoCAN сам
|
||||
по себе не считается номером УМП. «Демо УМП» запускается с режимом «Авто».
|
||||
|
||||
Открыть `CAN → Шина CAN / мост → Настройки → Демо УМП`.
|
||||
Генератор `python/set_devices/bus_demo.py:UmpDemoSource` моделирует два
|
||||
узла с демонстрационными адресами `Device Type=3`, `Device=5/6`.
|
||||
Он передаёт PULSE раз в секунду и четыре GAS-слова каждого узла каждые
|
||||
250 мс: ID 5000…5127 и 6000…6127, полный обход за 8 секунд.
|
||||
Это имитация назначенной карты, а не эмуляция текущего транспорта 167/v1.
|
||||
|
||||
Кадры проходят обычное кадрирование AA 55, CRC и общий парсер ProtoCAN.
|
||||
Их можно смотреть в онлайн-мониторе, трассировке и адресном пространстве;
|
||||
в буфере сессии и его CSV источник обозначен `DEMO УМП`.
|
||||
Значения синтетические, в формате raw u16. LOAD_STATUS показывает RUN
|
||||
(бит 3), LOAD_ATTEMPTS — 0, LOAD_CAN_TIMEOUT — 1000 мс.
|
||||
Кнопка «Стоп демо УМП» останавливает поток. Для запуска нужно отключить
|
||||
COM-порт и CAN-снифер; генератор работает локально без передачи в оборудование.
|
||||
|
||||
@@ -642,11 +642,11 @@ CRC32 обнаруживает случайное повреждение, но
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x96">64 битовых адресса</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<tr style="height:72px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td colspan="3" class="x131">0x3</td>
|
||||
<td colspan="4" class="x136">0x0 - 0xF устройств</td>
|
||||
<td colspan="4" class="x136">0x0 - 0xF устройств; прежнее общее назначение F28335 3/7 отменено</td>
|
||||
<td colspan="12" class="x138">0x3. Установка времени</td>
|
||||
<td colspan="4" class="x140"> </td>
|
||||
<td class="x14"> </td>
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Сравнение сигналов с CSV-эталоном
|
||||
|
||||
Общее ядро `set_devices.signal_reference` и Qt-порт
|
||||
`set_devices.qt_ports.reference_comparison` сравнивают измеренную запись с
|
||||
эталоном. Они не импортируют приложение, не выбирают его каталоги и не
|
||||
требуют конкретного движка отрисовки. Ядро использует только стандартную
|
||||
библиотеку Python; Qt-порт поддерживает PySide6 и PySide2.
|
||||
|
||||
## Численный API
|
||||
|
||||
`Comparison.analysis` содержит локальный параметрический отчёт:
|
||||
`summary`, `details`, `attention`, `findings`, `features`, `model_report`.
|
||||
Он рассчитывается `set_devices.signal_diagnostics` по тому же интервалу,
|
||||
временному сдвигу и допускам. Исходные метрики используют все общие отсчёты;
|
||||
объём выборки подбора моделей явно указан в `model_report`.
|
||||
Добавлено необязательное поле с пустым словарём по умолчанию: прежний
|
||||
конструктор `Comparison` с пятью аргументами сохраняет совместимость.
|
||||
|
||||
Для одинаковых известных обозначений `Uce`, `Uge`, `Ic` анализ различает
|
||||
повторные полные переходы, усиленный звон Uce после выключения, отсутствие
|
||||
переключения, растянутые фронты и временной сдвиг. Общие причины включают
|
||||
смещение, шум и неопределённое изменение формы. Разные известные физические
|
||||
каналы, недостаточное число точек и неполное покрытие отражаются в выводе.
|
||||
На неизвестных каналах аппаратные гипотезы о снаббере и транзисторе не строятся.
|
||||
|
||||
`set_devices.signal_fault_models` подбирает смещение, масштаб, дрейф,
|
||||
задержку, растяжение фронтов, постоянный уровень, кусочные прерывания и
|
||||
затухающий отклик RLC. `set_devices.signal_joint_models` добавляет локальную
|
||||
ВАХ `Uce=U0+Rdiff*Ic`, остаточный ток выключенного состояния и совпадение
|
||||
провалов управления/проводимости по трём каналам.
|
||||
|
||||
`compare(..., circuit=..., channels=...)` принимает необязательные известные
|
||||
`inductance_h`, `capacitance_f`, `resistance_ohm` в СИ и словарь связанных
|
||||
каналов `dict(measured=..., reference=...)`. GUI формирует эти снимки сам.
|
||||
Однозначность физических имён и единиц проверяется до совместного анализа.
|
||||
|
||||
Глобальные модели сравниваются по BIC общей обучающей выборки и RMSE на
|
||||
каждой третьей отложенной точке. JSON сохраняет всех конкурентов, число
|
||||
параметров, формулы, критерии и ограничения. BIC не интерпретируется как
|
||||
вероятность аппаратного отказа. Без известного элемента R/L/C не выдаются
|
||||
три произвольных номинала: доступны только LC и R/L последовательного
|
||||
эквивалента полюсов.
|
||||
|
||||
`ComparisonAnalysis` в `qt_ports.reference_results` показывает одну строку
|
||||
и кнопку отчёта с таблицей моделей и подробным текстом по образцу Android GUI. Компонент
|
||||
используется и над исходным графиком, и в полной форме сравнения; изменение
|
||||
входов, чтение другого эталона и ошибки очищают прежний анализ. JSON-экспорт
|
||||
обоих видов включает `analysis` рядом с `metrics`. Ядро не использует сеть
|
||||
или обученную модель; пороги признаков и ограничения указаны в подробностях.
|
||||
|
||||
`Trace(name, times, values)` содержит имя, строго возрастающие временные
|
||||
метки в миллисекундах и амплитуды в единицах канала. `trace_from_pairs`
|
||||
создаёт такой снимок из пар `(time_ms, value)`; необязательный `transform`
|
||||
применяет множитель или иное преобразование амплитуды.
|
||||
|
||||
`read_reference(path, cancel=...)` потоково читает UTF-8 CSV с необязательным
|
||||
BOM. Первый столбец — `time_ms` либо `timestamp` с ISO-временем и явным
|
||||
часовым поясом. Остальные столбцы имеют непустые уникальные имена каналов.
|
||||
Поддерживаются запятая, точка с запятой и табуляция. Пустые ячейки и NaN
|
||||
сохраняются как пропуски; повторное время, бесконечности и повреждённые
|
||||
строки отклоняются.
|
||||
|
||||
```python
|
||||
from set_devices.signal_reference import compare, read_reference, trace_from_pairs # Ядро можно использовать без Qt и графика.
|
||||
measured = trace_from_pairs('U (V)', [(0., 2.), (1., 4.), (2., 6.)]) # Время задано в миллисекундах, амплитуда — в вольтах.
|
||||
reference = read_reference('reference.csv')['U (V)'] # Столбец выбирается по полному имени канала.
|
||||
result = compare(measured, reference, absolute_tolerance=.1, relative_tolerance_pct=2.) # Допуск равен 0,1 В плюс 2% модуля эталона.
|
||||
print(result.metrics['rmse']) # Показатель рассчитан по всем корректным точкам измерения в общем интервале.
|
||||
```
|
||||
|
||||
`compare` возвращает `Comparison`: нормализованные кривые `measured` и
|
||||
`reference`, остаток `difference`, эталон на сетке остатка `expected` и
|
||||
словарь `metrics`. Статистика включает покрытие, пропуски, среднее смещение,
|
||||
MAE, RMSE, СКО без смещения, максимальную ошибку и выходы за допуск.
|
||||
|
||||
Эталон линейно интерполируется на исходную сетку измерения без
|
||||
экстраполяции и соединения через NaN. Остаток равен измерению минус эталон.
|
||||
Допуск равен `absolute_tolerance + relative_tolerance_pct / 100 * abs(reference)`;
|
||||
равенство границе допуска не считается превышением. Экранная децимация
|
||||
не участвует в расчёте.
|
||||
|
||||
По умолчанию начала записей совмещаются. `align_start=False` сохраняет
|
||||
различие абсолютных начал; положительный `shift_ms` сдвигает эталон вправо.
|
||||
`interval=(start_ms, stop_ms)` задаётся от начала измерения, обе границы
|
||||
включены. Callback `cancel()` прерывает подготовку, чтение и сравнение через
|
||||
`InterruptedError`.
|
||||
|
||||
## Наложение на существующий график
|
||||
|
||||
Для добавления эталона в уже работающий график используйте
|
||||
`set_devices.qt_ports.reference_overlay.ReferenceOverlayPanel`. Это
|
||||
компактные параметры без canvas. Оригинальный график
|
||||
остаётся у потребителя и продолжает показывать измерение при загрузке и
|
||||
изменении эталона.
|
||||
|
||||
Конструктор принимает `sources`, `parent`, `source_path` и необязательный
|
||||
`path_provider` с тем же смыслом, что в таблице ниже. `load_reference(path)`
|
||||
автоматически читает CSV, совмещает выбранные каналы и рассчитывает показатели
|
||||
в рабочем потоке. Отдельной кнопки запуска расчёта нет. Изменение канала,
|
||||
сдвига, совмещения начал или допусков обновляет результат автоматически.
|
||||
|
||||
Сигнал `changed` сообщает потребителю, что `overlay()` вернёт актуальные
|
||||
спецификации и точки одной дополнительной кривой. Потребитель **добавляет**
|
||||
их к оригинальным рядам и общей легенде своего canvas. `RESULT_KEY`
|
||||
отличает эталон от измерений; оранжевая кривая имеет подпись
|
||||
`Эталон: <канал>`. Временные метки эталона уже находятся в исходной шкале
|
||||
измерения: начало оригинала не обнуляется. Потребитель применяет своё
|
||||
перебазирование короткой развёртки к обеим кривым одинаково.
|
||||
|
||||
`set_sources(sources)` обновляет кадр прибора или преобразование амплитуд,
|
||||
сохраняя загруженный эталон. Сигнал `remove_requested` предлагает владельцу
|
||||
удалить дополнительный слой и параметры. `report()` и раскрываемая строка
|
||||
«Допуски и отчёт» предоставляют актуальную статистику и экспорт JSON.
|
||||
Если общей части времени нет, кривая остаётся доступна, а показатели
|
||||
заменяются объяснением отсутствия перекрытия. `close()` останавливает worker,
|
||||
не закрывая график потребителя.
|
||||
|
||||
Истории, переданные в одном снимке `sources`, должны быть неизменяемыми;
|
||||
для нового кадра передайте новую историю через `set_sources`. Порт
|
||||
поддерживает PySide6 и PySide2 без импорта приложения.
|
||||
|
||||
### Подробные показатели рядом с исходным графиком
|
||||
|
||||
`ReferenceOverlayPanel.results` — виджет `ComparisonResults` с полной
|
||||
таблицей всех 14 показателей: покрытие, пропуски, смещение, MAE, RMSE,
|
||||
СКО, максимум и его время, нормированная ошибка и выходы за допуск.
|
||||
Потребитель размещает его в существующей боковой области рядом с canvas;
|
||||
в SETGUI он открывается автоматически во вкладке «Отклонения и расчёты».
|
||||
Сигнал `results_requested` кнопки «Расчёты» позволяет вернуться к этой
|
||||
области после выбора обычной легенды.
|
||||
|
||||
Флажок `results.delta_curve` добавляет в `overlay()` кривую с ключом
|
||||
`DIFFERENCE_KEY`. Её временные метки находятся в той же исходной шкале,
|
||||
что эталон и оригинал. Исходные кривые сохраняются, отдельный canvas не
|
||||
создаётся. Кнопка JSON рядом с таблицей экспортирует тот же актуальный
|
||||
результат. Изменение параметров и ошибки очищают устаревшие показатели.
|
||||
|
||||
`comparison_rows(metrics)` из `qt_ports.reference_results` задаёт единые
|
||||
подписи, порядок и единицы как для этой таблицы, так и для прежнего диалога.
|
||||
|
||||
## Совместимые Qt-панель и отдельное окно
|
||||
|
||||
`ReferenceComparisonPanel` — обычный `QWidget` для существующей страницы
|
||||
графика. Кнопка «К записи» испускает `back_requested`; потребитель сам
|
||||
скрывает панель и возвращает исходный вид. При повторном показе той же
|
||||
панели сохраняются эталон, параметры и рассчитанные показатели.
|
||||
|
||||
`ReferenceComparisonDialog` — отдельный `QDialog` с тем же расчётом и
|
||||
настройками. Оба конструктора принимают:
|
||||
|
||||
| Параметр | Контракт |
|
||||
| --- | --- |
|
||||
| `sources` | Последовательность пар `(source, points)`: `source.name` — имя, `source.display_value(value)` — преобразование амплитуды, `points` — полная неизменяемая история `(time_ms, value)` |
|
||||
| `parent` | Родительский QWidget; для встроенной панели передайте страницу графика |
|
||||
| `source_path` | Происхождение измерения для JSON-отчёта, по умолчанию пустая строка |
|
||||
| `plot_factory` | Обязательная фабрика без аргументов, возвращающая настроенный QWidget графика |
|
||||
| `series_factory` | Обязательная фабрика `factory(key, name, color=..., timestamps=..., values=...)`, создающая кривую потребителя |
|
||||
| `path_provider` | Необязательный callback `filename → начальный путь`; пустое имя используется для открытия эталона, `reference_comparison.json` — для отчёта |
|
||||
|
||||
```python
|
||||
from set_devices.qt_ports.reference_comparison import ReferenceComparisonPanel # Qt загружается только при импорте конкретного порта.
|
||||
panel = ReferenceComparisonPanel( # Один компонент обслуживает выбор каналов, параметры, фоновый расчёт и экспорт.
|
||||
sources=recording_channels, # Потребитель фиксирует полные истории и преобразование амплитуд до создания панели.
|
||||
parent=chart_page, # Сравнение размещается в существующей рабочей области.
|
||||
source_path=recording_filename, # Имя исходной записи попадёт в отчёт.
|
||||
plot_factory=create_comparison_canvas, # Адаптер создаёт график с единой шкалой Y для обеих кривых.
|
||||
series_factory=create_series, # Контейнеры кривых принадлежат используемому движку отрисовки.
|
||||
path_provider=recording_path, # Папки и правила хранения остаются политикой потребителя.
|
||||
) # Все функции и объекты адаптера задаются приложением до этого вызова.
|
||||
chart_page.layout().addWidget(panel, 1) # Панель занимает свободное место страницы.
|
||||
panel.back_requested.connect(show_recording) # Обработчик скрывает сравнение и возвращает исходный график.
|
||||
```
|
||||
|
||||
Canvas должен предоставлять изменяемый список `_series`, методы
|
||||
`reset_interaction()`, `_sync_bounds()`, стандартные `update()` и `close()`,
|
||||
флажки `axis_lock_x` и `axis_lock_y` с `setChecked()`, поле `axis_limits`,
|
||||
`visible_bounds.left/right` в миллисекундах и `viewport` типа
|
||||
`protocan.plot.Viewport`. `_sync_bounds()` задаёт конечный ненулевой диапазон
|
||||
X даже для пустого графика. Обе наложенные кривые используют **одну шкалу Y**;
|
||||
автоматическое разделение каналов на независимые оси отключает адаптер.
|
||||
Дополнительные кнопки canvas, подписи и правила снимков также настраиваются
|
||||
в `plot_factory`.
|
||||
|
||||
Панель передаёт фабрике кривых полные массивы: их экранное сокращение остаётся
|
||||
ответственностью canvas. Временные поля интерфейса показывают микросекунды,
|
||||
ядро и JSON-отчёт используют миллисекунды. Явно различающиеся единицы в
|
||||
скобках имён каналов блокируют сравнение; автоматической конвертации нет.
|
||||
|
||||
Истории и настройки источников нельзя менять, пока панель использует их
|
||||
снимок. При смене записи или преобразования канала закройте старую панель
|
||||
и создайте новую. `close()` встроенной панели отменяет и дожидается worker;
|
||||
уничтожение родителя также останавливает поток до удаления дочерних объектов.
|
||||
Отдельный диалог откладывает закрытие до завершения кооперативной отмены.
|
||||
|
||||
`load_reference(path)` позволяет открыть эталон программно, `report()`
|
||||
возвращает актуальный словарь отчёта либо `None`. Изменение входных
|
||||
параметров очищает старые кривые и показатели до следующего расчёта.
|
||||
|
||||
## Проверки
|
||||
|
||||
`python/tests/test_reference_comparison_port.py` проверяет внедрение
|
||||
стороннего canvas, преобразование амплитуд, отклонение и экспорт с внешним
|
||||
provider путей, а также API отдельного диалога. Его можно запустить из
|
||||
каталога `templates/python` командой
|
||||
`python -m unittest discover -s tests -p test_reference_comparison_port.py`.
|
||||
|
||||
`python/tests/test_shared_library_boundary.py` проверяет независимость
|
||||
вычислительных модулей от GUI и Qt. Интеграционные проверки потребителя
|
||||
дополнительно покрывают полные истории, общий масштаб Y, отмену, смену
|
||||
записи, возврат к графику и полноэкранный режим.
|
||||
@@ -15,6 +15,10 @@
|
||||
| `set_devices.qt_ports` | UART/mock, SLCAN, Candle/WinUSB, STM/TMS boot, STM settings, UMP CAN |
|
||||
| `set_devices.plot_processing` | Общий контракт графиков: аппроксимация, интерполяция, восстановление, единицы и CSV через C99 |
|
||||
| `set_devices.qt_ports.plot_processing` | Переиспользуемая панель всех методов, фоновый расчёт и отдельный слой кривой |
|
||||
| `set_devices.signal_reference` | Сравнение полной записи с CSV-эталоном: совмещение времени, допуски, остаток и показатели ошибки без Qt |
|
||||
| `set_devices.qt_ports.reference_comparison` | Общие встроенная панель и диалог сравнения, фоновое чтение/расчёт, внедряемые график и папки, JSON-отчёт |
|
||||
| `set_devices.qt_ports.reference_overlay` | Компактные параметры и автоматически подготовленный эталон для наложения на существующий canvas без создания второго графика |
|
||||
| `set_devices.qt_ports.reference_results` | Полная таблица отклонений, единое форматирование 14 показателей, флажок кривой Δ и экспорт |
|
||||
| `protocan`, `setprotocol` | Существующие обёртки общего C99-ядра и SET v2 |
|
||||
|
||||
Импорт `logic_analyzer` / `set_devices` не загружает Qt и не зависит от GUI.
|
||||
@@ -24,6 +28,9 @@ Saleae SDK подключается при обращении к устройс
|
||||
в приложении. Общая панель обработки графиков размещена в `qt_ports.plot_processing`:
|
||||
[контракт, подключение нового графика и примеры](../c/set-protocol/docs/PLOT_PROCESSING.md).
|
||||
|
||||
Сравнение с эталоном подключается через фабрики canvas и кривой без
|
||||
зависимости от приложения: [API ядра, контракт Qt-порта и пример](../docs/reference-comparison.md).
|
||||
|
||||
Конструкторы CAN-адаптера, CAN DevBoard и RS485 DevBoard используют общий
|
||||
компонент `set_devices.qt_ports.can_frame_builder.CanFrameFields`. Он задаёт
|
||||
одинаковые поля и исходный режим «Обычный CAN — ID / DLC / Data», сохраняет
|
||||
|
||||
@@ -16,20 +16,9 @@ BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIF
|
||||
def legacy(ident, data, profile='protocan'):
|
||||
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
|
||||
raise ValueError('Invalid classic CAN frame')
|
||||
relative = ident - 0xba0000
|
||||
if profile == 'balsam':
|
||||
if not (0 <= relative < 13 or 16 <= relative < 29):
|
||||
return None
|
||||
if len(data) != 8:
|
||||
raise ValueError('Balsam requires DLC=8')
|
||||
header = number(data[:2], 'big')
|
||||
address, mask = header & 0x1fff, header >> 13
|
||||
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
|
||||
for i in range(3) if mask & (4 >> i)]
|
||||
return dict(protocol='Balsam', registers=registers, summary=
|
||||
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
|
||||
'data' if relative >= 16 else 'command', mask,
|
||||
' '.join('R%04X=%04X' % pair for pair in registers)))
|
||||
from .template_protocols import decode_can # Единая реализация templates знает порядок value2/value3/header/value1 и специальные ID пульта.
|
||||
return decode_can(ident, data, 'balsam') # Старый публичный вызов получает исправленную расшифровку без второго варианта wire-формата.
|
||||
msg, body = (ident >> 16) & 15, ident & 0xffff
|
||||
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
|
||||
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from pathlib import Path # Путь к поставляемым библиотекам вычисляется относительно ядра декодера, независимо от рабочего каталога DSView.
|
||||
import sys # Встроенному Python требуется путь к переносимым пакетам, которые поставляются вместе с декодерами.
|
||||
from types import SimpleNamespace # Общим CAN-парсерам нужен только набор свойств кадра, без подключения транспорта или Qt.
|
||||
|
||||
_vendor = Path(__file__).with_name('templates') # Установщик помещает исходные пакеты templates в закрытый подкаталог общего ядра.
|
||||
if _vendor.is_dir() and str(_vendor) not in sys.path: # При запуске из исходников используются штатные пакеты; установленный DSView получает собственные копии.
|
||||
sys.path.insert(0, str(_vendor)) # Локальные версии пакетов имеют приоритет и не требуют установленного SETGUI или внешнего Python.
|
||||
|
||||
from protocan import balsam, legacycan, protocan as pc # Форматы ID, порядок слов и каталоги проектов берутся из действующих библиотек templates.
|
||||
from set_devices import deif_xdi, ump_can # Профиль DEIF и словарь УМП используются теми же функциями, что в CAN-мониторе.
|
||||
|
||||
CAN_PROFILES = ('protocan', 'auto', 'devboard', 'balsam', 'ump-protocan', 'deif', 'legacy', 'set-v2') # Старые ключи сохранены для ранее настроенных записей DSView.
|
||||
LEGACY_PROJECTS = tuple(project.name for project in legacycan.PROJECTS) # Список выбора обновляется вместе с единым каталогом исторических проектов.
|
||||
|
||||
def _decoded(result): # Преобразует нейтральную модель CAN-монитора в словарь событий анализатора.
|
||||
return dict(protocol=result.protocol, summary=result.summary, fields=list(result.fields), registers=result.registers, warnings=list(result.warnings), device=result.device_label) # Копии списков исключают изменение результата другим слоем декодирования.
|
||||
|
||||
def _balsam(ident, payload): # Разбирает фактический порядок слов Balsam 167 вместо упрощённого заголовка в начале данных.
|
||||
result = balsam.decode(ident, payload, native=pc._SHARED_PROTOCOL) if pc._SHARED_PROTOCOL is not None else balsam._decode_fallback(ident, payload) # Без DLL используется существующий переносимый разбор; повторный поиск DLL на каждом кадре исключён.
|
||||
if result is None: # Чужой идентификатор не должен получать подпись Balsam.
|
||||
return None # Отсутствие подходящего кадра не создаёт ложную ошибку на общей шине.
|
||||
registers = [(item.address, item.value) for item in result.registers] # Общий парсер уже применил маску присутствия и восстановил порядок трёх слов.
|
||||
fields = [('Узел', result.device_name), ('Маршрут', result.route), ('Начальный адрес', '0x%04X' % result.start_address), ('Маска слов', format(result.present_mask, '03b'))] # Адрес имеет ширину 13 бит, а маска содержит три признака из wire-формата Balsam.
|
||||
return dict(protocol='Balsam', summary=result.summary, registers=registers, fields=fields, warnings=[]) # Сохраняет совместимое имя протокола и полную сводку с командами общего парсера.
|
||||
|
||||
def decode_can(ident, payload, profile='protocan', *, extended=True, ump_map='gas167', ump_node=1, project=LEGACY_PROJECTS[0]): # Явные настройки предотвращают угадывание несовместимых карт регистров по содержимому данных.
|
||||
payload = bytes(payload) # Парсеры получают неизменяемые байты вне зависимости от контейнера, переданного sigrok.
|
||||
if not 0 <= ident <= (0x1fffffff if extended else 0x7ff) or len(payload) > 8: # Classic CAN допускает 29-битный EXT либо 11-битный STD ID и не более восьми байт.
|
||||
raise ValueError('Invalid classic CAN frame') # Повреждённый ID или DLC отклоняется до интерпретации прикладных полей.
|
||||
if profile not in CAN_PROFILES or profile == 'set-v2': # Сегментированный SET v2 обрабатывается отдельной машиной состояний с тайм-аутом.
|
||||
raise ValueError('Unsupported CAN profile: ' + profile) # Неизвестный профиль не подменяется ProtoCAN молча.
|
||||
if not extended and profile != 'deif': # Все поставляемые профили SET используют EXT; DEIF работает со стандартными CANopen ID.
|
||||
return None # Стандартный кадр не интерпретируется как адресное поле ProtoCAN.
|
||||
frame = SimpleNamespace(can_id=ident, data=payload, ide=extended, rtr=False, is_error=False) # Минимальный кадр соответствует контракту общих функций CAN-монитора.
|
||||
if profile == 'deif': # CANopen/DEIF использует собственную адресацию и проверку селектора.
|
||||
return _decoded(deif_xdi.decode(frame)) # Поля, физические единицы и замечания полностью определяет имеющийся парсер.
|
||||
if profile == 'balsam' or profile == 'auto' and balsam.is_balsam_id(ident): # В автоматическом режиме Balsam распознаётся только по его документированным идентификаторам.
|
||||
return _balsam(ident, payload) # Включает отдельную пару CAN ID пульта, присутствующую в общей библиотеке.
|
||||
if profile == 'legacy': # Старые проектные форматы требуют явного проекта, поскольку диапазоны ID могут пересекаться.
|
||||
selected = next((item for item in legacycan.PROJECTS if item.name == project), None) # Поиск использует общий каталог без копирования ID в анализатор.
|
||||
if selected is None: # Ошибка выбора не должна давать расшифровку другого проекта.
|
||||
raise ValueError('Unknown legacy CAN project: ' + project) # Сообщение остаётся видимым на дорожке ошибок.
|
||||
node = selected.node_for(ident) # Маршрутизация зависит от формата: точный ID либо старшие биты ID.
|
||||
if node is None: # На общей шине допустимы кадры других устройств и проектов.
|
||||
return None # Чужие ID остаются только в дорожках базового CAN-декодера.
|
||||
packet = legacycan.decode(selected.format, node, ident, payload) # Общая реализация восстанавливает адрес, маску и слова с нужным порядком байтов.
|
||||
if packet is None: # Известный ID с неподходящим DLC обозначает повреждённый кадр выбранного проекта.
|
||||
raise ValueError('Invalid legacy CAN payload') # Некорректные данные не выдаются как достоверные регистры.
|
||||
registers = list(packet.present_values) # Получает только отмеченные маской слова в исходном адресном порядке.
|
||||
commands = ['%s: %s' % ('R%04X' % address, ', '.join(selected.command_names[bit] for bit in range(16) if value & (1 << bit))) for address, value in registers if address == 127] # Регистр 127 хранит 16 командных битов с названиями выбранного проекта.
|
||||
summary = '%s · %s · %s · %s' % (project, node.name, packet.source.value, ' '.join('R%04X=%04X' % pair for pair in registers)) # Краткая подпись связывает проект, устройство, направление и значения слов.
|
||||
return dict(protocol='Legacy CAN', summary=summary, fields=[('Проект', project), ('Устройство', node.name), ('Направление', packet.source.value)] + [('Команды', text) for text in commands], registers=registers, warnings=[]) # Все подробности доступны на самостоятельных дорожках DSView.
|
||||
if profile == 'ump-protocan' and ump_map == 'gas167': # Текущая карта 167/v1 назначается вручную и не смешивается с новыми глобальными ID.
|
||||
if ump_node not in (1, 2): # В профиле имеются два УМП; номер не извлекается из локального адреса регистра.
|
||||
raise ValueError('UMP node must be 1 or 2') # Ошибочный номер узла должен быть исправлен в настройках.
|
||||
return _decoded(ump_can.decode_gas167(frame, ump_node)) # Имена параметров и активные биты берутся из действующей карты templates.
|
||||
if profile == 'ump-protocan' and ump_map != 'assigned': # Назначенная глобальная карта обрабатывается штатным ProtoCAN ниже.
|
||||
raise ValueError('Unknown UMP map: ' + ump_map) # Неизвестную карту нельзя интерпретировать как другую раскладку регистров.
|
||||
from .set_can import legacy # Поздний импорт сохраняет совместимость существующего ядра и предотвращает циклическую загрузку модулей.
|
||||
result = legacy(ident, payload) # Сохраняет строгие проверки DLC GAS/Boot, сегментных длин и старые структурированные поля событий.
|
||||
application = _decoded(pc.decode(ident, payload)) # Общий ProtoCAN добавляет имена устройств, GAS, Settings, дискретов и служебных полей.
|
||||
result.update(fields=application['fields'], warnings=application['warnings'], summary=result['summary'] + ' · ' + application['summary']) # Старые поля сохраняются, а полная прикладная расшифровка становится доступной на осциллограмме.
|
||||
return result # Кадр выдаётся только после проверки wire-формата обоими существующими уровнями.
|
||||
@@ -0,0 +1,29 @@
|
||||
from collections import deque # Кольцевой буфер хранит только координаты одного максимального кадра.
|
||||
from . import template_protocols # Подключает поставляемые пакеты templates в отдельном Python-процессе DSView.
|
||||
from set_devices import tms_terminal as tms # Раскладки, CRC и потоковый поиск границ используются из действующего парсера терминала.
|
||||
|
||||
LAYOUTS = tuple(tms.PRESETS) + ('custom',) # Имена готовых раскладок совпадают с настройками терминала SETGUI.
|
||||
|
||||
class TmsStreamParser: # Адаптирует общий байтовый парсер к событиям с координатами sigrok.
|
||||
def __init__(self, preset=tms.DEFAULT_PRESET, digit_bytes=38, analog_count=34, crc='MODBUS'): # Значения 38/34 соответствуют штатной раскладке message2.h.
|
||||
selected = tms.PRESETS[preset] if preset != 'custom' else tms.Layout(digit_bytes, analog_count, crc) # Индивидуальная раскладка проверяется существующей моделью Layout.
|
||||
self.layout = tms.Layout(selected.digit_bytes, selected.analog_count, crc) # CRC выбирается отдельно, поскольку в источниках есть MODBUS и CCITT-FALSE.
|
||||
self.parser = tms.TmsParser(self.layout) # Поиск границ и подтверждение CRC выполняет общий парсер.
|
||||
self.positions = deque(maxlen=self.layout.frame_size) # Число хранимых координат ограничено длиной одного кадра в байтах.
|
||||
|
||||
def flush(self): # Пауза либо ошибка UART запрещает объединять части разных посылок.
|
||||
self.parser.reset() # Общий парсер отбрасывает недособранные байты и состояние синхронизации.
|
||||
self.positions.clear() # Координаты старого хвоста также исключаются из следующего события.
|
||||
return [] # Формат без сигнатуры не позволяет отличить неполный кадр от произвольного шума.
|
||||
|
||||
def feed(self, value, ss, es): # Один UART-байт приходит вместе с точными границами в отсчётах.
|
||||
self.positions.append((ss, es)) # Последние frame_size позиций соответствуют окну поиска общего парсера.
|
||||
events = [] # За один байт может завершиться один кадр фиксированной длины.
|
||||
for record in self.parser.feed(bytes([value])): # Только CRC-подтверждённые записи выдаются прикладному слою.
|
||||
fields = [('Команда', record.command), ('Адрес', str(record.address)), ('CRC', '%04X' % record.crc_frame)] # Служебные поля берутся из готовой модели TmsRecord.
|
||||
fields += [(tms.analog_name(index), str(word)) for index, word in enumerate(record.analogs)] # Имена аналогов и знаковые int16 совпадают с терминалом SETGUI.
|
||||
fields += [('Дискреты', ', '.join(record.set_bits()) or 'нет активных битов')] # Активные биты выдаются с исходной нумерацией байтов и битов.
|
||||
result = dict(protocol='TMS UART', summary=record.summary, payload=record.raw[2:-3], raw=record.raw, fields=fields, analogs=record.analogs, digits=record.digits) # CRC и завершающий байт исключены из полезной нагрузки по контракту TMS_TO_TERMINAL.
|
||||
events.append((self.positions[0][0], es, result, None)) # Начало подтверждённого окна сохраняется даже после пропущенного шума.
|
||||
self.positions.clear() # Координаты завершённого кадра не удерживаются до следующей посылки.
|
||||
return events # События имеют тот же контракт, что парсер SET UART.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user