Chapter 23 — Wiring It All Together

This chapter describes how a user application brings up the entire OpenLCB stack: configuring the two configuration structs, calling the initialization functions in the correct order, allocating nodes, running the main loop, and providing the 100ms timer tick.

23.1 The can_config_t Structure

The CAN transport layer is configured by populating a can_config_t struct and passing it to CanConfig_initialize(). This struct contains only hardware driver functions -- all internal CAN wiring is handled automatically by can_config.c.

FieldRequired?Description
transmit_raw_can_frameREQUIREDTransmit a raw CAN frame to the bus. Returns true on success.
is_tx_buffer_clearREQUIREDReturns true if the CAN TX hardware can accept another frame.
lock_shared_resourcesREQUIREDDisable interrupts or acquire mutex (same function as in openlcb_config_t).
unlock_shared_resourcesREQUIREDRe-enable interrupts or release mutex.
on_rxOptionalCallback invoked when a CAN frame is received (for logging/debugging).
on_txOptionalCallback invoked when a CAN frame is transmitted (for logging/debugging).
on_alias_changeOptionalCallback invoked when a node's CAN alias changes.

23.2 The openlcb_config_t Structure

The OpenLCB protocol layer is configured by populating an openlcb_config_t struct and passing it to OpenLcb_initialize(). Fields are grouped by feature flag.

Required Fields (Always)

FieldDescription
lock_shared_resourcesDisable interrupts / acquire mutex. Same function as CAN config.
unlock_shared_resourcesRe-enable interrupts / release mutex.
config_mem_readRead from configuration memory (EEPROM/Flash/file).
config_mem_writeWrite to configuration memory.
rebootReboot the processor.

Optional Fields (Grouped by Feature Flag)

Feature FlagCallbacks Enabled
(always)on_optional_interaction_rejected, on_terminate_due_to_error, on_100ms_timer, on_login_complete
OPENLCB_COMPILE_EVENTSon_consumed_event_pcer, on_consumed_event_identified, on_event_learn, on_pc_event_report, consumer/producer identified callbacks
OPENLCB_COMPILE_MEMORY_CONFIGURATIONfactory_reset, config_mem_read_delayed_reply_time, config_mem_write_delayed_reply_time
OPENLCB_COMPILE_FIRMWAREfreeze, unfreeze, firmware_write
OPENLCB_COMPILE_BROADCAST_TIMEon_broadcast_time_changed, on_broadcast_time_received, on_broadcast_date_received, on_broadcast_year_received, on_broadcast_rate_received, on_broadcast_clock_started, on_broadcast_clock_stopped, on_broadcast_date_rollover
OPENLCB_COMPILE_TRAINSpeed/function/emergency notifiers, controller assign/release, listener changed, heartbeat, decision callbacks, throttle-side reply notifiers
OPENLCB_COMPILE_TRAIN_SEARCHon_train_search_matched, on_train_search_no_match

23.3 Initialization Sequence

The correct order for bringing up the stack is:

flowchart TD A["CanConfig_initialize(&can_config)"] --> B["OpenLcb_initialize(&openlcb_config)"] B --> C["OpenLcb_create_node(node_id_1, ¶ms_1)"] C --> D["OpenLcb_create_node(node_id_2, ¶ms_2)"] D --> E["... additional nodes ..."] E --> F["Start 100ms timer interrupt"] F --> G["Enter main loop"] style A fill:#fff3e0,stroke:#e65100 style B fill:#e3f2fd,stroke:#1565c0 style C fill:#e8f5e9,stroke:#388e3c style D fill:#e8f5e9,stroke:#388e3c style F fill:#fce4ec,stroke:#c62828 style G fill:#f3e5f5,stroke:#7b1fa2
Order Matters

CanConfig_initialize() must be called BEFORE OpenLcb_initialize(). The OpenLCB config wiring module references CAN TX functions (e.g., CanTxStatemachine_send_openlcb_message) that must already be initialized.

What happens inside CanConfig_initialize()

  1. Save the user config pointer.
  2. Initialize CAN buffer infrastructure: CanBufferStore_initialize(), CanBufferFifo_initialize().
  3. Build all 7 internal CAN interface structs from user config + library functions.
  4. Initialize all CAN modules in dependency order: RX handler, RX SM, TX handler, TX SM, login handler, login SM, main SM, alias mappings, listener alias table.

What happens inside OpenLcb_initialize()

  1. Save the user config pointer.
  2. Initialize OpenLCB buffer infrastructure: OpenLcbBufferStore, OpenLcbBufferList, OpenLcbBufferFifo.
  3. Build all internal interface structs from user config and compile flags.
  4. Initialize all compiled-in protocol modules: SNIP, Datagram, Config Mem (read/write/ops), Events, Message Network, Broadcast Time, Train, Train Search.
  5. Initialize Node management, Login SM, Main SM, Application layer.

23.4 The Main Loop

The application's main loop calls OpenLcb_run() as fast as possible. This single function runs one iteration of all state machines:

void OpenLcb_run(void) {
    CanMainStateMachine_run();       // CAN transport layer
    OpenLcbLoginMainStatemachine_run(); // OpenLCB login (Init Complete, events)
    OpenLcbMainStatemachine_run();   // OpenLCB protocol dispatch

    _run_periodic_services();        // 100ms timer-driven tasks
}

The periodic services, driven by the 100ms tick, include:

23.5 The 100ms Timer Tick

The library's timekeeping is built on a single volatile uint8_t counter incremented every 100ms:

static volatile uint8_t _global_100ms_tick = 0;

void OpenLcb_100ms_timer_tick(void) {
    _global_100ms_tick++;
}

The application must call OpenLcb_100ms_timer_tick() from a hardware timer interrupt or periodic RTOS task. This is the ONLY action performed in the interrupt context. All real protocol work runs in the main loop.

Why volatile uint8_t is Safe

A volatile uint8_t read/write is a single instruction on all target architectures (8-bit PIC through 64-bit ARM). Only the timer interrupt performs the increment (read-modify-write), and timer interrupts do not re-enter, so no locking is needed for the counter itself. All other contexts only read.

Elapsed time is computed by subtraction: elapsed = current_tick - snapshot. The uint8_t wraps at 256, and unsigned subtraction handles the wrap correctly for durations up to 25.5 seconds.

23.6 Platform-Specific Lock/Unlock Examples

Platformlock_shared_resources()unlock_shared_resources()
Bare-metal (dsPIC) __builtin_disi(0x3FFF); (disable interrupts) __builtin_disi(0); (re-enable)
Bare-metal (ARM) __disable_irq(); __enable_irq();
FreeRTOS taskENTER_CRITICAL(); taskEXIT_CRITICAL();
POSIX (testing) pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex);

23.7 Feature Flag Dependency Tree

Compile-time feature flags control which protocol modules are included. Dependencies are enforced at compile time with #error directives:

flowchart TD EVENTS["OPENLCB_COMPILE_EVENTS"] DATAGRAMS["OPENLCB_COMPILE_DATAGRAMS"] MEMCFG["OPENLCB_COMPILE_MEMORY_CONFIGURATION"] FIRMWARE["OPENLCB_COMPILE_FIRMWARE"] BTIME["OPENLCB_COMPILE_BROADCAST_TIME"] TRAIN["OPENLCB_COMPILE_TRAIN"] TSEARCH["OPENLCB_COMPILE_TRAIN_SEARCH"] MEMCFG -->|requires| DATAGRAMS FIRMWARE -->|requires| MEMCFG BTIME -->|requires| EVENTS TSEARCH -->|requires| EVENTS TSEARCH -->|requires| TRAIN style EVENTS fill:#e8f5e9,stroke:#388e3c style DATAGRAMS fill:#e3f2fd,stroke:#1565c0 style MEMCFG fill:#e3f2fd,stroke:#1565c0 style FIRMWARE fill:#fff3e0,stroke:#e65100 style BTIME fill:#e8f5e9,stroke:#388e3c style TRAIN fill:#fce4ec,stroke:#c62828 style TSEARCH fill:#fce4ec,stroke:#c62828

23.8 Minimal Application Template

// openlcb_user_config.h defines all USER_DEFINED_* macros
// plus feature flags like OPENLCB_COMPILE_EVENTS

#include "drivers/canbus/can_config.h"
#include "openlcb/openlcb_config.h"

static const can_config_t can_cfg = {
    .transmit_raw_can_frame  = &MyCanDriver_transmit,
    .is_tx_buffer_clear      = &MyCanDriver_is_tx_clear,
    .lock_shared_resources   = &MyPlatform_lock,
    .unlock_shared_resources = &MyPlatform_unlock,
};

static const openlcb_config_t olcb_cfg = {
    .lock_shared_resources   = &MyPlatform_lock,
    .unlock_shared_resources = &MyPlatform_unlock,
    .config_mem_read         = &MyEeprom_read,
    .config_mem_write        = &MyEeprom_write,
    .reboot                  = &MyPlatform_reboot,
    .on_login_complete       = &my_login_handler,
};

int main(void) {
    // 1. Initialize hardware (CAN, timers, GPIO)
    MyPlatform_init();

    // 2. Initialize library (order matters!)
    CanConfig_initialize(&can_cfg);
    OpenLcb_initialize(&olcb_cfg);

    // 3. Create nodes
    OpenLcb_create_node(0x050101010001ULL, &my_node_params);

    // 4. Start 100ms timer (calls OpenLcb_100ms_timer_tick)
    MyPlatform_start_timer();

    // 5. Main loop
    while (1) {
        OpenLcb_run();
    }
}

23.9 Source Files

FilePurpose
drivers/canbus/can_config.hCAN configuration struct definition
drivers/canbus/can_config.cCAN wiring -- builds 7 interface structs, initializes all CAN modules
openlcb/openlcb_config.hOpenLCB configuration struct definition, feature flag validation
openlcb/openlcb_config.cOpenLCB wiring -- builds all interface structs, initializes all protocol modules
← Prev: Ch 22 — CAN Login Next: Ch 24 — Data Flow Walkthrough →