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.
| Field | Required? | Description |
|---|---|---|
transmit_raw_can_frame | REQUIRED | Transmit a raw CAN frame to the bus. Returns true on success. |
is_tx_buffer_clear | REQUIRED | Returns true if the CAN TX hardware can accept another frame. |
lock_shared_resources | REQUIRED | Disable interrupts or acquire mutex (same function as in openlcb_config_t). |
unlock_shared_resources | REQUIRED | Re-enable interrupts or release mutex. |
on_rx | Optional | Callback invoked when a CAN frame is received (for logging/debugging). |
on_tx | Optional | Callback invoked when a CAN frame is transmitted (for logging/debugging). |
on_alias_change | Optional | Callback 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)
| Field | Description |
|---|---|
lock_shared_resources | Disable interrupts / acquire mutex. Same function as CAN config. |
unlock_shared_resources | Re-enable interrupts / release mutex. |
config_mem_read | Read from configuration memory (EEPROM/Flash/file). |
config_mem_write | Write to configuration memory. |
reboot | Reboot the processor. |
Optional Fields (Grouped by Feature Flag)
| Feature Flag | Callbacks Enabled |
|---|---|
| (always) | on_optional_interaction_rejected, on_terminate_due_to_error, on_100ms_timer, on_login_complete |
OPENLCB_COMPILE_EVENTS | on_consumed_event_pcer, on_consumed_event_identified, on_event_learn, on_pc_event_report, consumer/producer identified callbacks |
OPENLCB_COMPILE_MEMORY_CONFIGURATION | factory_reset, config_mem_read_delayed_reply_time, config_mem_write_delayed_reply_time |
OPENLCB_COMPILE_FIRMWARE | freeze, unfreeze, firmware_write |
OPENLCB_COMPILE_BROADCAST_TIME | on_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_TRAIN | Speed/function/emergency notifiers, controller assign/release, listener changed, heartbeat, decision callbacks, throttle-side reply notifiers |
OPENLCB_COMPILE_TRAIN_SEARCH | on_train_search_matched, on_train_search_no_match |
23.3 Initialization Sequence
The correct order for bringing up the stack is:
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()
- Save the user config pointer.
- Initialize CAN buffer infrastructure:
CanBufferStore_initialize(),CanBufferFifo_initialize(). - Build all 7 internal CAN interface structs from user config + library functions.
- 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()
- Save the user config pointer.
- Initialize OpenLCB buffer infrastructure:
OpenLcbBufferStore,OpenLcbBufferList,OpenLcbBufferFifo. - Build all internal interface structs from user config and compile flags.
- Initialize all compiled-in protocol modules: SNIP, Datagram, Config Mem (read/write/ops), Events, Message Network, Broadcast Time, Train, Train Search.
- 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:
OpenLcbNode_100ms_timer_tick()-- user timer callbackProtocolDatagramHandler_100ms_timer_tick()/_check_timeouts()-- datagram retry/timeoutOpenLcbApplicationBroadcastTime_100ms_time_tick()-- clock minute accumulationOpenLcbApplicationTrain_100ms_timer_tick()-- heartbeat countdown
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.
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
| Platform | lock_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:
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
| File | Purpose |
|---|---|
drivers/canbus/can_config.h | CAN configuration struct definition |
drivers/canbus/can_config.c | CAN wiring -- builds 7 interface structs, initializes all CAN modules |
openlcb/openlcb_config.h | OpenLCB configuration struct definition, feature flag validation |
openlcb/openlcb_config.c | OpenLCB wiring -- builds all interface structs, initializes all protocol modules |