Add reusable firmware info module
This commit is contained in:
@@ -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` |
|
||||
|
||||
8
c/firmware-info/CMakeLists.txt
Normal file
8
c/firmware-info/CMakeLists.txt
Normal file
@@ -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)
|
||||
11
c/firmware-info/PORTING.md
Normal file
11
c/firmware-info/PORTING.md
Normal file
@@ -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-байтов в каждом логическом слове.
|
||||
27
c/firmware-info/README.md
Normal file
27
c/firmware-info/README.md
Normal file
@@ -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/<mcu>/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` без изменений.
|
||||
48
c/firmware-info/include/firmware_info.h
Normal file
48
c/firmware-info/include/firmware_info.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef FIRMWARE_INFO_H
|
||||
#define FIRMWARE_INFO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
18
c/firmware-info/include/firmware_info_port.h
Normal file
18
c/firmware-info/include/firmware_info_port.h
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
121
c/firmware-info/src/firmware_info.c
Normal file
121
c/firmware-info/src/firmware_info.c
Normal file
@@ -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;
|
||||
}
|
||||
13
c/firmware-info/src/firmware_info_port.c
Normal file
13
c/firmware-info/src/firmware_info_port.c
Normal file
@@ -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);
|
||||
}
|
||||
20
c/firmware-info/tests/test_firmware_info.c
Normal file
20
c/firmware-info/tests/test_firmware_info.c
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "firmware_info.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
18
c/firmware-info/tools/make_build_id.ps1
Normal file
18
c/firmware-info/tools/make_build_id.ps1
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user