Нумерация версий компьютерного программного обеспечения указывает на изменения в продукте, и хотя единой схемы нет, чаще всего используются форматы с тремя или четырьмя числами: Мажорная.Минорная.Патч (A.B.C), где мажорная версия указывает на кардинальные изменения, минорная — на добавление нового функционала, а патч — на исправление ошибок.
Для прошивок (микропрограмм) не делают большого количества сборок, поэтому такое понятие как patch здесь не используется. Или, если быть точнее, и нововведения и исправления ошибок будут приращивать минорное число (номер сборки).
В таких средах разработки как Keil и ему подобных, для нумерации версий прошивок (сборок ПО) мы часто вписываем ручками определения, например:
#define FW_VER 0x0105 // Версия прошивки
#define FW_BUILD 0x2510 // Год и месяц сборки
И эта нумерация рутинная. Иногда даже пропускаешь этот момент. А в имени выходного файла вообще нет этой информации. Приходится потом так же ручками переименовывать myfirmware.hex в myfirmware_1.05_20251030.hex, чтобы передать его людям.
Это решение для автоматизации нумерации версий и именования файлов прошивок на примере программы keil uVision. Для вас не составит труда адаптировать это решение для других сред (eclipse, IAR, etc.)
Я видел примеры, где люди не заморачиваются над контролем нумерации версии и сопровождают имя файла штампом даты и времени, типа myfirmware_202510302359.hex.

Хотелось бы традиционного представления версии, поэтому я написал программу для Windows. Она будет вести подсчет версий, генерить файл version.h. Затем будет вытаскивать в отдельную папку полученный hex файл прошивки и переименовывать его "правильно". Поместите version.exe в папку с проектом и пропишите в настройках проекта команды, которые выполнятся после успешной сборки:

После version.exe пропишите относительный путь к готовой прошивке. Она будет скопирована в папку под названием RELEASES (рядом с version.exe), а формат ее имени будет таким NAME_1.05_20251117.hex. Папку hex самостоятельно создавать не нужно, она создается автоматически, так же как и файл version.hex (который, кстати вы можете редактировать в любом текстовом редакторе).
программу version.exe Скачать можно отсюда
В основной программе вашей прошивки в keil вы можете подгрузить версию и дату сборки
// Подгрузка версии и даты сборки
sets.fw_ver = FW_VER;
sets.fw_build = getBuildDate();
В моем случае формат версии простой, два байта в BCD формате, образующие одно 16-битное слово для удобства передачи по Modbus.
Я думаю, если вы привыкнете к этому инструменту, то оцените по достоинству.
Обсудить эту статью вы можете здесь >> обсуждение в Telegram <<
И конечно, господа, подписывайтесь на канал ipaSoft Electronics.
Исходный код программы:
// Компилятор TCC https://bellard.org/tcc/
#include <stdio.h>
#include <io.h>
#include <stdlib.h> // для atoi
#include <time.h>
#include <string.h>
#include <sys/stat.h>
static const char *VCTL_FILENAME = "version.hex";
static const char *VCTL_HEADER = "version.h";
static const char *OUT_FOLDER = "RELEASES";
static FILE *fp;
static FILE *fp2;
int folder_exists(const char *path);
int create_folder(const char *path);
const char* get_filename(const char *path);
void get_filename_without_ext(const char *path, char *result, size_t size);
int main(int argc, char *argv[]) {
int maj = 0, min = 1;
int len = 0;
char dig1[3], dig2[3];
if (argc != 2) { // Проверка, передали ли путь к HEX
printf("ipaSoft Embed Version Control\n");
printf("using: %s path_to_hex_file\n", argv[0]);
return 1;
}
if (_access(VCTL_FILENAME, 0) == 0) { // Файл существует
// Прочитать из него версию
fp = fopen(VCTL_FILENAME, "r");
if (!fp) {
printf("Error opening file %s\n", VCTL_FILENAME);
return 1;
}
fseek(fp, 0, SEEK_END); // В конец
len = ftell(fp);
if (len == 6) { // Правильный размер. Используем
fseek(fp, 2, SEEK_SET);
// Читаем мажорную версию
fread(dig1, 1, 2, fp);
dig1[2] = 0; // Нуль-терминатор
// Читаем минорную версию
fread(dig2, 1, 2, fp);
dig2[2] = 0; // Нуль-терминатор
maj = atoi(dig1); // Преобразуем текст в число
min = atoi(dig2);
fclose(fp);
}
else { // Неправильный формат
printf("File %s is corrupted and will be recreated\n", VCTL_FILENAME);
fclose(fp);
remove(VCTL_FILENAME); // Удаляем поврежденный файл
}
}
else { // Файл не существует
printf("File %s does not exist. It will be created.\n", VCTL_FILENAME);
}
////// Нынешнюю сборку нужно скопировать в папку с релизами ///////
// Проверка папки RELEASES (и создание, если нет)
if (!folder_exists(OUT_FOLDER)) {
if (create_folder(OUT_FOLDER)) {
printf("Unable to create folder \"%s\"\n", OUT_FOLDER);
return 1;
}
}
// Перемещение файла в эту папку
char *source_path = argv[1];
fp = fopen(source_path, "rb"); // Открыть файл входной
if (!fp) {
printf ("Unable to open file %s\n", source_path);
return 1;
}
// Получить системную дату
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
// Сформировать имя нового файла
char output_file[200] = {0};
char temp_name[200] = {0};
#ifdef _WIN32
sprintf(output_file, "%s\\", OUT_FOLDER);
#else
sprintf(output_file, "%s/", OUT_FOLDER);
#endif
get_filename_without_ext(source_path, temp_name, 128); // Получить название прошивки без расширения и пути
strcat(output_file, temp_name);
sprintf(temp_name, "-%i.%i-", maj, min); // Дать имя с версией
strcat(output_file, temp_name);
sprintf(temp_name, "%04i%02i%02i.hex", timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday);
strcat(output_file, temp_name);
fp2 = fopen(output_file, "wb");
if (!fp2) {
printf("Unable to create %s\n", output_file);
return 1;
}
// Копирование побайтовое
char buffer[4096]; // Буфер для чтения/записи
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
size_t bytes_written = fwrite(buffer, 1, bytes_read, fp2);
if (bytes_written != bytes_read) {
perror("Error file copying\n");
fclose(fp);
fclose(fp2);
return 1;
}
}
// Закрываем файлы
fclose(fp);
fclose(fp2);
// Увеличиваем версию для следующей сборки
if (++min > 99) {
min = 0;
++maj;
}
// Создание заголовочного файла
fp = fopen(VCTL_HEADER, "w");
if (!fp) {
printf("Could not create file %s\n", VCTL_HEADER);
return 1;
}
// Записываем шаблонный текст и версию
fprintf(fp, "// ipaSoft Version Control Header\n");
fprintf(fp, "// Auto-generated - do not edit\n");
fprintf(fp, "#ifndef VERSION_H\n");
fprintf(fp, "#define VERSION_H\n");
// Выводим числа в формате BCD (Hex представление)
fprintf(fp, "#define FW_VER 0x%02i%02i\n", maj, min);
fprintf(fp, "unsigned short getBuildDate(void);\n");
fprintf(fp, "#endif\n");
fclose(fp);
printf("Next build version will be: %02i.%02i\n", maj, min);
// Сохраняем новую версию в контрольном файле
fp = fopen(VCTL_FILENAME, "w");
if (!fp) {
printf("Error opening file %s for writing\n", VCTL_FILENAME);
return 1;
}
fprintf(fp, "0x%02i%02i", maj, min);
fclose(fp);
return 0;
}
// Функция проверки существования папки
int folder_exists(const char *path) {
struct stat info;
if (stat(path, &info) != 0) {
return 0; // Не существует или нет доступа
}
return S_ISDIR(info.st_mode); // Проверяем, что это именно папка
}
// Функция создания папки (создает все промежуточные папки)
int create_folder(const char *path) {
#ifdef _WIN32
return mkdir(path);
#else
return mkdir(path, 0755); // Права: владелец - чтение/запись/исполнение, остальные - чтение/исполнение
#endif
}
// Функция 1: Получить только имя файла (без пути)
const char* get_filename(const char *path) {
const char *filename = strrchr(path, '/');
if (filename == NULL) {
filename = strrchr(path, '\\'); // Для Windows
if (filename == NULL) {
return path; // Путь уже является именем файла
}
}
return filename + 1; // Пропускаем разделитель
}
// Функция 2: Получить имя файла без расширения
void get_filename_without_ext(const char *path, char *result, size_t size) {
// Получаем имя файла с расширением
const char *filename = get_filename(path);
// Находим последнюю точку в имени файла
const char *dot = strrchr(filename, '.');
if (dot != NULL && dot != filename) { // Проверяем, что точка не в начале имени
// Копируем часть до точки
size_t length = dot - filename;
if (length < size) {
strncpy(result, filename, length);
result[length] = '\0';
} else {
result[0] = '\0';
}
} else {
// Если нет расширения, копируем всё имя
strncpy(result, filename, size - 1);
result[size - 1] = '\0';
}
}

