This content originally appeared on DEV Community and was authored by Ripan Deuri
In the previous part of this series, we built and flashed our first Zephyr app on the STM32 Nucleo-F401RE. Now, we’re going a step deeper embedding firmware version metadata right inside the image.
The result is a self-describing binary that can tell you its build version at runtime — ideal for debugging, OTA rollbacks, and CI pipelines.
🧩 Enabling Zephyr Shell
The Zephyr shell subsystem lets you interact with your firmware over a UART, USB, or virtual terminal. Enable it in your project configuration (prj.conf
):
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_LOG_CMDS=y
Re-build the app:
west build -t run -d build_native_sim
Typical console output on native_sim:
-- west build: running target run
uart connected to pseudotty: /dev/pts/4
*** Booting Zephyr OS build v4.2.0-6152-gfd51dde8f5ca ***
[00:00:00.000,000] <inf> app_main: Starting nucleo-zephyr-app
A pseudo-TTY such as /dev/pts/4
appears — this is your shell interface.
Connect to it:
screen /dev/pts/4
🧱 Building a Firmware Version Command
I wanted each binary to carry its own version signature.
That meant embedding build metadata directly in flash, then exposing it through a shell command called version
.
🧩 Step 1 — Add a Custom Linker Section
We’ll store version information (using version_info
) in a custom .app_version
section.
version_info.h
/* Magic for validation */
#define VERSION_INFO_MAGIC 0xDEADBEAF
typedef struct __attribute__((packed)) {
uint32_t magic;
uint32_t version_major;
uint32_t version_minor;
uint32_t version_patch;
uint8_t reserved[4];
char zephyr_ver[ZEPHYR_VER_SIZE];
char app_ver[APP_VER_SIZE];
char build_info[BUILD_INFO_SIZE];
uint32_t crc32;
} version_info_t;
Following linker snippet in app_version.ld
places this section in ROM across all boards:
app_version.ld
/* app_version.ld */
SECTION_PROLOGUE(.app_version,,)
{
__app_version_start = .;
KEEP(*(SORT_BY_NAME(.app_version*)))
__app_version_end = .;
} GROUP_LINK_IN(ROMABLE_REGION)
Explanation:
- SECTION_PROLOGUE(.app_version,,): This is Zephyr’s macro wrapper around a regular SECTIONS { ... } block entry. It defines a linker output section named .app_version and wires in Zephyr’s platform defaults (alignment, flags).
-
KEEP((SORT_BY_NAME(.app_version)))
*(.app_version*)
grabs all input sections whose names start with .app_version (e.g., .app_version, .app_version.header, .app_version.zephyr).SORT_BY_NAME(...)
makes the order deterministic by sorting those input section names alphabetically. That way builds are reproducible and predictable.KEEP(...)
tells the linker’s garbage collector not to discard this section even if nothing references it. GROUP_LINK_IN(ROMABLE_REGION): It places the output section into the board’s ROM-able memory group. On STM32 it lives in flash; on native_sim it lands in a read-only ELF segment.
Finally, link it in from CMakeLists.txt:
zephyr_linker_sources(SECTIONS app_version.ld)
🧩 Step 2 — Generate Version Data at Build Time
Pull the current Git tag and inject it into generated C code.
git describe --tags --long --dirty --always
CMake Helper Function
function(execute_command output_variable)
set(var_to_set "${output_variable}")
execute_process(
COMMAND ${ARGN}
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
OUTPUT_VARIABLE "${var_to_set}"
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
set("${var_to_set}" "${${var_to_set}}" PARENT_SCOPE)
endfunction()
execute_command(APP_GIT_DESCRIBE git describe --tags --long --dirty --always)
set(APP_VERSION_STRING "${APP_GIT_DESCRIBE}")
Sample result:
APP_VERSION_STRING : v1.0.0-0-gc5a9106
Template Source File – version_info.c.in
/* generated_version_info.c - auto-generated during CMake configure */
#include "version_info.h"
__attribute__((used, section(".app_version")))
const version_info_t app_version_info = {
.magic = VERSION_INFO_MAGIC,
.version_major = @APP_VER_MAJOR@,
.version_minor = @APP_VER_MINOR@,
.version_patch = @APP_VER_PATCH@,
.reserved = {0,0,0,0},
.zephyr_ver = "@ZEPHYR_VERSION_STRING@",
.app_ver = "@APP_VERSION_STRING@",
.build_info = "@BUILD_INFO_STRING@",
};
__attribute__((used, section(".app_version")))
above forces the struct into the .app_version
region even if unreferenced.
CMake Configuration to Generate and Link
set(VERSION_INFO_C_IN "${CMAKE_CURRENT_SOURCE_DIR}/version_info.c.in")
set(VERSION_INFO_C_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated_version_info.c")
configure_file(${VERSION_INFO_C_IN} ${VERSION_INFO_C_OUT} @ONLY)
target_sources(app PRIVATE ${VERSION_INFO_C_OUT})
🧩 Step 3 — Verify the Linker Section
For Native Builds
objdump -s -j .app_version build_native_sim/zephyr/zephyr.elf
readelf -p .app_version build_native_sim/zephyr/zephyr.elf
For STM32 (ARM) Builds
arm-none-eabi-objdump -s -j .app_version build/zephyr/zephyr.elf
arm-none-eabi-readelf -p .app_version build/zephyr/zephyr.elf
Example dump:
objdump -s -j .app_version build_native_sim/zephyr/zephyr.elf
build_native_sim/zephyr/zephyr.elf: file format elf32-i386
Contents of section .app_version:
0340 afbeadde 01000000 00000000 00000000 ................
0350 00000000 76342e32 2e302d36 3135322d ....v4.2.0-6152-
0360 67666435 31646465 38663563 00000000 gfd51dde8f5c....
0370 00000000 00000000 00000000 00000000 ................
0380 00000000 00000000 00000000 00000000 ................
0390 00000000 76312e30 2e302d30 2d673566 ....v1.0.0-0-g5f
03a0 34333662 332d6469 72747900 00000000 436b3-dirty.....
03b0 00000000 00000000 00000000 00000000 ................
03c0 00000000 00000000 00000000 00000000 ................
03d0 00000000 32303235 2d31302d 31395432 ....2025-10-19T2
03e0 323a3130 3a33305a 2d726970 616e4072 2:10:30Z-ripan@r
03f0 6970616e 2d6e6f74 65626f6f 6b000000 ipan-notebook...
0400 00000000 00000000 00000000 00000000 ................
0410 00000000 00000000
Strings view:
readelf -p .app_version build_native_sim/zephyr/zephyr.elf
String dump of section '.app_version':
[ 14] v4.2.0-6152-gfd51dde8f5c
[ 54] v1.0.0-0-g5f436b3-dirty
[ 94] 2025-10-19T22:10:30Z-ripan@ripan-notebook
That confirms the metadata is embedded inside the image.
🧩 Step 4 — Access Version Info at Runtime
The linker exposes two symbols marking section boundaries:
__app_version_start
and __app_version_end
.
Use them to retrieve data safely:
extern const char __app_version_start[];
extern const char __app_version_end[];
const char *start = __app_version_start;
const char *end = __app_version_end;
if (start >= end) {
shell_print(sh, "No .app_version present");
return 0;
}
if ((size_t)(end - start) < sizeof(version_info_t)) {
shell_print(sh, "app_version section too small");
return 0;
}
const version_info_t *info = (const version_info_t *)start;
Above code snippet can be used directly inside your custom shell command implementation.
uart:~$ version
magic : 0xdeadbeaf
version : v1.0.0
build_info: 2025-10-20T08:43:38Z-ripan@ripan-notebook
app ver : v1.0.0-0-gc5a9106
zephyr_ver: v4.2.0-6152-gfd51dde8f5c
This content originally appeared on DEV Community and was authored by Ripan Deuri

Ripan Deuri | Sciencx (2025-10-20T13:09:20+00:00) How to Create a Firmware Version System in Zephyr RTOS. Retrieved from https://www.scien.cx/2025/10/20/how-to-create-a-firmware-version-system-in-zephyr-rtos/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.