From 9ce32b56d48b3dac519ac2c7abf6cdf301b03d09 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sun, 30 Aug 2026 07:01:35 +0300 Subject: [PATCH] Add reusable firmware info module --- README.md | 3 +- c/firmware-info/CMakeLists.txt | 8 ++ c/firmware-info/PORTING.md | 11 ++ c/firmware-info/README.md | 27 ++++ c/firmware-info/include/firmware_info.h | 48 +++++++ c/firmware-info/include/firmware_info_port.h | 18 +++ .../firmware_info_config.template.h | 11 ++ .../stm32f1/firmware_info_config.template.h | 12 ++ .../stm32f4/firmware_info_config.template.h | 11 ++ .../stm32g4/firmware_info_config.template.h | 11 ++ c/firmware-info/src/firmware_info.c | 121 ++++++++++++++++++ c/firmware-info/src/firmware_info_port.c | 13 ++ c/firmware-info/tests/test_firmware_info.c | 20 +++ c/firmware-info/tools/make_build_id.ps1 | 18 +++ 14 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 c/firmware-info/CMakeLists.txt create mode 100644 c/firmware-info/PORTING.md create mode 100644 c/firmware-info/README.md create mode 100644 c/firmware-info/include/firmware_info.h create mode 100644 c/firmware-info/include/firmware_info_port.h create mode 100644 c/firmware-info/ports/k1921vk028/firmware_info_config.template.h create mode 100644 c/firmware-info/ports/stm32f1/firmware_info_config.template.h create mode 100644 c/firmware-info/ports/stm32f4/firmware_info_config.template.h create mode 100644 c/firmware-info/ports/stm32g4/firmware_info_config.template.h create mode 100644 c/firmware-info/src/firmware_info.c create mode 100644 c/firmware-info/src/firmware_info_port.c create mode 100644 c/firmware-info/tests/test_firmware_info.c create mode 100644 c/firmware-info/tools/make_build_id.ps1 diff --git a/README.md b/README.md index fc6bdbe..9d7d80b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ templates/ | [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** | | [`c/set-protocol`](c/set-protocol) | единый SET protocol v2: управление, real-time телеметрия и обновление прошивки через UART/CAN/USB/Ethernet | C99 | доставка целого stream/datagram-кадра, часы, backend карты и загрузчика | | [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | +| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI | ### Python @@ -75,7 +76,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git | Проект | Что берёт | |---|---| -| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 | +| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info | | `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 | | `CAN_to_RS485` | protocan-transport, python/protocan | | `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` | diff --git a/c/firmware-info/CMakeLists.txt b/c/firmware-info/CMakeLists.txt new file mode 100644 index 0000000..6444a95 --- /dev/null +++ b/c/firmware-info/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(firmware_info C) +add_library(firmware_info src/firmware_info.c) +target_include_directories(firmware_info PUBLIC include) +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) diff --git a/c/firmware-info/PORTING.md b/c/firmware-info/PORTING.md new file mode 100644 index 0000000..addc92e --- /dev/null +++ b/c/firmware-info/PORTING.md @@ -0,0 +1,11 @@ +# PORTING + +Аппаратно-зависимый контракт ограничен `firmware_info_config.h`: +`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и +необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте +его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus +либо 24 little-endian байта для SETGUI. + +При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в +include path, запуск генератора до компиляции и декодирование build ID как двух +ASCII-байтов в каждом логическом слове. diff --git a/c/firmware-info/README.md b/c/firmware-info/README.md new file mode 100644 index 0000000..2626f6e --- /dev/null +++ b/c/firmware-info/README.md @@ -0,0 +1,27 @@ +# Firmware Info + +Переносимая библиотека версии реально запущенной прошивки. Повторяет контракт +`FirmwareInfo` из `john103C6T6NewVer`, но оформлена как template: ядро не знает +ни о HAL, ни о конкретном протоколе, ни о модели МК. + +Контракт v1 содержит 12 слов: версия контракта, `major.minor.patch`, дата/время +компиляции и 8-символьный build ID. `firmware_info_to_registers()` подходит для +Modbus, `firmware_info_to_le_bytes()` — для SETGUI и других byte-протоколов. + +## Подключение + +1. Добавьте `include`, `src/firmware_info.c` и `src/firmware_info_port.c`. +2. Скопируйте подходящий `ports//firmware_info_config.template.h` в проект + как `firmware_info_config.h` и задайте SemVer. +3. Добавьте каталог с config и `include` в include path. +4. Перед сборкой запустите `tools/make_build_id.ps1`; каталог generated также + должен быть в include path. Чистая сборка получает 8 знаков git commit, + dirty-сборка — 7 знаков и `+`, сборка без Git — `NOGIT000`. + +Готовые конфигурации есть для STM32F1, STM32F4, STM32G4 и К1921ВК028. Порт не +содержит регистров МК: стандартные `__DATE__`/`__TIME__` одинаковы для GCC, +Arm Compiler 5/6 и Clang. Поэтому добавление следующего МК требует только +config, а не дублирования ядра. + +KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый +32-байтовый `DEVICE_INFO` без изменений. diff --git a/c/firmware-info/include/firmware_info.h b/c/firmware-info/include/firmware_info.h new file mode 100644 index 0000000..7dc8dd6 --- /dev/null +++ b/c/firmware-info/include/firmware_info.h @@ -0,0 +1,48 @@ +#ifndef FIRMWARE_INFO_H +#define FIRMWARE_INFO_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define FIRMWARE_INFO_CONTRACT_VERSION 1U +#define FIRMWARE_INFO_REGISTER_COUNT 12U +#define FIRMWARE_INFO_PAYLOAD_SIZE (FIRMWARE_INFO_REGISTER_COUNT * 2U) +#define FIRMWARE_INFO_BUILD_ID_SIZE 8U + +typedef struct { + uint16_t major; + uint16_t minor; + uint16_t patch; + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + char build_id[FIRMWARE_INFO_BUILD_ID_SIZE + 1U]; +} firmware_info_t; + +typedef enum { + FIRMWARE_INFO_OK = 0, + FIRMWARE_INFO_INVALID_ARGUMENT, + FIRMWARE_INFO_OUT_OF_RANGE +} firmware_info_status_t; + +firmware_info_status_t firmware_info_validate(const 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); +firmware_info_status_t firmware_info_set_build_id( + firmware_info_t *info, const char *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); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/c/firmware-info/include/firmware_info_port.h b/c/firmware-info/include/firmware_info_port.h new file mode 100644 index 0000000..15a82ea --- /dev/null +++ b/c/firmware-info/include/firmware_info_port.h @@ -0,0 +1,18 @@ +#ifndef FIRMWARE_INFO_PORT_H +#define FIRMWARE_INFO_PORT_H + +#include "firmware_info.h" + +/* Include-path selects one MCU project configuration named firmware_info_config.h. */ +#include "firmware_info_config.h" + +#ifndef FIRMWARE_VERSION_PATCH +#define FIRMWARE_VERSION_PATCH 0U +#endif +#ifndef FIRMWARE_BUILD_ID +#define FIRMWARE_BUILD_ID "LOCALDEV" +#endif + +firmware_info_status_t firmware_info_port_describe(firmware_info_t *info); + +#endif diff --git a/c/firmware-info/ports/k1921vk028/firmware_info_config.template.h b/c/firmware-info/ports/k1921vk028/firmware_info_config.template.h new file mode 100644 index 0000000..128a14f --- /dev/null +++ b/c/firmware-info/ports/k1921vk028/firmware_info_config.template.h @@ -0,0 +1,11 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 0U +#define FIRMWARE_VERSION_PATCH 0U +#if defined(__has_include) +# if __has_include("firmware_build_id.h") +# include "firmware_build_id.h" +# endif +#endif +#endif diff --git a/c/firmware-info/ports/stm32f1/firmware_info_config.template.h b/c/firmware-info/ports/stm32f1/firmware_info_config.template.h new file mode 100644 index 0000000..d4ce748 --- /dev/null +++ b/c/firmware-info/ports/stm32f1/firmware_info_config.template.h @@ -0,0 +1,12 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 0U +#define FIRMWARE_VERSION_PATCH 0U +/* Optional: generated by tools/make_build_id.ps1. */ +#if defined(__has_include) +# if __has_include("firmware_build_id.h") +# include "firmware_build_id.h" +# endif +#endif +#endif diff --git a/c/firmware-info/ports/stm32f4/firmware_info_config.template.h b/c/firmware-info/ports/stm32f4/firmware_info_config.template.h new file mode 100644 index 0000000..128a14f --- /dev/null +++ b/c/firmware-info/ports/stm32f4/firmware_info_config.template.h @@ -0,0 +1,11 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 0U +#define FIRMWARE_VERSION_PATCH 0U +#if defined(__has_include) +# if __has_include("firmware_build_id.h") +# include "firmware_build_id.h" +# endif +#endif +#endif diff --git a/c/firmware-info/ports/stm32g4/firmware_info_config.template.h b/c/firmware-info/ports/stm32g4/firmware_info_config.template.h new file mode 100644 index 0000000..128a14f --- /dev/null +++ b/c/firmware-info/ports/stm32g4/firmware_info_config.template.h @@ -0,0 +1,11 @@ +#ifndef FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_INFO_CONFIG_H +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 0U +#define FIRMWARE_VERSION_PATCH 0U +#if defined(__has_include) +# if __has_include("firmware_build_id.h") +# include "firmware_build_id.h" +# endif +#endif +#endif diff --git a/c/firmware-info/src/firmware_info.c b/c/firmware-info/src/firmware_info.c new file mode 100644 index 0000000..a30cc19 --- /dev/null +++ b/c/firmware-info/src/firmware_info.c @@ -0,0 +1,121 @@ +#include "firmware_info.h" + +static const char months[12][4] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +static int is_digit(char value) { return (value >= '0') && (value <= '9'); } + +static int two_digits(const char *text, uint8_t *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')); + return 1; +} + +static int four_digits(const char *text, uint16_t *result) +{ + uint16_t value = 0U; + uint8_t i; + for (i = 0U; i < 4U; ++i) { + if (!is_digit(text[i])) return 0; + value = (uint16_t)(value * 10U + (uint16_t)(text[i] - '0')); + } + *result = value; + return 1; +} + +firmware_info_status_t firmware_info_validate(const firmware_info_t *info) +{ + uint8_t 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) + || (info->month < 1U) || (info->month > 12U) + || (info->day < 1U) || (info->day > 31U) + || (info->hour > 23U) || (info->minute > 59U) || (info->second > 59U)) { + return FIRMWARE_INFO_OUT_OF_RANGE; + } + for (i = 0U; i < FIRMWARE_INFO_BUILD_ID_SIZE; ++i) { + if ((info->build_id[i] < 0x20) || (info->build_id[i] > 0x7e)) + return FIRMWARE_INFO_OUT_OF_RANGE; + } + return FIRMWARE_INFO_OK; +} + +firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info, + const char *build_id) +{ + uint8_t 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)) + return FIRMWARE_INFO_OUT_OF_RANGE; + info->build_id[i] = build_id[i]; + } + for (; i < FIRMWARE_INFO_BUILD_ID_SIZE; ++i) info->build_id[i] = '0'; + info->build_id[FIRMWARE_INFO_BUILD_ID_SIZE] = '\0'; + return FIRMWARE_INFO_OK; +} + +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; + if ((date_text == NULL) || (time_text == NULL) || (info == NULL)) + return FIRMWARE_INFO_INVALID_ARGUMENT; + for (month = 0U; month < 12U; ++month) { + if ((date_text[0] == months[month][0]) && (date_text[1] == months[month][1]) + && (date_text[2] == months[month][2])) { found = 1U; break; } + } + if (!found || !two_digits(&date_text[4], &info->day) + || !four_digits(&date_text[7], &info->year) + || !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); + 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_status_t status; + if ((registers == NULL) || (count < FIRMWARE_INFO_REGISTER_COUNT)) + return FIRMWARE_INFO_INVALID_ARGUMENT; + status = firmware_info_validate(info); + if (status != FIRMWARE_INFO_OK) return status; + registers[0] = FIRMWARE_INFO_CONTRACT_VERSION; + registers[1] = info->major; registers[2] = info->minor; registers[3] = info->patch; + registers[4] = info->year; + registers[5] = (uint16_t)(((uint16_t)info->month << 8U) | info->day); + 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]); + return FIRMWARE_INFO_OK; +} + +firmware_info_status_t firmware_info_to_le_bytes( + const firmware_info_t *info, uint8_t *payload, size_t size) +{ + uint16_t words[FIRMWARE_INFO_REGISTER_COUNT]; + uint8_t 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); + } + return FIRMWARE_INFO_OK; +} diff --git a/c/firmware-info/src/firmware_info_port.c b/c/firmware-info/src/firmware_info_port.c new file mode 100644 index 0000000..cced115 --- /dev/null +++ b/c/firmware-info/src/firmware_info_port.c @@ -0,0 +1,13 @@ +#include "firmware_info_port.h" + +firmware_info_status_t firmware_info_port_describe(firmware_info_t *info) +{ + firmware_info_status_t status; + if (info == NULL) return FIRMWARE_INFO_INVALID_ARGUMENT; + info->major = (uint16_t)FIRMWARE_VERSION_MAJOR; + info->minor = (uint16_t)FIRMWARE_VERSION_MINOR; + info->patch = (uint16_t)FIRMWARE_VERSION_PATCH; + status = firmware_info_set_build_id(info, FIRMWARE_BUILD_ID); + if (status != FIRMWARE_INFO_OK) return status; + return firmware_info_parse_build_stamp(__DATE__, __TIME__, info); +} diff --git a/c/firmware-info/tests/test_firmware_info.c b/c/firmware-info/tests/test_firmware_info.c new file mode 100644 index 0000000..70fad0c --- /dev/null +++ b/c/firmware-info/tests/test_firmware_info.c @@ -0,0 +1,20 @@ +#include "firmware_info.h" +#include +#include + +int main(void) +{ + firmware_info_t info = {1U, 2U, 3U, 0U, 0U, 0U, 0U, 0U, 0U, ""}; + uint16_t words[FIRMWARE_INFO_REGISTER_COUNT]; + uint8_t bytes[FIRMWARE_INFO_PAYLOAD_SIZE]; + assert(firmware_info_set_build_id(&info, "abc") == FIRMWARE_INFO_OK); + assert(strcmp(info.build_id, "abc00000") == 0); + assert(firmware_info_parse_build_stamp("Aug 30 2026", "12:34:56", &info) == FIRMWARE_INFO_OK); + assert(firmware_info_to_registers(&info, words, FIRMWARE_INFO_REGISTER_COUNT) == FIRMWARE_INFO_OK); + assert(words[0] == 1U && words[1] == 1U && words[4] == 2026U); + assert(words[5] == 0x081eU && words[6] == 0x0c22U && words[7] == 56U); + assert(words[8] == 0x6162U && words[9] == 0x6330U); + assert(firmware_info_to_le_bytes(&info, bytes, sizeof(bytes)) == FIRMWARE_INFO_OK); + assert(bytes[0] == 1U && bytes[1] == 0U && bytes[16] == 0x62U && bytes[17] == 0x61U); + return 0; +} diff --git a/c/firmware-info/tools/make_build_id.ps1 b/c/firmware-info/tools/make_build_id.ps1 new file mode 100644 index 0000000..e217f12 --- /dev/null +++ b/c/firmware-info/tools/make_build_id.ps1 @@ -0,0 +1,18 @@ +param( + [string]$Repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..\..')).Path, + [string]$Output = (Join-Path $PSScriptRoot '..\generated\firmware_build_id.h') +) + +$id = 'NOGIT000' +$hash = (& git -C $Repository rev-parse --short=8 HEAD 2>$null) +if ($LASTEXITCODE -eq 0 -and $hash) { + & git -C $Repository diff --quiet HEAD 2>$null + $id = if ($LASTEXITCODE -eq 0) { $hash.Substring(0, 8) } else { $hash.Substring(0, 7) + '+' } +} +$directory = Split-Path -Parent $Output +New-Item -ItemType Directory -Force -Path $directory | Out-Null +$body = "#ifndef FIRMWARE_BUILD_ID_H`n#define FIRMWARE_BUILD_ID_H`n#define FIRMWARE_BUILD_ID `"$id`"`n#endif`n" +if (!(Test-Path -LiteralPath $Output) -or ((Get-Content -Raw $Output) -ne $body)) { + [IO.File]::WriteAllText($Output, $body, [Text.UTF8Encoding]::new($false)) +} +Write-Host "[firmware-info] build-id: $id"