Автоматическая нумерация версий прошивок

Нумерация версий компьютерного программного обеспечения указывает на изменения в продукте, и хотя единой схемы нет, чаще всего используются форматы с тремя или четырьмя числами: Мажорная.Минорная.Патч (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';
    }
}