From 90046de7fd4a220f11da9e3a5b76c1afae2b4df6 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 5 Oct 2026 17:48:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D0=B4=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D1=80=D0=BE=D1=88=D0=B8=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?EEPROM=20=D0=91=D0=B0=D0=BB=D1=8C=D0=B7=D0=B0=D0=BC-167?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/firmware-info/BUILD_IDENTITY.md | 275 ++++++++++++++++++ c/firmware-info/CMakeLists.txt | 5 + c/firmware-info/PORTING.md | 4 + c/firmware-info/README.md | 4 + c/firmware-info/include/firmware_info.h | 16 +- .../include/firmware_info_static.h | 22 ++ .../stm32/firmware_info_config.template.h | 8 + .../firmware_info_config.template.h | 7 + .../firmware_info_config.template.h | 7 + c/firmware-info/src/firmware_info.c | 30 +- c/firmware-info/src/firmware_info_port.c | 8 + c/firmware-info/src/firmware_info_static.c | 15 + .../tests/fixtures/firmware_identity.h | 19 ++ c/firmware-info/tests/test_identity.py | 74 +++++ c/firmware-info/tests/test_static.c | 53 ++++ c/firmware-info/tools/identity.py | 124 ++++++++ c/set-protocol/CMakeLists.txt | 8 + c/set-protocol/README.md | 6 + .../include/balsam167_eeprom_layout.h | 17 ++ c/set-protocol/include/pcan_abi.h | 9 + c/set-protocol/include/periph28335.h | 25 ++ c/set-protocol/include/ump_protocol.h | 4 + .../ports/tms320f28335/ump_tms28335.c | 12 +- c/set-protocol/src/pcan_abi.c | 11 + c/set-protocol/src/periph28335.c | 54 ++++ c/set-protocol/src/ump_protocol.c | 5 + .../tests/test_periph28335_eeprom.c | 64 ++++ python/protocan/periph28335_eeprom.py | 60 ++++ 28 files changed, 924 insertions(+), 22 deletions(-) create mode 100644 c/firmware-info/BUILD_IDENTITY.md create mode 100644 c/firmware-info/include/firmware_info_static.h create mode 100644 c/firmware-info/ports/stm32/firmware_info_config.template.h create mode 100644 c/firmware-info/ports/tms320f2812/firmware_info_config.template.h create mode 100644 c/firmware-info/ports/tms320f28335/firmware_info_config.template.h create mode 100644 c/firmware-info/src/firmware_info_static.c create mode 100644 c/firmware-info/tests/fixtures/firmware_identity.h create mode 100644 c/firmware-info/tests/test_identity.py create mode 100644 c/firmware-info/tests/test_static.c create mode 100644 c/firmware-info/tools/identity.py create mode 100644 c/set-protocol/include/balsam167_eeprom_layout.h create mode 100644 c/set-protocol/tests/test_periph28335_eeprom.c create mode 100644 python/protocan/periph28335_eeprom.py diff --git a/c/firmware-info/BUILD_IDENTITY.md b/c/firmware-info/BUILD_IDENTITY.md new file mode 100644 index 0000000..202fd06 --- /dev/null +++ b/c/firmware-info/BUILD_IDENTITY.md @@ -0,0 +1,275 @@ +# Версия работающей прошивки и SHA-256 файла: TMS2812, TMS28335, STM32 + +## 1. Какие задачи решает механизм + +Механизм продолжает контракт Climate: устройство сообщает SemVer, дату/время +сборки и Git Build ID в 12 регистрах. Компьютер после сборки вычисляет SHA-256 +готового файла и сохраняет паспорт `*.manifest.json` и файл `*.sha256`. + +Это три разных идентификатора: + +| Идентификатор | Что определяет | Где хранится | +|---|---|---| +| `major.minor.patch` | Назначенный разработчиком номер версии | В образе и паспорте | +| Git Build ID | Исходный коммит; `+` отмечает локальные изменения | В образе и паспорте | +| SHA-256 | Точные байты конкретного BIN/HEX, включая заголовки | Рядом с образом | + +SHA Git-коммита и SHA-256 файла прошивки не взаимозаменяемы. Один коммит может +давать разные бинарные файлы при смене компилятора, опций, времени сборки или +зависимостей. Полные коммиты приложения и templates записываются в паспорт. + +Контроллер не вычисляет SHA своей памяти. Ответ версии подтверждает, какие +метаданные содержит работающий образ; SHA файла проверяет файл на компьютере. +Для проверки фактической записи нужна отдельная штатная проверка программатора +или чтение памяти назад. SHA рядом с файлом не является цифровой подписью. + +## 2. Слои и файлы + +```text +firmware_identity.json проекта + Git приложения + Git templates + | + tools/identity.py prepare (до компиляции) + | + generated/firmware_identity.h + .json + | + src/firmware_info_static.c (общий C-код) + | + 12 неизменяемых слов -> порт транспорта -> клиент + +линковка -> преобразование BIN -> проверка размера -> identity.py seal + | + BIN + BIN.manifest.json + BIN.sha256 +``` + +| Файл | Задача и зависимости | +|---|---| +| `include/firmware_info_static.h` | Контракт компактного порта и упаковка слов, только `stdint.h` | +| `src/firmware_info_static.c` | Константная таблица и чтение одного слова; нужен generated header | +| `include/firmware_info.h`, `src/firmware_info.c` | Существующий динамический API Climate, валидация и сериализация | +| `src/firmware_info_port.c` | Динамический адаптер конфигурации; поддерживает новый UTC stamp | +| `ports/tms320f2812`, `ports/tms320f28335`, `ports/stm32` | Примеры конфигурации для нового генератора | +| `tools/identity.py` | Git, JSON, SHA-256 файла; Python 3.10+ и Git, без pip-зависимостей | +| `tests/test_static.c` | Совпадение компактного и динамического контрактов, чтение RS/CAN, запрет записи | +| `tests/test_identity.py` | Генерация, dirty-зависимость, отсутствие Git, порча файла | + +Ядро не знает HAL, CMSIS, XINTF, EEPROM, UART, CAN-регистры и адрес устройства. +`firmware_info_static_read(unsigned index)` принимает индекс **0..11**, возвращает +`uint16_t`; вне диапазона возвращает 0. Проверять диапазон запроса и запрещать +запись обязан транспорт. Нулевую версию контракта клиент показывает как +«версия неизвестна», а не подставляет версию из выбранного файла. + +## 3. Конфигурация проекта и генерация + +В репозитории приложения создайте `firmware_identity.json`: + +```json +{ + "product": "MyController", + "target": "TMS320F2812", + "version": [1, 0, 0] +} +``` + +`product` и `target` нужны для паспорта файла. В контракт v1 они не входят: +тип подключённого устройства по-прежнему определяет его собственный протокол. +Не используйте совпадение версии как разрешение залить образ другого изделия. + +До каждой компиляции выполните (пути приведены для checkout в `lib/templates`): + +```powershell +python lib/templates/c/firmware-info/tools/identity.py prepare ` + --repository . --templates lib/templates ` + --config firmware_identity.json --output build/generated +``` + +Добавьте `build/generated` и `lib/templates/c/firmware-info/include` в include +paths. Скомпилируйте **один экземпляр** +`lib/templates/c/firmware-info/src/firmware_info_static.c`. Если проект включает +этот файл через агрегирующий `.c`, отдельно добавлять его в сборку нельзя. + +Генератор намеренно переписывает header при каждом запуске: зависимый объект +должен пересобраться. Отсутствие header — ошибка компиляции, а не `LOCALDEV`. +Нужна корректная обработка зависимостей сборщиком; после изменения настроек IDE +выполните полную пересборку. Не редактируйте исходники во время сборки. + +Для разных конфигураций и параллельных сборок используйте разные `generated` +каталоги. Не запускайте две сборки с одним output одновременно. + +Время в новом пути — **UTC**, снято перед компиляцией. Если задан +`SOURCE_DATE_EPOCH`, используется этот Unix timestamp; это помогает повторяемым +сборкам, но не делает весь toolchain автоматически воспроизводимым. + +### Значения Build ID + +| Значение | Смысл | +|---|---| +| `a1b2c3d4` | Приложение и templates доступны в Git и оба дерева чистые | +| `a1b2c3d+` | Изменено приложение или templates, включая неотслеживаемые файлы | +| `NOGIT000` | Не удалось определить хотя бы один из двух Git-репозиториев | +| `LOCALDEV` | Только старый динамический путь без generated header | + +Полные commit SHA находятся в `source.commit` и `templates.commit` паспорта. +`+` не идентифицирует содержимое локальных правок. Точный файл определяет его +SHA-256; для воспроизводимого выпуска нужны сохранённые исходники и настройки. +Добавляйте generated/build каталоги в `.gitignore`, иначе они будут честно +делать следующую сборку dirty. Намеренно не игнорируйте новые исходные файлы. + +## 4. Формат: одинаковые 12 слов на всех МК + +| Индекс | Значение | +|---:|---| +| 0 | Версия контракта: 1 | +| 1 | Major, 0..255 | +| 2 | Minor, 0..255 | +| 3 | Patch, 0..999 | +| 4 | Год, 2000..2199 | +| 5 | Месяц в старшем октете, день в младшем | +| 6 | Час в старшем октете, минута в младшем | +| 7 | Секунда | +| 8 | ASCII Build ID: символы 0 и 1, первый в старшем октете | +| 9 | Символы 2 и 3 | +| 10 | Символы 4 и 5 | +| 11 | Символы 6 и 7 | + +Например, `abcdef0+` даёт `0x6162, 0x6364, 0x6566, 0x302B` в словах 8..11. +Modbus передаёт каждое слово старшим октетом вперёд. Старый +`firmware_info_to_le_bytes()` передаёт слова little-endian для соответствующих +byte-протоколов; эти два представления нельзя путать. + +На TMS320F2812/F28335 `CHAR_BIT == 16`, `sizeof(uint16_t) == 1`, тип `uint8_t` +отсутствует. В динамическом API используется `firmware_info_octet`, основанный +на `uint_least8_t`: на STM32 это прежние 8 бит, на C28x — контейнер 16 бит. +Размеры byte-API означают число **элементов-октетов**, не физический `sizeof` +переданного массива. На проводе всё равно 24 восьмибитных октета. + +## 5. Порты и включение в транспорт + +### TMS320F28335 / ПМ35 + +В 167 уже подключено через `Source/Internal/shared_ump.c`. Макрос +`UMP_FIRMWARE_INFO_READ` связывает общий порт UMP с +`firmware_info_static_read`. Диапазон `0x1200..0x120B` обслуживается до +обращения к регистратору. Окно параметров `0..127` и лог не расширяются. + +RS: функция 03, начальный адрес `0x1200` (4608), количество 12. +Разрешены частичные чтения, не выходящие за конец окна. Функция 06 возвращает +исключение 02, не изменяя ни версию, ни настройки регистратора. + +Служебный CAN ПМ35: тот же UMP-запрос функции 03 на `0x1200`, количество 12, +ненулевой token. Ответ — шесть фрагментов по два слова с тем же token. +Частичные CAN-чтения этого окна отвергаются. Используйте существующие UMP +сборщики/парсеры templates, не создавайте второй кодек в GUI. + +### TMS320F2812 / ПМ67 + +Добавьте тот же `firmware_info_static.c` и include paths. Компактный порт не +требует FPU и не использует регистры F28335. Конфигурация-пример лежит в +`ports/tms320f2812/firmware_info_config.template.h`. + +Выберите свободное окно в карте **конкретного ПМ67**. В обработчике чтения +отдавайте `firmware_info_static_read(address - base)` после проверки диапазона. +Не переносите автоматически CAN ID и UMP ПМ35: у ПМ67 отдельный протокол. +Это поставляемый переносимый порт, а не заявление, что конкретная прошивка +ПМ67 уже изменена или испытана на устройстве. + +### STM32 / Modbus или SET + +Компактный порт одинаков для F1/F4/G4. Для HAL он не требует ни инициализации, +ни callback. Пример адаптации чтения **после проверки диапазона транспортом**: + +```c +#include "firmware_info_static.h" + +/* FW_BASE выбирает проект; соответствие адресов проверяет Modbus/SET handler. */ +uint16_t app_firmware_word(uint16_t address) +{ + unsigned index = (unsigned)(address - FW_BASE); + return firmware_info_static_read(index); +} +``` + +Для STM32 UMP-периферии можно использовать уже существующий +`set-protocol/ports/stm32f407-periph` и подставить этот reader в `ump_backend`. +Для обычного Modbus используйте собственное свободное input/holding read-only +окно, сохраняя 12 слов контракта. В Climate адрес был 1960; это **не** адрес +167. Если удобнее заполнить RAM-регистры при старте, оставьте динамический API: +`firmware_info_port_describe()` и `firmware_info_to_registers()`. + +## 6. SHA-256 и паспорт после сборки + +Сначала завершите линковку и преобразование в файл, который реально отправляется +загрузчику. Затем проверьте формат и допустимый размер. Только после успеха: + +```powershell +python lib/templates/c/firmware-info/tools/identity.py seal ` + --image build/controller.bin --metadata build/generated/firmware_identity.json +``` + +Результат: + +```text +controller.bin +controller.bin.manifest.json +controller.bin.sha256 +``` + +Паспорт содержит product, target, version, build_id, built_at_utc, полные commit +SHA и dirty-состояния двух репозиториев, имя файла, размер и SHA-256. Передавайте +эти три файла вместе. `seal` не извлекает версию из произвольного бинарника: +metadata должен быть из **той же сборки**. Нельзя перепривязать старый BIN к +новому pre-build JSON. Проверка версии на работающей плате замыкает этот цикл. + +Проверка точного файла: + +```powershell +python lib/templates/c/firmware-info/tools/identity.py verify ` + --image build/controller.bin --manifest build/controller.bin.manifest.json +``` + +При изменении даже одного байта команда завершится ненулевым кодом. SHA считается +для всего файла: для SCI8 — вместе с boot table, для HEX — для текстового HEX, +а не для нормализованного содержимого Flash. Выбирайте BIN для однозначного +сравнения с бинарным файлом программатора. + +## 7. Выпуск и проверка на устройстве + +1. Назначьте SemVer в конфигурации приложения. +2. Сохраните изменения templates отдельным коммитом и закрепите именно эту + зависимость в потребителе. Затем сохраните исходники приложения. +3. Убедитесь, что оба дерева чистые; соберите в отдельном output. +4. Проверьте ограничения образа, наличие паспорта и результат `verify`. +5. Загрузите именно этот файл штатным загрузчиком и перезапустите устройство. +6. Прочитайте версию с устройства заново, отключив кэш клиента. +7. Сравните все 12 слов с SemVer, UTC stamp и Build ID паспорта. +8. Сохраните BIN, паспорт, настройки компилятора и результаты проверки вместе. + +Изменение SemVer не должно менять `contract_version`: последняя меняется только +при несовместимом изменении формата. При несовпадении даты/Build ID сначала +проверьте выбранный BIN, конфигурацию IDE, generated include path и полную +пересборку, затем адрес и кэш клиента. + +## 8. Автоматические проверки + +```powershell +cmake -S c/firmware-info -B build/firmware-info +cmake --build build/firmware-info +ctest --test-dir build/firmware-info --output-on-failure +python -m unittest discover -s c/firmware-info/tests -p test_identity.py +``` + +Дополнительно компилируйте core/static через TI C2000 (`-v28 -ml`) и ARM +toolchain. Host-тест не заменяет проверку C28x ABI и фактического обмена с платой. +Для 167 есть отдельный `tests/firmware_identity/port_test.c`: он проверяет +реальную привязку reader, RTU CRC, CAN prepare и сохранение окна регистратора. + +## 9. Совместимость с существующими проектами + +Старый `make_build_id.ps1`, конфигурации STM32F1/F4/G4 и динамический API остаются. +Новый генератор не подключается к другим проектам сам: добавьте pre/post-build +и generated include path по этой инструкции. На STM32 размеры публичных полей +и сигнатура byte-буфера остаются прежними благодаря `uint_least8_t`. + +Изменения сделаны в checkout templates, который уже использует 167: +`newProject/SETGUI/third_party/templates`. Другие checkout/submodule не следует +обновлять копированием отдельных `.c`: перенесите проверенный коммит templates +и обновите ссылку зависимости. ПМ67 и ПМ35 сохраняют разные транспортные порты. diff --git a/c/firmware-info/CMakeLists.txt b/c/firmware-info/CMakeLists.txt index 80e2680..4aad4c4 100644 --- a/c/firmware-info/CMakeLists.txt +++ b/c/firmware-info/CMakeLists.txt @@ -10,3 +10,8 @@ add_executable(test_firmware_info tests/test_firmware_info.c) target_link_libraries(test_firmware_info PRIVATE firmware_info) enable_testing() add_test(NAME firmware_info COMMAND test_firmware_info) +add_executable(test_firmware_info_static tests/test_static.c src/firmware_info_static.c + ../set-protocol/src/ump_protocol.c) +target_include_directories(test_firmware_info_static PRIVATE tests/fixtures ../set-protocol/include) +target_link_libraries(test_firmware_info_static PRIVATE firmware_info) +add_test(NAME firmware_info_static COMMAND test_firmware_info_static) diff --git a/c/firmware-info/PORTING.md b/c/firmware-info/PORTING.md index b112be8..27d8fa0 100644 --- a/c/firmware-info/PORTING.md +++ b/c/firmware-info/PORTING.md @@ -1,5 +1,9 @@ # Портирование firmware-info +Для компактного пути без парсера даты, TMS320F2812/F28335 и STM32, а также +SHA-256/паспортов сборки используйте [BUILD_IDENTITY.md](BUILD_IDENTITY.md). +Ниже сохранена инструкция для совместимого динамического API. + ## Назначение и границы Это C-библиотека описания работающего образа: SemVer, дата/время компиляции, diff --git a/c/firmware-info/README.md b/c/firmware-info/README.md index 5b4eefe..8c35221 100644 --- a/c/firmware-info/README.md +++ b/c/firmware-info/README.md @@ -1,5 +1,9 @@ # Firmware Info +Компактный вариант для TMS320F2812, TMS320F28335 и STM32, общий pre/post-build, +SHA-256 файла и паспорт сборки: [подробная инструкция](BUILD_IDENTITY.md). +Он сохраняет 12-словный контракт Climate и не требует парсера даты на MCU. + Переносимая библиотека версии реально запущенной прошивки. Повторяет контракт `FirmwareInfo` из `john103C6T6NewVer`, но оформлена как template: ядро не знает ни о HAL, ни о конкретном протоколе, ни о модели МК. diff --git a/c/firmware-info/include/firmware_info.h b/c/firmware-info/include/firmware_info.h index bc2961b..be69019 100644 --- a/c/firmware-info/include/firmware_info.h +++ b/c/firmware-info/include/firmware_info.h @@ -10,6 +10,10 @@ #include #include +/* Least-width octet storage preserves STM ABI, and compiles on C28x where + * CHAR_BIT=16 and uint8_t does not exist. Values sent on wire are masked to 8 bits. */ +typedef uint_least8_t firmware_info_octet; + #ifdef __cplusplus extern "C" { #endif @@ -24,11 +28,11 @@ typedef struct { uint16_t minor; uint16_t patch; uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; + firmware_info_octet month; + firmware_info_octet day; + firmware_info_octet hour; + firmware_info_octet minute; + firmware_info_octet second; char build_id[FIRMWARE_INFO_BUILD_ID_SIZE + 1U]; } firmware_info_t; @@ -56,7 +60,7 @@ firmware_info_status_t firmware_info_set_build_id( firmware_info_status_t firmware_info_to_registers( const firmware_info_t *info, uint16_t *registers, size_t count); firmware_info_status_t firmware_info_to_le_bytes( - const firmware_info_t *info, uint8_t *payload, size_t size); + const firmware_info_t *info, firmware_info_octet *payload, size_t size); #ifdef __cplusplus } diff --git a/c/firmware-info/include/firmware_info_static.h b/c/firmware-info/include/firmware_info_static.h new file mode 100644 index 0000000..42d85cd --- /dev/null +++ b/c/firmware-info/include/firmware_info_static.h @@ -0,0 +1,22 @@ +/* Small, immutable firmware identity for targets with little code space. + * The build tool supplies scalar constants; the C core owns register packing. + * No startup parser, RAM copy, heap, HAL or runtime SHA implementation is needed. + * Include firmware_identity.h (generated BEFORE compilation) before this file. + */ +#ifndef FIRMWARE_INFO_STATIC_H +#define FIRMWARE_INFO_STATIC_H +#include + +#define FIRMWARE_INFO_STATIC_COUNT 12U +/* Cast BEFORE shifting: an int is only 16 bits on both C28x targets. */ +#define FI_PAIR(high, low) ((uint16_t)(((uint16_t)(high) << 8U) | (uint16_t)(low))) +#define FIRMWARE_INFO_STATIC_INITIALIZER { \ + 1U, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCH, \ + FI_YEAR, FI_PAIR(FI_MONTH, FI_DAY), FI_PAIR(FI_HOUR, FI_MINUTE), FI_SECOND, \ + FI_PAIR(FI_ID_0, FI_ID_1), FI_PAIR(FI_ID_2, FI_ID_3), \ + FI_PAIR(FI_ID_4, FI_ID_5), FI_PAIR(FI_ID_6, FI_ID_7) } + +/* Return zero outside the contract. The transport must reject invalid ranges + * before calling this function; zero is NOT an invented firmware version. */ +uint16_t firmware_info_static_read(unsigned index); +#endif diff --git a/c/firmware-info/ports/stm32/firmware_info_config.template.h b/c/firmware-info/ports/stm32/firmware_info_config.template.h new file mode 100644 index 0000000..bb113f0 --- /dev/null +++ b/c/firmware-info/ports/stm32/firmware_info_config.template.h @@ -0,0 +1,8 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +/* Family-independent STM32 static port: no HAL/CMSIS dependency. + * F1/F4/G4 applications may use this instead of the runtime date parser. + * Generate the header before compilation, then compile firmware_info_static.c. + * Modbus sends each returned word high octet first; SET uses its own encoder. */ +#include "firmware_identity.h" +#endif diff --git a/c/firmware-info/ports/tms320f2812/firmware_info_config.template.h b/c/firmware-info/ports/tms320f2812/firmware_info_config.template.h new file mode 100644 index 0000000..1d82c11 --- /dev/null +++ b/c/firmware-info/ports/tms320f2812/firmware_info_config.template.h @@ -0,0 +1,7 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +/* Same C28x word representation as F28335, but transport remains PM67-specific. + * Do not route PM67 commands through the PM35 logger/CAN adapter. + * Compile firmware_info_static.c and expose its 12 words in a read-only window. */ +#include "firmware_identity.h" +#endif diff --git a/c/firmware-info/ports/tms320f28335/firmware_info_config.template.h b/c/firmware-info/ports/tms320f28335/firmware_info_config.template.h new file mode 100644 index 0000000..6102cb2 --- /dev/null +++ b/c/firmware-info/ports/tms320f28335/firmware_info_config.template.h @@ -0,0 +1,7 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +/* Generate before EVERY build. C28x has 16-bit char and no uint8_t. + * For the compact port compile firmware_info_static.c, not the date parser. + * The same generated header can also configure firmware_info_port.c. */ +#include "firmware_identity.h" +#endif diff --git a/c/firmware-info/src/firmware_info.c b/c/firmware-info/src/firmware_info.c index db3fbf8..1e91ffa 100644 --- a/c/firmware-info/src/firmware_info.c +++ b/c/firmware-info/src/firmware_info.c @@ -13,20 +13,20 @@ static const char months[12][4] = { static int is_digit(char value) { return (value >= '0') && (value <= '9'); } -static int two_digits(const char *text, uint8_t *result) +static int two_digits(const char *text, firmware_info_octet *result) { char high = text[0]; char low = text[1]; if (high == ' ') high = '0'; if (!is_digit(high) || !is_digit(low)) return 0; - *result = (uint8_t)(((uint8_t)(high - '0') * 10U) + (uint8_t)(low - '0')); + *result = (firmware_info_octet)(((firmware_info_octet)(high - '0') * 10U) + (firmware_info_octet)(low - '0')); return 1; } static int four_digits(const char *text, uint16_t *result) { uint16_t value = 0U; - uint8_t i; + firmware_info_octet i; for (i = 0U; i < 4U; ++i) { if (!is_digit(text[i])) return 0; value = (uint16_t)(value * 10U + (uint16_t)(text[i] - '0')); @@ -37,7 +37,7 @@ static int four_digits(const char *text, uint16_t *result) firmware_info_status_t firmware_info_validate(const firmware_info_t *info) { - uint8_t i; + firmware_info_octet i; if (info == NULL) return FIRMWARE_INFO_INVALID_ARGUMENT; if ((info->major > 255U) || (info->minor > 255U) || (info->patch > 999U) || (info->year < 2000U) || (info->year > 2199U) @@ -56,7 +56,7 @@ firmware_info_status_t firmware_info_validate(const firmware_info_t *info) firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info, const char *build_id) { - uint8_t i; + firmware_info_octet i; if ((info == NULL) || (build_id == NULL)) return FIRMWARE_INFO_INVALID_ARGUMENT; for (i = 0U; i < FIRMWARE_INFO_BUILD_ID_SIZE && build_id[i] != '\0'; ++i) { if ((build_id[i] < 0x20) || (build_id[i] > 0x7e)) @@ -71,8 +71,8 @@ firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info, firmware_info_status_t firmware_info_parse_build_stamp( const char *date_text, const char *time_text, firmware_info_t *info) { - uint8_t month; - uint8_t found = 0U; + firmware_info_octet month; + firmware_info_octet found = 0U; if ((date_text == NULL) || (time_text == NULL) || (info == NULL)) return FIRMWARE_INFO_INVALID_ARGUMENT; for (month = 0U; month < 12U; ++month) { @@ -84,14 +84,14 @@ firmware_info_status_t firmware_info_parse_build_stamp( || !two_digits(&time_text[0], &info->hour) || !two_digits(&time_text[3], &info->minute) || !two_digits(&time_text[6], &info->second)) return FIRMWARE_INFO_OUT_OF_RANGE; - info->month = (uint8_t)(month + 1U); + info->month = (firmware_info_octet)(month + 1U); return FIRMWARE_INFO_OK; } firmware_info_status_t firmware_info_to_registers( const firmware_info_t *info, uint16_t *registers, size_t count) { - uint8_t i; + firmware_info_octet i; firmware_info_status_t status; if ((registers == NULL) || (count < FIRMWARE_INFO_REGISTER_COUNT)) return FIRMWARE_INFO_INVALID_ARGUMENT; @@ -104,24 +104,24 @@ firmware_info_status_t firmware_info_to_registers( registers[6] = (uint16_t)(((uint16_t)info->hour << 8U) | info->minute); registers[7] = info->second; for (i = 0U; i < 4U; ++i) - registers[8U + i] = (uint16_t)(((uint16_t)(uint8_t)info->build_id[i * 2U] << 8U) - | (uint8_t)info->build_id[i * 2U + 1U]); + registers[8U + i] = (uint16_t)(((uint16_t)(firmware_info_octet)info->build_id[i * 2U] << 8U) + | (firmware_info_octet)info->build_id[i * 2U + 1U]); return FIRMWARE_INFO_OK; } firmware_info_status_t firmware_info_to_le_bytes( - const firmware_info_t *info, uint8_t *payload, size_t size) + const firmware_info_t *info, firmware_info_octet *payload, size_t size) { uint16_t words[FIRMWARE_INFO_REGISTER_COUNT]; - uint8_t i; + firmware_info_octet i; firmware_info_status_t status; if ((payload == NULL) || (size < FIRMWARE_INFO_PAYLOAD_SIZE)) return FIRMWARE_INFO_INVALID_ARGUMENT; status = firmware_info_to_registers(info, words, FIRMWARE_INFO_REGISTER_COUNT); if (status != FIRMWARE_INFO_OK) return status; for (i = 0U; i < FIRMWARE_INFO_REGISTER_COUNT; ++i) { - payload[i * 2U] = (uint8_t)words[i]; - payload[i * 2U + 1U] = (uint8_t)(words[i] >> 8U); + payload[i * 2U] = (firmware_info_octet)(words[i] & 0xffU); + payload[i * 2U + 1U] = (firmware_info_octet)(words[i] >> 8U); } return FIRMWARE_INFO_OK; } diff --git a/c/firmware-info/src/firmware_info_port.c b/c/firmware-info/src/firmware_info_port.c index db9fdff..56fb565 100644 --- a/c/firmware-info/src/firmware_info_port.c +++ b/c/firmware-info/src/firmware_info_port.c @@ -15,5 +15,13 @@ firmware_info_status_t firmware_info_port_describe(firmware_info_t *info) info->patch = (uint16_t)FIRMWARE_VERSION_PATCH; status = firmware_info_set_build_id(info, FIRMWARE_BUILD_ID); if (status != FIRMWARE_INFO_OK) return status; +#ifdef FI_YEAR + /* New build pipeline: use the same UTC stamp as the static port/manifest. + * Existing consumers of make_build_id.ps1 keep their compiler-local stamp. */ + info->year = FI_YEAR; info->month = FI_MONTH; info->day = FI_DAY; + info->hour = FI_HOUR; info->minute = FI_MINUTE; info->second = FI_SECOND; + return firmware_info_validate(info); +#else return firmware_info_parse_build_stamp(__DATE__, __TIME__, info); +#endif } diff --git a/c/firmware-info/src/firmware_info_static.c b/c/firmware-info/src/firmware_info_static.c new file mode 100644 index 0000000..6b75f03 --- /dev/null +++ b/c/firmware-info/src/firmware_info_static.c @@ -0,0 +1,15 @@ +/* A required generated include deliberately makes a missing pre-build fail. + * Silently using LOCALDEV here could disguise an obsolete linked object. */ +#include "firmware_identity.h" +#include "firmware_info_static.h" + +/* On STM these 12 words occupy 24 bytes; on C28x sizeof reports 12 addressable + * 16-bit units, which are still 24 wire octets. Never use sizeof as a wire size. + * const keeps the identity inside the firmware, away from saved parameters. */ +static const uint16_t firmware_identity_words[FIRMWARE_INFO_STATIC_COUNT] = + FIRMWARE_INFO_STATIC_INITIALIZER; + +uint16_t firmware_info_static_read(unsigned index) +{ + return index < FIRMWARE_INFO_STATIC_COUNT ? firmware_identity_words[index] : 0U; +} diff --git a/c/firmware-info/tests/fixtures/firmware_identity.h b/c/firmware-info/tests/fixtures/firmware_identity.h new file mode 100644 index 0000000..a0d826a --- /dev/null +++ b/c/firmware-info/tests/fixtures/firmware_identity.h @@ -0,0 +1,19 @@ +/* Deliberately fixed vector, not production identity. Used only by host tests. */ +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 2U +#define FIRMWARE_VERSION_PATCH 3U +#define FI_YEAR 2026U +#define FI_MONTH 10U +#define FI_DAY 5U +#define FI_HOUR 12U +#define FI_MINUTE 34U +#define FI_SECOND 56U +#define FIRMWARE_BUILD_ID "abcdef0+" +#define FI_ID_0 'a' +#define FI_ID_1 'b' +#define FI_ID_2 'c' +#define FI_ID_3 'd' +#define FI_ID_4 'e' +#define FI_ID_5 'f' +#define FI_ID_6 '0' +#define FI_ID_7 '+' diff --git a/c/firmware-info/tests/test_identity.py b/c/firmware-info/tests/test_identity.py new file mode 100644 index 0000000..b9d4793 --- /dev/null +++ b/c/firmware-info/tests/test_identity.py @@ -0,0 +1,74 @@ +"""Host lifecycle tests: dirty dependencies, reproducible stamps, file integrity. + +Run: python -m unittest discover -s c/firmware-info/tests -p test_identity.py +""" +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +TOOL = Path(__file__).resolve().parents[1] / "tools/identity.py" +spec = importlib.util.spec_from_file_location("identity", TOOL) +identity = importlib.util.module_from_spec(spec) +spec.loader.exec_module(identity) + + +class IdentityTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.config = self.root / "config.json" + self.config.write_text(json.dumps({"product":"test", "target":"host", "version":[1,2,3]})) + + def prepare(self, states): + with patch.object(identity, "git_state", side_effect=states), patch.dict(os.environ, {"SOURCE_DATE_EPOCH":"1791194400"}): + return identity.prepare(self.root, self.root, self.config, self.root) + + def test_clean_and_dirty_dependency(self): + clean = {"commit":"abcdef0123456789", "dirty":False} + meta = self.prepare([clean,clean]) + self.assertEqual(meta["build_id"], "abcdef01") + header = (self.root / "firmware_identity.h").read_text() + self.assertIn("#define FIRMWARE_VERSION_PATCH 3U",header) + dirty = dict(clean, dirty=True) + self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+") + self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+") + + def test_missing_git_is_explicit(self): + unknown = {"commit":None,"dirty":None} + self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000") + + def test_invalid_version(self): + self.config.write_text(json.dumps({"product":"x","target":"x","version":[1,2,1000]})) + with self.assertRaises(ValueError): + identity.prepare(self.root,self.root,self.config,self.root) + + def test_untracked_source_is_dirty(self): + def git(*args): + subprocess.run(["git","-C",str(self.root),*args],check=True,capture_output=True) + git("init") + git("-c","user.name=Test","-c","user.email=test@example.invalid","commit","--allow-empty","-m","test") + # config.json is untracked and must prevent a clean-looking build ID. + self.assertTrue(identity.git_state(self.root)["dirty"]) + + def test_cli_seal_verify_and_corruption(self): + clean = {"commit":"abcdef0123456789", "dirty":False} + self.prepare([clean,clean]) + image = self.root / "firmware.bin" + image.write_bytes(b"abc") + import sys + subprocess.run([sys.executable,str(TOOL),"seal","--image",str(image),"--metadata",str(self.root/"firmware_identity.json")],check=True,capture_output=True) + manifest = image.with_suffix(".bin.manifest.json") + meta = identity.verify(image,manifest) + self.assertEqual(meta["sha256"],"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + image.write_bytes(b"abd") + with self.assertRaises(ValueError): identity.verify(image,manifest) + + +if __name__ == "__main__": + unittest.main() diff --git a/c/firmware-info/tests/test_static.c b/c/firmware-info/tests/test_static.c new file mode 100644 index 0000000..606de09 --- /dev/null +++ b/c/firmware-info/tests/test_static.c @@ -0,0 +1,53 @@ +/* One set of vectors checks static metadata against Climate's runtime encoder. + * This catches word order and ASCII order changes on the host, before MCU use. */ +#include "firmware_identity.h" +#include "firmware_info.h" +#include "firmware_info_static.h" +#include "ump_protocol.h" +#include + +static unsigned writes; +static uint16_t read_identity(void *context, uint16_t address) +{ + (void)context; + return firmware_info_static_read(address - UMP_FIRMWARE_INFO); +} +static int write_identity(void *context, uint16_t address, uint16_t value) +{ + (void)context; (void)address; (void)value; + writes++; + return 1; +} +int main(void) +{ + firmware_info_t info; + uint16_t expected[12], reply[UMP_MAX_WORDS]; + ump_octet frame[UMP_RTU_CAPACITY]; + ump_backend backend = {0, read_identity, write_identity}; + unsigned i, error; + info.major = FIRMWARE_VERSION_MAJOR; info.minor = FIRMWARE_VERSION_MINOR; + info.patch = FIRMWARE_VERSION_PATCH; info.year = FI_YEAR; + info.month = FI_MONTH; info.day = FI_DAY; info.hour = FI_HOUR; + info.minute = FI_MINUTE; info.second = FI_SECOND; + assert(firmware_info_set_build_id(&info, FIRMWARE_BUILD_ID) == FIRMWARE_INFO_OK); + assert(firmware_info_to_registers(&info, expected, 12) == FIRMWARE_INFO_OK); + for (i=0; i<12; ++i) assert(firmware_info_static_read(i) == expected[i]); + assert(firmware_info_static_read(12) == 0); + assert(ump_prepare(&backend, 3, UMP_FIRMWARE_INFO, 12, 1, reply, &error) == 12 && !error); + for (i=0; i<12; ++i) assert(reply[i] == expected[i]); + for (i=0; i<6; ++i) { + assert(ump_can_fragment(reply,12,i,42,0,frame) == 8); + assert(frame[0] == expected[2*i] >> 8 && frame[1] == (expected[2*i] & 255)); + assert(frame[6] == 42 && frame[7] == 0); + } + assert(ump_rtu_response(&backend, 1, 3, UMP_FIRMWARE_INFO, 12, frame, sizeof(frame)/sizeof(frame[0])) == 29); + assert(frame[2] == 24 && ump_crc(frame,29) == 0); + assert(ump_validate(3, UMP_FIRMWARE_INFO+11, 1, 0) == 0); + assert(ump_validate(3, UMP_FIRMWARE_INFO+11, 2, 0) == 2); + assert(ump_validate(3, UMP_FIRMWARE_INFO, 0, 0) == 2); + assert(ump_validate(3, UMP_FIRMWARE_INFO, 0xffffU, 0) == 2); + assert(ump_validate(3, UMP_FIRMWARE_INFO+1, 11, 1) == 2); + ump_prepare(&backend,6,UMP_FIRMWARE_INFO,1,0,reply,&error); + assert(error == 2 && writes == 0); + return 0; +} diff --git a/c/firmware-info/tools/identity.py b/c/firmware-info/tools/identity.py new file mode 100644 index 0000000..5d7fd0c --- /dev/null +++ b/c/firmware-info/tools/identity.py @@ -0,0 +1,124 @@ +"""Build provenance and exact-file SHA-256, shared by CCS, Keil and CLI builds. + +prepare runs BEFORE compiling; seal runs AFTER binary conversion and size checks. +The digest is external to the image, avoiding the self-hashing paradox. No MCU +protocol encoding lives here: the generated header contains scalar constants. +""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import subprocess + + +def git_state(repository: Path) -> dict: + """Record full commits, and flag untracked sources as well as tracked edits. + + Generated outputs must be ignored in the repository. Unlike Climate's older + helper, a new, untracked source file must not look like a clean release. + """ + def git(*args): + return subprocess.check_output( + ["git", "-C", str(repository), *args], stderr=subprocess.DEVNULL, + text=True, encoding="utf-8").strip() + try: + commit = git("rev-parse", "HEAD") + dirty = bool(git("status", "--porcelain", "--untracked-files=normal")) + return {"commit": commit, "dirty": dirty} + except (OSError, subprocess.CalledProcessError): + return {"commit": None, "dirty": None} + + +def write_atomic(path: Path, text: str): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + temporary.write_text(text, encoding="utf-8", newline="\n") + temporary.replace(path) + + +def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict: + settings = json.loads(config.read_text(encoding="utf-8")) + version = settings["version"] + if (len(version) != 3 or any(type(v) is not int for v in version) + or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))): + raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]") + for field in ("product", "target"): + if not isinstance(settings.get(field), str) or not settings[field].strip(): + raise ValueError(f"missing {field}") + source, dependency = git_state(repository), git_state(templates) + # A clean application built with a modified templates checkout is dirty too. + known = source["commit"] is not None and dependency["commit"] is not None + dirty = bool(source["dirty"] or dependency["dirty"]) + build_id = (source["commit"][:7] + "+" if dirty else source["commit"][:8]) if known else "NOGIT000" + # UTC has an explicit interpretation across build PCs; SOURCE_DATE_EPOCH is + # supported for reproducible release builds, not silently inferred from Git. + epoch = os.environ.get("SOURCE_DATE_EPOCH") + stamp = datetime.fromtimestamp(int(epoch), timezone.utc) if epoch else datetime.now(timezone.utc) + stamp = stamp.replace(microsecond=0) + if not 2000 <= stamp.year <= 2199: + raise ValueError("build year outside firmware-info v1 range") + metadata = {"schema": 1, "product": settings["product"], "target": settings["target"], + "version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(), + "source": source, "templates": dependency, + "identity_contract": 1, "identity_register_count": 12} + constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version)) + constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"), + (stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second)))) + constants.update({f"FI_ID_{i}": ord(c) for i, c in enumerate(build_id)}) + header = "/* Generated before compilation. Do not edit or commit. UTC stamp. */\n#ifndef FIRMWARE_IDENTITY_H\n#define FIRMWARE_IDENTITY_H\n" + header += f'#define FIRMWARE_BUILD_ID "{build_id}"\n' + header += "".join(f"#define {key} {value}U\n" for key, value in constants.items()) + header += "#endif\n" + # Rewriting the header forces the identity compilation unit to be rebuilt. + # Sharing this directory between concurrent configurations is unsupported. + write_atomic(output / "firmware_identity.h", header) + write_atomic(output / "firmware_identity.json", json.dumps(metadata, indent=2) + "\n") + return metadata + + +def seal(image: Path, metadata_path: Path) -> dict: + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + data = image.read_bytes() + if not data: + raise ValueError("empty firmware image") + # Seal records the exact file sent to the programmer, including SCI8 headers. + # It does not claim that the device has measured RAM/Flash or authenticated it. + metadata.update({"file": image.name, "size_bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest()}) + write_atomic(image.with_suffix(image.suffix + ".manifest.json"), json.dumps(metadata, indent=2) + "\n") + write_atomic(image.with_suffix(image.suffix + ".sha256"), f'{metadata["sha256"]} {image.name}\n') + return metadata + + +def verify(image: Path, manifest: Path) -> dict: + metadata = json.loads(manifest.read_text(encoding="utf-8")) + data = image.read_bytes() + if len(data) != metadata["size_bytes"] or hashlib.sha256(data).hexdigest() != metadata["sha256"]: + raise ValueError("firmware size/SHA-256 does not match manifest") + return metadata + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + p = sub.add_parser("prepare") + for name in ("repository", "templates", "config", "output"): + p.add_argument("--" + name, type=Path, required=True) + p = sub.add_parser("seal") + p.add_argument("--image", type=Path, required=True) + p.add_argument("--metadata", dest="metadata_path", type=Path, required=True) + p = sub.add_parser("verify") + p.add_argument("--image", type=Path, required=True) + p.add_argument("--manifest", type=Path, required=True) + args = vars(parser.parse_args()) + command = args.pop("command") + result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args) + print(json.dumps(result, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index a69c92a..6a6b56d 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -149,6 +149,14 @@ if(SETP_BUILD_TESTS) add_executable(test_periph28335 tests/test_periph28335.c) target_link_libraries(test_periph28335 PRIVATE setprotocol_static) add_test(NAME shared_periph28335 COMMAND test_periph28335) + add_executable(test_periph28335_eeprom tests/test_periph28335_eeprom.c) + target_link_libraries(test_periph28335_eeprom PRIVATE setprotocol_static) + if(MSVC) + target_compile_options(test_periph28335_eeprom PRIVATE /UNDEBUG) + else() + target_compile_options(test_periph28335_eeprom PRIVATE -UNDEBUG) + endif() + add_test(NAME periph28335_eeprom COMMAND test_periph28335_eeprom) add_executable(test_tms2812 tests/test_tms2812.c) target_link_libraries(test_tms2812 PRIVATE setprotocol_static) add_test(NAME shared_tms2812 COMMAND test_tms2812) diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index 17ed396..170cc1b 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -1,5 +1,11 @@ # SETProtocol +`periph28335_eeprom_*` — read-only reader физической EEPROM Бальзам-167 +(AT25256B, 32 КиБ), со стабильным C ABI и портом +`python/protocan/periph28335_eeprom.py`. Разметка платы задана в +`include/balsam167_eeprom_layout.h`; PEEK, CRC и порядок байтов реализованы +в `src/periph28335.c`. Проверка полного диапазона — `test_periph28335_eeprom`. + Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md). `set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек. diff --git a/c/set-protocol/include/balsam167_eeprom_layout.h b/c/set-protocol/include/balsam167_eeprom_layout.h new file mode 100644 index 0000000..c9322c6 --- /dev/null +++ b/c/set-protocol/include/balsam167_eeprom_layout.h @@ -0,0 +1,17 @@ +/* Board-specific layout shared by firmware, linker and host tools. + * Fitted part: AT25256B, top marking 5EBL = 256 Kbit = 32768 OCTETS. + * ATMLH516 is a manufacturer/date code, not a 512-Kbit density code. + * Numeric defines intentionally have no C suffix: TI linker also includes this. + */ +#ifndef BALSAM167_EEPROM_LAYOUT_H +#define BALSAM167_EEPROM_LAYOUT_H +#define BALSAM167_EEPROM_BYTES 0x8000 +#define BALSAM167_EEPROM_PAGE_BYTES 64 +#define BALSAM167_EEPROM_PARAM_WORDS 129 +#define BALSAM167_EEPROM_WORDS (BALSAM167_EEPROM_BYTES / 2) +/* Preserve the historical end-exclusive parameter boundary. The final word + * remains unused; moving it would break existing saved configurations. */ +#define BALSAM167_EEPROM_PARAMS_END_WORD (BALSAM167_EEPROM_WORDS - 1) +#define BALSAM167_EEPROM_PARAMS_START_WORD (BALSAM167_EEPROM_PARAMS_END_WORD - BALSAM167_EEPROM_PARAM_WORDS) +#define BALSAM167_EEPROM_FIRMWARE_BYTES (BALSAM167_EEPROM_PARAMS_START_WORD * 2) +#endif diff --git a/c/set-protocol/include/pcan_abi.h b/c/set-protocol/include/pcan_abi.h index 18ffe67..9ec1055 100644 --- a/c/set-protocol/include/pcan_abi.h +++ b/c/set-protocol/include/pcan_abi.h @@ -82,6 +82,15 @@ PCAN_ABI_API size_t pcan_abi_balsam_register_name( /* PM35/TMS320F28335 direct RS232/485 register terminal. */ PCAN_ABI_API uint16_t pcan_abi_periph28335_crc16( const uint8_t *data, size_t size); +/* Caller-owned EEPROM reader state: uint32_t[4], 16 bytes, see periph28335.h. + * Exported independently of PM67: the EEPROM routing is specific to 167. */ +PCAN_ABI_API int pcan_abi_periph28335_eeprom_begin(void *state, + uint32_t controller, uint32_t start, uint32_t length); +PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_request(const void *state, + uint8_t *output, size_t capacity); +PCAN_ABI_API int pcan_abi_periph28335_eeprom_accept(void *state, + const uint8_t *reply, size_t size, uint8_t *output, size_t capacity); +PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity(void); PCAN_ABI_API size_t pcan_abi_periph28335_append_crc( const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size); diff --git a/c/set-protocol/include/periph28335.h b/c/set-protocol/include/periph28335.h index a9b3010..3aaaac8 100644 --- a/c/set-protocol/include/periph28335.h +++ b/c/set-protocol/include/periph28335.h @@ -16,6 +16,7 @@ #include #include +#include "balsam167_eeprom_layout.h" #ifdef __cplusplus extern "C" { @@ -25,6 +26,30 @@ extern "C" { #define PERIPH28335_REQUEST_SIZE 8U #define PERIPH28335_COMMAND_COUNT 17U +/* Balsam-167 fitted AT25256B (marking 5EBL): 256 Kbit, NOT 256 Kbyte. + * All reader positions below count physical EEPROM octets, not C28x words. + * Four uint32_t fields make the caller-owned state stable for ctypes/JNI. */ +#define PERIPH28335_EEPROM_BYTES BALSAM167_EEPROM_BYTES +typedef struct { + uint32_t controller; + uint32_t start; + uint32_t length; + uint32_t received; +} periph28335_eeprom_reader; + +/* No writes or capacity probes. Odd starts/lengths are allowed: the core reads + * the containing 16-bit word and returns only the requested physical octets. */ +int periph28335_eeprom_begin(periph28335_eeprom_reader *reader, + uint32_t controller, uint32_t start, uint32_t length); +size_t periph28335_eeprom_request(const periph28335_eeprom_reader *reader, + uint8_t *output, size_t capacity); +/* Returns 1/2 decoded octets, or a negative status; errors do not advance state. + * PEEK payload = little-endian data WORD + timer WORD, not four memory bytes. + * SPI16 shifts the data word high-octet first, so physical EEPROM order is + * response[3], response[2]. This differs from a C28x little-endian RAM dump. */ +int periph28335_eeprom_accept(periph28335_eeprom_reader *reader, + const uint8_t *reply, size_t size, uint8_t *output, size_t capacity); + typedef enum { PERIPH28335_OK = 0, PERIPH28335_ERROR_ARGUMENT = -1, diff --git a/c/set-protocol/include/ump_protocol.h b/c/set-protocol/include/ump_protocol.h index fbb27cf..ed78e7d 100644 --- a/c/set-protocol/include/ump_protocol.h +++ b/c/set-protocol/include/ump_protocol.h @@ -17,6 +17,10 @@ extern "C" { #define UMP_BASE 0x1000U #define UMP_DATA 0x1010U #define UMP_LIVE 0x1100U +/* Read-only running firmware identity, same 12-word v1 contract as Climate. + * Kept outside logger control/data and the EEPROM-backed legacy parameters. */ +#define UMP_FIRMWARE_INFO 0x1200U +#define UMP_FIRMWARE_INFO_WORDS 12U #define UMP_MARKER 0x1F00U #define UMP_MAX_WORDS 124U #define UMP_RTU_CAPACITY 253U diff --git a/c/set-protocol/ports/tms320f28335/ump_tms28335.c b/c/set-protocol/ports/tms320f28335/ump_tms28335.c index 1135c88..8ae76e5 100644 --- a/c/set-protocol/ports/tms320f28335/ump_tms28335.c +++ b/c/set-protocol/ports/tms320f28335/ump_tms28335.c @@ -8,7 +8,17 @@ extern unsigned int ump_log_read(unsigned int address); extern int ump_log_write(unsigned int address, unsigned int value); static uint16_t read_word(void *context, uint16_t address) -{ (void)context; return (uint16_t)ump_log_read(address); } +{ + (void)context; + /* Optional board binding to the common firmware-info static port. Older + * consumers still link without it and return contract=0 (unknown version). + * This hook changes neither PM67 CAN nor the saved parameter layout. */ +#ifdef UMP_FIRMWARE_INFO_READ + if (address >= UMP_FIRMWARE_INFO && address < UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS) + return UMP_FIRMWARE_INFO_READ(address - UMP_FIRMWARE_INFO); +#endif + return (uint16_t)ump_log_read(address); +} static int write_word(void *context, uint16_t address, uint16_t value) { (void)context; return ump_log_write(address,value); } unsigned ump_tms28335_prepare(unsigned fn, uint16_t a, uint16_t n, diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c index 5a9d318..0ad7c48 100644 --- a/c/set-protocol/src/pcan_abi.c +++ b/c/set-protocol/src/pcan_abi.c @@ -101,6 +101,17 @@ uint16_t pcan_abi_periph28335_crc16(const uint8_t *data, size_t size) return periph28335_crc16_modbus(data, size); } +int pcan_abi_periph28335_eeprom_begin(void *state, + uint32_t controller, uint32_t start, uint32_t length) +{ return periph28335_eeprom_begin((periph28335_eeprom_reader *)state, controller, start, length); } +size_t pcan_abi_periph28335_eeprom_request(const void *state, uint8_t *out, size_t cap) +{ return periph28335_eeprom_request((const periph28335_eeprom_reader *)state, out, cap); } +int pcan_abi_periph28335_eeprom_accept(void *state, const uint8_t *reply, + size_t size, uint8_t *out, size_t cap) +{ return periph28335_eeprom_accept((periph28335_eeprom_reader *)state, reply, size, out, cap); } +uint32_t pcan_abi_periph28335_eeprom_capacity(void) +{ return PERIPH28335_EEPROM_BYTES; } + size_t pcan_abi_periph28335_append_crc( const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size) diff --git a/c/set-protocol/src/periph28335.c b/c/set-protocol/src/periph28335.c index a088666..6ae92c7 100644 --- a/c/set-protocol/src/periph28335.c +++ b/c/set-protocol/src/periph28335.c @@ -10,6 +10,60 @@ #include "set_crc.h" +static int eeprom_reader_valid(const periph28335_eeprom_reader *r) +{ + return r && r->controller >= 1U && r->controller <= 255U && + r->start < PERIPH28335_EEPROM_BYTES && r->length && + r->length <= PERIPH28335_EEPROM_BYTES - r->start && r->received < r->length; +} + +int periph28335_eeprom_begin(periph28335_eeprom_reader *r, + uint32_t controller, uint32_t start, uint32_t length) +{ + if (!r) return PERIPH28335_ERROR_ARGUMENT; + /* Invalidate first: a rejected new range must not leave an old read active. */ + memset(r, 0, sizeof(*r)); + if (!controller || controller > 255U || start >= PERIPH28335_EEPROM_BYTES || + !length || length > PERIPH28335_EEPROM_BYTES - start) + return PERIPH28335_ERROR_RANGE; + r->controller = controller; r->start = start; r->length = length; + return PERIPH28335_OK; +} + +size_t periph28335_eeprom_request(const periph28335_eeprom_reader *r, + uint8_t *output, size_t capacity) +{ + uint8_t payload[6]; + uint32_t word; + if (!eeprom_reader_valid(r)) return 0; + word = (r->start + r->received) / 2U; + payload[0] = (uint8_t)r->controller; payload[1] = 56U; /* CMD_PEEK only */ + /* 167 routes words 0..0x3fff to SPI EEPROM. The old terminal's + * 0x10000000 memory selector is NOT implemented by this firmware. */ + payload[2] = (uint8_t)word; payload[3] = (uint8_t)(word >> 8); + payload[4] = 0; payload[5] = 0; + return periph28335_append_crc(payload, sizeof(payload), output, capacity); +} + +int periph28335_eeprom_accept(periph28335_eeprom_reader *r, + const uint8_t *reply, size_t size, uint8_t *output, size_t capacity) +{ + unsigned odd, count; + if (!eeprom_reader_valid(r) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT; + /* The UART layer may strip two legacy zero-padding bytes after CRC. */ + if (size != 8U && size != 10U) return PERIPH28335_ERROR_LENGTH; + if (periph28335_crc16_modbus(reply, 8U) != 0) return PERIPH28335_ERROR_CRC; + if (reply[0] != r->controller || reply[1] != 56U) return PERIPH28335_ERROR_HEADER; + odd = (unsigned)((r->start + r->received) & 1U); + count = 2U - odd; + if (count > r->length - r->received) count = (unsigned)(r->length - r->received); + if (capacity < count) return PERIPH28335_ERROR_CAPACITY; + output[0] = reply[odd ? 2U : 3U]; + if (count == 2U) output[1] = reply[2]; + r->received += count; + return (int)count; +} + typedef struct { const char *project; const char *commands[PERIPH28335_COMMAND_COUNT]; diff --git a/c/set-protocol/src/ump_protocol.c b/c/set-protocol/src/ump_protocol.c index e8b4aa5..0aaf7e9 100644 --- a/c/set-protocol/src/ump_protocol.c +++ b/c/set-protocol/src/ump_protocol.c @@ -12,6 +12,11 @@ unsigned ump_validate(unsigned fn, uint16_t a, uint16_t n, int can) { if (fn == 6) return a >= UMP_BASE && a <= UMP_BASE+3 ? 0 : 2; if (fn != 3) return 1; + /* CAN reads the whole immutable block (six fragments); RTU may read slices. + * Check subtraction only after the lower bound, avoiding uint16 wraparound. */ + if (a >= UMP_FIRMWARE_INFO && a < UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS) + return can ? ((a == UMP_FIRMWARE_INFO && n == UMP_FIRMWARE_INFO_WORDS) ? 0 : 2) + : ((n && n <= UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS - a) ? 0 : 2); if (can) return ((a == UMP_BASE && n == 16) || (a == UMP_DATA && (n == 40 || n == 80 || n == 120 || n == 42 || n == 84)) || (a == UMP_LIVE && n == 124)) ? 0 : 2; diff --git a/c/set-protocol/tests/test_periph28335_eeprom.c b/c/set-protocol/tests/test_periph28335_eeprom.c new file mode 100644 index 0000000..3a455ba --- /dev/null +++ b/c/set-protocol/tests/test_periph28335_eeprom.c @@ -0,0 +1,64 @@ +#include "periph28335.h" +#include +#include + +static void response(uint8_t *out, unsigned high, unsigned low) +{ + /* Reply is low data byte, high data byte, then an unrelated timer. + * Nonzero timer detects accidentally copying four memory bytes per PEEK. */ + uint8_t payload[] = {16,56,0,0,0xef,0xbe}; + payload[2]=(uint8_t)low; payload[3]=(uint8_t)high; + assert(periph28335_append_crc(payload,6,out,10)==8); + out[8]=out[9]=0; +} + +int main(void) +{ + periph28335_eeprom_reader reader; + uint8_t request[8], reply[10], data[2]; + unsigned i; + assert(sizeof(reader)==16); + assert(periph28335_eeprom_begin(&reader,16,0,0)==PERIPH28335_ERROR_RANGE); + assert(periph28335_eeprom_begin(&reader,0,0,2)==PERIPH28335_ERROR_RANGE); + assert(periph28335_eeprom_begin(&reader,16,0x8000,1)==PERIPH28335_ERROR_RANGE); + assert(periph28335_eeprom_begin(&reader,16,0x7fff,2)==PERIPH28335_ERROR_RANGE); + assert(periph28335_eeprom_begin(&reader,16,1,0xffffffffUL)==PERIPH28335_ERROR_RANGE); + assert(periph28335_eeprom_request(&reader,request,8)==0); + assert(periph28335_eeprom_begin(&reader,16,1,3)==0); + assert(periph28335_eeprom_request(&reader,request,7)==0); + assert(periph28335_eeprom_request(&reader,request,8)==8); + assert(memcmp(request,"\x10\x38\0\0\0\0",6)==0); + response(reply,0x12,0x34); + assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==1 && data[0]==0x34); + assert(reader.received==1); + assert(periph28335_eeprom_request(&reader,request,8)==8 && request[2]==1); + reply[2]^=1; + assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==PERIPH28335_ERROR_CRC); + assert(reader.received==1); + response(reply,0x56,0x78); + assert(periph28335_eeprom_accept(&reader,reply,8,data,1)==PERIPH28335_ERROR_CAPACITY); + assert(reader.received==1); + assert(periph28335_eeprom_accept(&reader,reply,10,data,2)==2); + assert(data[0]==0x56 && data[1]==0x78 && reader.received==3); + assert(periph28335_eeprom_request(&reader,request,8)==0); + + /* Every requested word of a complete dump remains in the EEPROM window. + * In particular, neither the RAM route nor selector 0x10000000 is used. */ + assert(periph28335_eeprom_begin(&reader,16,0,32768)==0); + for (i=0;i<16384;i++) { + assert(periph28335_eeprom_request(&reader,request,8)==8); + assert(request[1]==56 && request[4]==0 && request[5]==0); + assert(((unsigned)request[3]*256+request[2])==i); + assert(periph28335_crc16_modbus(request,8)==0); + response(reply,i>>8,i); + assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==2); + assert(data[0]==(i>>8) && data[1]==(i&255)); + } + assert(reader.received==32768 && periph28335_eeprom_request(&reader,request,8)==0); + assert(periph28335_eeprom_begin(&reader,16,0x7fff,1)==0); + assert(periph28335_eeprom_request(&reader,request,8)==8); + assert(request[2]==0xff && request[3]==0x3f); + response(reply,0x9a,0xbc); + assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==1 && data[0]==0xbc); + return 0; +} diff --git a/python/protocan/periph28335_eeprom.py b/python/protocan/periph28335_eeprom.py new file mode 100644 index 0000000..a3412c8 --- /dev/null +++ b/python/protocan/periph28335_eeprom.py @@ -0,0 +1,60 @@ +"""Thin binding to the Balsam-167 physical EEPROM reader in the shared C core. + +No address conversion, endian/CRC codec or memory-size inference in this port. +The caller owns serial scheduling, timeouts, cancellation and dump file storage. +""" +import ctypes as ct +from .native import get_native_protocol, NativeProtocolUnavailable + + +class _State(ct.Structure): + _fields_ = [(name, ct.c_uint32) for name in ("controller", "start", "length", "received")] + + +def _api(name, args, result): + try: + function = getattr(get_native_protocol().lib, "pcan_abi_periph28335_eeprom_" + name) + except AttributeError as error: + raise NativeProtocolUnavailable("Обновите templates/setprotocol.dll: нет API чтения EEPROM 167") from error + function.argtypes, function.restype = args, result + return function + + +def capacity(): + return int(_api("capacity", [], ct.c_uint32)()) + + +class Reader: + def __init__(self, controller, start, length): + for value in (controller, start, length): + if type(value) is not int or not 0 <= value <= 0xffffffff: + raise ValueError("Адрес и длина должны быть неотрицательными целыми числами") + self._state = _State() + result = _api("begin", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32], ct.c_int)( + ct.byref(self._state), controller, start, length) + if result: + raise ValueError("Диапазон вне EEPROM AT25256B: 0…0x7FFF байт; длина должна быть больше нуля") + + @property + def received(self): return int(self._state.received) + @property + def length(self): return int(self._state.length) + @property + def start(self): return int(self._state.start) + + def request(self): + output = ct.create_string_buffer(8) + size = _api("request", [ct.c_void_p, ct.c_void_p, ct.c_size_t], ct.c_size_t)( + ct.byref(self._state), output, len(output)) + return output.raw[:size] + + def accept(self, reply): + output = ct.create_string_buffer(2) + reply = bytes(reply) + size = _api("accept", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t], ct.c_int)( + ct.byref(self._state), reply, len(reply), output, len(output)) + if size < 0: + errors = {-3: "Неверная длина ответа EEPROM", -4: "Ошибка CRC ответа EEPROM", + -5: "Неверный адрес контроллера или команда ответа EEPROM"} + raise ValueError(errors.get(size, "Ответ EEPROM не соответствует активному чтению")) + return output.raw[:size]