5 Commits

Author SHA1 Message Date
a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00
95c8b8ac7d Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:26:37 +03:00
bbb4bc2b9d Уточнить обработку плотных трендов и восстановление сигналов 2026-10-12 00:48:15 +03:00
20454ff7fa Добавить поддержку GAS 167 и УМП, улучшить обмен по CAN и последовательному порту 2026-10-11 21:23:21 +03:00
3c0f6fd9e6 Добавить загрузку F28335 по ProtoCAN и сервисные команды клиента 2026-10-11 03:24:44 +03:00
115 changed files with 7992 additions and 843 deletions

View File

@@ -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})

View File

@@ -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);

View File

@@ -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.

View File

@@ -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);

View File

@@ -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-контроллера. */
} /* Завершение управления встроенным окончанием шины. */

View File

@@ -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 — включён. */

View File

@@ -1,6 +1,6 @@
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
cmake_minimum_required(VERSION 3.13)
project(protocan_boot C)
@@ -17,10 +17,31 @@ else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
if(PCAN_BOOT_BUILD_TESTS)
enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c)
target_link_libraries(test_pcan_boot PRIVATE protocan_boot)
add_test(NAME pcan_boot COMMAND test_pcan_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_can_boot tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_can_boot PRIVATE PCAN_BOOT_WIDE_BYTES)
target_include_directories(test_f28335_can_boot PRIVATE tests/f28335_stubs)
add_test(NAME f28335_can_boot COMMAND test_f28335_can_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_application tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_application PRIVATE PCAN_BOOT_WIDE_BYTES F28335_BOOT_LOADER=0)
target_include_directories(test_f28335_application PRIVATE tests/f28335_stubs)
add_test(NAME f28335_application COMMAND test_f28335_application)
foreach(width native wide)
add_executable(test_f28335_store_${width} tests/test_f28335_boot_store.c ports/tms320f28335/f28335_boot_store.c)
if(width STREQUAL "wide")
target_compile_definitions(test_f28335_store_${width} PRIVATE PCAN_BOOT_WIDE_BYTES)
endif()
add_test(NAME f28335_store_${width} COMMAND test_f28335_store_${width})
endforeach()
endif()

View File

@@ -97,7 +97,8 @@ typedef struct {
} pcan_boot_port_t;
```
Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`.
Обязательны `send`, `erase_slot`, `write_slot`; в A/B также `set_pending_slot`.
В single-slot этот callback необязателен и может завершать активацию образа.
`authorize` проверяет политику до стирания (тип, версия, anti-rollback).
`verify_image` выполняет платформенную проверку подписанного контейнера после
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
@@ -175,5 +176,12 @@ ctest --test-dir build --output-on-failure
- включить watchdog и rollback при неподтверждённом запуске;
- согласовать фильтры CAN для `MsgType=0x9..0xD`.
Проект пока не подключён ни к одной конкретной плате; это самостоятельный
шаблон для интеграции в будущие загрузчики приборов SET.
Ядро поддерживает платформенные порты; особенности интеграции перечислены ниже.
## F28335 / большая EEPROM
[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`.
Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт.
Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск,
CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash-
загрузчика и выбор Flash boot описаны в README порта.

View File

@@ -63,6 +63,10 @@ BlockIndex = 0x0000…0xFFFF
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
| `0x0C` | `PING` | 0 | отсутствует | любое |
| `0x0D` | `CAPABILITIES` | 0 | отсутствует | любое, при поддержке порта |
| `0x0E` | `DEVICE_NAME` | 1 | номер страницы имени | любое, при поддержке порта |
| `0x0F` | `MEMORY_INFO` | 1 | номер страницы памяти | любое, при поддержке порта |
### `BEGIN_IMAGE`
@@ -81,7 +85,8 @@ DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется
только при наличии соответствующей политики платформы (callback authorize).
## `BOOT_STATUS`
@@ -181,3 +186,16 @@ Boot metadata должна атомарно хранить:
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.
## F28335 M95M02
Реализованы отдельный Flash-загрузчик, A/B по 130560 байт, журнал, пробный запуск
и откат. Подробная раскладка памяти, сервисных ответов и первичная установка:
[README порта](../ports/tms320f28335/README.md).
Общий discovery: Route=0, DeviceType=7, DeviceID=15, MsgType=D, Body=0, DLC=0.
Он только запрашивает IDENTIFY; операции изменения всегда адресные.
ENTER_BOOT может отложить ACK до готовности отдельного загрузчика после reset.
CONFIRM с ненулевой session может обрабатываться приложением без ENTER_BOOT.
`pcan_boot_poll` обслуживает задержку discovery и тайм-аут сессии, если он задан
портом. Старым портам с timeout=0 периодический вызов нужен только для discovery.

View File

@@ -11,6 +11,13 @@
#include <stddef.h>
#include <stdint.h>
/* C28x has 16-bit addressable chars; wire octets use the low eight bits. */
#if defined(__TMS320C28XX__) || defined(PCAN_BOOT_WIDE_BYTES)
typedef unsigned int pcan_boot_byte_t;
#else
typedef uint8_t pcan_boot_byte_t;
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -41,7 +48,11 @@ typedef enum {
PCAN_BOOT_CMD_CONFIRM = 0x08U,
PCAN_BOOT_CMD_REBOOT = 0x09U,
PCAN_BOOT_CMD_ABORT = 0x0AU,
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU,
PCAN_BOOT_CMD_PING = 0x0CU,
PCAN_BOOT_CMD_CAPABILITIES = 0x0DU,
PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU,
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU
} pcan_boot_command_t;
typedef enum {
@@ -66,7 +77,8 @@ typedef enum {
PCAN_BOOT_STATE_READY_TO_ERASE,
PCAN_BOOT_STATE_RECEIVING,
PCAN_BOOT_STATE_VERIFIED,
PCAN_BOOT_STATE_FAILED
PCAN_BOOT_STATE_FAILED,
PCAN_BOOT_STATE_COMMITTED
} pcan_boot_state_t;
typedef struct {
@@ -74,36 +86,47 @@ typedef struct {
uint32_t image_crc32;
uint32_t firmware_version;
uint16_t product_type;
uint8_t hardware_revision_min;
uint8_t hardware_revision_max;
pcan_boot_byte_t hardware_revision_min;
pcan_boot_byte_t hardware_revision_max;
} pcan_boot_manifest_t;
typedef struct {
uint8_t device_type;
uint8_t device_id;
pcan_boot_byte_t device_type;
pcan_boot_byte_t device_id;
uint16_t product_type;
uint8_t hardware_revision;
pcan_boot_byte_t hardware_revision;
uint32_t firmware_version;
uint8_t active_slot;
pcan_boot_byte_t active_slot;
/** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */
uint8_t slot_count;
pcan_boot_byte_t slot_count;
/** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
uint32_t max_image_size;
uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
pcan_boot_byte_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
uint32_t session_timeout_ms; /**< 0 disables timeout for legacy ports. */
} pcan_boot_config_t;
typedef struct {
bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc);
bool (*erase_slot)(void *user, uint8_t slot);
bool (*write_slot)(void *user, uint8_t slot, uint32_t offset,
const uint8_t *data, uint8_t length);
bool (*send)(void *user, uint32_t can_id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
bool (*erase_slot)(void *user, pcan_boot_byte_t slot);
bool (*write_slot)(void *user, pcan_boot_byte_t slot, uint32_t offset,
const pcan_boot_byte_t *data, pcan_boot_byte_t length);
bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest);
bool (*verify_image)(void *user, uint8_t slot,
bool (*verify_image)(void *user, pcan_boot_byte_t slot,
const pcan_boot_manifest_t *manifest);
bool (*set_pending_slot)(void *user, uint8_t slot,
/** A/B metadata, or optional single-slot activation after verification. */
bool (*set_pending_slot)(void *user, pcan_boot_byte_t slot,
const pcan_boot_manifest_t *manifest);
bool (*confirm_running_slot)(void *user);
void (*reboot)(void *user);
/** ENTER: 0 ready, 1 deferred ACK after reset, 2 denied. NULL = legacy ready. */
unsigned int (*enter_boot)(void *user, pcan_boot_byte_t session);
bool (*can_reboot)(void *user);
/** Eight-octet service response, addressed by command + page. */
bool (*service_info)(void *user, pcan_boot_byte_t command,
pcan_boot_byte_t page, pcan_boot_byte_t *out);
/** 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_port_t;
typedef struct {
@@ -115,11 +138,13 @@ typedef struct {
uint32_t running_crc32;
uint32_t bytes_received;
uint16_t next_block;
uint8_t session_id;
uint8_t target_slot;
uint8_t last_status;
pcan_boot_byte_t session_id;
pcan_boot_byte_t target_slot;
pcan_boot_byte_t last_status;
bool have_image;
bool have_compat;
uint32_t now_ms, last_activity_ms, discovery_at;
bool discovery_pending;
} pcan_boot_t;
/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */
@@ -134,16 +159,19 @@ void pcan_boot_abort(pcan_boot_t *boot);
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
*/
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const uint8_t *data, uint8_t dlc);
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
/** Call regularly with a wrapping monotonic millisecond clock. */
void pcan_boot_poll(pcan_boot_t *boot, uint32_t now_ms);
/** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body);
uint8_t pcan_boot_id_route(uint32_t can_id);
uint8_t pcan_boot_id_device_type(uint32_t can_id);
uint8_t pcan_boot_id_device_id(uint32_t can_id);
uint8_t pcan_boot_id_msg_type(uint32_t can_id);
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body);
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id);
uint16_t pcan_boot_id_msg_body(uint32_t can_id);
#ifdef __cplusplus

View File

@@ -0,0 +1,156 @@
# F28335 + M95M02: CAN, отдельный загрузчик и A/B
Реализация состоит из Flash-загрузчика `periph_167/Bootloader`, переносимого
`f28335_boot_store.c`, аппаратного `f28335_boot_hw.c` и адаптера
`f28335_can_boot.c`. Приложение подключает их через `Source/Internal/can_boot.c`.
В сборке `EEPROM_M95M02=0` CAN Boot отключён. Для установленной M95M02 — значение 1.
## Адрес и формат
Device Type **3**, Device ID **7**, имя **F28335-M95M02**, Product **0x2833**, HW **1**.
В приложении FW берётся из `firmware_version.h`, формат `major<<24 | minor<<16 | patch`;
для версии 1.1.2 это `0x01010002`. Загрузчик сообщает версию подтверждённого образа
из журнала, при отсутствии такого образа — 0. Версия кандидата передаётся в BEGIN_COMPAT.
CAN **250000 бит/с**, Extended ID, окно **16 блоков по 8 октетов**.
ENTER_BOOT для session=1: `0x13790102`, STATUS: `0x1B7C0102`.
Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно.
Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное
слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный,
поэтому `pcan_boot_byte_t` хранит октет в младших 8 битах.
## Разметка EEPROM, байтовые адреса
| Область | Начало | Конец включительно | Размер |
|---|---:|---:|---:|
| Журнал 0 | 0x00000 | 0x000FF | 256 |
| Журнал 1 | 0x00100 | 0x001FF | 256 |
| Slot A | 0x00200 | 0x1FFFF | 130560 |
| Slot B | 0x20000 | 0x3FDFF | 130560 |
| Резерв | 0x3FE00 | 0x3FEFB | 252 |
| Параметры | 0x3FEFC | 0x3FFFD | 258 |
| Последнее слово | 0x3FFFE | 0x3FFFF | 2 |
В каждой странице журнала первые 64 октета содержат magic `FAB2`, версию 1,
sequence, active/pending/attempted, два manifest, CRC32 первых 56 октетов и
commit marker в октетах 60..63. Сначала обнуляется marker запасной страницы,
затем записывается тело с проверкой чтением, последним — marker. Предыдущая
страница остаётся действующей до полной публикации новой. Выбор sequence
учитывает переполнение u32. После ошибки публикации журнал перечитывается;
ошибка чтения блокирует последующие записи.
## Сценарий обновления и отката
1. Приложение принимает ENTER_BOOT, останавливает управление нагрузкой, выставляет 4 мА и выдерживает
секундный STOP, затем сохраняет session в NOINIT RAM 0x50..0x57. Watchdog выполняет reset.
ACK ENTER_BOOT отправляет уже Flash-загрузчик; приложение ничего не стирает.
2. BEGIN_IMAGE/BEGIN_COMPAT проверяют размер, Product, HW и сессию. Неактивный
слот выбирается автоматически; при первом образе это A. ERASE инвалидирует
только его manifest. Полного стирания EEPROM не требуется.
3. Последовательные DATA_A/B записываются с немедленным readback. VERIFY
проверяет потоковый CRC32, CRC32 EEPROM и полную таблицу SCI8.
Разрешена загрузка только в CPU word RAM `[0x8000,0xE000)`: максимум 64 секции,
без пересечений, усечений и хвостовых данных; entry должен попасть в секцию.
Запись в Flash, периферию, стек, RAM загрузчика и handoff запрещена.
4. COMMIT повторно проверяет образ и публикует pending в журнале. Подтверждённый
слот сохраняется. Повтор COMMIT безопасен. REBOOT выполняется после физического
CAN ACK ответа. Потеря ACK/тайм-аут передачи отменяет запрос reset.
5. Перед первым запуском кандидата загрузчик сохраняет attempted=1, затем
проверяет и копирует SCI8 в RAM. Следующий reset без CONFIRM сбрасывает pending
и запускает прежний active. Если прежнего образа нет — остаётся CAN recovery.
6. Новое приложение включает watchdog в начале запуска. До подтверждения
прерывания не кормят watchdog вместо основного цикла. После READY, 100 проходов
main и 5 секунд выполняется автоматический CONFIRM. Неудача сохранения
подтверждения через 10 секунд прекращает кормление watchdog, вызывая откат.
Адресная команда CONFIRM с ненулевой session также разрешена после READY.
CONFIRM в загрузчике и до первого запуска кандидата отклоняется.
PING, IDENTIFY и паспорт доступны без ENTER_BOOT. QUERY_PROGRESS/ABORT привязаны
к сессии. Сессия в RAM истекает через 30 секунд без подходящих команд/данных.
После reset передачу начинают заново; параметры и подтверждённый образ остаются.
ABORT после COMMIT не отменяет опубликованный pending. Для его пробного запуска
можно открыть новую сессию и отправить REBOOT. Pending запрещено перезаписывать.
Повторы ENTER_BOOT, метаданных, ERASE до первого блока, VERIFY и COMMIT учитывают
потерю ответа. Broadcast никогда не разрешает запись.
## Сервисные команды
Запрос: MsgType=9, Body=`session<<8 | command`. IDENTIFY/PING/паспорт не требуют сессии.
Ответ паспорта: MsgType=D, Body=`page<<8 | command`, DLC=8.
| Код | Имя | DLC запроса | Ответ |
|---:|---|---:|---|
| 01 | IDENTIFY | 0 | Product:u16, HW:u8, protocol:u8, FW:u32; Body=1 |
| 0C | PING | 0 | protocol, state, active, target, session, slot_count, last_status, 0 |
| 0D | CAPABILITIES | 0 | flags:u32, slot_capacity:u32 |
| 0E | DEVICE_NAME | 1 (page) | 8 ASCII-октетов имени; последний фрагмент с NUL |
| 0F | MEMORY_INFO | 1 (page) | Страницы ниже |
MEMORY_INFO 0: EEPROM bytes:u32, page bytes:u16, mode:u8 (0 app/1 boot), running:u8.
Страница 1: адреса A:u32, B:u32. Страница 2: active, pending, attempted, storage_healthy,
session_timeout_ms:u32. Страница 3: parameters_offset:u32, parameters_bytes:u32.
Слот FF означает отсутствие образа/запущенного приложения.
CAPABILITIES bits 0..11: ping, discovery, identify, name, memory, A/B, confirm,
rollback, separate loader, session timeout, readback, SCI8 validation.
Приложение, запущенное старым SPI ROM без handoff загрузчика, сообщает только
базовые сервисы (0x1F) и отклоняет ENTER_BOOT.
Обнаружение: MsgType=D, Body=0, DLC=0, Route=0. Адресный запрос или общий
DeviceType=7/DeviceID=15. Ответ IDENTIFY приходит с адреса прибора через
`2*(1+16*DeviceType+DeviceID)` мс. При нескольких одинаковых адресах конфликт
остаётся: каждому физическому устройству нужен уникальный адрес.
Python API: `protocan.can_boot.CanBootServices` — запросы и разбор паспорта;
`CanBootTransfer` — загрузка. Кнопка «Пинг» доступна в режиме ProtoCAN Boot
окна прошивки SETGUI. Для ENTER/VERIFY/COMMIT клиент ждёт до 15 секунд.
## Аппаратная интеграция
MBOX3 — адресные входящие Boot; MBOX4 — ответы; MBOX6 — точный broadcast discovery.
MBOX0..2 сохраняют legacy/UMP, MBOX5 остаётся heartbeat. ISR помещает сообщения
в очередь на 31 кадр. Чужие адреса, RTR и неподходящие типы отбрасываются до очереди.
При переполнении/потере mailbox сессия отменяется; повторная запись начинается заново.
SPI-драйвер загрузчика имеет ограничение ожидания байта и WIP, 24-битный адрес,
разбиение по страницам 256 байт и восстановление конфигурации SPI приложения.
Он работает только при остановленном Timer2. Обычный Seeprom_write приложения
ограничен областью параметров; старый RS EXTEND 0x44 запрещён для A/B.
Загрузчик исполняется из внутренней Flash A 0x338000..0x33FF7F, entry 0x33FFF6.
Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует
0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации,
0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует
NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash.
## Первичная установка
1. Собрать приложение с EEPROM_M95M02=1 и обновлённым F28335.cmd; получить SCI8 BIN.
Старые BIN/OUT автоматически не заменены. Приложение должно содержать интеграцию
READY/CONFIRM, иначе на следующем reset оно останется неподтверждённым кандидатом.
2. Собрать отдельный загрузчик TI C2000 COFF compiler:
`Bootloader/build.ps1 -CompilerRoot <путь-к-ti-cgt-c2000>`.
Результат — `Bootloader/build/F28335_CAN_AB.out` и MAP.
3. Один раз установить этот OUT через JTAG и выбрать аппаратный Flash boot
согласно схеме платы. Не загружать поверх него старый application OUT с
секцией codestart во Flash.
4. После reset пустая EEPROM или старая разметка автоматически ведут в CAN recovery.
Передать новое приложение через ProtoCAN Boot, дождаться пробного запуска и
проверить MEMORY_INFO page 2: pending=FF, active=A. Затем проверить обновление в B.
Старая SPI-ROM разметка с AA08 по адресу 0 несовместима с новым журналом.
Первое обновление заменяет её; параметры по прежнему адресу сохраняются.
Загрузчик не выполняет скрытую миграцию и не меняет boot pins программно.
## Проверки и границы проверенного
`python c/protocan-boot/tests/run_tests.py` (переменная CC — clang/gcc) проверяет
ядро, приложение/загрузчик, wide-octet режим, журнал, обрыв публикации на каждом
из 69 возможных положений, пробный запуск, CONFIRM, откат, SCI8 и границы памяти.
Python-тесты проверяют клиентские запросы, паспорт, фильтрацию ответов и лимит A/B.
Проверка синтаксиса использует настоящие DSP2833x headers. Сборка TI/CCS,
раскладка итогового MAP и физические CAN/SPI/reset на плате ещё не подтверждены.
CRC32 защищает от повреждения, криптографической подписи и anti-rollback по
версии в этом порту нет. Удалённый CONFIRM подтверждает готовность приложения,
но не заменяет проверку датчиков/силовой части на стенде.
Аппаратный формат SPI boot описан в [TI Boot ROM Reference Guide](https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/171/2742.Boot-ROM.pdf).
В A/B-схеме ROM выбирает Flash, а разбор EEPROM выполняет этот загрузчик.

View File

@@ -0,0 +1,181 @@
/* Shared by the application and the separate Flash bootloader. */
/* Аппаратный слой: общая RAM между reset, watchdog и SPI EEPROM.
* Один исходник компилируется и в приложение, и в отдельный Flash-загрузчик.
* В отличие от store.c этот файл требует настоящих регистров F28335. */
#include "DSP2833x_Device.h"
#include "f28335_boot_platform.h"
#include "f28335_can_boot.h"
/* Must be NOINIT in BOTH linkers, outside ROM scratch and application payload. */
#pragma DATA_SECTION(boot_handoff,".boot_shared")
/* 8 слов NOINIT по адресу 0x50, одинаково зарезервированных обоими linker-файлами.
* [0..1] magic; [2] запрошенная session; [3] запущенный слот; [4..6] служебные
* поля; [7] XOR-контроль. Это кратковременная передача контекста через watchdog
* reset, а не замена энергонезависимого журнала EEPROM. */
static volatile unsigned int boot_handoff[8];
/* Проверяем magic и XOR прежде, чем трактовать RAM как запрос входа в boot.
* После включения питания содержимое NOINIT может быть произвольным. */
static unsigned int handoff_valid(void)
{
unsigned int i,sum=0;
for(i=0;i<8;++i) sum^=boot_handoff[i];
return boot_handoff[0]==0xB283 && boot_handoff[1]==0x4D7C && sum==0xA95B;
}
/* Сначала снимаем признак действительности, затем заполняем поля и контроль.
* Первый magic записываем последним; volatile сохраняет обращения к общей RAM.
* request=0 означает обычный запуск, ненулевая session — явный ENTER_BOOT. */
static void handoff_set(unsigned int request,unsigned int running)
{
unsigned int i,sum=0;
boot_handoff[0]=0; boot_handoff[1]=0x4D7C;
boot_handoff[2]=request; boot_handoff[3]=running;
boot_handoff[4]=0xF335; boot_handoff[5]=0x0CCA; boot_handoff[6]=1;
sum=0xB283;
for(i=1;i<7;++i) sum^=boot_handoff[i];
boot_handoff[7]=sum^0xA95B;
boot_handoff[0]=0xB283; /* publish last */
}
/* Приложение узнаёт свой слот из handoff, а не предполагает, что оно active:
* во время пробного запуска выполняется pending, active ещё указывает на старый образ. */
unsigned int F28335Boot_RunningSlot(void)
{ return handoff_valid()?boot_handoff[3]:F28335_AB_NONE; }
unsigned int F28335Boot_RequestedSession(void)
{ return handoff_valid() && boot_handoff[2]<256?boot_handoff[2]:0; }
/* Одновременно публикуем выбранный слот и потребляем запрос ENTER_BOOT:
* он не должен повторяться при следующем обычном reset. */
void F28335Boot_SetRunning(unsigned int slot) { handoff_set(0,slot); }
/* Timer1 должен давать 1 мс на InterruptCount и в приложении, и в загрузчике.
* Тайм-ауты используют разность unsigned-счётчиков для переноса через wrap. */
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
/* Вызвать сразу после InitSysCtrl, который штатно отключает watchdog.
* Сбрасываем разрешение кормления из ISR до полной инициализации CAN-сервиса.
* Для приложения, запущенного нашим загрузчиком, включаем watchdog с делителем. */
void F28335Boot_StartupWatchdog(void)
{
F28335CanBoot_EarlyInit();
if(F28335Boot_RunningSlot()<2) {
EALLOW; SysCtrlRegs.WDKEY=0x55; SysCtrlRegs.WDKEY=0xAA;
SysCtrlRegs.WDCR=0x002F; EDIS;
}
}
/* Платформенный Quiesce останавливает управление выходами перед reset.
* Session сохраняется до включения watchdog; ненулевую session загрузчик
* использует для отложенного ACK ENTER_BOOT. В бесконечном цикле watchdog
* намеренно не обслуживается — выход из функции должен произойти через reset. */
void F28335Boot_Reset(unsigned int session)
{
F28335Boot_Quiesce();
DINT;
handoff_set(session,F28335_AB_NONE);
EALLOW; SysCtrlRegs.WDCR=0x0028; EDIS;
for(;;) { }
}
/* Main context only. Legacy Timer2 EEPROM driver must be idle/stopped. Save
* its SPI configuration: it changes the character width during transactions. */
typedef struct { unsigned int ccr,ctl,brr,tx,rx,ct,pri; } boot_spi_saved;
static void spi_cs(unsigned int high)
{ if(high) GpioDataRegs.GPASET.bit.GPIO19=1; else GpioDataRegs.GPACLEAR.bit.GPIO19=1; }
/* Доступ разрешён только из main при остановленном Timer2 старого SPI-драйвера.
* Это проверка занятости в однопоточном приложении, не универсальный mutex.
* Сохраняем режим SPI, поскольку штатный драйвер меняет разрядность передачи.
* CS сначала поднимаем, затем переключаем SPI на 8-битные транзакции. */
static bool spi_take(boot_spi_saved *s)
{
volatile unsigned int discard;
if(!CpuTimer2Regs.TCR.bit.TSS) return false;
s->ccr=SpiaRegs.SPICCR.all; s->ctl=SpiaRegs.SPICTL.all; s->brr=SpiaRegs.SPIBRR;
s->tx=SpiaRegs.SPIFFTX.all; s->rx=SpiaRegs.SPIFFRX.all;
s->ct=SpiaRegs.SPIFFCT.all; s->pri=SpiaRegs.SPIPRI.all;
EALLOW;
GpioCtrlRegs.GPAMUX2.bit.GPIO16=1; GpioCtrlRegs.GPAMUX2.bit.GPIO17=1;
GpioCtrlRegs.GPAMUX2.bit.GPIO18=1; GpioCtrlRegs.GPAMUX2.bit.GPIO19=0;
GpioDataRegs.GPASET.bit.GPIO19=1; GpioCtrlRegs.GPADIR.bit.GPIO19=1;
GpioCtrlRegs.GPAQSEL2.bit.GPIO17=3;
EDIS;
SpiaRegs.SPICCR.all=7; SpiaRegs.SPICTL.all=0x0E; /* master, phase=1, IRQ off */
SpiaRegs.SPIBRR=CLKMULT*6+1;
SpiaRegs.SPIFFTX.all=0x8000; SpiaRegs.SPIFFRX.all=0; SpiaRegs.SPIFFCT.all=0;
SpiaRegs.SPIPRI.all=0x10; SpiaRegs.SPICCR.all=0x87;
/* Снимаем оставшийся RX/INT_FLAG от прежней операции, чтобы первый обмен
* не принял старое содержимое RXBUF за ответ EEPROM. */
discard=SpiaRegs.SPIRXBUF; (void)discard;
return true;
}
/* Выход и при успехе, и при ошибке: CS не должен остаться активным.
* Настройки возвращаются при удержании SPI в reset; исходный SPISWRESET
* восстанавливается последним, после остальных регистров. */
static void spi_release(const boot_spi_saved *s)
{
volatile unsigned int discard;
spi_cs(1); discard=SpiaRegs.SPIRXBUF; (void)discard;
SpiaRegs.SPICCR.all=s->ccr&0x7FU; SpiaRegs.SPICTL.all=s->ctl;
SpiaRegs.SPIBRR=s->brr; SpiaRegs.SPIFFTX.all=s->tx; SpiaRegs.SPIFFRX.all=s->rx;
SpiaRegs.SPIFFCT.all=s->ct; SpiaRegs.SPIPRI.all=s->pri; SpiaRegs.SPICCR.all=s->ccr;
}
/* У F28335 8-битный символ для передачи выравнивается в старшие биты TXBUF.
* Полученный октет читается из младших битов RXBUF. Счётчик budget ограничивает
* ожидание даже при неработающем Timer1; это число итераций, а не миллисекунды. */
static bool spi_octet(unsigned int tx,unsigned int *rx)
{
uint32_t budget=100000UL;
SpiaRegs.SPITXBUF=(tx&255U)<<8;
while(!SpiaRegs.SPISTS.bit.INT_FLAG) if(--budget==0) return false;
*rx=SpiaRegs.SPIRXBUF&255U; return true;
}
/* Команда EEPROM и 24-битный байтовый адрес, старший октет адреса первым.
* CS удерживается вызывающей функцией на всю команду и полезные данные. */
static bool spi_prefix(unsigned int command,uint32_t address)
{
unsigned int discard;
return spi_octet(command,&discard) && spi_octet((unsigned int)(address>>16),&discard) &&
spi_octet((unsigned int)(address>>8),&discard) && spi_octet((unsigned int)address,&discard);
}
/* RDSR (05h): WIP в бите 0 означает незавершённую внутреннюю запись EEPROM.
* Ожидание ограничено и 50 мс, и числом опросов на случай остановки tick.
* После каждого чтения статуса CS снимается, в том числе при ошибке SPI. */
static bool spi_ready(void)
{
unsigned int status,discard; uint32_t started=F28335Boot_Now(),budget=20000UL;
do {
spi_cs(0);
if(!spi_octet(5,&discard) || !spi_octet(255,&status)) { spi_cs(1); return false; }
spi_cs(1);
if(!(status&1U)) return true;
} while(--budget && (uint32_t)(F28335Boot_Now()-started)<50UL);
return false;
}
/* Читать разрешено всю физическую EEPROM, включая параметры. Проверка
* через вычитание защищает от переполнения адреса. spi_release обязателен
* после успешного захвата SPI независимо от результата последующего обмена. */
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,value; bool ok; (void)u;
if(address>0x40000UL || n>0x40000UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
spi_cs(0); if(ok) ok=spi_prefix(3,address);
for(i=0;ok && i<n;++i) { ok=spi_octet(255,&value); p[i]=value; }
spi_release(&saved); return ok;
}
/* Нижний аппаратный барьер записи: разрешены журнал и слоты до 0x3FE00.
* Резерв, параметры и последнее слово нельзя изменить даже ошибочным вызовом
* этой функции. Защита активного слота дополнительно выполняется в store.c. */
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,chunk,discard; bool ok; (void)u;
/* This driver cannot overwrite parameters, even if a caller is defective. */
if(address>0x3FE00UL || n>0x3FE00UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
while(ok && n) {
/* Нельзя пересекать страницу одной командой WRITE: EEPROM может завернуть
* адрес внутри страницы. Для каждого фрагмента заново выдаём WREN (06h),
* WRITE (02h) и ждём снятия WIP. Сравнение readback выполняет верхний слой. */
chunk=256U-(unsigned int)(address&255U); if(chunk>n) chunk=n;
spi_cs(0); ok=spi_octet(6,&discard); spi_cs(1); /* WREN */
spi_cs(0); if(ok) ok=spi_prefix(2,address);
for(i=0;ok && i<chunk;++i) ok=spi_octet(p[i],&discard);
spi_cs(1); if(ok) ok=spi_ready();
n-=chunk; p+=chunk; address+=chunk;
}
spi_release(&saved); return ok;
}

View File

@@ -0,0 +1,17 @@
#ifndef F28335_BOOT_PLATFORM_H
#define F28335_BOOT_PLATFORM_H
#include "f28335_boot_store.h"
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 0
#endif
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n);
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n);
uint32_t F28335Boot_Now(void);
void F28335Boot_Reset(unsigned int session);
unsigned int F28335Boot_RunningSlot(void);
unsigned int F28335Boot_RequestedSession(void);
void F28335Boot_SetRunning(unsigned int slot);
void F28335Boot_StartupWatchdog(void);
/* Application hook must stop outputs before reset. No EEPROM operations here. */
void F28335Boot_Quiesce(void);
#endif

View File

@@ -0,0 +1,254 @@
/* Хранилище A/B без зависимости от регистров F28335.
* Здесь находятся правила сохранения образа при сбое питания и выбора слота.
* CAN отвечает за порядок команд; этот слой повторно защищает active/pending
* и границы EEPROM, даже если вызывающий код передал неверные параметры.
* Адрес EEPROM измеряется в октетах, адрес назначения SCI8 — в словах CPU. */
#include "f28335_boot_store.h"
#include <string.h>
/* Формат журнала фиксирован: u32 little endian. Нельзя сохранять C-структуру
* через memcpy: выравнивание и sizeof на C28x отличаются от обычного ПК. */
static uint32_t bs_get32(const pcan_boot_byte_t *p)
{ return (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24); }
static void bs_put32(pcan_boot_byte_t *p, uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(8*i))&255U); }
/* Потоковый CRC32: первый вызов получает crc=0, следующие — предыдущий результат.
* Внутренняя инверсия позволяет считать один CRC по нескольким порциям EEPROM.
* Маска 255 исключает старшие биты 16-битного представления октета на C28x.
* Это проверка повреждения данных, а не криптографическая подпись. */
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n)
{ unsigned int i,b; crc=~crc; for(i=0;i<n;++i) { crc^=p[i]&255U;
for(b=0;b<8;++b) crc=(crc>>1)^((crc&1U)?0xEDB88320UL:0); } return ~crc; }
static uint32_t bs_base(unsigned int slot) { return slot?F28335_AB_SLOT_B:F28335_AB_SLOT_A; }
/* Принимаем запись только при совпадении magic, версии, marker и CRC.
* Дополнительно проверяем смысл номеров слотов: pending не может быть active.
* Наличие правильного marker без правильного CRC не разрешает запуск. */
static bool bs_record_valid(const pcan_boot_byte_t *p)
{
return bs_get32(p)==0x32424146UL && bs_get32(p+4)==1 &&
bs_get32(p+60)==0x51AFC03DUL && bs_get32(p+56)==f28335_boot_crc(0,p,56) &&
(p[12]<2 || p[12]==F28335_AB_NONE) &&
(p[13]<2 || p[13]==F28335_AB_NONE) && p[14]<=1 &&
(p[13]==F28335_AB_NONE || p[13]!=p[12]);
}
/* Карта записи, смещения в октетах:
* 0..3 magic FAB2; 4..7 версия; 8..11 sequence; 12 active; 13 pending;
* 14 attempted; 15 резерв; 16..35 manifest A; 36..55 manifest B;
* 56..59 CRC первых 56 октетов; 60..63 marker завершённой публикации.
* Manifest: size, CRC, FW (по u32), Product (u16), HW min/max, 4 октета резерва. */
static void bs_decode(f28335_boot_store_t *s, const pcan_boot_byte_t *p)
{
unsigned int i; s->sequence=bs_get32(p+8); s->active=p[12];
s->pending=p[13]; s->attempted=p[14];
for(i=0;i<2;++i) {
const pcan_boot_byte_t *q=p+16+i*20;
s->image[i].image_size=bs_get32(q); s->image[i].image_crc32=bs_get32(q+4);
s->image[i].firmware_version=bs_get32(q+8);
s->image[i].product_type=(uint16_t)(q[12]|(q[13]<<8));
s->image[i].hardware_revision_min=q[14]; s->image[i].hardware_revision_max=q[15];
}
}
/* Читаем обе копии журнала. Нераспознанная разметка допускает CAN recovery
* с active=NONE; ошибка чтения запрещает запись, поскольку реальный active неизвестен.
* Эта функция не преобразует старый SCI8 по адресу 0 в новую A/B-разметку. */
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io)
{
pcan_boot_byte_t a[64],b[64]; bool va,vb;
if(!s || !io || !io->read || !io->write) return false;
memset(s,0,sizeof(*s)); s->io=*io; s->active=s->pending=F28335_AB_NONE;
if(!io->read(io->user,0,a,64) || !io->read(io->user,256,b,64)) { s->io_fault=true; return false; }
va=bs_record_valid(a); vb=bs_record_valid(b);
if(!va && !vb) return true;
/* Сравнение sequence по модулю 2^32: разность менее половины диапазона
* считается более новой записью. При равенстве выбирается страница 1.
* Соседние публикации отличаются на единицу, в том числе после переполнения. */
s->page=vb && (!va || (uint32_t)(bs_get32(b+8)-bs_get32(a+8))<0x80000000UL)?1:0;
bs_decode(s,s->page?b:a); s->valid=true; return true;
}
/* Публикация выполняется в другую страницу, текущую действующую не трогаем.
* Порядок принципиален: обнулить marker -> проверить -> записать тело ->
* проверить тело -> записать marker последним -> проверить всю запись.
* При обрыве питания загрузчик выбирает прежнюю либо полностью новую запись.
* Каждый memcmp учитывает sizeof элемента, поскольку элемент здесь — октет
* протокола, но на C28x занимает адресуемое 16-битное слово. */
static bool bs_save(f28335_boot_store_t *s)
{
pcan_boot_byte_t p[64],check[64],invalid[4]={0,0,0,0}; unsigned int i;
unsigned int page=s->valid?(s->page^1U):0;
uint32_t addr=(uint32_t)page*256;
memset(p,0,sizeof(p)); bs_put32(p,0x32424146UL); bs_put32(p+4,1);
bs_put32(p+8,s->sequence+1); p[12]=s->active; p[13]=s->pending; p[14]=s->attempted;
for(i=0;i<2;++i) {
pcan_boot_byte_t *q=p+16+i*20;
bs_put32(q,s->image[i].image_size); bs_put32(q+4,s->image[i].image_crc32);
bs_put32(q+8,s->image[i].firmware_version);
q[12]=s->image[i].product_type&255U; q[13]=(s->image[i].product_type>>8)&255U;
q[14]=s->image[i].hardware_revision_min; q[15]=s->image[i].hardware_revision_max;
}
bs_put32(p+56,f28335_boot_crc(0,p,56)); bs_put32(p+60,0x51AFC03DUL);
/* The old page is never erased. A torn write cannot publish a partial record. */
if(!s->io.write(s->io.user,addr+60,invalid,4) ||
!s->io.read(s->io.user,addr+60,check,4) || memcmp(check,invalid,sizeof(invalid)) ||
!s->io.write(s->io.user,addr,p,60) ||
!s->io.read(s->io.user,addr,check,60) || memcmp(p,check,60*sizeof(*p)) ||
!s->io.write(s->io.user,addr+60,p+60,4) ||
!s->io.read(s->io.user,addr,check,64) || memcmp(p,check,sizeof(p))) return false;
s->page=page; s->sequence++; s->valid=true; return true;
}
/* Изменения сначала готовятся в копии состояния next. Рабочее состояние
* заменяем только после публикации. Если запись/контрольное чтение сорвались,
* перечитываем журнал: marker мог успеть записаться до ошибки readback.
* После неудачи чтения io_fault блокирует дальнейшие изменения хранилища. */
static bool bs_publish(f28335_boot_store_t *s, f28335_boot_store_t *next)
{
if(bs_save(next)) { *s=*next; return true; }
/* A failed final read may still have committed: reload before any more writes. */
{ f28335_boot_io_t io=s->io; if(!f28335_boot_open(s,&io)) {
s->valid=false; s->io_fault=true; return false; } }
return false;
}
/* Логический ERASE: обнуляем manifest неактивного слота в журнале.
* Саму EEPROM стирать целиком не требуется — каждый байт будет перезаписан.
* Active защищён всегда; наличие pending запрещает начало новой записи,
* пока кандидат не подтверждён либо не снят при следующем выборе загрузки. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE) return false;
memset(&next.image[slot],0,sizeof(next.image[slot]));
return bs_publish(s,&next);
}
/* Записываем не более одного CAN-блока и немедленно сравниваем прочитанное.
* Проверка n <= capacity-off выполняется после off <= capacity: это исключает
* переполнение суммы off+n и выход из слота в соседний слот или параметры. */
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t off,
const pcan_boot_byte_t *p, unsigned int n)
{
pcan_boot_byte_t check[8];
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE || !n || n>8 ||
off>F28335_AB_SLOT_BYTES || n>F28335_AB_SLOT_BYTES-off) return false;
return s->io.write(s->io.user,bs_base(slot)+off,p,n) &&
s->io.read(s->io.user,bs_base(slot)+off,check,n) && !memcmp(check,p,n*sizeof(*p));
}
/* Слово SCI8 занимает два октета файла, младший октет идёт первым.
* Смещение увеличиваем только после успешного чтения; усечённый файл отклоняем. */
static bool bs_u16(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint16_t *v)
{
pcan_boot_byte_t p[2]; if(*off>size || size-*off<2 ||
!s->io.read(s->io.user,base+*off,p,2)) return false;
*v=(uint16_t)(p[0]|(p[1]<<8)); *off+=2; return true;
}
/* Особенность таблицы TI: адрес u32 передаётся старшим словом вперёд,
* при этом внутри каждого слова остаётся little endian. Не заменять на get32. */
static bool bs_addr(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint32_t *v)
{
uint16_t hi,lo; if(!bs_u16(s,base,size,off,&hi) || !bs_u16(s,base,size,off,&lo)) return false;
*v=((uint32_t)hi<<16)|lo; return true;
}
/* The complete table is validated before any application RAM is written.
* Loader RAM is E000..FFFF and M0/M1. Flash, peripherals and XINTF are forbidden. */
/* writer=NULL: только разбираем и проверяем таблицу, RAM приложения не меняем.
* writer задан: повторно проверяем таблицу и переносим слова в разрешённую RAM.
* Массивы starts/ends ограничены MAX_SECTIONS; они нужны для обнаружения
* пересекающихся секций, в том числе секций с разным порядком адресов. */
static bool bs_table(f28335_boot_store_t *s,unsigned int slot,uint32_t size,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
uint32_t starts[F28335_AB_MAX_SECTIONS],ends[F28335_AB_MAX_SECTIONS];
uint32_t off=0,addr,end,base=bs_base(slot); unsigned int sections=0,i;
uint16_t count,word; bool entry_loaded=false;
if(!bs_u16(s,base,size,&off,&word) || word!=0x08AA) return false;
/* После ключа AA 08 пропускаем 16 резервных октетов; далее расположен entry.
* Нулевой count завершает таблицу строго на конце файла, без хвостовых данных. */
off=18; if(!bs_addr(s,base,size,&off,entry)) return false;
for(;;) {
if(!bs_u16(s,base,size,&off,&count)) return false;
if(!count) return off==size && entry_loaded;
if(sections==F28335_AB_MAX_SECTIONS || !bs_addr(s,base,size,&off,&addr)) return false;
end=addr+count;
/* Разрешён только полуинтервал RAM [0x8000,0xE000) в словах CPU.
* Верхняя граница защищает RAM работающего загрузчика; нижняя — стек,
* handoff и регистры. Проверка размера payload использует уже октеты файла. */
if(addr<0x8000UL || end>0xE000UL || end<=addr ||
off>size || (uint32_t)count*2>size-off) return false;
for(i=0;i<sections;++i) if(addr<ends[i] && end>starts[i]) return false;
starts[sections]=addr; ends[sections++]=end;
if(*entry>=addr && *entry<end) entry_loaded=true;
if(writer) { for(i=0;i<count;++i) {
if(!bs_u16(s,base,size,&off,&word) || !writer(user,addr+i,word)) return false;
} } else off+=(uint32_t)count*2;
}
}
/* VERIFY проверяет содержимое самой EEPROM, а не только CRC принятого потока.
* Затем проверяется структура SCI8 и попадание entry в загружаемую секцию.
* Выходной entry необязателен: COMMIT нужна проверка, но не адрес перехода. */
bool f28335_boot_verify(f28335_boot_store_t *s,unsigned int slot,
const pcan_boot_manifest_t *m,uint32_t *entry)
{
uint32_t off,crc=0,local_entry; unsigned int n; pcan_boot_byte_t p[32];
if(slot>1 || m->image_size<24 || m->image_size>F28335_AB_SLOT_BYTES ||
(m->image_size&1) || m->product_type!=0x2833 ||
m->hardware_revision_min>1 || m->hardware_revision_max<1) return false;
for(off=0;off<m->image_size;off+=n) {
n=(m->image_size-off<32)?(unsigned int)(m->image_size-off):32;
if(!s->io.read(s->io.user,bs_base(slot)+off,p,n)) return false;
crc=f28335_boot_crc(crc,p,n);
}
if(crc!=m->image_crc32 || !bs_table(s,slot,m->image_size,0,0,&local_entry)) return false;
if(entry) *entry=local_entry;
return true;
}
/* COMMIT публикует проверенный образ как pending, не меняя active.
* Повторная проверка EEPROM непосредственно перед публикацией не позволяет
* назначить кандидатом образ, испорченный после предыдущего VERIFY. */
bool f28335_boot_commit(f28335_boot_store_t *s,unsigned int slot,const pcan_boot_manifest_t *m)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE ||
!f28335_boot_verify(s,slot,m,0)) return false;
next.image[slot]=*m; next.pending=slot; next.attempted=0;
return bs_publish(s,&next);
}
/* Подтверждать можно только действительно выбранный для пробного запуска слот.
* Требование attempted исключает подтверждение ещё ни разу не запускавшегося
* кандидата. Повтор CONFIRM уже активного слота возвращает успех без записи. */
bool f28335_boot_confirm(f28335_boot_store_t *s,unsigned int running_slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || running_slot>1) return false;
if(s->pending==F28335_AB_NONE) return s->valid && s->active==running_slot;
if(s->pending!=running_slot || !s->attempted) return false;
next.active=running_slot; next.pending=F28335_AB_NONE; next.attempted=0;
return bs_publish(s,&next);
}
/* Выбор выполняется при старте загрузчика. attempted сохраняется ДО перехода
* в приложение: после зависания, watchdog reset или потери питания следующий
* старт уже знает о незавершённой пробе и возвращает прежний active.
* Успех функции с slot=NONE означает recovery, а не разрешение перехода в RAM. */
bool f28335_boot_select(f28335_boot_store_t *s,unsigned int *slot)
{
f28335_boot_store_t next=*s; *slot=F28335_AB_NONE;
if(s->io_fault) return false;
if(s->pending!=F28335_AB_NONE) {
if(!s->attempted && f28335_boot_verify(s,s->pending,&s->image[s->pending],0)) {
next.attempted=1;
if(!bs_publish(s,&next)) return false;
*slot=s->pending; return true;
}
/* Кандидат уже запускался без подтверждения либо не прошёл проверку.
* Снимаем pending отдельной публикацией; подтверждённый active остаётся прежним. */
next.pending=F28335_AB_NONE; next.attempted=0;
if(!bs_publish(s,&next)) return false;
}
if(s->active<2 && f28335_boot_verify(s,s->active,&s->image[s->active],0)) *slot=s->active;
return true;
}
/* Сначала полная проверка CRC и всех адресов без записи, затем перенос слов.
* Платформа вызывает entry только при true; частичная загрузка при ошибке I/O
* не даёт разрешения на запуск приложения. */
bool f28335_boot_load(f28335_boot_store_t *s,unsigned int slot,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
return slot<2 && writer && entry && f28335_boot_verify(s,slot,&s->image[slot],entry) &&
bs_table(s,slot,s->image[slot].image_size,writer,user,entry);
}

View File

@@ -0,0 +1,55 @@
#ifndef F28335_BOOT_STORE_H
#define F28335_BOOT_STORE_H
#include "../../include/pcan_boot.h"
/* All EEPROM offsets/lengths below are OCTETS, CPU destinations are WORDS. */
/* Физическая EEPROM 256 КиБ: две страницы журнала по 256 октетов,
* два слота по 130560 октетов. С 0x3FE00 начинается резерв, с 0x3FEFC —
* прежняя область параметров. Константы ниже не являются CPU word-адресами. */
#define F28335_AB_SLOT_BYTES 0x1FE00UL
#define F28335_AB_SLOT_A 0x00200UL
#define F28335_AB_SLOT_B 0x20000UL
#define F28335_AB_PARAMS 0x3FEFCUL
#define F28335_AB_NONE 0xFFU
#define F28335_AB_RECORD 64U
#define F28335_AB_MAX_SECTIONS 64U
/* Контракт I/O: address и n измеряются в октетах, независимо от sizeof(char).
* true означает, что выполнена вся операция; короткий обмен должен вернуть false.
* Callbacks вызываются синхронно из main, а user передаётся им без изменений. */
typedef struct {
bool (*read)(void *, uint32_t, pcan_boot_byte_t *, unsigned int);
bool (*write)(void *, uint32_t, const pcan_boot_byte_t *, unsigned int);
void *user;
} f28335_boot_io_t;
typedef struct {
f28335_boot_io_t io;
/* sequence — номер последней публикации; page — выбранная копия журнала.
* active — подтверждённый слот; pending — кандидат; attempted — уже была проба.
* valid показывает наличие распознанной записи, io_fault запрещает изменения
* после ошибки чтения. Отсутствующий слот обозначается NONE, а не нулём. */
uint32_t sequence;
unsigned int page, active, pending, attempted;
bool valid, io_fault;
pcan_boot_manifest_t image[2];
} f28335_boot_store_t;
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n);
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io);
/* Blank/unrecognised EEPROM opens successfully with active=NONE. I/O errors fail. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot);
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t offset,
const pcan_boot_byte_t *data, unsigned int n);
bool f28335_boot_verify(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m, uint32_t *entry);
bool f28335_boot_commit(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m);
bool f28335_boot_confirm(f28335_boot_store_t *s, unsigned int running_slot);
/* Records a trial BEFORE returning it; a second unconfirmed boot rolls back. */
bool f28335_boot_select(f28335_boot_store_t *s, unsigned int *slot);
/* Запись одного проверенного слова приложения по CPU word-адресу.
* Должна вернуть false при невозможности записи; после отказа запуск запрещён. */
typedef bool (*f28335_boot_ram_writer)(void *, uint32_t, uint16_t);
bool f28335_boot_load(f28335_boot_store_t *s, unsigned int slot,
f28335_boot_ram_writer write_word, void *user, uint32_t *entry);
#endif

View File

@@ -0,0 +1,276 @@
/* Separate Flash recovery loader and application service adapter. */
#include "DSP2833x_Device.h"
#include "f28335_can_boot.h"
#if EEPROM_M95M02
#include "f28335_boot_platform.h"
#include <string.h>
/* boot хранит текущую CAN-сессию в RAM, store — прочитанное состояние журнала.
* Сброс сессии не удаляет active/pending из EEPROM. */
static pcan_boot_t boot;
static f28335_boot_store_t store;
/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов
* используется 31, чтобы head==tail однозначно означало пустую очередь.
* Долгие операции SPI запрещены внутри обработчика CAN. */
static volatile unsigned int head,tail,overflow;
static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue[32];
/* reset_requested только планирует reset; Service ждёт завершения TX.
* ready сообщает об окончании старта приложения, confirmed — о сохранённом
* подтверждении слота. healthy отражает результат открытия журнала при Init. */
static unsigned int reset_requested,reset_session,ready,healthy,confirmed;
static uint32_t sent_at,ready_at;
static unsigned int main_passes;
static int tx_busy,tx_canceling;
/* Один исходящий mailbox исключает перезапись ещё передаваемого ответа.
* MDL содержит первые четыре октета, MDH — следующие четыре, младшим вперёд.
* Запрос CANTRS ещё не означает успешную доставку: CANTA проверяется в Service.
* Аппаратный CAN ACK не является отдельным прикладным подтверждением от ПК. */
static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_byte_t n)
{
unsigned int i; unsigned long lo=0, hi=0; (void)u;
if (tx_busy) return false;
for(i=0;i<n;++i) {
if(i<4) lo|=(unsigned long)(d[i]&255U)<<(8U*i);
else hi|=(unsigned long)(d[i]&255U)<<(8U*(i-4U));
}
EALLOW;
ECanaRegs.CANME.all &= ~16UL;
ECanaMboxes.MBOX4.MSGID.all=0x80000000UL|id;
ECanaMboxes.MBOX4.MSGCTRL.all=n;
ECanaMboxes.MBOX4.MDL.all=lo; ECanaMboxes.MBOX4.MDH.all=hi;
ECanaRegs.CANME.all |=16UL;
ECanaRegs.CANTA.all=16UL; ECanaRegs.CANAA.all=16UL;
ECanaRegs.CANTRS.all=16UL;
EDIS;
sent_at=F28335Boot_Now(); tx_busy=1; tx_canceling=0; return true;
}
/* Изменение образа разрешено только сборке Flash-загрузчика. В приложении
* эти 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); }
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); }
/* Проверка включает CRC EEPROM и структуру SCI8. Сейчас отказ этого слоя
* кодируется CRC_ERROR как общий отказ проверки образа; причины внутри
* парсера SCI8 отдельными статусами протокола не различаются. */
static pcan_boot_status_t verify(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{
(void)u;
if(!F28335_BOOT_LOADER || !healthy) return PCAN_BOOT_STATUS_INVALID_STATE;
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); }
/* Подтверждение принадлежит приложению после READY. Загрузчик не может
* подтвердить кандидата сам: иначе исчезнет смысл проверки пробного запуска.
* Разрешение кормления watchdog из ISR открываем только после записи журнала. */
static bool confirm(void *u)
{
unsigned int running=F28335Boot_RunningSlot(); (void)u;
if(F28335_BOOT_LOADER || !healthy || !ready || running>1) return false;
if(!f28335_boot_confirm(&store,running)) return false;
boot.config.active_slot=running; confirmed=1; return true;
}
static bool can_reboot(void *u) { (void)u; return healthy; }
static void reboot(void *u) { (void)u; reset_session=0; reset_requested=1; }
/* Контракт enter_boot: 0 — загрузчик готов и ядро может отправить ACK;
* 1 — переход отложен, ACK придёт после reset; 2 — вход запрещён.
* Приложение требует handoff нашего загрузчика и подтверждённый текущий слот,
* чтобы не начать обновление из старого SPI-ROM старта или незавершённой пробы. */
static unsigned int enter(void *u,pcan_boot_byte_t session)
{
(void)u;
if(!healthy) return 2;
if(F28335_BOOT_LOADER) return 0;
/* No destructive fallback on boards still starting through the SPI ROM. */
if(F28335Boot_RunningSlot()>1 || !confirmed) return 2;
reset_session=session; reset_requested=1; return 1;
}
static void info32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(i*8))&255U); }
/* Паспорт помещается в classic CAN по страницам из восьми октетов.
* NAME возвращает фрагменты строки с завершающим NUL; неизвестная страница
* отклоняется. MEMORY: 0 — ёмкость/страница/режим/running, 1 — базы A/B,
* 2 — active/pending/attempted/healthy/тайм-аут, 3 — область параметров. */
static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boot_byte_t *out)
{
static const char name[]=F28335_CAN_BOOT_DEVICE_NAME;
unsigned int i,index; (void)u;
if(command==PCAN_BOOT_CMD_CAPABILITIES && page==0) {
/* 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)?0xFFFUL:0x1FUL);
info32(out+4,F28335_AB_SLOT_BYTES); return true;
}
if(command==PCAN_BOOT_CMD_DEVICE_NAME) {
if((unsigned int)page*8U>=sizeof(name)) return false;
for(i=0;i<8;++i) { index=(unsigned int)page*8U+i;
out[i]=index<sizeof(name)?(pcan_boot_byte_t)name[index]:0; }
return true;
}
if(command!=PCAN_BOOT_CMD_MEMORY_INFO) return false;
if(page==0) { info32(out,0x40000UL); out[4]=0; out[5]=1;
out[6]=F28335_BOOT_LOADER?1:0; out[7]=F28335Boot_RunningSlot(); return true; }
if(page==1) { info32(out,F28335_AB_SLOT_A); info32(out+4,F28335_AB_SLOT_B); return true; }
if(page==2) { out[0]=store.active; out[1]=store.pending; out[2]=store.attempted;
out[3]=healthy?1:0; info32(out+4,boot.config.session_timeout_ms); return true; }
if(page==3) { info32(out,F28335_AB_PARAMS); info32(out+4,258); return true; }
return false;
}
/* Вызывается до разрешения прикладных IRQ. Нельзя полагаться на случайное
* содержимое RAM или оставшееся состояние confirmed от предыдущей программы. */
void F28335CanBoot_EarlyInit(void) { confirmed=0; ready=0; }
/* Общий Init для приложения и загрузчика: открываем журнал, привязываем
* callbacks, задаём идентификацию, ёмкость слота, окно 16 и тайм-аут 30 секунд.
* В загрузчике FW берётся из active manifest; приложение сообщает версию сборки. */
void F28335CanBoot_Init(void)
{
pcan_boot_config_t cfg; pcan_boot_port_t port;
f28335_boot_io_t io; unsigned int running=F28335Boot_RunningSlot();
memset(&cfg,0,sizeof(cfg)); memset(&port,0,sizeof(port));
io.read=F28335Boot_Read; io.write=F28335Boot_Write; io.user=0;
healthy=f28335_boot_open(&store,&io);
cfg.device_type=F28335_CAN_BOOT_DEVICE_TYPE; cfg.device_id=F28335_CAN_BOOT_DEVICE_ID;
cfg.product_type=F28335_CAN_BOOT_PRODUCT; cfg.hardware_revision=1;
#if F28335_BOOT_LOADER
cfg.firmware_version=store.active<2?store.image[store.active].firmware_version:0;
#else
cfg.firmware_version=F28335_CAN_BOOT_FIRMWARE_VERSION;
#endif
head=tail=overflow=reset_requested=reset_session=ready=main_passes=0; tx_busy=tx_canceling=0;
confirmed=healthy && running<2 && store.active==running && store.pending==F28335_AB_NONE;
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;
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;
port.can_reboot=can_reboot; port.service_info=info;
if(!pcan_boot_init(&boot,&cfg,&port,0)) return;
EALLOW;
/* Меняем только MBOX3/4/6: маска 88=8+16+64. MBOX0..2 — старый обмен,
* MBOX5 — heartbeat. Приём broadcast выделен в отдельный точный mailbox,
* чтобы слишком широкая маска Boot не перехватывала сообщения приложения.
* 72=8+64 — оба RX, 16 — TX. Регистры флагов очищаются записью единицы. */
ECanaRegs.CANME.all &= ~88UL;
ECanaMboxes.MBOX3.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,cfg.device_type,cfg.device_id,0,0);
ECanaLAMRegs.LAM3.all=0x100FFFFFUL;
ECanaMboxes.MBOX3.MSGCTRL.all=8;
/* MBOX5 belongs to the application's heartbeat. MBOX6 is discovery only. */
ECanaMboxes.MBOX6.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,7,15,13,0);
ECanaLAMRegs.LAM6.all=0x10000000UL; /* Ignore priority only. */
ECanaMboxes.MBOX6.MSGCTRL.all=0;
ECanaRegs.CANMD.all=(ECanaRegs.CANMD.all|72UL)&~16UL;
ECanaRegs.CANRMP.all=72UL;
ECanaRegs.CANMIM.all |=72UL;
ECanaRegs.CANME.all |=88UL;
EDIS;
}
/* ISR только проверяет заголовок и копирует кадр. Чужие адреса и RTR
* отбрасываются до очереди. Глобальный адрес допускается только для discovery;
* DLC и смысл команды окончательно проверит ядро в основном цикле. */
static void receive_box(volatile struct MBOX *box,uint32_t mask)
{
unsigned int next=(head+1U)&31U, i;
unsigned long lo, hi;
uint32_t id; unsigned int type;
if(!(ECanaRegs.CANRMP.all&mask)) return;
if(box->MSGCTRL.bit.RTR) { ECanaRegs.CANRMP.all=mask; return; }
id=box->MSGID.all&0x1FFFFFFFUL;
type=pcan_boot_id_msg_type(id);
if(!(box->MSGID.all&0x80000000UL) || pcan_boot_id_route(id)!=0 ||
(type!=9 && type!=10 && type!=11 && type!=13) ||
!((pcan_boot_id_device_type(id)==F28335_CAN_BOOT_DEVICE_TYPE &&
pcan_boot_id_device_id(id)==F28335_CAN_BOOT_DEVICE_ID) ||
(type==13 && pcan_boot_id_device_type(id)==7 && pcan_boot_id_device_id(id)==15 &&
pcan_boot_id_msg_body(id)==0))) { ECanaRegs.CANRMP.all=mask; return; }
if(next==tail) overflow=1;
else {
queue[head].id=box->MSGID.all&0x1FFFFFFFUL;
queue[head].dlc=box->MSGCTRL.bit.DLC;
lo=box->MDL.all; hi=box->MDH.all;
for(i=0;i<4;++i) { queue[head].data[i]=(lo>>(8U*i))&255U;
queue[head].data[i+4]=(hi>>(8U*i))&255U; }
head=next;
}
ECanaRegs.CANRMP.all=mask;
}
void F28335CanBoot_RxISR(void)
{
receive_box(&ECanaMboxes.MBOX3,8UL);
receive_box(&ECanaMboxes.MBOX6,64UL);
}
/* Вызывать после инициализации параметров, датчиков и прикладного режима.
* От этого момента отсчитывается интервал наблюдения перед авто-CONFIRM. */
void F28335CanBoot_Ready(void)
{ ready=1; ready_at=F28335Boot_Now(); }
/* Используется таймерными ISR приложения. Во время пробы кормление watchdog
* должно доказывать выполнение main: работа одного ISR не доказывает готовность.
* Старый старт без handoff сохраняет прежнюю политику watchdog. */
unsigned int F28335CanBoot_WatchdogAllowed(void)
{ return F28335Boot_RunningSlot()>1 || confirmed; }
/* Неблокирующий диспетчер относительно ожидания CAN: вызывать в каждом проходе main.
* Сами callbacks проверки/записи EEPROM синхронные. Порядок здесь важен:
* проба приложения -> завершение TX -> reset -> poll -> ошибки RX -> один кадр. */
void F28335CanBoot_Service(void)
{
uint32_t now=F28335Boot_Now();
if(!F28335_BOOT_LOADER && ready && !confirmed && healthy && F28335Boot_RunningSlot()<2) {
/* A trial must keep executing main, not merely a timer ISR. */
/* После 100 проходов main и 5 секунд пробуем сохранить CONFIRM.
* Если до 10 секунд сохранение не удалось, main перестаёт кормить watchdog,
* что позволяет следующему старту откатиться к подтверждённому образу. */
if((uint32_t)(now-ready_at)<10000UL) ServiceDog();
if(main_passes<100) ++main_passes;
if(main_passes==100 && (uint32_t)(now-ready_at)>=5000UL) (void)confirm(0);
}
if(tx_busy) {
/* После CANTRR ждём завершения отмены или запоздавшего ACK, прежде чем
* повторно использовать mailbox. reset_requested к этому моменту уже снят,
* поэтому позднее подтверждение не должно перезапустить устройство. */
if(tx_canceling) {
if(!((ECanaRegs.CANAA.all|ECanaRegs.CANTA.all)&16UL)) return;
ECanaRegs.CANAA.all=16UL; ECanaRegs.CANTA.all=16UL;
tx_busy=tx_canceling=0;
} else if((uint32_t)(now-sent_at)>250UL) {
/* Cancel reset immediately, even if a late CAN ACK races CANTRR. */
reset_requested=0; pcan_boot_abort(&boot); tail=head;
tx_canceling=1; ECanaRegs.CANTRR.all=16UL; return;
} else if(ECanaRegs.CANTA.all&16UL) {
ECanaRegs.CANTA.all=16UL; tx_busy=0;
} else return;
}
if(reset_requested) { F28335Boot_Reset(reset_session); return; }
pcan_boot_poll(&boot,now);
if(tx_busy) return;
/* После потери кадра нельзя молча продолжать запись: сбрасываем RAM-сессию
* и накопленную очередь. Подтверждённый образ и опубликованный pending сохраняются. */
if(overflow || (ECanaRegs.CANRML.all&72UL)) {
overflow=0; ECanaRegs.CANRML.all=72UL;
pcan_boot_abort(&boot); tail=head; return;
}
if(tail!=head) {
pcan_boot_process(&boot,queue[tail].id,queue[tail].data,queue[tail].dlc);
tail=(tail+1U)&31U;
}
}
/* Используем session из NOINIT только для нового ENTER_BOOT после reset.
* Это отправка отложенного ACK готовности, а не восстановление принятых блоков
* или CRC незавершённой передачи: данные сессии через reset не сохраняются. */
void F28335CanBoot_Resume(unsigned int session)
{
if(F28335_BOOT_LOADER && session && session<256)
(void)pcan_boot_process(&boot,pcan_boot_make_id(1,0,
F28335_CAN_BOOT_DEVICE_TYPE,F28335_CAN_BOOT_DEVICE_ID,9,(session<<8)|2),0,0);
}
#else
void F28335CanBoot_EarlyInit(void) {}
void F28335CanBoot_Init(void) {}
void F28335CanBoot_RxISR(void) {}
void F28335CanBoot_Service(void) {}
void F28335CanBoot_Ready(void) {}
unsigned int F28335CanBoot_WatchdogAllowed(void) { return 1; }
void F28335CanBoot_Resume(unsigned int session) { (void)session; }
#endif

View File

@@ -0,0 +1,28 @@
#ifndef F28335_CAN_BOOT_H
#define F28335_CAN_BOOT_H
#include "../../include/pcan_boot.h"
/* Приложение задаёт версию из firmware_version.h до подключения этого заголовка.
* Формат поля FW: major[31:24], minor[23:16], patch[15:0]. Ноль — запасное
* значение при отсутствии версии сборки, а не прошитая здесь версия устройства. */
#ifndef F28335_CAN_BOOT_FIRMWARE_VERSION
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0UL /* Board supplies the build identity. */
#endif
/* Адрес ProtoCAN состоит из Device Type (3 бита) и Device ID (4 бита).
* ProductType — отдельный код совместимости прошивки; это не CAN ID и не RS-адрес. */
#define F28335_CAN_BOOT_DEVICE_TYPE 3U
#define F28335_CAN_BOOT_DEVICE_ID 7U
#define F28335_CAN_BOOT_PRODUCT 0x2833U
#define F28335_CAN_BOOT_DEVICE_NAME "F28335-M95M02"
/* Порядок интеграции приложения:
* StartupWatchdog/EarlyInit сразу после InitSysCtrl; Init после настройки
* таймера и EEPROM; Ready после старта прикладных подсистем; Service в main.
* RxISR вызывается из CAN ISR, WatchdogAllowed проверяется перед кормлением
* watchdog таймерным ISR. Resume применяется только отдельным загрузчиком. */
void F28335CanBoot_Init(void);
void F28335CanBoot_EarlyInit(void);
void F28335CanBoot_RxISR(void);
void F28335CanBoot_Service(void);
void F28335CanBoot_Ready(void);
void F28335CanBoot_Resume(unsigned int session);
unsigned int F28335CanBoot_WatchdogAllowed(void);
#endif

View File

@@ -1,21 +1,16 @@
/*
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами.
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
* слотов предоставляет платформа.
*/
#include "pcan_boot.h"
/* ProtoCAN update state machine; platform owns storage and reset. */
#include "../include/pcan_boot.h"
#include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
static uint16_t get_u16(const uint8_t *p)
static uint16_t get_u16(const pcan_boot_byte_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t get_u32(const uint8_t *p)
static uint32_t get_u32(const pcan_boot_byte_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
@@ -23,26 +18,26 @@ static uint32_t get_u32(const uint8_t *p)
| ((uint32_t)p[3] << 24);
}
static void put_u16(uint8_t *p, uint16_t value)
static void put_u16(pcan_boot_byte_t *p, uint16_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
}
static void put_u32(uint8_t *p, uint32_t value)
static void put_u32(pcan_boot_byte_t *p, uint32_t value)
{
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU);
p[3] = (pcan_boot_byte_t)(value >> 24);
}
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length)
{
size_t i;
crc = ~crc;
for (i = 0U; i < length; ++i) {
uint8_t bit;
pcan_boot_byte_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
@@ -51,9 +46,9 @@ static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
return ~crc;
}
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body)
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body)
{
return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27)
@@ -63,14 +58,14 @@ uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
| msg_body) & PCAN_BOOT_ID_MASK;
}
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); }
uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); }
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); }
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); }
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 20) & 15U); }
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 16) & 15U); }
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc)
static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type,
@@ -79,9 +74,12 @@ static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
return boot->port.send(boot->port_user, id, data, dlc);
}
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
/* STATUS всегда содержит состояние передачи: status, target_slot, next_block, CRC32.
* CRC здесь относится к уже принятому потоку, а не заменяет проверку EEPROM.
* Номер следующего блока помогает клиенту продолжить обмен после потери ACK. */
static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_byte_t status)
{
uint8_t data[8];
pcan_boot_byte_t data[8];
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status;
data[1] = boot->target_slot;
@@ -91,9 +89,11 @@ static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
}
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
static bool send_identity(pcan_boot_t *boot)
{
uint8_t data[8];
pcan_boot_byte_t data[8];
put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION;
@@ -101,6 +101,9 @@ static bool send_identity(pcan_boot_t *boot)
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
}
/* Отменяется только состояние протокола в RAM: manifest, счётчики и session.
* Функция не стирает EEPROM и не снимает ранее опубликованный pending.
* Ответственность за постоянное состояние слотов остаётся у платформы. */
void pcan_boot_abort(pcan_boot_t *boot)
{
if (boot == NULL) {
@@ -118,19 +121,24 @@ void pcan_boot_abort(pcan_boot_t *boot)
boot->have_compat = false;
}
/* Конфигурация копируется в экземпляр; указатели callbacks остаются привязаны
* к port_user. Значения slot_count=0 и max_image_size=0 сохраняют исторические
* настройки по умолчанию. Для пустого A/B-хранилища допустим active_slot=NONE. */
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user)
{
uint8_t slot_count;
pcan_boot_byte_t slot_count;
if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U)
|| (config->active_slot > 1U)
|| ((config->active_slot > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE))
|| (port->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) {
return false;
}
slot_count = config->slot_count == 0U ? 2U : config->slot_count;
if ((slot_count > 2U) || (config->active_slot >= slot_count)
if ((slot_count > 2U) || ((config->active_slot >= slot_count) &&
(config->active_slot != PCAN_BOOT_SLOT_NONE || slot_count != 2U))
|| config->max_image_size > PCAN_BOOT_SLOT_SIZE
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false;
}
@@ -149,7 +157,10 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
return true;
}
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
/* BEGIN_IMAGE и BEGIN_COMPAT приходят отдельными кадрами. До получения обоих
* не разрешаем ERASE. Проверяем размер, Product, HW и необязательную политику
* authorize. В A/B выбираем противоположный active слот; для первого образа — A. */
static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
{
if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA;
@@ -160,8 +171,11 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if ((boot->manifest.product_type != boot->config.product_type)
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min)
if (boot->manifest.product_type != boot->config.product_type) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_DEVICE);
}
if ((boot->config.hardware_revision < boot->manifest.hardware_revision_min)
|| (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE);
@@ -171,19 +185,51 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->target_slot = boot->config.slot_count == 1U
boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U :
boot->config.slot_count == 1U
? boot->config.active_slot
: (uint8_t)(boot->config.active_slot ^ 1U);
: (pcan_boot_byte_t)(boot->config.active_slot ^ 1U);
boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
/* Body состоит из session в старшем октете и command в младшем.
* Сначала проверяем DLC, чтобы обработчики не читали отсутствующие данные.
* Идентификация и паспорт не открывают сессию; CONFIRM обрабатывается отдельно,
* поскольку приложение не должно входить в загрузчик ради подтверждения себя.
* Остальные команды изменяют RAM-автомат только в допустимой сессии/состоянии. */
static bool process_control(pcan_boot_t *boot, uint16_t body,
const uint8_t *data, uint8_t dlc)
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t session = (uint8_t)(body >> 8);
uint8_t command = (uint8_t)body;
pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
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)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (command == PCAN_BOOT_CMD_PING) {
pcan_boot_byte_t reply[8] = {0};
reply[0] = PCAN_BOOT_VERSION; reply[1] = boot->state;
reply[2] = boot->config.active_slot; reply[3] = boot->target_slot;
reply[4] = boot->session_id; reply[5] = boot->config.slot_count;
reply[6] = boot->last_status;
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, command, reply, 8);
}
if (command >= PCAN_BOOT_CMD_CAPABILITIES && command <= PCAN_BOOT_CMD_MEMORY_INFO) {
pcan_boot_byte_t reply[8] = {0};
pcan_boot_byte_t page = dlc ? data[0] : 0;
if (!boot->port.service_info || !boot->port.service_info(boot->port_user,command,page,reply))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_COMMAND);
return send_frame(boot,PCAN_BOOT_MSG_DISCOVERY,(uint16_t)((page<<8)|command),reply,8);
}
if (command == PCAN_BOOT_CMD_CONFIRM) {
pcan_boot_byte_t saved=boot->session_id; bool sent;
boot->session_id=session;
sent=send_status(boot,command,session && boot->port.confirm_running_slot &&
boot->port.confirm_running_slot(boot->port_user) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_INVALID_STATE);
boot->session_id=saved; return sent;
}
if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot);
}
@@ -191,8 +237,18 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
if (boot->state != PCAN_BOOT_STATE_IDLE && boot->session_id != session)
return send_status(boot,command,PCAN_BOOT_STATUS_BUSY);
if (boot->port.enter_boot) {
unsigned int result=boot->port.enter_boot(boot->port_user,session);
if(result==1) return true;
if(result!=0) return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
}
if (boot->session_id==session && boot->state!=PCAN_BOOT_STATE_IDLE)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
pcan_boot_abort(boot);
boot->session_id = session;
boot->last_activity_ms=boot->now_ms;
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
@@ -200,8 +256,11 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
boot->last_activity_ms=boot->now_ms;
switch (command) {
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);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -211,6 +270,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.product_type==get_u16(data) &&
boot->manifest.hardware_revision_min==data[2] && boot->manifest.hardware_revision_max==data[3] &&
boot->manifest.firmware_version==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -222,6 +284,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE:
if(boot->state==PCAN_BOOT_STATE_RECEIVING && boot->bytes_received==0)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -236,6 +300,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY:
if(boot->state==PCAN_BOOT_STATE_VERIFIED || boot->state==PCAN_BOOT_STATE_COMMITTED)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
@@ -244,7 +310,13 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR);
}
if ((boot->port.verify_image != NULL)
if (boot->port.verify_status) {
pcan_boot_status_t status=boot->port.verify_status(boot->port_user,boot->target_slot,&boot->manifest);
if(status!=PCAN_BOOT_STATUS_OK) {
boot->state=PCAN_BOOT_STATE_FAILED; return send_status(boot,command,status);
}
}
if (!boot->port.verify_status && (boot->port.verify_image != NULL)
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
@@ -254,21 +326,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT:
if(boot->state==PCAN_BOOT_STATE_COMMITTED) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((boot->config.slot_count == 2U)
if ((boot->port.set_pending_slot != NULL)
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_CONFIRM:
if ((boot->port.confirm_running_slot == NULL)
|| !boot->port.confirm_running_slot(boot->port_user)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->state=PCAN_BOOT_STATE_COMMITTED;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS:
@@ -280,7 +347,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT:
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
if(!boot->port.reboot || (boot->port.can_reboot && !boot->port.can_reboot(boot->port_user)))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
if(!send_status(boot,command,PCAN_BOOT_STATUS_OK)) return false;
if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user);
}
@@ -291,11 +360,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
}
}
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
const uint8_t *data, uint8_t dlc)
/* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому
* кадры данных принимаются только в состоянии RECEIVING и для выбранного A/B.
* При неверном номере ничего не записываем и возвращаем ожидаемый next_block.
* Последний кадр дополняется клиентом до 8 октетов, но запись и CRC учитывают
* только оставшиеся октеты image_size. ACK выдаётся на границе окна или в конце. */
static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t block,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t expected_type;
uint8_t write_length;
pcan_boot_byte_t expected_type;
pcan_boot_byte_t write_length;
uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
@@ -309,8 +383,9 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR);
}
boot->last_activity_ms=boot->now_ms;
remaining = boot->manifest.image_size - boot->bytes_received;
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining
: PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
@@ -329,10 +404,14 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
return true;
}
/* Общая точка входа для одного кадра после аппаратной фильтрации.
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const uint8_t *data, uint8_t dlc)
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{
uint8_t msg_type;
pcan_boot_byte_t msg_type;
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false;
}
@@ -340,6 +419,17 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false;
}
if(msg_type==PCAN_BOOT_MSG_DISCOVERY && pcan_boot_id_route(can_id)==0 &&
pcan_boot_id_msg_body(can_id)==0 && dlc==0 &&
((pcan_boot_id_device_type(can_id)==7 && pcan_boot_id_device_id(can_id)==15) ||
(pcan_boot_id_device_type(can_id)==boot->config.device_type &&
pcan_boot_id_device_id(can_id)==boot->config.device_id))) {
if(!boot->discovery_pending) {
boot->discovery_at=boot->now_ms+2U*(1U+16U*boot->config.device_type+boot->config.device_id);
boot->discovery_pending=true;
}
return true;
}
if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM)
|| (pcan_boot_id_device_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) {
@@ -353,3 +443,18 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
}
return false;
}
/* Обслуживание времени не требует входящего кадра. Регулярно вызывать из main
* с монотонным счётчиком миллисекунд: иначе не истечёт сессия и не выйдет discovery.
* При занятом TX discovery остаётся pending до успешного send_identity.
* Вычитание unsigned учитывает переход счётчика через 0xFFFFFFFF. */
void pcan_boot_poll(pcan_boot_t *boot,uint32_t now_ms)
{
if(!boot) return;
boot->now_ms=now_ms;
if(boot->config.session_timeout_ms && boot->state!=PCAN_BOOT_STATE_IDLE &&
(uint32_t)(now_ms-boot->last_activity_ms)>=boot->config.session_timeout_ms)
pcan_boot_abort(boot);
if(boot->discovery_pending && (uint32_t)(now_ms-boot->discovery_at)<0x80000000UL && send_identity(boot))
boot->discovery_pending=false;
}

View File

@@ -0,0 +1,13 @@
#ifndef TEST_DSP_H
#define TEST_DSP_H
#include <stdint.h>
#define EALLOW
#define EDIS
struct test_reg { uint32_t all; struct { unsigned DLC, RTR; } bit; };
struct MBOX { struct test_reg MSGID, MSGCTRL, MDL, MDH; };
static struct { struct MBOX MBOX3, MBOX4, MBOX6; } ECanaMboxes;
static struct { struct test_reg CANME,CANMD,CANRMP,CANRML,CANMIM,CANTA,CANAA,CANTRS,CANTRR; } ECanaRegs;
static struct { struct test_reg LAM3,LAM6; } ECanaLAMRegs;
static struct { unsigned long InterruptCount; } CpuTimer1;
static struct { unsigned WDCR; } SysCtrlRegs;
#endif

View File

@@ -0,0 +1,7 @@
#include <stdint.h>
#ifndef EEPROM_M95M02
#define EEPROM_M95M02 1
#endif
#define BALSAM167_EEPROM_FIRMWARE_BYTES 261884UL
void Seeprom_write(unsigned long word, unsigned int *data, unsigned long bytes);
void Seeprom_read(unsigned long word, unsigned int *data, unsigned long bytes);

View File

@@ -0,0 +1,27 @@
"""Host regression suite. CC may point to clang/gcc; this does not replace a TI build."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
cc = os.environ.get('CC') or shutil.which('clang') or shutil.which('gcc')
if not cc:
raise SystemExit('Set CC to a C99 host compiler')
store = 'ports/tms320f28335/f28335_boot_store.c'
cases = [
('core', [], ['src/pcan_boot.c', 'tests/test_pcan_boot.c']),
('store_native', [], [store, 'tests/test_f28335_boot_store.c']),
('store_wide', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'tests/test_f28335_boot_store.c']),
('loader', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
('application', ['-DPCAN_BOOT_WIDE_BYTES', '-DF28335_BOOT_LOADER=0'],
[store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
]
with tempfile.TemporaryDirectory(prefix='f28335-ab-') as tmp:
for name, defines, sources in cases:
exe = Path(tmp) / (name + '.exe')
subprocess.run([cc, '-std=c99', '-Wall', '-Wextra', '-Werror', '-Iinclude',
'-Itests/f28335_stubs', *defines, *sources, '-o', str(exe)], cwd=root, check=True)
sample = os.environ.get('F28335_SAMPLE_IMAGE') if name == 'store_wide' else None
subprocess.run([str(exe), *([sample] if sample else [])], check=True, timeout=30)

View File

@@ -0,0 +1,118 @@
#define _CRT_SECURE_NO_WARNINGS
#include "../ports/tms320f28335/f28335_boot_store.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static unsigned char eeprom[0x40000],backup[0x40000];
static long budget=-1;
static int read_fail;
static unsigned written_words;
static bool read_mem(void *u,uint32_t addr,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=sizeof(eeprom)); if(read_fail) return false;
for(i=0;i<n;++i) p[i]=eeprom[addr+i]; return true; }
static bool write_mem(void *u,uint32_t addr,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=0x3FE00);
for(i=0;i<n;++i) { if(budget==0) return false; if(budget>0) --budget; eeprom[addr+i]=(unsigned char)p[i]; }
return true; }
static f28335_boot_io_t io={read_mem,write_mem,0};
static pcan_boot_byte_t image[40];
static pcan_boot_manifest_t manifest;
static void make_image(void)
{
memset(image,0,sizeof(image)); image[0]=0xAA; image[1]=8;
image[20]=0; image[21]=0x80; /* entry=8000, high-word first */
image[22]=5; image[26]=0; image[27]=0x80;
image[28]=0x34; image[29]=0x12;
memset(&manifest,0,sizeof(manifest)); manifest.product_type=0x2833;
manifest.hardware_revision_min=manifest.hardware_revision_max=1;
manifest.image_size=40; manifest.image_crc32=f28335_boot_crc(0,image,40);
manifest.firmware_version=0x01010002;
}
static void image_to(f28335_boot_store_t *s,unsigned slot)
{ unsigned i; assert(f28335_boot_prepare(s,slot));
for(i=0;i<40;i+=8) assert(f28335_boot_write(s,slot,i,image+i,8)); }
static bool ram(void *u,uint32_t address,uint16_t word)
{ (void)u; assert(address==0x8000+written_words); if(!written_words) assert(word==0x1234);
++written_words; return true; }
static void reopen(f28335_boot_store_t *s) { assert(f28335_boot_open(s,&io)); }
int main(int argc,char **argv)
{
f28335_boot_store_t s; unsigned slot; uint32_t entry; long cut;
memset(eeprom,0xA5,sizeof(eeprom)); make_image(); reopen(&s);
assert(s.active==255 && !s.valid); assert(f28335_boot_select(&s,&slot) && slot==255);
image_to(&s,0); assert(f28335_boot_verify(&s,0,&manifest,&entry) && entry==0x8000);
assert(f28335_boot_commit(&s,0,&manifest));
assert(!f28335_boot_confirm(&s,0)); /* Cannot confirm an image not yet started. */
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.attempted);
assert(f28335_boot_load(&s,slot,ram,0,&entry) && written_words==5);
assert(!f28335_boot_confirm(&s,1)); assert(f28335_boot_confirm(&s,0));
assert(f28335_boot_confirm(&s,0)); /* idempotent */
assert(!f28335_boot_prepare(&s,0));
assert(!f28335_boot_write(&s,0,0,image,8));
assert(!f28335_boot_write(&s,1,F28335_AB_SLOT_BYTES,image,8));
image_to(&s,1); memcpy(backup,eeprom,sizeof(eeprom));
/* Every torn write position in journal publication keeps confirmed A. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_commit(&s,1,&manifest); budget=-1; reopen(&s);
assert(s.active==0); assert(f28335_boot_verify(&s,0,&s.image[0],0));
assert(s.pending==255 || s.pending==1);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_commit(&s,1,&manifest));
memcpy(backup,eeprom,sizeof(eeprom));
/* Torn trial bookkeeping must never select an unrecorded trial. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
if(f28335_boot_select(&s,&slot)) { assert(slot==1); assert(s.attempted); }
budget=-1; reopen(&s); assert(s.active==0);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s);
assert(f28335_boot_select(&s,&slot) && slot==1);
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.pending==255);
image_to(&s,1); assert(f28335_boot_commit(&s,1,&manifest));
assert(f28335_boot_select(&s,&slot) && slot==1);
memcpy(backup,eeprom,sizeof(eeprom));
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_confirm(&s,1); budget=-1; reopen(&s);
assert(s.active==0 || s.active==1);
assert(f28335_boot_select(&s,&slot) && (slot==0 || slot==1));
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_confirm(&s,1));
/* CRC correct but malicious SCI8 destination and entry are rejected. */
image_to(&s,0); image[26]=0; image[27]=0xE0;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
image[27]=0x80; image[21]=0x90; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
make_image(); image[22]=1; image[30]=1; image[35]=0x80;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* overlapping sections */
make_image(); image_to(&s,0); manifest.image_size=38;
manifest.image_crc32=f28335_boot_crc(0,image,38);
assert(!f28335_boot_verify(&s,0,&manifest,0)); /* missing terminator */
make_image(); image[22]=4; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* trailing octets */
make_image(); image_to(&s,0); eeprom[F28335_AB_SLOT_A+29]^=1;
assert(!f28335_boot_verify(&s,0,&manifest,0));
/* Failed EEPROM reads fail closed, including subsequent writes. */
read_fail=1; assert(!f28335_boot_open(&s,&io));
assert(!f28335_boot_prepare(&s,0)); assert(!f28335_boot_write(&s,0,0,image,8)); read_fail=0;
for(entry=0x3FE00;entry<0x40000;++entry) assert(eeprom[entry]==0xA5);
if(argc>1) {
FILE *file=fopen(argv[1],"rb"); uint32_t offset,crc=0;
pcan_boot_byte_t bytes[32]; unsigned n;
assert(file); make_image();
manifest.image_size=(uint32_t)fread(eeprom+F28335_AB_SLOT_A,1,F28335_AB_SLOT_BYTES,file);
assert(!ferror(file) && fgetc(file)==EOF); fclose(file); reopen(&s);
for(offset=0;offset<manifest.image_size;offset+=n) {
n=manifest.image_size-offset<32?(unsigned)(manifest.image_size-offset):32;
assert(read_mem(0,F28335_AB_SLOT_A+offset,bytes,n)); crc=f28335_boot_crc(crc,bytes,n);
}
manifest.image_crc32=crc;
assert(f28335_boot_verify(&s,0,&manifest,&entry));
puts("Production SCI8 binary: CRC, destinations and entry accepted");
}
puts("F28335 A/B: journal power cuts, trial, rollback, confirm, bounds, SCI8: OK");
return 0;
}

View File

@@ -0,0 +1,94 @@
/* Production CAN adapter, fake hardware, wide C28x wire octets. */
#define EEPROM_M95M02 1
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 1
#endif
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0x12345678UL
#include <assert.h>
#include <stdio.h>
#include <string.h>
void ServiceDog(void);
#include "../ports/tms320f28335/f28335_can_boot.c"
static unsigned char memory[0x40000];
static unsigned int running=255,resets,reset_arg,dog_feeds;
static int fail_write;
void ServiceDog(void) { ++dog_feeds; }
bool F28335Boot_Read(void *u,uint32_t a,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=sizeof(memory)); for(i=0;i<n;++i)p[i]=memory[a+i]; return true; }
bool F28335Boot_Write(void *u,uint32_t a,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=0x3FE00); if(fail_write)return false;
for(i=0;i<n;++i)memory[a+i]=(unsigned char)p[i]; return true; }
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
unsigned int F28335Boot_RunningSlot(void) { return running; }
void F28335Boot_Reset(unsigned int session) { ++resets;reset_arg=session;reset_requested=0; }
static void command(unsigned cmd,const pcan_boot_byte_t *d,unsigned n)
{ tx_busy=0; assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,7,9,0x100|cmd),d,n)); }
static void put32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned i;for(i=0;i<4;++i)p[i]=(v>>(i*8))&255; }
int main(void)
{
pcan_boot_byte_t image[40]={0xAA,8},meta[8],compat[8]={0x33,0x28,1,1,2,0,1,1};
unsigned i,slot; pcan_boot_manifest_t m;
memset(memory,0xA5,sizeof(memory)); image[21]=0x80; image[22]=5; image[27]=0x80;
F28335CanBoot_Init(); assert(boot.config.max_image_size==130560 && boot.config.slot_count==2);
if(!F28335_BOOT_LOADER) {
/* SPI-ROM application without Flash-loader handoff cannot erase anything. */
command(2,0,0); assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE && !reset_requested);
memset(&m,0,sizeof(m));m.image_size=40;m.image_crc32=f28335_boot_crc(0,image,40);
m.product_type=0x2833;m.hardware_revision_min=m.hardware_revision_max=1;
assert(f28335_boot_prepare(&store,0));
for(i=0;i<40;i+=8)assert(f28335_boot_write(&store,0,i,image+i,8));
assert(f28335_boot_commit(&store,0,&m)); assert(f28335_boot_select(&store,&slot));
running=0; F28335CanBoot_Init();
assert(!F28335CanBoot_WatchdogAllowed());
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
F28335CanBoot_Ready();
for(i=0;i<100;++i) F28335CanBoot_Service();
assert(!confirmed && store.pending==0);
CpuTimer1.InterruptCount=5000; F28335CanBoot_Service();
assert(confirmed && store.active==0 && dog_feeds);
command(2,0,0); assert(reset_requested && reset_session==1 && boot.state==PCAN_BOOT_STATE_IDLE);
F28335CanBoot_Service();assert(resets==1 && reset_arg==1);
command(1,0,0);assert(ECanaMboxes.MBOX4.MDH.all==0x12345678UL);
} else {
command(2,0,0); put32(meta,40);put32(meta+4,f28335_boot_crc(0,image,40));
command(3,meta,8);command(4,compat,8);assert(boot.target_slot==0);
command(5,0,0);
for(i=0;i<5;++i) { tx_busy=0; assert(pcan_boot_process(&boot,
pcan_boot_make_id(1,0,3,7,10,i),image+i*8,8)); }
command(6,0,0);assert(boot.state==PCAN_BOOT_STATE_VERIFIED);
command(7,0,0);assert(store.pending==0 && boot.state==PCAN_BOOT_STATE_COMMITTED);
command(7,0,0);assert(boot.last_status==0);
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
command(9,0,0);assert(reset_requested);tx_busy=0;
F28335CanBoot_Service();assert(resets==1 && reset_arg==0);
command(10,0,0);command(2,0,0);command(3,meta,8);command(4,compat,8);
command(5,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_FLASH_ERROR); /* Pending is protected. */
}
command(12,0,0); assert((ECanaMboxes.MBOX4.MSGID.all&65535)==12);
command(13,0,0); assert(ECanaMboxes.MBOX4.MDH.all==130560);
meta[0]=0;command(14,meta,1); assert(ECanaMboxes.MBOX4.MDL.all==0x33383246UL); /* F283 */
meta[0]=3;command(15,meta,1);assert(ECanaMboxes.MBOX4.MDL.all==0x3FEFCUL);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,6,9,0x102),0,0));
tx_busy=0;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),0,0));
assert(boot.discovery_pending); pcan_boot_poll(&boot,boot.discovery_at);
assert(!boot.discovery_pending && (ECanaMboxes.MBOX4.MSGID.all&65535)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x102),0,0));
head=tail=0; ECanaRegs.CANRMP.all=8;
ECanaMboxes.MBOX3.MSGID.all=0x80000000UL|pcan_boot_make_id(1,0,3,7,9,12);
ECanaMboxes.MBOX3.MSGCTRL.bit.DLC=0;
F28335CanBoot_RxISR(); assert(head==1 && tail==0);
tx_busy=0; F28335CanBoot_Service(); assert(tail==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=1; F28335CanBoot_RxISR(); assert(head==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=0;
overflow=1;tx_busy=0;F28335CanBoot_Service();assert(boot.state==PCAN_BOOT_STATE_IDLE);
/* An ACK arriving after cancellation must never execute a queued reset. */
tx_busy=1;tx_canceling=0;reset_requested=1;sent_at=0;
CpuTimer1.InterruptCount=300;ECanaRegs.CANTA.all=0;ECanaRegs.CANAA.all=0;
F28335CanBoot_Service();assert(!reset_requested && tx_canceling);
ECanaRegs.CANTA.all=16; F28335CanBoot_Service();assert(!tx_busy && resets==1);
for(i=0x3FE00;i<0x40000;++i)assert(memory[i]==0xA5);
puts(F28335_BOOT_LOADER?"F28335 loader CAN services: OK":"F28335 application handoff/confirmation: OK");
return 0;
}

View File

@@ -264,7 +264,7 @@ static void test_rejects_wrong_session_and_compatibility(void)
data[3] = 3U;
put_u32(&data[4], 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE);
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_DEVICE);
assert(boot.state == PCAN_BOOT_STATE_FAILED);
}
@@ -306,8 +306,41 @@ static void test_single_slot_update(void)
assert(memcmp(fake.flash[0], image, sizeof(image)) == 0);
}
static void test_services_timeout_and_retries(void)
{
pcan_boot_t boot; fake_t fake;
uint8_t image[8]={0},compat[8]={0}; unsigned count;
setup(&boot,&fake); boot.config.session_timeout_ms=30000;
pcan_boot_poll(&boot,0xFFFFFF00UL);
assert(command(&boot,7,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(command(&boot,8,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(boot.session_id==7 && fake.last_data[0]==PCAN_BOOT_STATUS_BUSY);
assert(command(&boot,7,PCAN_BOOT_CMD_ERASE,image,1));
assert(fake.last_data[0]==PCAN_BOOT_STATUS_INVALID_SIZE);
pcan_boot_poll(&boot,29743); assert(boot.state!=PCAN_BOOT_STATE_IDLE);
pcan_boot_poll(&boot,29744); assert(boot.state==PCAN_BOOT_STATE_IDLE);
assert(command(&boot,0,PCAN_BOOT_CMD_PING,NULL,0));
assert(pcan_boot_id_msg_body(fake.last_id)==12);
count=fake.sends;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),NULL,0));
assert(fake.sends==count); pcan_boot_poll(&boot,boot.discovery_at);
assert(fake.sends==count+1 && pcan_boot_id_msg_body(fake.last_id)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x702),NULL,0));
begin_update(&boot,image,8);
put_u16(compat,boot.manifest.product_type);
compat[2]=boot.manifest.hardware_revision_min; compat[3]=boot.manifest.hardware_revision_max;
put_u32(compat+4,boot.manifest.firmware_version);
assert(command(&boot,7,4,compat,8)); assert(fake.last_data[0]==0);
assert(command(&boot,7,5,NULL,0)); count=fake.erases;
assert(command(&boot,7,5,NULL,0)); assert(fake.erases==count && fake.last_data[0]==0);
assert(pcan_boot_process(&boot,request_id(PCAN_BOOT_MSG_DATA_B,0),image,8));
assert(command(&boot,7,6,NULL,0)); assert(command(&boot,7,6,NULL,0)); assert(fake.last_data[0]==0);
assert(command(&boot,7,7,NULL,0)); assert(command(&boot,7,7,NULL,0)); assert(fake.last_data[0]==0);
}
int main(void)
{
test_services_timeout_and_retries();
test_id_layout();
test_crc32_reference();
test_complete_update();

View File

@@ -6,6 +6,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 и других оболочек.

View File

@@ -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,

View File

@@ -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).

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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);

View File

@@ -40,7 +40,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
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;

View File

@@ -18,7 +18,8 @@ unsigned ump_validate(unsigned fn, uint16_t a, uint16_t n, int can)
return can ? ((a == UMP_FIRMWARE_INFO && n == UMP_FIRMWARE_INFO_WORDS) ? 0 : 2)
: ((n && n <= UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS - a) ? 0 : 2);
if (can) return ((a == UMP_BASE && n == 16) ||
(a == UMP_DATA && (n == 40 || n == 80 || n == 120 || n == 42 || n == 84)) ||
/* Разрешает целые 12-словные записи v4 до 120 слов; размеры v2/v3 сохраняются для прежних устройств. */
(a == UMP_DATA && (n == 40 || n == 80 || n == 42 || n == 84 || (n && n <= 120 && !(n % 12U)))) ||
(a == UMP_LIVE && n == 124)) ? 0 : 2;
if (!n || n > 125) return 2;
return ((a >= UMP_BASE && a < UMP_BASE+16 && n <= UMP_BASE+16-a) ||

View File

@@ -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]},

View File

@@ -7,6 +7,7 @@ Device Name — отображаемое имя адреса, а не допол
| DeviceType | DeviceID | Device Name | Назначение |
|---|---|---|---|
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
| `0x3` | `0x7` | **F28335-M95M02** | Загрузка прошивки по ProtoCAN в EEPROM M95M02 платы TMS320F28335; ProductType `0x2833` |
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков |

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -112,3 +112,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-снифер; генератор работает локально без передачи в оборудование.

View File

@@ -597,11 +597,11 @@ CRC32 обнаруживает случайное повреждение, но
<td class="x14">&nbsp;</td>
<td colspan="8" class="x96">64 битовых адресса</td>
</tr>
<tr style="height:17px">
<tr style="height:72px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td colspan="3" class="x131">0x3</td>
<td colspan="4" class="x136">0x0 - 0xF устройств</td>
<td colspan="4" class="x136">0x0 - 0xF устройств<br>0x7: F28335-M95M02<br>ProductType: 0x2833</td>
<td colspan="12" class="x138">0x3. Установка времени</td>
<td colspan="4" class="x140">&nbsp;</td>
<td class="x14">&nbsp;</td>

View File

@@ -24,6 +24,13 @@ Saleae SDK подключается при обращении к устройс
в приложении. Общая панель обработки графиков размещена в `qt_ports.plot_processing`:
[контракт, подключение нового графика и примеры](../c/set-protocol/docs/PLOT_PROCESSING.md).
Конструкторы CAN-адаптера, CAN DevBoard и RS485 DevBoard используют общий
компонент `set_devices.qt_ports.can_frame_builder.CanFrameFields`. Он задаёт
одинаковые поля и исходный режим «Обычный CAN — ID / DLC / Data», сохраняет
альтернативный режим SETCAN и сообщает об изменениях сигналом `changed`.
`frame_values()` возвращает проверенные ID, IDE, Data и необязательный Body;
упаковка конкретного транспорта и отправка остаются у приложения.
Пути к нативным библиотекам задаёт потребитель перед импортом:
`SETPROTOCOL_LIBRARY`, при необходимости `ALTERA_LOGIC_LIBRARY`, `CANDLE_LIBRARY`.
Порты не ищут DLL в каталогах SETGUI. C99-ядро собирается из `c/set-protocol`,

View File

@@ -0,0 +1,46 @@
"""Три автономных примера общей консоли templates без подключения к аппаратуре.""" # Примеры показывают служебный текст, CAN и двоичный RS485 на одном компоненте.
import argparse # Ключ --kind выбирает один пример для отдельного запуска.
import time # Время в модели кадра соответствует обычному источнику пакетов приложения.
from set_devices.can485_board import BoardFrame # Штатная модель строит RS485-пакет с настоящим CRC.
from set_devices.can_monitor import decode_protocol # CAN расшифровывается действующим декодером templates.
from set_devices.can_transport import build_frame # Транспортный кадр не собирается вручную в UI.
from set_devices.qt_ports.console import ConsoleView # Все примеры используют ту же консоль, что SETGUI.
try: # Выбираем доступную версию Qt без зависимости от приложения SETGUI.
from PySide6.QtWidgets import QApplication, QTabWidget # Вкладки позволяют сравнить три независимых журнала.
except ImportError: # Сохраняем совместимость примера с портом Qt 5.
from PySide2.QtWidgets import QApplication, QTabWidget # Интерфейс запуска одинаков для обеих версий Qt.
def create_examples(): # Фабрика доступна также тесту и генератору снимков интерфейса.
service = ConsoleView(limit=4000) # Первый журнал показывает служебные ответы и команды платы.
service.append_message(">>> V", direction="TX", color="#8fd18f") # Метка отправки демонстрирует независимый фильтр TX.
service.append_message("#OK driver started", direction="RX", color="#6fa8dc") # Ответ устройства показывает цвет и фильтр RX.
service.append_message("#ERR transmit failed (no ack / bus busy)", color="#ff7b7b") # Ошибка остаётся видна независимо от фильтров направления.
can = ConsoleView(limit=4000, payloads=True) # Второй пример добавляет переключение представления настоящих байтов CAN.
frame = build_frame(0x701, b"\x05", ide=False, rtr=False) # ID 0x701 и байт 05 задают heartbeat CANopen узла 1 в Operational.
decoded = decode_protocol(frame, "deif") # Подпись события берётся из штатного протокольного декодера.
can.append_message("RX CAN 0x701 · " + decoded.summary, direction="RX", payload=b"\x05") # Исходный payload позволяет сравнить режимы HEX и ASCII.
rs485 = ConsoleView(limit=4000, payloads=True) # Третий пример показывает транспортный пакет платы отдельно от прикладного CAN.
packet = BoardFrame(time.time(), False, 0x701, False, 1, b"\x05", "RS485").rs485_packet() # Пакет AA55 и CRC8 формируются кодеком templates.
rs485.append_message("RX RS485 · пакет CAN485", direction="RX", payload=packet) # Консоль отображает байты, не повторяя алгоритм разбора пакета.
for console in (can, rs485): # Двоичные примеры сразу демонстрируют одновременно байты и печатное представление.
console.display.setCurrentText("HEX + ASCII") # Пользователь может вернуться к исходной подписи или выбрать один формат.
return (("Служебная консоль", service), ("CAN", can), ("RS485", rs485)) # Каждый журнал владеет своей историей и настройками просмотра.
def main(): # Точка входа запускается обычным Python с установленным Qt-портом templates.
parser = argparse.ArgumentParser(description=__doc__) # Справка объясняет назначение примеров без отдельного приложения.
parser.add_argument("--kind", choices=("all", "service", "can", "rs485"), default="all") # Один пример каждого источника доступен отдельной командой.
args = parser.parse_args() # Пользователь выбирает источник до создания окна.
app = QApplication.instance() or QApplication([]) # Одно приложение Qt обслуживает все примеры.
window = QTabWidget() # Общий просмотр не связывает состояния независимых консолей.
for key, (title, console) in zip(("service", "can", "rs485"), create_examples()): # Стабильные ключи соответствуют значениям --kind.
if args.kind in ("all", key): window.addTab(console.panel, title) # Выбранные примеры добавляются с общей панелью инструментов.
window.setWindowTitle("Примеры консоли templates") # Заголовок явно отличает демонстрацию от подключения к плате.
window.resize(1100, 480) # Начальный размер в логических пикселях позволяет видеть все инструменты.
window.show() # Окно появляется только при прямом запуске примера.
return app.exec() # Цикл Qt обрабатывает поиск, паузу, экспорт и изменение масштаба.
if __name__ == "__main__": # Импорт фабрики в тестах не должен открывать окно или запускать цикл событий.
raise SystemExit(main()) # Код завершения Qt возвращается вызывающему процессу.

View File

@@ -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,

View File

@@ -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-формата обоими существующими уровнями.

View File

@@ -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.

View File

@@ -15,6 +15,19 @@ DATA_OFFSET = 0x10
NODE_COUNT = 13
PULT_COMMAND_ID = 0x00745009
PULT_DATA_ID = 0x00745019
HEARTBEAT_ADDRESS = 0x0100 # CAN_HEARTBEAT_ADDR в ecan.c периферии 167 начинает три служебных слова вне технологической карты R0..R127.
HEARTBEAT_NAMES = { # Назначения слов соответствуют hiword и loword ветки CAN_send для CAN_HEARTBEAT_ADDR.
HEARTBEAT_ADDRESS: "Heartbeat: счётчик, с", # Первое слово содержит секунды после запуска по модулю 65536, а не знаковое измерение.
HEARTBEAT_ADDRESS + 1: "Heartbeat: Mode", # Второе слово передаёт аппаратный номер Mode платы без преобразования CAN-индекса.
HEARTBEAT_ADDRESS + 2: "Heartbeat: тип платы (Desk)", # Третье слово содержит код Desk из прошивки, без подмены его именем CAN-узла.
} # Все три адреса общие для поддерживаемых узлов периферии 167.
COMMON_REGISTER_NAMES = { # Общие назначения подтверждены package.h; единицы времени зависят от версии планировщика CAN.
0x60: "Настройка быстрого потока CAN", # Cancount[m_FAST]: в новом планировщике ноль отключает поток, ненулевое значение включает его.
0x61: "Настройка медленного потока CAN", # Cancount[m_SLOW]: исторический период нельзя переводить в миллисекунды без версии прошивки.
0x62: "Яркость индикации", # Brightness в package.h содержит исходный уровень яркости, а не подтверждённый процент.
0x7E: "Последний режим", # LastMode хранится в предпоследнем слове технологической карты R0–R127.
0x7F: "Команды", # Commands — битовая маска, которую дополнительно расшифровывает command_description.
} # Таблица доступна с любой версией DLL и при использовании резервного Python-декодера.
@dataclass(frozen=True)
@@ -38,10 +51,21 @@ class BalsamFrame:
present_mask: int
registers: tuple[BalsamRegister, ...]
@property
def route(self) -> str:
return ("%s → ПМ" % self.device_name if self.from_device
else "ПМ → %s" % self.device_name)
@property # Признак доступен адаптерам монитора и общей текстовой расшифровке без повторного разбора байтов.
def is_heartbeat(self) -> bool: # Штатный CAN_heartbeat_service передаёт ответ с адресом 0x0100 и всеми тремя словами.
return self.from_device and self.start_address == HEARTBEAT_ADDRESS and self.present_mask == 7 # Маска 111 из ecan.c исключает команды и неполные служебные пакеты.
@property # Имя отправителя определяется направлением кадра, тогда как device_name сохраняет имя периферийного узла.
def sender_name(self) -> str: # Все устройства 167 подписываются по источнику кадра, как в предоставленной таблице адресов.
return self.device_name if self.from_device else "TMS" # Команды базового диапазона передаёт TMS, а ответы со смещением 0x10 — соответствующий периферийный узел.
@property # Получатель дополняет отправителя, чтобы подробная расшифровка совпадала с подписью CAN ID.
def recipient_name(self) -> str: # Имя адресата нужно для обоих направлений каждой пары устройств.
return "TMS" if self.from_device else self.device_name # Периферия возвращает данные в TMS; получателем команды остаётся узел, выбранный CAN ID.
@property # Краткий маршрут используется также в сводке и истории CAN-монитора.
def route(self) -> str: # Собираем направление из тех же имён, которые показаны в подробностях кадра.
return "%s → %s" % (self.sender_name, self.recipient_name) # Единые подписи исключают различия между строкой ID и полями отправителя и получателя.
def command_description(self, value: int) -> str:
"""Command register 127; named UMP bits from BALZAM_167 automats.h."""
@@ -56,13 +80,16 @@ class BalsamFrame:
for bit in range(16) if value & (1 << bit))
@property
def summary(self) -> str:
direction = "данные" if self.from_device else "команда"
values = ", ".join(
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
item.value, item.signed_value)
def summary(self) -> str: # Сводка показывает служебный пакет компактно, а технологические регистры — с адресами и известными назначениями.
if self.is_heartbeat: # Только полный ответ с адреса 0x0100 содержит три служебных слова в гарантированном порядке.
seconds, mode, desk = (item.value for item in self.registers) # Значения heartbeat беззнаковые; счётчик циклически проходит 0–65535 секунд.
return "BALZAM · %s · Heartbeat · %d с · Mode=%d · Desk=%d" % (self.route, seconds, mode, desk) # Один heartbeat получает одну подпись; исходные HEX остаются в байтах и подробных полях.
direction = "данные" if self.from_device else "команда" # Направление технологического кадра определяется диапазоном CAN ID.
values = ", ".join( # Каждое отмеченное маской слово выводится ровно один раз вместе с его назначением.
"R%d=0x%04X (%d)" % (item.address, item.value, item.signed_value) # Десятичный адрес совпадает с таблицей R0–R127, HEX сохраняет точное исходное слово.
+ (" — " + item.name if item.name else "") # Для неизвестного адреса сохраняем только достоверные номер и значение, не придумывая назначение.
+ (" — " + self.command_description(item.value)
if not self.from_device and item.address == 0x7F else "")
if item.address == 0x7F else "")
for item in self.registers
) or "нет отмеченных регистров"
return "BALZAM · %s · %s · %s" % (self.route, direction, values)
@@ -93,7 +120,7 @@ def decode(can_id: int, data: bytes, native=None) -> BalsamFrame | None:
device, direction, mask, start, values = decoded
registers = tuple(
BalsamRegister(start + index, values[index],
native.balsam_register_name(device, start + index))
HEARTBEAT_NAMES.get(start + index) or COMMON_REGISTER_NAMES.get(start + index) or native.balsam_register_name(device, start + index)) # Общие настройки получают одинаковые подписи со старой DLL; назначения каналов определяет C-ядро.
for index in range(3) if mask & (4 >> index)
)
return BalsamFrame(can_id & 0x1FFFFFFF, device,
@@ -107,7 +134,7 @@ def _decode_fallback(can_id: int, data: bytes) -> BalsamFrame | None:
if len(data) != 8:
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
relative = (can_id & 0x1FFFFFFF) - BASE_ID
device = (relative & 0x0F) + 1
device = (relative & 0x0F) + 1 # Прошивка ecan.c уменьшает DevNum перед сборкой ID; маска выделяет нулевой CAN-индекс, а +1 возвращает номер Mode.
from_device = relative >= DATA_OFFSET
if can_id & 0x1FFFFFFF in (PULT_COMMAND_ID, PULT_DATA_ID):
device = 10
@@ -117,12 +144,12 @@ def _decode_fallback(can_id: int, data: bytes) -> BalsamFrame | None:
for offset in (6, 0, 2))
names = {
1: "Трансформатор 1", 2: "Трансформатор 2",
3: "Силовой блок 1", 4: "Силовой блок 2", 5: "УМП 1", 6: "УМП 2",
7: "Двигатель", 8: "ВЭП", 9: "Задатчик", 10: "Пульт (МПУ)", 13: "Терминал",
3: "Силовой 1", 4: "Силовой 2", 5: "УМП 1", 6: "УМП 2", # Названия силовых узлов и УМП соответствуют строкам 3..6 предоставленной таблицы адресов 167.
7: "Двигатель", 8: "Задатчик", 9: "ВЭП", 10: "ПМУ", 13: "Терминал", # Таблица задаёт задатчик 8, ВЭП 9 и ПМУ для пары 0xBA0009/0xBA0019; служебный терминал сохраняет номер 13.
}
start, mask = header & 0x1FFF, (header >> 13) & 7
registers = tuple(BalsamRegister(start + i, values[i],
"Команды" if start + i == 0x7F else "")
HEARTBEAT_NAMES.get(start + i) or COMMON_REGISTER_NAMES.get(start + i, "")) # Без DLL сохраняем подтверждённые назначения heartbeat и общих настроек, оставляя неизвестные каналы без подписи.
for i in range(3) if mask & (4 >> i))
return BalsamFrame(can_id & 0x1FFFFFFF, device,
names.get(device, "Узел %d" % device),

View File

@@ -37,6 +37,8 @@ class CanBootTransfer:
def __init__(self, image, target: CanBootTarget):
if not 0 < len(image.data) <= 512 * 1024:
raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ")
if target.product_type == 0x2833 and len(image.data) > 0x1FE00:
raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт")
if not 0 <= image.version <= 0xFFFFFFFF:
raise ValueError("Версия образа вне диапазона u32")
self.image, self.target = image, target
@@ -46,6 +48,10 @@ class CanBootTransfer:
self.finished = False
self._window_end = 0
@property
def response_timeout_ms(self):
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
@property
def percent(self):
return min(100, self.next_block * 8 * 100 // len(self.image.data))
@@ -128,4 +134,93 @@ class CanBootTransfer:
self.stage = "reboot"
return self._control(9), "Перезапуск прибора"
self.stage, self.finished = "finished", True
if self.target.product_type == 0x2833:
return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск"
return [], "Прошивка ProtoCAN Boot завершена"
class CanBootServices:
"""Read-only passport queries and explicit session service commands.
Replies use message D, body=(page<<8)|command. IDENTIFY/discovery reply body=1.
All multibyte values are little endian; version is major:8/minor:8/patch:16.
"""
def __init__(self, target: CanBootTarget):
self.target = target
def request(self, command: int, page: int = 0):
if command not in (1, 8, 11, 12, 13, 14, 15):
raise ValueError("Unsupported service command")
if not 0 <= page <= 255 or (page and command not in (14, 15)):
raise ValueError("Invalid service page")
t = self.target
payload = bytes([page]) if command in (14, 15) else b""
return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, 9,
t.session_id << 8 | command), payload, to_can=True)
@staticmethod
def discover():
return build_frame(ProtoCanId.build(1, 0, 7, 15, 13, 0), b"", to_can=True)
def decode(self, frame, *, discovery=False):
p = ProtoCanId.parse(frame.can_id)
if (frame.to_can or not frame.ide or frame.rtr or frame.is_error or p.pm != 1
or p.msg_type not in (12, 13) or len(frame.data) != 8):
return None
if discovery:
if p.msg_type != 13 or p.body != 1:
return None
elif (p.device_type, p.device) != (self.target.device_type, self.target.device):
return None
if p.msg_type == 12:
if p.body >> 8 != self.target.session_id:
return None
status, slot, next_block, crc32 = struct.unpack("<BBHI", frame.data)
return {"kind": "status", "device_type": p.device_type, "device": p.device,
"command": p.body & 255, "status": status, "target_slot": slot,
"next_block": next_block, "crc32": crc32}
command, page = p.body & 255, p.body >> 8
d = frame.data
result = {"kind": "passport", "device_type": p.device_type, "device": p.device, "command": command, "page": page}
if command == 1 and page == 0:
product, hardware, protocol, version = struct.unpack("<HBBI", d)
result.update(product_type=product, hardware_revision=hardware, protocol_version=protocol,
firmware_version=version, version=(version >> 24, (version >> 16) & 255, version & 65535))
elif command == 12 and page == 0:
result.update(protocol_version=d[0], state=d[1], active_slot=d[2], target_slot=d[3],
session_id=d[4], slot_count=d[5], last_status=d[6])
elif command == 13 and page == 0:
flags, capacity = struct.unpack("<II", d)
result.update(capabilities=flags, slot_capacity=capacity)
elif command == 14:
result["name_fragment"] = d.split(b"\0", 1)[0].decode("ascii", errors="replace")
elif command == 15:
if page == 0:
size, page_size, mode, running = struct.unpack("<IHBB", d)
result.update(memory_bytes=size, page_bytes=page_size, bootloader=bool(mode), running_slot=running)
elif page == 1:
result["slot_offsets"] = struct.unpack("<II", d)
elif page == 2:
active, pending, attempted, healthy, timeout = struct.unpack("<BBBBI", d)
result.update(active_slot=active, pending_slot=pending, trial_attempted=bool(attempted),
storage_healthy=bool(healthy), session_timeout_ms=timeout)
elif page == 3:
result["parameters_offset"], result["parameters_bytes"] = struct.unpack("<II", d)
else:
return None
else:
return None
return result
class CanBootPingExchange:
"""PING adapter with the same request/feed shape as the SETProtocol client."""
def __init__(self, target: CanBootTarget):
self.services = CanBootServices(target)
def request(self):
return [self.services.request(12)]
def feed(self, frame):
reply = self.services.decode(frame)
return reply if reply and reply["kind"] == "passport" and reply["command"] == 12 else None

View File

@@ -0,0 +1,51 @@
"""GAS 167/v1 dictionary and assigned UKSS IDs; values remain raw u16."""
import json
from pathlib import Path
REGISTERS = tuple(json.loads(Path(__file__).with_name('gas_167_registers.json').read_text(encoding='utf-8'))['registers'])
BY_ADDRESS = {entry['address']: entry for entry in REGISTERS}
BY_ASSIGNED_ID = {entry[key]: (node, entry) for node, key in ((5, 'ump1_id'), (6, 'ump2_id')) for entry in REGISTERS}
COMMAND_NAMES = (
'cTestLamp — проверка ламп', 'cDefParam — параметры по умолчанию',
'cSaveParam — сохранение параметров', 'cLoadParam — загрузка параметров',
'cTermoCal — термокалибровка', 'cKoefCalc — расчёт коэффициентов',
'cUMPreset — останов заряда', 'cUMPstart — пуск заряда',
'cInitDac — инициализация ЦАП', 'cCalibrDac — калибровка ЦАП',
'cSecretBt', 'cLiteFire', 'cRawMeat', 'бит 13 (не назначен)',
'бит 14 (не назначен)', 'cReset — сброс',
)
def describe_word(offset, value):
"""Describe active bits using the selected LOAD register map."""
if offset == 127:
return ', '.join(name for bit, name in enumerate(COMMAND_NAMES)
if value & (1 << bit)) or 'нет активных команд'
if offset == 124:
names = ('LOAD_FAULT — ошибка пуска', 'нет свежего напряжения ЗПТ',
'LOAD_RELOAD — сброс перед пуском', 'LOAD_RUN — работа рампы',
'имитация потери ЗПТ', 'имитация 0 В')
return ', '.join(names[bit] if bit < len(names) else 'бит %d' % bit
for bit in range(16) if value & (1 << bit)) or 'нет активных флагов LOAD'
return ''
def metadata(address):
"""Metadata for the explicitly selected legacy Modbus profile."""
index = address if 0 <= address < 128 else address - 0x5000
if not 0 <= index < 128:
return {}
entry = REGISTERS[index]
return {'name': entry['description'], 'gas_id': f"{entry['ump1_id']} / {entry['ump2_id']}"}
def assigned_metadata(address):
"""Only assigned global IDs; reserved offsets have no parameter name."""
match = BY_ASSIGNED_ID.get(address)
if match is None:
return {}
node, entry = match
return {'node': node, 'name': entry['description'], 'access': entry['access'],
'modbus': entry['modbus'], 'legacy_address': entry['address'],
'status': 'назначен; прошивка 167/v1 ещё не обслуживает'}

View File

@@ -0,0 +1,1030 @@
{
"profile": "167/v1",
"source_commit": "378e06a",
"registers": [
{
"modbus": 0,
"address": 20480,
"ump1_id": 5000,
"ump2_id": 6000,
"description": "sens_error[0]; InputRep0/1/2 — биты A/B/C",
"access": "RW"
},
{
"modbus": 1,
"address": 20481,
"ump1_id": 5001,
"ump2_id": 6001,
"description": "sens_error[1]",
"access": "RW"
},
{
"modbus": 2,
"address": 20482,
"ump1_id": 5002,
"ump2_id": 6002,
"description": "sens_error[2]",
"access": "RW"
},
{
"modbus": 3,
"address": 20483,
"ump1_id": 5003,
"ump2_id": 6003,
"description": "sens_error[3]",
"access": "RW"
},
{
"modbus": 4,
"address": 20484,
"ump1_id": 5004,
"ump2_id": 6004,
"description": "sens_error[4]",
"access": "RW"
},
{
"modbus": 5,
"address": 20485,
"ump1_id": 5005,
"ump2_id": 6005,
"description": "sens_error[5]",
"access": "RW"
},
{
"modbus": 6,
"address": 20486,
"ump1_id": 5006,
"ump2_id": 6006,
"description": "sens_error[6]",
"access": "RW"
},
{
"modbus": 7,
"address": 20487,
"ump1_id": 5007,
"ump2_id": 6007,
"description": "sens_error[7]",
"access": "RW"
},
{
"modbus": 8,
"address": 20488,
"ump1_id": 5008,
"ump2_id": 6008,
"description": "sens_error[8]",
"access": "RW"
},
{
"modbus": 9,
"address": 20489,
"ump1_id": 5009,
"ump2_id": 6009,
"description": "sens_error[9]",
"access": "RW"
},
{
"modbus": 10,
"address": 20490,
"ump1_id": 5010,
"ump2_id": 6010,
"description": "sens_error[10]",
"access": "RW"
},
{
"modbus": 11,
"address": 20491,
"ump1_id": 5011,
"ump2_id": 6011,
"description": "sens_error[11]",
"access": "RW"
},
{
"modbus": 12,
"address": 20492,
"ump1_id": 5012,
"ump2_id": 6012,
"description": "sens_error[12]",
"access": "RW"
},
{
"modbus": 13,
"address": 20493,
"ump1_id": 5013,
"ump2_id": 6013,
"description": "sens_error[13]",
"access": "RW"
},
{
"modbus": 14,
"address": 20494,
"ump1_id": 5014,
"ump2_id": 6014,
"description": "sens_error[14]",
"access": "RW"
},
{
"modbus": 15,
"address": 20495,
"ump1_id": 5015,
"ump2_id": 6015,
"description": "sens_error[15]",
"access": "RW"
},
{
"modbus": 16,
"address": 20496,
"ump1_id": 5016,
"ump2_id": 6016,
"description": "sens_error[16]; Inputs",
"access": "RW"
},
{
"modbus": 17,
"address": 20497,
"ump1_id": 5017,
"ump2_id": 6017,
"description": "sens_error[17]",
"access": "RW"
},
{
"modbus": 18,
"address": 20498,
"ump1_id": 5018,
"ump2_id": 6018,
"description": "sens_error[18]",
"access": "RW"
},
{
"modbus": 19,
"address": 20499,
"ump1_id": 5019,
"ump2_id": 6019,
"description": "sens_error[19]",
"access": "RW"
},
{
"modbus": 20,
"address": 20500,
"ump1_id": 5020,
"ump2_id": 6020,
"description": "sens_error[20]",
"access": "RW"
},
{
"modbus": 21,
"address": 20501,
"ump1_id": 5021,
"ump2_id": 6021,
"description": "sens_error[21]",
"access": "RW"
},
{
"modbus": 22,
"address": 20502,
"ump1_id": 5022,
"ump2_id": 6022,
"description": "sens_error[22]; Buttons",
"access": "RW"
},
{
"modbus": 23,
"address": 20503,
"ump1_id": 5023,
"ump2_id": 6023,
"description": "sens_error[23]; Jumpers",
"access": "RW"
},
{
"modbus": 24,
"address": 20504,
"ump1_id": 5024,
"ump2_id": 6024,
"description": "sens_data[0]",
"access": "RW"
},
{
"modbus": 25,
"address": 20505,
"ump1_id": 5025,
"ump2_id": 6025,
"description": "sens_data[1]",
"access": "RW"
},
{
"modbus": 26,
"address": 20506,
"ump1_id": 5026,
"ump2_id": 6026,
"description": "sens_data[2]",
"access": "RW"
},
{
"modbus": 27,
"address": 20507,
"ump1_id": 5027,
"ump2_id": 6027,
"description": "sens_data[3]",
"access": "RW"
},
{
"modbus": 28,
"address": 20508,
"ump1_id": 5028,
"ump2_id": 6028,
"description": "sens_data[4]; LOAD: расчётный выход 4–20 мА, raw ×10",
"access": "RW"
},
{
"modbus": 29,
"address": 20509,
"ump1_id": 5029,
"ump2_id": 6029,
"description": "sens_data[5]; LOAD: измеренный вход 4–20 мА, raw ×10",
"access": "RW"
},
{
"modbus": 30,
"address": 20510,
"ump1_id": 5030,
"ump2_id": 6030,
"description": "sens_data[6]",
"access": "RW"
},
{
"modbus": 31,
"address": 20511,
"ump1_id": 5031,
"ump2_id": 6031,
"description": "sens_data[7]",
"access": "RW"
},
{
"modbus": 32,
"address": 20512,
"ump1_id": 5032,
"ump2_id": 6032,
"description": "sens_data[8]",
"access": "RW"
},
{
"modbus": 33,
"address": 20513,
"ump1_id": 5033,
"ump2_id": 6033,
"description": "sens_data[9]",
"access": "RW"
},
{
"modbus": 34,
"address": 20514,
"ump1_id": 5034,
"ump2_id": 6034,
"description": "sens_data[10]",
"access": "RW"
},
{
"modbus": 35,
"address": 20515,
"ump1_id": 5035,
"ump2_id": 6035,
"description": "sens_data[11]",
"access": "RW"
},
{
"modbus": 36,
"address": 20516,
"ump1_id": 5036,
"ump2_id": 6036,
"description": "sens_data[12]",
"access": "RW"
},
{
"modbus": 37,
"address": 20517,
"ump1_id": 5037,
"ump2_id": 6037,
"description": "sens_data[13]",
"access": "RW"
},
{
"modbus": 38,
"address": 20518,
"ump1_id": 5038,
"ump2_id": 6038,
"description": "sens_data[14]",
"access": "RW"
},
{
"modbus": 39,
"address": 20519,
"ump1_id": 5039,
"ump2_id": 6039,
"description": "sens_data[15]",
"access": "RW"
},
{
"modbus": 40,
"address": 20520,
"ump1_id": 5040,
"ump2_id": 6040,
"description": "sens_data[16]",
"access": "RW"
},
{
"modbus": 41,
"address": 20521,
"ump1_id": 5041,
"ump2_id": 6041,
"description": "sens_data[17]",
"access": "RW"
},
{
"modbus": 42,
"address": 20522,
"ump1_id": 5042,
"ump2_id": 6042,
"description": "sens_data[18]",
"access": "RW"
},
{
"modbus": 43,
"address": 20523,
"ump1_id": 5043,
"ump2_id": 6043,
"description": "sens_data[19]",
"access": "RW"
},
{
"modbus": 44,
"address": 20524,
"ump1_id": 5044,
"ump2_id": 6044,
"description": "sens_data[20]",
"access": "RW"
},
{
"modbus": 45,
"address": 20525,
"ump1_id": 5045,
"ump2_id": 6045,
"description": "sens_data[21]",
"access": "RW"
},
{
"modbus": 46,
"address": 20526,
"ump1_id": 5046,
"ump2_id": 6046,
"description": "sens_data[22]",
"access": "RW"
},
{
"modbus": 47,
"address": 20527,
"ump1_id": 5047,
"ump2_id": 6047,
"description": "sens_data[23]",
"access": "RW"
},
{
"modbus": 48,
"address": 20528,
"ump1_id": 5048,
"ump2_id": 6048,
"description": "sens_hi_edge[0]",
"access": "RW"
},
{
"modbus": 49,
"address": 20529,
"ump1_id": 5049,
"ump2_id": 6049,
"description": "sens_hi_edge[1]",
"access": "RW"
},
{
"modbus": 50,
"address": 20530,
"ump1_id": 5050,
"ump2_id": 6050,
"description": "sens_hi_edge[2]",
"access": "RW"
},
{
"modbus": 51,
"address": 20531,
"ump1_id": 5051,
"ump2_id": 6051,
"description": "sens_hi_edge[3]",
"access": "RW"
},
{
"modbus": 52,
"address": 20532,
"ump1_id": 5052,
"ump2_id": 6052,
"description": "sens_hi_edge[4]",
"access": "RW"
},
{
"modbus": 53,
"address": 20533,
"ump1_id": 5053,
"ump2_id": 6053,
"description": "sens_hi_edge[5]",
"access": "RW"
},
{
"modbus": 54,
"address": 20534,
"ump1_id": 5054,
"ump2_id": 6054,
"description": "sens_hi_edge[6]",
"access": "RW"
},
{
"modbus": 55,
"address": 20535,
"ump1_id": 5055,
"ump2_id": 6055,
"description": "sens_hi_edge[7]",
"access": "RW"
},
{
"modbus": 56,
"address": 20536,
"ump1_id": 5056,
"ump2_id": 6056,
"description": "sens_hi_edge[8]",
"access": "RW"
},
{
"modbus": 57,
"address": 20537,
"ump1_id": 5057,
"ump2_id": 6057,
"description": "sens_hi_edge[9]",
"access": "RW"
},
{
"modbus": 58,
"address": 20538,
"ump1_id": 5058,
"ump2_id": 6058,
"description": "sens_hi_edge[10]",
"access": "RW"
},
{
"modbus": 59,
"address": 20539,
"ump1_id": 5059,
"ump2_id": 6059,
"description": "sens_hi_edge[11]",
"access": "RW"
},
{
"modbus": 60,
"address": 20540,
"ump1_id": 5060,
"ump2_id": 6060,
"description": "sens_hi_edge[12]",
"access": "RW"
},
{
"modbus": 61,
"address": 20541,
"ump1_id": 5061,
"ump2_id": 6061,
"description": "sens_hi_edge[13]",
"access": "RW"
},
{
"modbus": 62,
"address": 20542,
"ump1_id": 5062,
"ump2_id": 6062,
"description": "sens_hi_edge[14]",
"access": "RW"
},
{
"modbus": 63,
"address": 20543,
"ump1_id": 5063,
"ump2_id": 6063,
"description": "sens_hi_edge[15]",
"access": "RW"
},
{
"modbus": 64,
"address": 20544,
"ump1_id": 5064,
"ump2_id": 6064,
"description": "sens_hi_edge[16]",
"access": "RW"
},
{
"modbus": 65,
"address": 20545,
"ump1_id": 5065,
"ump2_id": 6065,
"description": "sens_hi_edge[17]",
"access": "RW"
},
{
"modbus": 66,
"address": 20546,
"ump1_id": 5066,
"ump2_id": 6066,
"description": "sens_hi_edge[18]",
"access": "RW"
},
{
"modbus": 67,
"address": 20547,
"ump1_id": 5067,
"ump2_id": 6067,
"description": "sens_hi_edge[19]",
"access": "RW"
},
{
"modbus": 68,
"address": 20548,
"ump1_id": 5068,
"ump2_id": 6068,
"description": "sens_hi_edge[20]",
"access": "RW"
},
{
"modbus": 69,
"address": 20549,
"ump1_id": 5069,
"ump2_id": 6069,
"description": "sens_hi_edge[21]",
"access": "RW"
},
{
"modbus": 70,
"address": 20550,
"ump1_id": 5070,
"ump2_id": 6070,
"description": "sens_hi_edge[22]",
"access": "RW"
},
{
"modbus": 71,
"address": 20551,
"ump1_id": 5071,
"ump2_id": 6071,
"description": "sens_hi_edge[23]",
"access": "RW"
},
{
"modbus": 72,
"address": 20552,
"ump1_id": 5072,
"ump2_id": 6072,
"description": "sens_lo_edge[0]",
"access": "RW"
},
{
"modbus": 73,
"address": 20553,
"ump1_id": 5073,
"ump2_id": 6073,
"description": "sens_lo_edge[1]",
"access": "RW"
},
{
"modbus": 74,
"address": 20554,
"ump1_id": 5074,
"ump2_id": 6074,
"description": "sens_lo_edge[2]",
"access": "RW"
},
{
"modbus": 75,
"address": 20555,
"ump1_id": 5075,
"ump2_id": 6075,
"description": "sens_lo_edge[3]",
"access": "RW"
},
{
"modbus": 76,
"address": 20556,
"ump1_id": 5076,
"ump2_id": 6076,
"description": "sens_lo_edge[4]",
"access": "RW"
},
{
"modbus": 77,
"address": 20557,
"ump1_id": 5077,
"ump2_id": 6077,
"description": "sens_lo_edge[5]",
"access": "RW"
},
{
"modbus": 78,
"address": 20558,
"ump1_id": 5078,
"ump2_id": 6078,
"description": "sens_lo_edge[6]",
"access": "RW"
},
{
"modbus": 79,
"address": 20559,
"ump1_id": 5079,
"ump2_id": 6079,
"description": "sens_lo_edge[7]",
"access": "RW"
},
{
"modbus": 80,
"address": 20560,
"ump1_id": 5080,
"ump2_id": 6080,
"description": "sens_lo_edge[8]; LOAD: DAC_go — параметр начала рампы",
"access": "RW"
},
{
"modbus": 81,
"address": 20561,
"ump1_id": 5081,
"ump2_id": 6081,
"description": "sens_lo_edge[9]; LOAD: DAC_stop — параметр конца рампы",
"access": "RW"
},
{
"modbus": 82,
"address": 20562,
"ump1_id": 5082,
"ump2_id": 6082,
"description": "sens_lo_edge[10]; LOAD: LOAD_CAN_TIMEOUT, мс",
"access": "RW; LOAD: 1…60000"
},
{
"modbus": 83,
"address": 20563,
"ump1_id": 5083,
"ump2_id": 6083,
"description": "sens_lo_edge[11]",
"access": "RW"
},
{
"modbus": 84,
"address": 20564,
"ump1_id": 5084,
"ump2_id": 6084,
"description": "sens_lo_edge[12]",
"access": "RW"
},
{
"modbus": 85,
"address": 20565,
"ump1_id": 5085,
"ump2_id": 6085,
"description": "sens_lo_edge[13]",
"access": "RW"
},
{
"modbus": 86,
"address": 20566,
"ump1_id": 5086,
"ump2_id": 6086,
"description": "sens_lo_edge[14]",
"access": "RW"
},
{
"modbus": 87,
"address": 20567,
"ump1_id": 5087,
"ump2_id": 6087,
"description": "sens_lo_edge[15]",
"access": "RW"
},
{
"modbus": 88,
"address": 20568,
"ump1_id": 5088,
"ump2_id": 6088,
"description": "sens_lo_edge[16]",
"access": "RW"
},
{
"modbus": 89,
"address": 20569,
"ump1_id": 5089,
"ump2_id": 6089,
"description": "sens_lo_edge[17]",
"access": "RW"
},
{
"modbus": 90,
"address": 20570,
"ump1_id": 5090,
"ump2_id": 6090,
"description": "sens_lo_edge[18]",
"access": "RW"
},
{
"modbus": 91,
"address": 20571,
"ump1_id": 5091,
"ump2_id": 6091,
"description": "sens_lo_edge[19]",
"access": "RW"
},
{
"modbus": 92,
"address": 20572,
"ump1_id": 5092,
"ump2_id": 6092,
"description": "sens_lo_edge[20]",
"access": "RW"
},
{
"modbus": 93,
"address": 20573,
"ump1_id": 5093,
"ump2_id": 6093,
"description": "sens_lo_edge[21]",
"access": "RW"
},
{
"modbus": 94,
"address": 20574,
"ump1_id": 5094,
"ump2_id": 6094,
"description": "sens_lo_edge[22]",
"access": "RW"
},
{
"modbus": 95,
"address": 20575,
"ump1_id": 5095,
"ump2_id": 6095,
"description": "sens_lo_edge[23]",
"access": "RW"
},
{
"modbus": 96,
"address": 20576,
"ump1_id": 5096,
"ump2_id": 6096,
"description": "Cancount[m_FAST] — пауза быстрых посылок CAN",
"access": "RW"
},
{
"modbus": 97,
"address": 20577,
"ump1_id": 5097,
"ump2_id": 6097,
"description": "Cancount[m_SLOW] — пауза медленных посылок CAN",
"access": "RW"
},
{
"modbus": 98,
"address": 20578,
"ump1_id": 5098,
"ump2_id": 6098,
"description": "Brightness / Bright[0] — яркость",
"access": "RW"
},
{
"modbus": 99,
"address": 20579,
"ump1_id": 5099,
"ump2_id": 6099,
"description": "Owncount / Bright[1]; LOAD: исходное значение АЦП",
"access": "RW"
},
{
"modbus": 100,
"address": 20580,
"ump1_id": 5100,
"ump2_id": 6100,
"description": "Bright[2]; Current_count: исходное значение канала 0",
"access": "RW"
},
{
"modbus": 101,
"address": 20581,
"ump1_id": 5101,
"ump2_id": 6101,
"description": "Bright[3]; Current_count: исходное значение канала 1",
"access": "RW"
},
{
"modbus": 102,
"address": 20582,
"ump1_id": 5102,
"ump2_id": 6102,
"description": "Bright[4]; Current_count: исходное значение канала 2",
"access": "RW"
},
{
"modbus": 103,
"address": 20583,
"ump1_id": 5103,
"ump2_id": 6103,
"description": "Bright[5]; Current_count: исходное значение канала 3",
"access": "RW"
},
{
"modbus": 104,
"address": 20584,
"ump1_id": 5104,
"ump2_id": 6104,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 105,
"address": 20585,
"ump1_id": 5105,
"ump2_id": 6105,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 106,
"address": 20586,
"ump1_id": 5106,
"ump2_id": 6106,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 107,
"address": 20587,
"ump1_id": 5107,
"ump2_id": 6107,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 108,
"address": 20588,
"ump1_id": 5108,
"ump2_id": 6108,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 109,
"address": 20589,
"ump1_id": 5109,
"ump2_id": 6109,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 110,
"address": 20590,
"ump1_id": 5110,
"ump2_id": 6110,
"description": "DAC_20 — калибровка входа 20 мА",
"access": "RW"
},
{
"modbus": 111,
"address": 20591,
"ump1_id": 5111,
"ump2_id": 6111,
"description": "DAC_04 — калибровка входа 4 мА",
"access": "RW"
},
{
"modbus": 112,
"address": 20592,
"ump1_id": 5112,
"ump2_id": 6112,
"description": "Zero_lev[0] — нулевой уровень канала тока 0",
"access": "RW"
},
{
"modbus": 113,
"address": 20593,
"ump1_id": 5113,
"ump2_id": 6113,
"description": "Zero_lev[1] — нулевой уровень канала тока 1",
"access": "RW"
},
{
"modbus": 114,
"address": 20594,
"ump1_id": 5114,
"ump2_id": 6114,
"description": "Zero_lev[2] — нулевой уровень канала тока 2",
"access": "RW"
},
{
"modbus": 115,
"address": 20595,
"ump1_id": 5115,
"ump2_id": 6115,
"description": "Zero_lev[3] — нулевой уровень канала тока 3",
"access": "RW"
},
{
"modbus": 116,
"address": 20596,
"ump1_id": 5116,
"ump2_id": 6116,
"description": "Caliber[0]: K380_1 / KPOW_1",
"access": "RW"
},
{
"modbus": 117,
"address": 20597,
"ump1_id": 5117,
"ump2_id": 6117,
"description": "Caliber[1]: K380_2 / KPOW_2",
"access": "RW"
},
{
"modbus": 118,
"address": 20598,
"ump1_id": 5118,
"ump2_id": 6118,
"description": "Caliber[2]: KPOW_3",
"access": "RW"
},
{
"modbus": 119,
"address": 20599,
"ump1_id": 5119,
"ump2_id": 6119,
"description": "Caliber[3]: KPOW_4",
"access": "RW"
},
{
"modbus": 120,
"address": 20600,
"ump1_id": 5120,
"ump2_id": 6120,
"description": "Caliber[4] / TCaliber[0]; LOAD: DAC_max()",
"access": "RW"
},
{
"modbus": 121,
"address": 20601,
"ump1_id": 5121,
"ump2_id": 6121,
"description": "Caliber[5] / TCaliber[1]; LOAD: DAC_min()",
"access": "RW"
},
{
"modbus": 122,
"address": 20602,
"ump1_id": 5122,
"ump2_id": 6122,
"description": "Caliber[6] / TCaliber[2]; LOAD: DAC_cal()",
"access": "RW"
},
{
"modbus": 123,
"address": 20603,
"ump1_id": 5123,
"ump2_id": 6123,
"description": "Caliber[7] / TCaliber[3]; LOAD: текущий 12-битный код ЦАП",
"access": "RW"
},
{
"modbus": 124,
"address": 20604,
"ump1_id": 5124,
"ump2_id": 6124,
"description": "NormaU = Caliber[8]; LOAD: LOAD_STATUS",
"access": "RO для LOAD; иначе RW"
},
{
"modbus": 125,
"address": 20605,
"ump1_id": 5125,
"ump2_id": 6125,
"description": "DeltaU = Caliber[9]; LOAD: LOAD_ATTEMPTS",
"access": "RO для LOAD; иначе RW"
},
{
"modbus": 126,
"address": 20606,
"ump1_id": 5126,
"ump2_id": 6126,
"description": "LastMode",
"access": "RW"
},
{
"modbus": 127,
"address": 20607,
"ump1_id": 5127,
"ump2_id": 6127,
"description": "Commands — битовая маска команд",
"access": "RW"
}
]
}

View File

@@ -17,6 +17,8 @@ from dataclasses import dataclass, field
from enum import IntEnum
from typing import List, Optional
from .gas_167 import assigned_metadata, describe_word
try:
from .native import get_native_core
_SHARED_PROTOCOL = get_native_core()
@@ -114,7 +116,7 @@ MSGTYPE_RU = {
MsgType.BROADCAST: "Broadcast",
MsgType.DISCRETE: "Discrete",
MsgType.ANALOG: "Analog",
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
MsgType.GENERAL_ADDRESS_SPACE: "GAS (General Address Space)",
MsgType.MODBUS_COIL: "Modbus Coil",
MsgType.MODBUS_DISCRETE: "Modbus Discrete",
MsgType.MODBUS_HOLDING: "Modbus Holding",
@@ -622,11 +624,49 @@ def _decode_gas(res: Decoded) -> None:
return
for adr, v in res.registers:
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v)))
_append_gas_metadata(res, adr)
if not vals:
_append_gas_metadata(res, start)
if vals:
res.summary = "GAS: %d рег. с 0x%04X = %s" % (
len(vals), start, " ".join("%04X" % v for v in vals))
else:
res.summary = "GAS: запрос по адресу 0x%04X" % start
# Every monitor, manual decoder and export uses this shared summary.
# Keep names in the summary as well as in the expandable field list.
addresses = res.registers if vals else [(start, None)]
if any(assigned_metadata(address) for address, _value in addresses):
words = []
for address, value in addresses:
entry = assigned_metadata(address)
label = "ID %d (0x%04X)" % (address, address)
if entry:
label = "УМП %d · %s · %s" % (
entry['node'] - 4, label, entry['name'])
else:
label += " · имя не назначено"
if value is not None:
label += " = %d [0x%04X]" % (value, value)
description = describe_word(entry['modbus'], value) if entry else ''
if description:
label += " — " + description
res.fields.append((" GAS ID %d / биты" % address, description))
words.append(label)
res.summary = ("GAS: " if vals else "GAS запрос: ") + "; ".join(words)
def _append_gas_metadata(res: Decoded, address: int) -> None:
entry = assigned_metadata(address)
if not entry:
return
label = " GAS ID %d" % address
res.fields += [
(label, "УМП %d — %s" % (entry['node'] - 4, entry['name'])),
(label + " / статус", entry['status']),
(label + " / Modbus 167/v1", "0x%04X" % entry['modbus']),
(label + " / GAS 167/v1", "0x%04X" % entry['legacy_address']),
(label + " / доступ", entry['access']),
]
def _decode_modbus_bit(res: Decoded) -> None:

View File

@@ -19,4 +19,4 @@ include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*"
[tool.setuptools.package-data]
set_devices = ["gas_logger_maps/*.json"]
protocan = ["native/*.dll", "native/*.so", "native/*.dylib"]
protocan = ["*.json", "native/*.dll", "native/*.so", "native/*.dylib"]

Some files were not shown because too many files have changed in this diff Show More