Your First Application (STM32)
A guided walkthrough of the led-indicator example — concrete Hal peripheral construction, event wiring, KnxLogicBase application logic, and the main loop on the STM32 (HAL) platform.
This page walks through the led-indicator example end to end. It is a
complete KNX device built entirely on the public stack API — no product code —
so you can copy it and change it freely.
This is the Standard / Non-Secure walkthrough. Building a KNX Data Secure device instead? See Your First Secure Application (STM32).
What it does
- 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 group object table
Recreate these in your ETS project. The # column is the object number and must
match the objectId values in LedIndicatorParameterMapping.h.
| # | 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)
The object numbers and parameter addresses are collected in one header so the embedded side stays aligned with ETS:
enum LedIndicatorObjectId : uint16_t {
Object_OnOff_Bit = 1, // incoming on/off (Write)
Object_OnOffFeedback_Bit = 2, // outgoing on/off state (Read + Transmit)
Object_LedState_Byte = 3, // incoming state 0/1/2 (Write)
Object_LedStateFeedback_Byte = 4, // outgoing state (Read + Transmit)
Object_AliveNotification_Bit = 5, // outgoing alive bit (Read + Transmit)
};
enum LedIndicatorParameterId : uint16_t {
Parameter_AliveNotificationControl_Bit = 0xFB00, // 1 bit: 0/1
Parameter_AliveNotificationPeriodSecond_Word = 0xFB01, // 2 bytes: seconds
};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.
Global construction
Every peripheral, the stack parameters, and the stack object itself are defined
at file scope in main.cpp — they live for the whole program and use no
heap. This is where the concrete Hal* drivers are built; the core
Constructing the Stack page shows the same
step in terms of the abstract interface types.
// Watchdog and seconds counter.
HalIWDGWatchDog watchDog(hiwdg);
BaseClock clock;
// KNX bus driver: talks to the KNX line over USART2 (using DMA).
HalKnxBusDriver busDriver(huart2, hdma_usart2_rx, hdma_usart2_tx,
MX_USART2_UART_Init, clock, watchDog);
// KNX line transceiver. Swap to ElmosE98123 for an Elmos front-end.
Ncn5130 transmitter(busDriver, clock, watchDog);
// FLASH area for all of this device's saved data (stack state + app data).
// (0x0801, 0xF400) => base address 0x0801F400, inside the reserved region.
HalFlash baseFlash(0x0801u, 0xF400u);
// LEDs: indicator (yellow, PB1) the app controls; knx (red, PB2) the stack drives.
HalLed indicatorLed(GPIOB, YELLOW_LED_Pin, clock);
HalLed knxLed(GPIOB, RED_LED_Pin, clock);
// KNX programming button (physical-address assignment mode).
BaseButton button;The device identity (PIDs) and the KNX table sizes are packed into a
StackParameters, together with the six event callbacks:
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, // address / association / group-object / app-program table sizes
PID_PROGRAM_VERSION, PID_HARDWARE_TYPE, PID_SERIAL_NUMBER,
emptyStore,
0x023Eu // KNX manufacturer id
);
// Static storage for this device: bundles the flash-layout table with the
// stack's own working memory (no heap).
static KnxDeviceStorage device;
// Splits baseFlash into the stack's own region ("stack-state") 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);
// Build the stack (Standard / Non-Secure, 9 arguments).
IKnxStack& stack = MakeStack(device, stackParameters, busDriver, watchDog,
knxLed, clock, transmitter, button, flashLayout);
// The application brain (global so stackevents.cpp can reach it via extern).
LedIndicatorLogic logic(stack, indicatorLed, clock);Order matters. Each global refers to the ones defined before it: the
transmitter needs busDriver; MakeStack needs the driver, watchdog, LED,
clock, transmitter, button, and flash; logic needs stack. Define them in
dependency order, and define your application object last.
Wiring events
The stack reports events through C-style function pointers (they cannot point at
a C++ method), so stackevents.cpp bridges them to the global logic object.
This example only uses onDifference (a group-write change); the rest are empty:
// Our application brain, defined in main.cpp.
extern LedIndicatorLogic logic;
// A group write arrived from KNX: forward the change to the application brain.
void onDifference(Difference& difference) {
logic.OnDifference(difference);
}
// Unused in this example.
void onKnxTelegram(KnxTelegram& knxTelegram) {}
void onTelegram(ByteSpan telegram) {}
void onErrorKnxTelegram(KnxTelegram& knxTelegram) {}
void onResetCommand() {}onStackError deliberately spins on a table-size error so a mis-sized
StackParameters is caught early (in Release the watchdog turns the spin into a
periodic reset).
Application logic
LedIndicatorLogic derives from KnxLogicBase. In its constructor it registers
each group object into the stack database with Tables().Push, so telegrams from
ETS map onto them:
class LedIndicatorLogic : public KnxLogicBase {
public:
LedIndicatorLogic(IKnxStack& stack, ILed& led, IClock& clock);
void Init();
void OnDifference(Difference& difference);
void Work() override;
void SaveStateToFlash(); // called from the power-loss interrupt
private:
ILed& led;
IClock& clock;
BitObject onOff_{Object_OnOff_Bit};
BitObject onOffFeedback_{Object_OnOffFeedback_Bit};
ByteObject ledState_{Object_LedState_Byte};
ByteObject ledStateFeedback_{Object_LedStateFeedback_Byte};
BitObject alive_{Object_AliveNotification_Bit};
DeviceParameter aliveControl_{Parameter_AliveNotificationControl_Bit};
DeviceParameter alivePeriod_{Parameter_AliveNotificationPeriodSecond_Word};
uint32_t lastAliveSecond_ = 0;
uint8_t currentLedState_ = 0; // 0 == LED off; kept current for SaveStateToFlash()
void ApplyLedState(uint8_t state);
};LedIndicatorLogic::LedIndicatorLogic(IKnxStack& stack, ILed& led, IClock& clock)
: KnxLogicBase(stack), led(led), clock(clock) {
this->stack.Tables().Push(&onOff_);
this->stack.Tables().Push(&onOffFeedback_);
this->stack.Tables().Push(&ledState_);
this->stack.Tables().Push(&ledStateFeedback_);
this->stack.Tables().Push(&alive_);
}OnDifference routes an incoming group write by objectId, drives the LED, and
sends the matching feedback object back with SendTelegramGroupValueWrite. It
also keeps currentLedState_ up to date (using the file-scope state constants
below) so SaveStateToFlash() can persist it without reading the LED back:
static constexpr uint8_t kLedStateOff = 0u; // LED off
static constexpr uint8_t kLedStateOn = 1u; // LED on (steady)
static constexpr uint8_t kLedStateSos = 2u; // LED SOS (emergency) blink
void LedIndicatorLogic::OnDifference(Difference& difference) {
switch (difference.objectId) {
case Object_OnOff_Bit: {
bool turnOn = difference.newData.GetDataBit();
if (turnOn) {
led.SetOn();
} else {
led.SetOff();
}
currentLedState_ = turnOn ? kLedStateOn : kLedStateOff;
onOffFeedback_.SetValueAsBit(turnOn);
this->stack.SendTelegramGroupValueWrite(onOffFeedback_);
break;
}
case Object_LedState_Byte: {
uint8_t state = difference.newData.GetDataByte();
ApplyLedState(state); // 0=Off, 1=On, 2=SOS blink
currentLedState_ = state;
ledStateFeedback_.SetValueAsByte(state);
this->stack.SendTelegramGroupValueWrite(ledStateFeedback_);
break;
}
default:
break;
}
}Work() runs every loop iteration: it advances the LED (needed for SOS blink)
and, if the ETS parameter is enabled, sends the alive bit once its period has
elapsed:
void LedIndicatorLogic::Work() {
led.Work();
if (!aliveControl_.GetValueAsBit()) return;
uint16_t periodSeconds = alivePeriod_.GetValueAsFloat();
if (periodSeconds == 0u) return; // 0 => treat as "not configured"
uint32_t now = clock.GetSecond();
if (now >= lastAliveSecond_ + periodSeconds) {
lastAliveSecond_ = now;
alive_.SetValueAsBit(true);
this->stack.SendTelegramGroupValueWrite(alive_);
}
}The main loop
main() runs HAL init, starts the 1 ms time base, then calls logic.Init()
followed by stack.Init() before spinning the stack forever:
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
MX_USART2_UART_Init();
MX_IWDG_Init();
HAL_TIM_Base_Start_IT(&htim2); // 1 ms tick for clock + LED timing
// Note: ETS parameter reads are already wired up automatically when the
// stack is built above — nothing extra needed here.
logic.Init(); // restores the last saved LED state from flash (see below)
// REQUIRED: starts the KNX stack. Loads the saved device state from flash
// and arms the bus driver so the device can receive telegrams — skip this
// and the device stays deaf on the bus, ETS can never reach it. Call it
// after logic.Init() and before the main loop.
stack.Init();
#ifdef WATCHDOG_ENABLED
watchDog.Enable();
#endif
while (1) {
// Processes incoming telegrams, drives the status LED, and calls logic.Work().
stack.Work();
}
}logic.Init() calls stack.LoadUserData(...) to restore the LED state that
was last saved before a power loss (defaulting to "off" if there is no valid
saved record yet). The save side runs from the KNX bus power-loss interrupt,
which — like the other interrupt callbacks — is covered on the
STM32 HAL Peripherals page.
Adapt it
Every point you normally change is marked with EDIT THIS: in the source.
Identity and tables (main.cpp): set your manufacturer id, the PID byte
arrays, and — as your object count grows — the four table sizes in
StackParameters.
Transceiver and pins (main.cpp): switch Ncn5130 to ElmosE98123 if your
board uses an Elmos front-end, and set the LED GPIO ports/pins and the
baseFlash address for your linker script's reserved flash region.
Object and parameter map (LedIndicatorParameterMapping.h): keep the
objectId order and parameter addresses aligned with ETS.
Behavior (LedIndicatorLogic.cpp): declare any new objects, Tables().Push
them in the constructor, and implement their behavior in OnDifference / Work.
The concrete peripheral drivers used above — constructor arguments, transceiver choices, and the interrupt callbacks that feed them — are detailed on the STM32 HAL Peripherals page.