Kbox-UniStack Docs

Integration Contract

The portable obligations a consumer project must honour when hosting the kbox-stack — object lifetimes, mandatory flash, boot order, and the error contract.

The stack is a portable library: its public surface is MCU-agnostic and every hardware dependency sits behind an I<X> interface (IFlash&, IClock&, IBusDriver&, …). This page describes the contract a consumer must honour on any platform. Concrete flash addresses, ISR wiring, and HAL classes live on the STM32 (HAL) platform pages and are linked where they apply.

The current prebuilt .a targets Cortex-M3 / STM32F103xB. The API and contract below are platform-neutral; only the shipped binary is target-specific.

Object lifetimes

MakeStack() does not own its dependencies. It placement-news the concrete stack into an opaque KnxStackStorage you provide, and returns an IKnxStack& that points into that buffer. Every reference you pass in — the StackParameters, every I<X> peripheral, the KnxFlashLayout& (and, in Secure mode, the security object it feeds into), sizing, buffers and clock — must outlive the returned handle.

All peripherals, parameters, and the stack itself are long-lived: construction order is dependency order. Construct the peripherals first, then the flash layout, then the stack (and, in Secure mode, the security object) that borrow it, then the logic that borrows the stack.

Give KnxFlashLayoutStorage, KnxStackStorage (and SecureStackStorage) static / file-scope lifetime. If the storage is a stack-local, the IKnxStack& you get back dangles the moment the enclosing function returns. The same rule applies to a FlashView handed out as an IFlash& (e.g. via layout.StackStateView()): keep the view alive for as long as its consumer.

// Static lifetime — MANDATORY. The handle points into this buffer.
static KnxFlashLayoutStorage layoutStorage;
KnxFlashLayout& layout = MakeFlashLayout(
    layoutStorage, baseFlash, stackParameters, /*userDataPageCount=*/1u);

static KnxStackStorage stackStorage;
IKnxStack& stack = MakeStack(
    stackStorage, stackParameters, busDriver, watchDog, knxLed,
    clock, transmitter, button, layout);

Flash responsibilities

One thing is non-negotiable before the device runs: a base persistence region must be reserved and exposed as a single IFlash&, and split into the stack's own named sub-regions with MakeFlashLayout()before either MakeSecure() or MakeStack() runs. MakeFlashLayout() derives the stack-state, user-data, and (Secure mode) counter/SIO-key regions from that one base flash; the stack and, in Secure mode, the security object each read their region through the returned KnxFlashLayout&.

static KnxFlashLayoutStorage layoutStorage;
KnxFlashLayout& layout = MakeFlashLayout(
    layoutStorage, baseFlash, stackParameters, /*userDataPageCount=*/1u);
// DeviceParameter::SetFlash(...) is called for you, inside MakeStack()'s
// DeviceModel construction — there is nothing further to wire here.

DeviceParameter::SetFlash(...) used to be a manual call the consumer made before Init(). It is now automatic: DeviceModel's constructor calls it with the flash view it owns (DeviceModel.cpp:37), and the shipped examples (examples/STM32F103-HAL/led-indicator{,-secure}/Core/Src/main.cpp) do not call it themselves. Calling it yourself is unnecessary and, if done with the wrong flash view, would fight the stack's own wiring.

Skipping the persistence region, or passing an invalid flash layout, is not silent: an invalid KnxFlashLayout (misaligned base tail, or a region window overflowing the 64KB tail-address space) is reported through onStackError as StackErrorTypeEnum_FLASH_LAYOUT_INVALID at Init(), and every flash-dependent step (machine-state load, secure bootstrap, SaveUserData()/ LoadUserData()) is a no-op from then on. See the error contract below — the consumer must still halt/reset on this error; the no-ops are a corruption backstop, not a substitute for that.

Concrete flash addresses, the FLASH_USER window, region tables, and the HalFlash implementation are platform-specific. See STM32 (HAL) · Project Setup for the reference layout and its compile-time / linker / runtime guards.

Boot sequence

The portable boot order is the same on every platform. ISR wiring — connecting the bus-driver interrupt to stack.Process() — is the one platform-specific step; see STM32 (HAL) · Peripherals.

Construct the peripherals (IFlash, IClock, IBusDriver, ILed, ITransmitter, IWatchDog, IButton) at static / long lifetime.

Call MakeFlashLayout(...) with the base flash to derive the stack's named regions. Do this before MakeSecure()/MakeStack() — both consume the returned KnxFlashLayout&. DeviceParameter::SetFlash(...) does not need a manual call — it happens automatically inside MakeStack()'s DeviceModel construction.

Build StackParameters, then (Secure mode) MakeSecure(...), then MakeStack(...) (add the ISecurity& argument for Secure mode). Derive your logic from KnxLogicBase so it auto-registers with the stack — no manual SetLogic needed.

Wire the bus-driver ISR to stack.Process()platform-specific.

Call stack.Init(), then run stack.Work() on every iteration of the main loop. Use stack.IsAllMachineStateLoaded() to confirm the stored tables loaded.

stack.Init();
for (;;) {
    stack.Work();   // drives logic.Work() and the whole state machine
}

Error contract

The stack reports fatal problems through the onStackError callback you install in StackParameters. Most fire when the storage sizes declared in StackParameters cannot hold what an ETS download tried to write; two more (below) fire when the flash layout or secure storage itself is invalid. The correct reaction in every case is a safe stop — halt (or, with the watchdog enabled, let it reset) rather than continue with a corrupt machine state.

void onStackError(StackErrorTypeEnum errorType) {
  if (errorType == StackErrorTypeEnum_MACHINE_STATE_SIZE_ERROR) {
    while (1) { /* safe stop — sizes in StackParameters are too small */ }
  }
  if (errorType == StackErrorTypeEnum_SEGMET1_SIZE_ERROR) { while (1) {} }
  if (errorType == StackErrorTypeEnum_SEGMET2_SIZE_ERROR) { while (1) {} }
  if (errorType == StackErrorTypeEnum_SEGMET3_SIZE_ERROR) { while (1) {} }
  if (errorType == StackErrorTypeEnum_SEGMET4_SIZE_ERROR) { while (1) {} }
  if (errorType == StackErrorTypeEnum_FLASH_LAYOUT_INVALID) { while (1) {} }
  if (errorType == StackErrorTypeEnum_SECURE_STORAGE_INVALID) { while (1) {} }
}

A size error almost always means a table size in StackParameters is smaller than the ETS project needs. See Database · Table sizing vs ETS download.

FLASH_LAYOUT_INVALID (an invalid KnxFlashLayout — misaligned base tail, or a region window overflowing the 64KB tail-address space) and SECURE_STORAGE_INVALID (caller-owned SIO buffers mis-sized) share the same consumer contract:

After either error fires, the consumer must perform an explicit safe-stop (an LED error code + while(1), or a controlled reset) — do not start any new work. The stack narrows the blast radius on its own side (Process() stops pumping the bus/watchdog once the layout is invalid, and SaveUserData()/LoadUserData()/UserDataCapacity() become no-ops), but this is a corruption backstop, not a substitute for the consumer's own halt/reset decision — a device that ignores the error and keeps looping will simply do nothing useful (and, with the watchdog enabled, reset-loop, which does not fix the underlying misconfiguration).

layout.IsValid() validates only the derived region table (page-aligned base, additive offsets, 64KB block bound) — it does not cross-check the physical linker FLASH_USER length. If a future StackParameters/SioSizing/ userDataPageCount change makes the derived page total exceed the linker window, IsValid() still returns true; the only guard against that case is the .ld's own ASSERT(LENGTH(FLASH_USER)==N*0x400), which must be kept in sync by hand.