Your First Secure Application (STM32)
A guided walkthrough of the led-indicator-secure example — building a KNX Data Secure device on the STM32 (HAL) platform: SioSizing, the secure device bundle, MakeSecure, the FDSK, and secure ETS commissioning.
This page walks through led-indicator-secure, the KNX Data Secure
counterpart of led-indicator.
It is the exact same application — same group objects, same LED behavior — with
the secure path wired in. If you have not read the non-secure walkthrough yet,
start there; this page only covers what is different for a secure device.
Standard vs. Secure. If your device does not need KNX Data Secure, use Your First Application (STM32) instead — it is simpler (no key tables, no FDSK, one fewer flash region).
What it does
Identical to the non-secure example:
- On/Off (1 bit): KNX
1turns the indicator LED on,0turns it off. - State (1 byte):
0= Off,1= On,2= SOS (emergency blink). - Feedback: both objects report their current state back to KNX.
- Alive notification: if enabled by an ETS parameter, the device
periodically sends a
1-bit so a controller can see it is alive.
The only difference is that every telegram is now encrypted and replay-protected by KNX Data Secure (KNX Vol 3/5/1) before it goes on the bus.
The group object table
Same table, same objectId values, as the non-secure example — recreate these in
your ETS project (.knxproj), marked as secure group objects:
| # | Name | DPT | Size | Flags | Direction |
|---|---|---|---|---|---|
| 1 | LED On/Off | 1.001 Switch | 1 bit | C, W | Bus → Device |
| 2 | On/Off Feedback | 1.001 Switch | 1 bit | C, R, T | Device → Bus |
| 3 | LED State | 5.010 (0/1/2) | 1 byte | C, W | Bus → Device |
| 4 | LED State Feedback | 5.010 | 1 byte | C, R, T | Device → Bus |
| 5 | Alive Notification | 1.002 Bool | 1 bit | C, R, T | Device → Bus |
(C = Communication, R = Read, W = Write, T = Transmit)
These objectId values must match the order of the group objects in your
ETS application, and the parameter addresses must match ETS memory addresses. If
the two sides disagree, incoming telegrams land on the wrong object — this is
unchanged from the non-secure example.
Global construction — same peripherals, one extra section
The watchdog, clock, bus driver, transmitter, LEDs, button, and StackParameters
are constructed exactly like the non-secure example:
HalIWDGWatchDog watchDog(hiwdg);
BaseClock clock;
HalKnxBusDriver busDriver(huart2, hdma_usart2_rx, hdma_usart2_tx,
MX_USART2_UART_Init, clock, watchDog);
Ncn5130 transmitter(busDriver, clock, watchDog);
HalLed indicatorLed(GPIOB, YELLOW_LED_Pin, clock);
HalLed knxLed(GPIOB, RED_LED_Pin, clock);
BaseButton button;
static uint8_t PID_PROGRAM_VERSION[5] = {0x02, 0x3E, 0x00, 0x01, 0x00};
static uint8_t PID_HARDWARE_TYPE[6] = {0x3E, 0x02, 0x00, 0x01, 0x00, 0x01};
static uint8_t PID_SERIAL_NUMBER[6] = {0x02, 0x3E, 0x12, 0x34, 0x56, 0x78};
static PropertyStore emptyStore{nullptr, 0};
StackParameters stackParameters(
onKnxTelegram, onDifference, onTelegram,
onErrorKnxTelegram, onResetCommand, onStackError,
256, 256, 256, 256,
PID_PROGRAM_VERSION, PID_HARDWARE_TYPE, PID_SERIAL_NUMBER,
emptyStore,
0x023Eu
);See Your First Application (STM32) for what each of these does — none of it changes for a secure device. What is new is everything below: sizing the secure key tables, the secure device bundle, deriving the flash layout, building the security object, and the FDSK.
Step 1 — Size the secure key tables (SioSizing)
KNX Data Secure keeps its keys and associations in fixed-size tables that ETS
provisions during commissioning. You size these tables once, at compile time,
with a SioSizing:
// {group keys, point-to-point keys, IA table size, group-object flags, roles}
// The group-object flags count must be at least the number of group objects
// you have (5 here). Keep these small to save RAM.
static constexpr SioSizing kSioSizing = {5u, 0u, 16u, 8u, 0u};| Field | Meaning | This example |
|---|---|---|
grpKeyCount | how many group keys ETS can provision | 5 (one per secure group object) |
p2pKeyCount | how many point-to-point keys (device-to-device) | 0 (not used) |
iaCount | Security Individual Address table size | 16 |
goFlagCount | must be ≥ your group object count | 8 |
roleCount | AN190 role table size | 0 (not used) |
goFlagCount must be at least the number of group objects your application
registers (5 here). A count that is too small leaves objects without a security
flag slot.
Step 2 — The secure device bundle
The non-secure example uses a plain KnxDeviceStorage for its static working
memory. A secure device needs more: crypto scratch space, the SIO key-table
buffers, and the replay-counter state. KnxSecureDeviceStorage<S> bundles all
of it into one object, sized by the kSioSizing from Step 1:
// Static storage for this device: bundles the flash-layout table, the secure
// crypto working memory, the stack's own working memory, and the SIO
// key-table buffers (sized by kSioSizing above) all in one place.
static KnxSecureDeviceStorage<kSioSizing> device;Nothing else needs to touch this object directly — it is only ever passed to
MakeFlashLayout, MakeSecure, and MakeStack below, exactly like the
non-secure example passes its plain KnxDeviceStorage around.
Step 3 — Derive the flash layout (secure regions included)
MakeFlashLayout is called the same way as in the non-secure example, but on a
secure bundle it also derives the replay-counter and SIO key-table flash
regions (in addition to the stack-state and user-data regions):
// Flash area for all of this device's saved data: the secure stack's
// counter/key/state regions plus this app's own small state (last LED value).
HalFlash baseFlash(0x0801u, 0xE800u);
// Splits baseFlash into the secure counter/key regions, the stack's own
// region, and this app's region ("user-data"). userDataPageCount=1 is enough
// for the single LED-state byte this example saves.
KnxFlashLayout& flashLayout = MakeFlashLayout(
device, baseFlash, stackParameters, /*userDataPageCount=*/1u);You still hand it one base flash window — there are no hand-placed
counterFlash/sioKeyFlash addresses to get wrong; MakeFlashLayout derives
every region's offset from StackParameters and kSioSizing.
The reserved flash window is bigger than the non-secure example. Secure
needs room for the replay counter and key tables on top of the stack state, so
FLASH_USER grows from 3 pages (non-secure) to 6 pages in this example's
linker script. If you change kSioSizing or userDataPageCount, re-check that
FLASH_USER is still large enough — the linker script asserts the exact size it
expects.
Step 4 — Build the security object (MakeSecure)
MakeSecure builds the KNX Data Secure object: AES-CCM encryption, the
monotonic replay counter, and the Security Interface Object that holds the key
tables.
// Builds the KNX Data Secure object: handles encryption, replay protection,
// and the security key tables. Keys start empty — ETS provisions them during
// commissioning.
ISecurity& secure = MakeSecure(device, flashLayout, clock);The deviceIA parameter is optional and defaults to 0xFFFF — the KNX
Vol 3/5/1 S-Mode "unprogrammed" individual address. Leave it at the default
unless your device already ships with a fixed individual address; ETS assigns
the real one during commissioning either way:
// Only if the device already owns a fixed individual address:
ISecurity& secure = MakeSecure(device, flashLayout, clock, KnxAddress(1, 1, 5));Step 5 — The factory key (FDSK)
Every KNX Data Secure device ships with a Factory Default Setup Key (FDSK) — a 16-byte key ETS uses to securely commission the device for the first time (you enter it, or scan it from a QR code, when adding the device to a secured ETS project).
// EDIT THIS: use a unique key per device in production (e.g. derived from the
// MCU UID) — this example value is not safe to ship.
static constexpr uint8_t kDeviceFdsk[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10};Set it once in main(), before stack.Init():
stackParameters.SetFdsk(kDeviceFdsk);That is the only FDSK-related call main.cpp makes. stack.Init() does the
rest: it validates the secure storage, provisions the Tool Key from the FDSK on
first boot, and links the device serial number.
The example FDSK is not safe to ship. It is a fixed, publicly-visible value used only so the example builds and commissions out of the box. A real product needs a unique FDSK per device — typically derived from the MCU's unique ID and printed on the device (or on a QR code sticker) at production time. Never ship two devices with the same FDSK.
Step 6 — Build the stack (secure variant)
MakeStack is called with the same arguments as the non-secure example, plus
one: the secure object from Step 4.
// Create the KNX stack object — SECURE version (10 arguments). The only
// difference from the non-secure example is the final "secure" argument.
IKnxStack& stack =
MakeStack(device, stackParameters, busDriver, watchDog, knxLed, clock,
transmitter, button, flashLayout, secure);Order matters. MakeFlashLayout must run before both MakeSecure and
MakeStack — they both consume its flashLayout. MakeSecure must run before
MakeStack, since MakeStack takes the secure object it returns. This is the
same "define in dependency order" rule as the non-secure example, with two more
links in the chain.
The application object (LedIndicatorLogic) is constructed last, exactly as in
the non-secure example — it takes the finished stack (plus the LED and clock).
main() — one extra line
main() is identical to the non-secure example, with a single addition:
stackParameters.SetFdsk(...) before stack.Init().
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
MX_USART2_UART_Init();
MX_IWDG_Init();
stackParameters.SetFdsk(kDeviceFdsk); // secure-only: set once, before Init()
HAL_TIM_Base_Start_IT(&htim2); // 1 ms tick for clock + LED timing
logic.Init(); // LED off, feedback objects ready
// In this secure build, Init() also sets up KNX Data Secure: it checks the
// key storage, provisions the Tool Key from the FDSK on first boot, and
// links the device serial number — no extra secure.* calls needed here.
stack.Init();
#ifdef WATCHDOG_ENABLED
watchDog.Enable();
#endif
while (1) {
stack.Work();
}
}Everything else — event wiring (stackevents.cpp), the LedIndicatorLogic
application class, and the main loop's stack.Work() — is identical to the
non-secure example. See
Wiring events,
Application logic,
and The main loop
on the non-secure page — nothing there changes for a secure device.
Secure commissioning with ETS
Unlike the non-secure example, adding this device to an ETS project is a secure commissioning flow:
Mark the ETS project as secured (or add the device to an existing secured
project) and add your product's .knxprod device.
Enter the FDSK when ETS prompts for it — type the 16 bytes, or scan a QR code if your production process encodes the FDSK that way. This is the only secret you provide; ETS uses it to securely exchange the real operational keys with the device.
Let ETS provision the keys. ETS generates and downloads the group keys
(and, if configured, point-to-point/tool keys) into the device's key tables —
the ones sized by kSioSizing in Step 1. Your code never sees or handles these
keys directly.
Assign the individual address and download as usual. From here on, commissioning looks like any other KNX device — group address assignment, parameter download, and so on.
Creating the ETS product (.knxprod) itself is your job and outside the scope
of this example — just make sure it is marked as a KNX Data Secure product and
its group objects/parameters match the tables above.
Build and flash
Building and flashing a secure project uses the same CMake/OpenOCD flow as
the non-secure example — see
Getting Started (STM32) and
Flashing (STM32). From
examples/STM32F103-HAL/led-indicator-secure/:
# Debug build
cmake -S . -B build_dbg -DCMAKE_BUILD_TYPE=Debug
cmake --build build_dbg
# Release build (size-optimized, watchdog enabled)
cmake -S . -B build_rel -DCMAKE_BUILD_TYPE=Release
cmake --build build_rel-DCMAKE_BUILD_TYPE is required, not optional. Configuring without it
(cmake -S . -B build alone) leaves CMAKE_BUILD_TYPE empty; the project's
CMakeLists.txt detects this and fails the configure step with a
FATAL_ERROR rather than silently picking a default. Always pass Debug or
Release explicitly.
Flash the resulting .elf/.hex the same way as the non-secure example (see
Flashing (STM32)):
openocd -f "led-indicator-secure Debug.cfg" \
-c "program build_dbg/led-indicator-secure.elf verify reset exit"Adapt it
Every point you normally change is marked with EDIT THIS: in the source — the
same points as the non-secure example, plus the secure-specific ones below.
Identity and tables (main.cpp): manufacturer id, PID byte arrays, and the
four StackParameters table sizes — same as the non-secure example.
Secure key-table sizing (main.cpp): grow kSioSizing's counts as your
number of secure group objects/associations grows. Keep goFlagCount ≥ your
group object count.
FDSK (main.cpp): replace kDeviceFdsk with a real per-device key before
shipping — never the example value.
Transceiver, pins, and flash address (main.cpp): same as the non-secure
example — switch Ncn5130 to ElmosE98123 if needed, set your LED GPIO
pins, and match baseFlash to your linker script's reserved (now larger)
FLASH_USER window.
Object and parameter map (LedIndicatorParameterMapping.h) and
behavior (LedIndicatorLogic.cpp): identical process to the non-secure
example.
The concrete peripheral drivers — constructor arguments, transceiver choices, and the interrupt callbacks that feed them — are unchanged from the non-secure example and detailed on the STM32 HAL Peripherals page.