3 Commits

54 changed files with 3590 additions and 148 deletions

View File

@@ -1,6 +1,6 @@
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают # РЎР±РѕСЂРєР° библиотеки protocan_boot. Состав РёСЃС…РѕРґРЅРёРєРѕРІ Рё публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки; # подключение Рє проекту потребителя. Файл также собирает Рё регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки. # запускать РёС… следует через CTest РёР· каталога СЃР±РѕСЂРєРё.
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
project(protocan_boot C) project(protocan_boot C)
@@ -17,10 +17,31 @@ else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif() endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON) option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
if(PCAN_BOOT_BUILD_TESTS) if(PCAN_BOOT_BUILD_TESTS)
enable_testing() enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c) add_executable(test_pcan_boot tests/test_pcan_boot.c)
target_link_libraries(test_pcan_boot PRIVATE protocan_boot) target_link_libraries(test_pcan_boot PRIVATE protocan_boot)
add_test(NAME pcan_boot COMMAND test_pcan_boot) add_test(NAME pcan_boot COMMAND test_pcan_boot)
endif() 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; } 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). `authorize` проверяет политику до стирания (тип, версия, anti-rollback).
`verify_image` выполняет платформенную проверку подписанного контейнера после `verify_image` выполняет платформенную проверку подписанного контейнера после
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
@@ -175,5 +176,12 @@ ctest --test-dir build --output-on-failure
- включить watchdog и rollback при неподтверждённом запуске; - включить watchdog и rollback при неподтверждённом запуске;
- согласовать фильтры CAN для `MsgType=0x9..0xD`. - согласовать фильтры 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 | отсутствует | активная сессия | | `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия | | `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия | | `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
| `0x0C` | `PING` | 0 | отсутствует | любое |
| `0x0D` | `CAPABILITIES` | 0 | отсутствует | любое, при поддержке порта |
| `0x0E` | `DEVICE_NAME` | 1 | номер страницы имени | любое, при поддержке порта |
| `0x0F` | `MEMORY_INFO` | 1 | номер страницы памяти | любое, при поддержке порта |
### `BEGIN_IMAGE` ### `BEGIN_IMAGE`
@@ -81,7 +85,8 @@ DATA[4..7] FirmwareVersion, uint32 little-endian
``` ```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер, До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback. тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется
только при наличии соответствующей политики платформы (callback authorize).
## `BOOT_STATUS` ## `BOOT_STATUS`
@@ -181,3 +186,16 @@ Boot metadata должна атомарно хранить:
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`. 5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`. 6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`. 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 <stddef.h>
#include <stdint.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 #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@@ -41,7 +48,11 @@ typedef enum {
PCAN_BOOT_CMD_CONFIRM = 0x08U, PCAN_BOOT_CMD_CONFIRM = 0x08U,
PCAN_BOOT_CMD_REBOOT = 0x09U, PCAN_BOOT_CMD_REBOOT = 0x09U,
PCAN_BOOT_CMD_ABORT = 0x0AU, 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; } pcan_boot_command_t;
typedef enum { typedef enum {
@@ -66,7 +77,8 @@ typedef enum {
PCAN_BOOT_STATE_READY_TO_ERASE, PCAN_BOOT_STATE_READY_TO_ERASE,
PCAN_BOOT_STATE_RECEIVING, PCAN_BOOT_STATE_RECEIVING,
PCAN_BOOT_STATE_VERIFIED, PCAN_BOOT_STATE_VERIFIED,
PCAN_BOOT_STATE_FAILED PCAN_BOOT_STATE_FAILED,
PCAN_BOOT_STATE_COMMITTED
} pcan_boot_state_t; } pcan_boot_state_t;
typedef struct { typedef struct {
@@ -74,36 +86,47 @@ typedef struct {
uint32_t image_crc32; uint32_t image_crc32;
uint32_t firmware_version; uint32_t firmware_version;
uint16_t product_type; uint16_t product_type;
uint8_t hardware_revision_min; pcan_boot_byte_t hardware_revision_min;
uint8_t hardware_revision_max; pcan_boot_byte_t hardware_revision_max;
} pcan_boot_manifest_t; } pcan_boot_manifest_t;
typedef struct { typedef struct {
uint8_t device_type; pcan_boot_byte_t device_type;
uint8_t device_id; pcan_boot_byte_t device_id;
uint16_t product_type; uint16_t product_type;
uint8_t hardware_revision; pcan_boot_byte_t hardware_revision;
uint32_t firmware_version; 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. */ /** 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. */ /** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
uint32_t max_image_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; } pcan_boot_config_t;
typedef struct { typedef struct {
bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc); bool (*send)(void *user, uint32_t can_id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
bool (*erase_slot)(void *user, uint8_t slot); bool (*erase_slot)(void *user, pcan_boot_byte_t slot);
bool (*write_slot)(void *user, uint8_t slot, uint32_t offset, bool (*write_slot)(void *user, pcan_boot_byte_t slot, uint32_t offset,
const uint8_t *data, uint8_t length); const pcan_boot_byte_t *data, pcan_boot_byte_t length);
bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest); 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); 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); const pcan_boot_manifest_t *manifest);
bool (*confirm_running_slot)(void *user); bool (*confirm_running_slot)(void *user);
void (*reboot)(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; } pcan_boot_port_t;
typedef struct { typedef struct {
@@ -115,11 +138,13 @@ typedef struct {
uint32_t running_crc32; uint32_t running_crc32;
uint32_t bytes_received; uint32_t bytes_received;
uint16_t next_block; uint16_t next_block;
uint8_t session_id; pcan_boot_byte_t session_id;
uint8_t target_slot; pcan_boot_byte_t target_slot;
uint8_t last_status; pcan_boot_byte_t last_status;
bool have_image; bool have_image;
bool have_compat; bool have_compat;
uint32_t now_ms, last_activity_ms, discovery_at;
bool discovery_pending;
} pcan_boot_t; } pcan_boot_t;
/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */ /** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */
@@ -134,16 +159,19 @@ void pcan_boot_abort(pcan_boot_t *boot);
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора. * Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
*/ */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, 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. */ /** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
uint8_t device_type, uint8_t device_id, pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
uint8_t msg_type, uint16_t msg_body); pcan_boot_byte_t msg_type, uint16_t msg_body);
uint8_t pcan_boot_id_route(uint32_t can_id); pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id);
uint8_t pcan_boot_id_device_type(uint32_t can_id); pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id);
uint8_t pcan_boot_id_device_id(uint32_t can_id); pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id);
uint8_t pcan_boot_id_msg_type(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); uint16_t pcan_boot_id_msg_body(uint32_t can_id);
#ifdef __cplusplus #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 update state machine; platform owns storage and reset. */
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами. #include "../include/pcan_boot.h"
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
* слотов предоставляет платформа.
*/
#include "pcan_boot.h"
#include <string.h> #include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL #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)); 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] return (uint32_t)p[0]
| ((uint32_t)p[1] << 8) | ((uint32_t)p[1] << 8)
@@ -23,26 +18,26 @@ static uint32_t get_u32(const uint8_t *p)
| ((uint32_t)p[3] << 24); | ((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[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (uint8_t)(value >> 8); 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[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (uint8_t)(value >> 8); p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[2] = (uint8_t)(value >> 16); p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU);
p[3] = (uint8_t)(value >> 24); 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; size_t i;
crc = ~crc; crc = ~crc;
for (i = 0U; i < length; ++i) { for (i = 0U; i < length; ++i) {
uint8_t bit; pcan_boot_byte_t bit;
crc ^= data[i]; crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) { for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U); 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; return ~crc;
} }
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
uint8_t device_type, uint8_t device_id, pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
uint8_t msg_type, uint16_t msg_body) pcan_boot_byte_t msg_type, uint16_t msg_body)
{ {
return ((((uint32_t)priority & 1U) << 28) return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27) | (((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; | msg_body) & PCAN_BOOT_ID_MASK;
} }
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); } pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); } pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); } pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_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_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; } 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, static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc) const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
{ {
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE, uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type, 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); 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); uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status; data[0] = status;
data[1] = boot->target_slot; 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); return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
} }
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
static bool send_identity(pcan_boot_t *boot) 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); put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision; data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION; 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); return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
} }
/* Отменяется только состояние протокола в RAM: manifest, счётчики и session.
* Функция не стирает EEPROM и не снимает ранее опубликованный pending.
* Ответственность за постоянное состояние слотов остаётся у платформы. */
void pcan_boot_abort(pcan_boot_t *boot) void pcan_boot_abort(pcan_boot_t *boot)
{ {
if (boot == NULL) { if (boot == NULL) {
@@ -118,19 +121,24 @@ void pcan_boot_abort(pcan_boot_t *boot)
boot->have_compat = false; 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, bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user) 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) if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U) || (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->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) { || (port->write_slot == NULL)) {
return false; return false;
} }
slot_count = config->slot_count == 0U ? 2U : config->slot_count; 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))) { || ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false; return false;
} }
@@ -149,7 +157,10 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
return true; 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) { if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA; 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; boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE); return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
} }
if ((boot->manifest.product_type != boot->config.product_type) if (boot->manifest.product_type != boot->config.product_type) {
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min) 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->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED; boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE); 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; boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR); 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 ? 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; boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK); 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, 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); pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
uint8_t command = (uint8_t)body; pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
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) { if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot); return send_identity(boot);
} }
@@ -191,8 +237,18 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
if (session == 0U) { if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR); 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); pcan_boot_abort(boot);
boot->session_id = session; boot->session_id = session;
boot->last_activity_ms=boot->now_ms;
boot->state = PCAN_BOOT_STATE_METADATA; boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK); 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); return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
} }
boot->last_activity_ms=boot->now_ms;
switch (command) { switch (command) {
case PCAN_BOOT_CMD_BEGIN_IMAGE: 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)) { if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); 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); return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT: 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)) { if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); 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); return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE: 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) { if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); 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); return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY: 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) if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) { || (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); 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; boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR); 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->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) { &boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED; 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); return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT: 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) { if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); 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->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) { &boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
} }
return send_status(boot, command, PCAN_BOOT_STATUS_OK); boot->state=PCAN_BOOT_STATE_COMMITTED;
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);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK); return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS: 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); return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT: 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) { if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user); 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, /* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому
const uint8_t *data, uint8_t dlc) * кадры данных принимаются только в состоянии 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; pcan_boot_byte_t expected_type;
uint8_t write_length; pcan_boot_byte_t write_length;
uint32_t remaining; uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) { 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)) { if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR); 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; 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; : PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U) if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot, || !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; return true;
} }
/* Общая точка входа для одного кадра после аппаратной фильтрации.
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, 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))) { if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false; 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)) { if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false; 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) 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_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) { || (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; 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; data[3] = 3U;
put_u32(&data[4], 1U); put_u32(&data[4], 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U)); 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); 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); 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) int main(void)
{ {
test_services_timeout_and_retries();
test_id_layout(); test_id_layout();
test_crc32_reference(); test_crc32_reference();
test_complete_update(); test_complete_update();

View File

@@ -31,7 +31,8 @@ enum set_plot_operation {
PCAN_ABI_API uint32_t set_plot_abi_version(void); PCAN_ABI_API uint32_t set_plot_abi_version(void);
/** Evaluates one operation. Returns output count, or 0 for invalid arguments. /** Evaluates one operation. Returns output count, or 0 for invalid arguments.
* TRANSFORM rejects malformed viewports; invalid gesture values return the * 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. * 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, PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,

View File

@@ -13,6 +13,7 @@ extern "C" {
#endif #endif
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE }; 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. /* 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. * Inputs may be unsorted; duplicate times are averaged. No extrapolation.
* meta[0..2] = original count, unique count, RMSE at original measurements. * meta[0..2] = original count, unique count, RMSE at original measurements.
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last). * endpoint=1 includes the last time; 0 samples a cyclic period [first,last).

View File

@@ -7,6 +7,9 @@
#include "set_plot.h" #include "set_plot.h"
#include <math.h> #include <math.h>
/* Allow sample-level X navigation in multi-million-point recordings. */
#define MIN_VIEW_WIDTH (1.0 / 10000000)
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); } 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) { static int finite_values(const double *v, size_t n) {
size_t i; size_t i;
@@ -23,12 +26,12 @@ 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; cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
if (op == SET_PLOT_TRANSFORM) { if (op == SET_PLOT_TRANSFORM) {
double w, h, fx, fy; double w, h, fx, fy;
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 || if (!finite_values(v, 4) || v[2] < MIN_VIEW_WIDTH || v[2] > 1 ||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 || v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0; 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]; 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; if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
w = clamp(v[2] / v[4], 1.0/128, 1); w = clamp(v[2] / v[4], MIN_VIEW_WIDTH, 1);
h = clamp(v[3] / v[5], 1.0/128, 1); h = clamp(v[3] / v[5], 1.0/128, 1);
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1); 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[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);

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; double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
size_t i,j,k,n=0; unsigned pass; size_t i,j,k,n=0; unsigned pass;
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000 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; ||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; 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; s=h+count; q=s+count; column=q+6*count;

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_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":"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":"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,1000,1000,0,0,0.5,0.5],"output":[0.4995,0.49609375,0.001,0.0078125]},
{"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":"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":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]}, {"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},

View File

@@ -7,6 +7,7 @@ Device Name — отображаемое имя адреса, а не допол
| DeviceType | DeviceID | Device Name | Назначение | | DeviceType | DeviceID | Device Name | Назначение |
|---|---|---|---| |---|---|---|---|
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы | | `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
| `0x3` | `0x7` | **F28335-M95M02** | Загрузка прошивки по ProtoCAN в EEPROM M95M02 платы TMS320F28335; ProductType `0x2833` |
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов | | `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
| `0x7` | `0xD` | configurator | Существующий конфигуратор | | `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков | | `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`. Машинные варианты Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
находятся в [examples/test-vectors.json](examples/test-vectors.json). находятся в [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 class="x14">&nbsp;</td>
<td colspan="8" class="x96">64 битовых адресса</td> <td colspan="8" class="x96">64 битовых адресса</td>
</tr> </tr>
<tr style="height:17px"> <tr style="height:72px">
<td class="x26">&nbsp;</td> <td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td> <td class="x27">&nbsp;</td>
<td colspan="3" class="x131">0x3</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="12" class="x138">0x3. Установка времени</td>
<td colspan="4" class="x140">&nbsp;</td> <td colspan="4" class="x140">&nbsp;</td>
<td class="x14">&nbsp;</td> <td class="x14">&nbsp;</td>

View File

@@ -62,7 +62,7 @@ class BalsamFrame:
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address, "%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
item.value, item.signed_value) item.value, item.signed_value)
+ (" — " + self.command_description(item.value) + (" — " + 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 for item in self.registers
) or "нет отмеченных регистров" ) or "нет отмеченных регистров"
return "BALZAM · %s · %s · %s" % (self.route, direction, values) return "BALZAM · %s · %s · %s" % (self.route, direction, values)

View File

@@ -37,6 +37,8 @@ class CanBootTransfer:
def __init__(self, image, target: CanBootTarget): def __init__(self, image, target: CanBootTarget):
if not 0 < len(image.data) <= 512 * 1024: if not 0 < len(image.data) <= 512 * 1024:
raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ") 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: if not 0 <= image.version <= 0xFFFFFFFF:
raise ValueError("Версия образа вне диапазона u32") raise ValueError("Версия образа вне диапазона u32")
self.image, self.target = image, target self.image, self.target = image, target
@@ -46,6 +48,10 @@ class CanBootTransfer:
self.finished = False self.finished = False
self._window_end = 0 self._window_end = 0
@property
def response_timeout_ms(self):
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
@property @property
def percent(self): def percent(self):
return min(100, self.next_block * 8 * 100 // len(self.image.data)) return min(100, self.next_block * 8 * 100 // len(self.image.data))
@@ -128,4 +134,93 @@ class CanBootTransfer:
self.stage = "reboot" self.stage = "reboot"
return self._control(9), "Перезапуск прибора" return self._control(9), "Перезапуск прибора"
self.stage, self.finished = "finished", True self.stage, self.finished = "finished", True
if self.target.product_type == 0x2833:
return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск"
return [], "Прошивка ProtoCAN Boot завершена" 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 enum import IntEnum
from typing import List, Optional from typing import List, Optional
from .gas_167 import assigned_metadata, describe_word
try: try:
from .native import get_native_core from .native import get_native_core
_SHARED_PROTOCOL = get_native_core() _SHARED_PROTOCOL = get_native_core()
@@ -114,7 +116,7 @@ MSGTYPE_RU = {
MsgType.BROADCAST: "Broadcast", MsgType.BROADCAST: "Broadcast",
MsgType.DISCRETE: "Discrete", MsgType.DISCRETE: "Discrete",
MsgType.ANALOG: "Analog", 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_COIL: "Modbus Coil",
MsgType.MODBUS_DISCRETE: "Modbus Discrete", MsgType.MODBUS_DISCRETE: "Modbus Discrete",
MsgType.MODBUS_HOLDING: "Modbus Holding", MsgType.MODBUS_HOLDING: "Modbus Holding",
@@ -622,11 +624,49 @@ def _decode_gas(res: Decoded) -> None:
return return
for adr, v in res.registers: for adr, v in res.registers:
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v))) 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: if vals:
res.summary = "GAS: %d рег. с 0x%04X = %s" % ( res.summary = "GAS: %d рег. с 0x%04X = %s" % (
len(vals), start, " ".join("%04X" % v for v in vals)) len(vals), start, " ".join("%04X" % v for v in vals))
else: else:
res.summary = "GAS: запрос по адресу 0x%04X" % start 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: 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] [tool.setuptools.package-data]
set_devices = ["gas_logger_maps/*.json"] set_devices = ["gas_logger_maps/*.json"]
protocan = ["native/*.dll", "native/*.so", "native/*.dylib"] protocan = ["*.json", "native/*.dll", "native/*.so", "native/*.dylib"]

View File

@@ -206,10 +206,16 @@ class RawProtocolWidget(QWidget):
def init_ui(self): def init_ui(self):
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setProperty("fixedPanelLayout", True)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(4)
# Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE) # Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE)
mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)") mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)")
mem_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mem_layout = QGridLayout() mem_layout = QGridLayout()
mem_layout.setContentsMargins(6, 6, 6, 6)
mem_layout.setSpacing(4)
mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0) mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0)
self.mem_addr_edit = QLineEdit("2000") self.mem_addr_edit = QLineEdit("2000")
@@ -239,6 +245,9 @@ class RawProtocolWidget(QWidget):
# Группа для RAW отправки # Группа для RAW отправки
raw_group = QGroupBox("RAW отправка/чтение") raw_group = QGroupBox("RAW отправка/чтение")
raw_layout = QVBoxLayout() raw_layout = QVBoxLayout()
raw_layout.setProperty("fixedPanelLayout", True)
raw_layout.setContentsMargins(6, 6, 6, 6)
raw_layout.setSpacing(4)
self.raw_send_edit = QLineEdit() self.raw_send_edit = QLineEdit()
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки") self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
@@ -246,6 +255,7 @@ class RawProtocolWidget(QWidget):
raw_layout.addWidget(self.raw_send_edit) raw_layout.addWidget(self.raw_send_edit)
hbox = QHBoxLayout() hbox = QHBoxLayout()
hbox.setSpacing(4)
self.raw_send_btn = QPushButton("Отправить RAW") self.raw_send_btn = QPushButton("Отправить RAW")
self.raw_send_btn.clicked.connect(self.send_raw_data) self.raw_send_btn.clicked.connect(self.send_raw_data)
hbox.addWidget(self.raw_send_btn) hbox.addWidget(self.raw_send_btn)
@@ -260,16 +270,19 @@ class RawProtocolWidget(QWidget):
self.raw_output = QTextEdit() self.raw_output = QTextEdit()
self.raw_output.setReadOnly(True) self.raw_output.setReadOnly(True)
self.raw_output.setMaximumHeight(100) self.raw_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
raw_layout.addWidget(self.raw_output) raw_layout.addWidget(self.raw_output, 1)
self.raw_clear_btn = QPushButton("Очистить логи") self.raw_clear_btn = QPushButton("Очистить логи")
self.raw_clear_btn.clicked.connect(self.clear_logs) self.raw_clear_btn.clicked.connect(self.clear_logs)
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft) raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
for button in (self.mem_write_btn, self.mem_read_btn, self.raw_send_btn,
self.raw_read_btn, self.raw_clear_btn):
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
raw_group.setLayout(raw_layout) raw_group.setLayout(raw_layout)
layout.addWidget(raw_group) layout.addWidget(raw_group, 1)
self.setLayout(layout) self.setLayout(layout)
def memory_write(self): def memory_write(self):

View File

@@ -403,24 +403,38 @@ class SerialTab(QWidget):
button.setFixedHeight(30) button.setFixedHeight(30)
button.setStyleSheet("QPushButton { padding: 3px 10px; }") button.setStyleSheet("QPushButton { padding: 3px 10px; }")
# Прокручиваемая область для кнопок макросов self.macros_splitter = QSplitter(Qt.Orientation.Horizontal)
self.macros_scroll_area = QScrollArea() self.macros_splitter.setObjectName("macrosSplitter")
self.macros_scroll_area.setWidgetResizable(True) self.macros_splitter.setChildrenCollapsible(False)
# Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов self.macros_splitter.setHandleWidth(8)
self.macros_scroll_area.setMinimumHeight(120) # Высота для 3-4 строк self.macros_splitter.setToolTip("Перетащите разделитель, чтобы изменить ширину панелей")
self.macros_scroll_areas = []
self.macros_scroll_content = QWidget() self.macros_scroll_layouts = []
self.macros_scroll_layout = QGridLayout() for side in ("Левая", "Правая"):
self.macros_scroll_content.setLayout(self.macros_scroll_layout) area = QScrollArea()
self.macros_scroll_area.setWidget(self.macros_scroll_content) area.setAccessibleName(f"{side} панель макросов")
self.macros_scroll_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) area.setWidgetResizable(True)
self.macros_scroll_layout.setContentsMargins(6, 6, 6, 6) area.setMinimumSize(120, 120)
self.macros_scroll_layout.setHorizontalSpacing(8) content = QWidget()
self.macros_scroll_layout.setVerticalSpacing(6) grid = QGridLayout(content)
macros_layout.addWidget(self.macros_scroll_area) grid.setAlignment(Qt.AlignTop | Qt.AlignLeft)
grid.setContentsMargins(6, 6, 6, 6)
grid.setVerticalSpacing(6)
area.setWidget(content)
self.macros_splitter.addWidget(area)
self.macros_scroll_areas.append(area)
self.macros_scroll_layouts.append(grid)
self.macros_splitter.setStretchFactor(0, 1)
self.macros_splitter.setStretchFactor(1, 1)
self.macros_splitter.setSizes([1, 1])
# Keep the existing attributes available to terminal integrations.
self.macros_scroll_area = self.macros_scroll_areas[0]
self.macros_scroll_content = self.macros_scroll_area.widget()
self.macros_scroll_layout = self.macros_scroll_layouts[0]
macros_layout.addWidget(self.macros_splitter, 1)
macros_group.setLayout(macros_layout) macros_group.setLayout(macros_layout)
layout.addWidget(macros_group) layout.addWidget(macros_group, 1)
# --- ВНЕ ГРУППЫ: Результаты чтения --- # --- ВНЕ ГРУППЫ: Результаты чтения ---
results_layout = QGridLayout() results_layout = QGridLayout()
@@ -447,7 +461,6 @@ class SerialTab(QWidget):
results_frame.setLayout(results_layout) results_frame.setLayout(results_layout)
layout.addWidget(results_frame) layout.addWidget(results_frame)
layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
# После создания интерфейса создаем кнопки для загруженных макросов # После создания интерфейса создаем кнопки для загруженных макросов
@@ -590,7 +603,7 @@ class SerialTab(QWidget):
container_layout.addWidget(delete_btn) container_layout.addWidget(delete_btn)
container.setLayout(container_layout) container.setLayout(container_layout)
self.macros_scroll_layout.addWidget(container, row, col, 1, 1) # span изменен на 1 self.macros_scroll_layouts[col].addWidget(container, row, 0)
self.macro_buttons.append(container) self.macro_buttons.append(container)
@@ -680,7 +693,7 @@ class SerialTab(QWidget):
def _set_macro_controls(self, running): def _set_macro_controls(self, running):
self.stop_macro_btn.setEnabled(running) self.stop_macro_btn.setEnabled(running)
for widget in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, for widget in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn,
self.macros_scroll_area, self.raw_widget): self.macros_splitter, self.raw_widget):
widget.setEnabled(not running) widget.setEnabled(not running)
def _next_macro_command(self, generation): def _next_macro_command(self, generation):
@@ -748,10 +761,11 @@ class SerialTab(QWidget):
def rebuild_macros_layout(self): def rebuild_macros_layout(self):
"""Перестроить layout макросов""" """Перестроить layout макросов"""
# Очищаем layout # Очищаем layout
for i in reversed(range(self.macros_scroll_layout.count())): for grid in self.macros_scroll_layouts:
item = self.macros_scroll_layout.itemAt(i) while grid.count():
if item and item.widget(): item = grid.takeAt(0)
item.widget().setParent(None) if item.widget():
item.widget().deleteLater()
# Очищаем список кнопок # Очищаем список кнопок
self.macro_buttons.clear() self.macro_buttons.clear()

View File

@@ -53,8 +53,8 @@ def decode(raw_id: int, data: bytes, *, ide: bool = True,
"0x%04X %s" % (item.address, item.name or "регистр"), "0x%04X %s" % (item.address, item.name or "регистр"),
"0x%04X (%d)" % (item.value, item.signed_value), "0x%04X (%d)" % (item.value, item.signed_value),
)) ))
if not frame.from_device and item.address == 0x7F: if item.address == 0x7F:
result.fields.append(("Команды ПМ", frame.command_description(item.value))) result.fields.append(("Активные биты Commands", frame.command_description(item.value)))
if frame.present_mask == 0: if frame.present_mask == 0:
result.warnings.append("В адресном слове не отмечен ни один регистр") result.warnings.append("В адресном слове не отмечен ни один регистр")
return result return result

View File

@@ -13,15 +13,59 @@
from __future__ import annotations from __future__ import annotations
import random import random
import math
from datetime import datetime from datetime import datetime
from . import can_transport as tr from . import can_transport as tr
from protocan import protocan as pc from protocan import protocan as pc
from protocan.gas_167 import REGISTERS
#: Типы сообщений, которые генератор выдаёт по кругу случайно. #: Типы сообщений, которые генератор выдаёт по кругу случайно.
KINDS = ("pulse", "analog", "modbus", "gas", "status", "error", "discrete") KINDS = ("pulse", "analog", "modbus", "gas", "status", "error", "discrete")
class UmpDemoSource:
"""Two simulated UMP nodes using the assigned global GAS map.
Call next_frames every 250 ms: four words per node per tick and a
heartbeat every second. Device Type=3, Device=5/6 are demo addresses,
not discovery results or a claim about firmware 167/v1 support.
"""
def __init__(self) -> None:
self._tick = 0
self._seq = 0
def _frame(self, node: int, kind: int, body: int, data: bytes) -> bytes:
self._seq = (self._seq + 1) & 0xFF
identifier = pc.ProtoCanId.build(1, 1, 3, node, kind, body)
return tr.Frame(seq=self._seq, flags=tr.FLAG_IDE,
can_id=identifier, data=data).encode()
def next_frames(self) -> list[bytes]:
"""Return encoded bridge frames; one full map takes eight seconds."""
packets = []
# Show LOAD state immediately, then walk the entire dictionary.
start = (124 + self._tick * 4) % len(REGISTERS)
for node, key in ((5, "ump1_id"), (6, "ump2_id")):
if self._tick % 4 == 0:
packets.append(self._frame(
node, pc.MsgType.PULSE, 0,
bytes([(self._tick // 4) & 0xFF])))
values = []
for entry in REGISTERS[start:start + 4]:
offset = entry["modbus"]
value = int(2000 + 700 * math.sin(self._tick / 12 + offset / 8 + node))
command = 0x80 if (self._tick // 32) % 2 == 0 else 0x40
value = {82: 1000, 124: 8, 125: 0, 126: node, 127: command}.get(offset, value)
values.append(value.to_bytes(2, "little"))
packets.append(self._frame(
node, pc.MsgType.GENERAL_ADDRESS_SPACE,
REGISTERS[start][key], b"".join(values)))
self._tick += 1
return packets
class BusDemoSource: class BusDemoSource:
"""@brief Источник демонстрационных кадров шины. """@brief Источник демонстрационных кадров шины.
@@ -58,11 +102,15 @@ class BusDemoSource:
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little") data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
for _ in range(cnt)) for _ in range(cnt))
elif kind == "gas": elif kind == "gas":
adr = self._rng.randint(0, 0x400) # Exercise the shared named map in the ordinary demo too.
dev_type, dev_id = 3, self._rng.choice((5, 6))
key = "ump1_id" if dev_id == 5 else "ump2_id"
count = self._rng.randint(1, 4)
adr = REGISTERS[self._rng.randrange(len(REGISTERS) - count + 1)][key]
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id,
pc.MsgType.GENERAL_ADDRESS_SPACE, adr) pc.MsgType.GENERAL_ADDRESS_SPACE, adr)
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little") data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
for _ in range(self._rng.randint(1, 4))) for _ in range(count))
elif kind == "status": elif kind == "status":
now = datetime.now() now = datetime.now()
cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.BROADCAST, cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.BROADCAST,

View File

@@ -32,6 +32,8 @@ from collections import deque
from dataclasses import dataclass, field from dataclasses import dataclass, field
from itertools import chain from itertools import chain
from typing import Iterable from typing import Iterable
from protocan import protocan as pc
from protocan.gas_167 import REGISTERS
#: Распространённые скорости UART для подключения платы и RS485-приёмника. #: Распространённые скорости UART для подключения платы и RS485-приёмника.
UART_BAUDS = ( UART_BAUDS = (
@@ -596,7 +598,14 @@ class BoardDemoSource:
""" """
#: Идентификаторы, по которым генератор идёт по кругу. #: Идентификаторы, по которым генератор идёт по кругу.
IDS = ((True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF)) IDS = (
(True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF),
# Assigned UMP map, simulated to demonstrate the common GAS decoder.
(True, pc.ProtoCanId.build(1, 1, 3, 5, pc.MsgType.GENERAL_ADDRESS_SPACE,
REGISTERS[124]['ump1_id'])),
(True, pc.ProtoCanId.build(1, 1, 3, 6, pc.MsgType.GENERAL_ADDRESS_SPACE,
REGISTERS[124]['ump2_id'])),
)
def __init__(self, rng: random.Random | None = None) -> None: def __init__(self, rng: random.Random | None = None) -> None:
self._rng = rng or random.Random() self._rng = rng or random.Random()

View File

@@ -16,6 +16,7 @@ from __future__ import annotations
from . import can_transport as tr from . import can_transport as tr
from protocan import protocan as pc from protocan import protocan as pc
from protocan.gas_167 import assigned_metadata
from . import balsam_can from . import balsam_can
#: Расшифровка поля LEC регистра CAN_ESR. #: Расшифровка поля LEC регистра CAN_ESR.
@@ -64,8 +65,27 @@ def decode_frame(frame: tr.Frame) -> pc.Decoded:
decoded_balsam = balsam_can.decode( decoded_balsam = balsam_can.decode(
frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
if decoded_balsam is not None: if decoded_balsam is not None:
decoded_balsam.fields.append(("Автоопределение", "Balsam 167: идентификатор legacy CAN"))
return decoded_balsam return decoded_balsam
return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
if (frame.ide and not frame.rtr
and result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE
and len(frame.data) <= 8 and len(frame.data) % 2 == 0
and result.registers is not None):
addresses = [address for address, _value in result.registers] or [result.id.body]
entries = [assigned_metadata(address) for address in addresses]
if all(entries) and len({entry['node'] for entry in entries}) == 1:
node = entries[0]['node'] - 4
result.protocol = "УМП / ProtoCAN GAS (назначенные ID)"
result.device_label = "УМП %d" % node
result.message_label = "GAS · УМП %d" % node
result.fields.append(("Автоопределение",
"УМП %d: все GAS ID входят в назначенную карту; поддержка прошивкой не подтверждена" % node))
return result
result.fields.append(("Автоопределение", "Разбор ProtoCAN; профиль прибора не определён"))
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
result.summary += " · профиль/узел не определены — выберите протокол вручную"
return result
dec = pc.Decoded(id=pc.ProtoCanId.parse(frame.can_id), dlc=len(frame.data), dec = pc.Decoded(id=pc.ProtoCanId.parse(frame.can_id), dlc=len(frame.data),
data=frame.data, ide=frame.ide, rtr=frame.rtr) data=frame.data, ide=frame.ide, rtr=frame.rtr)

View File

@@ -10,6 +10,7 @@ from dataclasses import dataclass
from protocan.legacycan import PROJECTS, LegacyCanFormat, decode from protocan.legacycan import PROJECTS, LegacyCanFormat, decode
from .can485_board import BoardFrame, SdLogImport, parse_sd_log from .can485_board import BoardFrame, SdLogImport, parse_sd_log
from .trend_limits import MAX_TOTAL_POINTS
@dataclass @dataclass
@@ -63,7 +64,7 @@ def _values(frame: BoardFrame):
def read_can_log(lines, *, reference_timestamp: float, capacity: int, def read_can_log(lines, *, reference_timestamp: float, capacity: int,
max_total_points: int = 1_000_000, max_total_points: int = MAX_TOTAL_POINTS,
frame_limit: int = 100_000) -> tuple[list[CanLogChannel], SdLogImport]: frame_limit: int = 100_000) -> tuple[list[CanLogChannel], SdLogImport]:
imported = parse_sd_log(lines, reference_timestamp, limit=frame_limit) imported = parse_sd_log(lines, reference_timestamp, limit=frame_limit)
channels = {} channels = {}

View File

@@ -10,14 +10,33 @@ from .can_bridge import decode_frame
PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"), PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"),
("devboard", "DevBoard_V1 / SETCAN"), ("devboard", "DevBoard_V1 / SETCAN"),
("deif", "DEIF XDi-192 / CANopen")) ("deif", "DEIF XDi-192 / CANopen"),
("protocan", "ProtoCAN / GAS"),
("ump_balsam", "УМП / Balsam 167 (legacy CAN)"),
("ump1_gas167", "УМП 1 / ProtoCAN — карта GAS 167/v1"),
("ump2_gas167", "УМП 2 / ProtoCAN — карта GAS 167/v1"),
("ump_gas", "УМП / ProtoCAN GAS — новые ID 5000/6000"))
def decode_protocol(frame, protocol="auto"): def decode_protocol(frame, protocol="auto"):
if protocol == "deif": if protocol == "deif":
from .deif_xdi import decode from .deif_xdi import decode
return decode(frame) return decode(frame)
if protocol == "devboard": if protocol == "ump_balsam":
from .balsam_can import decode
result = decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
if result is not None:
return result
result = pc.Decoded(pc.ProtoCanId.parse(frame.can_id), len(frame.data),
bytes(frame.data), ide=frame.ide, rtr=frame.rtr,
protocol="BALZAM 167", message_label="Не Balsam 167")
result.summary = "УМП / Balsam 167: CAN ID или формат кадра не соответствует протоколу"
result.warnings.append(result.summary)
return result
if protocol in ("ump1_gas167", "ump2_gas167"):
from .ump_can import decode_gas167
return decode_gas167(frame, node=1 if protocol == "ump1_gas167" else 2)
if protocol in ("devboard", "protocan", "ump_gas"):
return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
return decode_frame(frame) return decode_frame(frame)

View File

@@ -9,11 +9,35 @@ are separate objects, never channels in the acquisition or measurement model.
""" """
from __future__ import annotations from __future__ import annotations
import csv import csv
from bisect import bisect_left, bisect_right
from operator import itemgetter
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from .signal_reconstruction import METHODS, reconstruct from .signal_reconstruction import METHODS, reconstruct
class FrozenPoints(tuple):
"""Normalized immutable samples, reusable across processing requests."""
def __new__(cls, points):
if isinstance(points, cls):
return points
result = super().__new__(cls, ((float(x), float(y)) for x, y in points))
result.ordered = all(result[i - 1][0] <= result[i][0] for i in range(1, len(result)))
return result
def between(self, left, right):
if self.ordered:
start = bisect_left(self, left, key=itemgetter(0))
stop = bisect_right(self, right, key=itemgetter(0))
if start == 0 and stop == len(self):
return self
result = tuple.__new__(type(self), self[start:stop])
result.ordered = True
return result
return FrozenPoints((x, y) for x, y in self if left <= x <= right)
@dataclass(frozen=True) @dataclass(frozen=True)
class Axis: class Axis:
label: str = "Время" label: str = "Время"
@@ -36,7 +60,7 @@ class Series:
def __post_init__(self): def __post_init__(self):
# A source may reuse mutable buffers immediately after publication. # A source may reuse mutable buffers immediately after publication.
object.__setattr__(self, "points", tuple((float(x), float(y)) for x, y in self.points)) object.__setattr__(self, "points", FrozenPoints(self.points))
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -103,7 +127,7 @@ def prepare(snapshot, key, method="pchip", output_count=1000, degree=2):
if snapshot.x_range is not None: if snapshot.x_range is not None:
left, right = snapshot.x_range left, right = snapshot.x_range
selected = Series(selected.key, selected.label, selected = Series(selected.key, selected.label,
tuple((x, y) for x, y in selected.points if left <= x <= right), selected.points.between(left, right),
y_unit=selected.y_unit) y_unit=selected.y_unit)
return Request(selected, snapshot.axis, snapshot.source, method, output_count, degree, snapshot.x_range) return Request(selected, snapshot.axis, snapshot.source, method, output_count, degree, snapshot.x_range)

View File

@@ -22,6 +22,12 @@ class MonitorControls(QWidget):
self.protocol = QComboBox() self.protocol = QComboBox()
for key, label in PROTOCOLS: for key, label in PROTOCOLS:
self.protocol.addItem(label, key) self.protocol.addItem(label, key)
self.protocol.setMinimumContentsLength(28)
self.protocol.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
self.protocol.setToolTip(
"Выберите формат CAN вручную. УМП / Balsam — legacy CAN; "
"карта 167/v1 — имена слов 0…127 и 0x5000…0x507F в ProtoCAN; "
"новые ID — назначенные диапазоны 5000…5127 и 6000…6127.")
self.protocol.setCurrentIndex(self.protocol.findData(default)) self.protocol.setCurrentIndex(self.protocol.findData(default))
layout.addWidget(QLabel("Протокол")) layout.addWidget(QLabel("Протокол"))
layout.addWidget(self.protocol) layout.addWidget(self.protocol)

View File

@@ -11,6 +11,9 @@ from protocan.plot import Bounds, Viewport
def small_amplitude(values, threshold=30): def small_amplitude(values, threshold=30):
extent = getattr(values, "finite_extent", None)
if extent is not None:
return max(abs(extent[0]), abs(extent[1])) <= threshold
magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None) magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None)
return magnitude is not None and magnitude <= threshold return magnitude is not None and magnitude <= threshold
@@ -52,7 +55,8 @@ class DualAxisPlot:
for series in self._series: for series in self._series:
if series.visible and not series.discrete: if series.visible and not series.discrete:
groups[series.key in self.right_axis_keys].extend( groups[series.key in self.right_axis_keys].extend(
v for v in series.values if math.isfinite(v)) getattr(series.values, "finite_extent", None) or
(v for v in series.values if math.isfinite(v)))
ranges = [] ranges = []
for values in groups: for values in groups:
low, high = (min(values), max(values)) if values else (0, 1) low, high = (min(values), max(values)) if values else (0, 1)

View File

@@ -21,7 +21,7 @@ except ImportError:
QVBoxLayout, QWidget, QVBoxLayout, QWidget,
) )
from set_devices.signal_reconstruction import METHODS from set_devices.signal_reconstruction import MAX_OUTPUT_POINTS, METHODS
from set_devices.plot_processing import Snapshot, prepare, process, write_csv from set_devices.plot_processing import Snapshot, prepare, process, write_csv
RESULT_COLORS = ("#FF70D0", "#50D5FF", "#FFD166", "#83E377", "#B99AFF", "#FF9870") RESULT_COLORS = ("#FF70D0", "#50D5FF", "#FFD166", "#83E377", "#B99AFF", "#FF9870")
@@ -88,8 +88,8 @@ class SignalProcessingPanel(QWidget):
row.addWidget(self.degree) row.addWidget(self.degree)
row.addWidget(QLabel("Точек результата")) row.addWidget(QLabel("Точек результата"))
self.count = QSpinBox() self.count = QSpinBox()
self.count.setRange(2, 10000) self.count.setRange(2, MAX_OUTPUT_POINTS)
self.count.setValue(1000) self.count.setValue(10000)
row.addWidget(self.count) row.addWidget(self.count)
self.apply_button = QPushButton("Рассчитать") self.apply_button = QPushButton("Рассчитать")
self.apply_button.clicked.connect(self.calculate) self.apply_button.clicked.connect(self.calculate)

View File

@@ -9,6 +9,8 @@ from dataclasses import dataclass
from functools import lru_cache from functools import lru_cache
from protocan.native import NativeProtocol from protocan.native import NativeProtocol
MAX_OUTPUT_POINTS = 1_000_000
METHODS = {"polynomial": "Аппроксимация: полином МНК", "linear": "Интерполяция: линейная", METHODS = {"polynomial": "Аппроксимация: полином МНК", "linear": "Интерполяция: линейная",
"pchip": "Интерполяция: PCHIP", "spline": "Восстановление: кубический сплайн"} "pchip": "Интерполяция: PCHIP", "spline": "Восстановление: кубический сплайн"}
@@ -35,8 +37,8 @@ def library():
raise ValueError("Пересоберите SETProtocol с set_signal.c и set_wavegen.c") from error raise ValueError("Пересоберите SETProtocol с set_signal.c и set_wavegen.c") from error
def reconstruct(points, method="pchip", output_count=1000, degree=2, *, endpoint=True, with_model=False): def reconstruct(points, method="pchip", output_count=1000, degree=2, *, endpoint=True, with_model=False):
if method not in METHODS or type(output_count) is not int or not 2 <= output_count <= 10000: if method not in METHODS or type(output_count) is not int or not 2 <= output_count <= MAX_OUTPUT_POINTS:
raise ValueError("Неизвестный метод или число выходных точек вне 2…10000") raise ValueError(f"Неизвестный метод или число выходных точек вне 2…{MAX_OUTPUT_POINTS}")
count = len(points) count = len(points)
if not 2 <= count <= 100000 or type(degree) is not int or not 1 <= degree <= 5: if not 2 <= count <= 100000 or type(degree) is not int or not 1 <= degree <= 5:
raise ValueError("Нужно 2…100000 исходных точек, степень 1…5") raise ValueError("Нужно 2…100000 исходных точек, степень 1…5")

View File

@@ -0,0 +1,5 @@
"""Shared limits for host-side trend history buffers."""
DEFAULT_CAPACITY = 100_000
MAX_CAPACITY = 5_000_000
MAX_TOTAL_POINTS = 10_000_000

View File

@@ -0,0 +1,50 @@
"""Explicit interpretation of ProtoCAN registers with the UMP 167/v1 map.
This is a manually selected dictionary, not detection of the legacy service
CAN packet format or a claim that a particular firmware implements ProtoCAN.
"""
from protocan import protocan as pc
from protocan.gas_167 import REGISTERS, describe_word
def decode_gas167(frame, node):
result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr)
result.device_label = "УМП %d (выбран вручную)" % node
result.protocol = "УМП %d / ProtoCAN GAS 167/v1" % node
if not frame.ide or frame.rtr or frame.is_error:
return result
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE,
pc.MsgType.MODBUS_HOLDING):
return result
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
start = result.id.body
else:
start, _count = pc.split_modbus(result.id.body)
words = result.registers
if words is None:
return result
labels = []
for address, value in words or [(start, None)]:
offset = address if 0 <= address < 128 else address - 0x5000
label = "0x%04X" % address
if 0 <= offset < len(REGISTERS):
entry = REGISTERS[offset]
label += " · " + entry['description']
result.fields += [
(" УМП 167/v1 0x%04X" % address, entry['description']),
(" Назначенные ID УМП 1/2", "%d / %d" % (entry['ump1_id'], entry['ump2_id'])),
(" Доступ", entry['access']),
]
else:
label += " · вне карты 167/v1"
if value is not None:
label += " = %d [0x%04X]" % (value, value)
description = describe_word(offset, value)
if description:
label += " — " + description
result.fields.append((" Биты 0x%04X" % address, description))
labels.append(label)
result.summary = "УМП %d · GAS 167/v1%s: %s" % (
node, " запрос" if not words else "", "; ".join(labels))
result.fields.append((" Карта", "167/v1 выбрана вручную"))
return result

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