Kbox-UniStack Docs
STM32 (HAL)

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 1 turns the indicator LED on, 0 turns 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:

#NameDPTSizeFlagsDirection
1LED On/Off1.001 Switch1 bitC, WBus → Device
2On/Off Feedback1.001 Switch1 bitC, R, TDevice → Bus
3LED State5.010 (0/1/2)1 byteC, WBus → Device
4LED State Feedback5.0101 byteC, R, TDevice → Bus
5Alive Notification1.002 Bool1 bitC, R, TDevice → 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};
FieldMeaningThis example
grpKeyCounthow many group keys ETS can provision5 (one per secure group object)
p2pKeyCounthow many point-to-point keys (device-to-device)0 (not used)
iaCountSecurity Individual Address table size16
goFlagCountmust be ≥ your group object count8
roleCountAN190 role table size0 (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.