Chapter 1 — Project Overview
1.1 What Is OpenLCB / LCC?
OpenLCB (Open Layout Control Bus) is a networking standard for model railroad control systems. Its NMRA-adopted profile is called LCC (Layout Command Control). The protocol defines a message-based communication system where every device on the network is a node identified by a globally unique 48-bit Node ID. Nodes exchange messages identified by a Message Type Indicator (MTI) to implement protocols such as events, datagrams, configuration memory access, train control, and broadcast time.
This library provides a portable, pure-C implementation of the OpenLCB protocol stack. It is designed to run on bare-metal microcontrollers (dsPIC, STM32, ESP32, RP2040) as well as desktop platforms (macOS, Linux) for testing and simulation.
1.2 Design Philosophy
Four principles govern every design decision in this library:
| Principle | Description |
|---|---|
| Zero dynamic allocation | All memory is statically allocated at compile time via fixed-size pools. There are no calls to malloc() or free() anywhere in the library. Pool sizes are controlled by USER_DEFINED_* macros in openlcb_user_config.h. |
| Non-blocking state machines | Every state machine does a fixed, bounded amount of work per call and returns. No function ever spins waiting for a condition. The main loop calls each state machine in round-robin fashion, ensuring all nodes and protocols make progress. |
| Static buffer pools | Message payloads come in four fixed sizes: BASIC (16 bytes), DATAGRAM (72 bytes), SNIP (256 bytes), and STREAM (512 bytes). Each pool has a compile-time depth. Reference counting allows the same buffer to be shared across queues without copying. |
| Callback-based dependency injection | Modules do not call each other directly. Instead, the application creates an interface struct containing function pointers, passes it to the module's *_initialize() function, and the module stores the pointer. This decouples the core protocol logic from platform-specific I/O. |
1.3 Architecture Boundary
The library is split into two major layers separated by the openlcb_msg_t boundary type:
(message_network, event_transport,
datagram, snip, config)"] B4["Buffer System
(buffer_store, buffer_fifo, buffer_list)"] B5["Node Management
(openlcb_node)"] B6["Utilities
(utilities, float16, gridconnect)"] end subgraph CAN["drivers/canbus/ — CAN Transport"] direction TB C1["can_main_statemachine"] C2["can_login_statemachine"] C3["can_rx_statemachine / can_tx_statemachine"] C4["alias_mappings / listener_alias_table"] C5["CAN Buffer System
(can_buffer_store, can_buffer_fifo)"] end subgraph FUTURE["drivers/tcpip/ — Future TCP/IP Transport"] D1["(not yet implemented)"] end APP --> CORE CORE <-->|"openlcb_msg_t"| CAN CORE <-->|"openlcb_msg_t"| FUTURE style APP fill:#e8f5e9,stroke:#388e3c style CORE fill:#e3f2fd,stroke:#1565c0 style CAN fill:#fff3e0,stroke:#e65100 style FUTURE fill:#f3e5f5,stroke:#7b1fa2
The openlcb/ directory contains everything that is transport-independent: types, defines, buffer management, node management, the main state machine, the login state machine, and all protocol handlers. The drivers/canbus/ directory contains the CAN-specific transport layer. A future drivers/tcpip/ directory would implement the TCP/IP transport, communicating with the core through the same openlcb_msg_t boundary.
1.4 The openlcb_msg_t Boundary Type
The openlcb_msg_t structure is the universal message currency of the library. It carries everything needed to process a message at the protocol level:
typedef struct {
openlcb_msg_state_t state; // allocated, inprocess, invalid, loopback
uint16_t mti; // Message Type Indicator
uint16_t source_alias; // 12-bit CAN alias of sender
uint16_t dest_alias; // 12-bit CAN alias of recipient (0 = global)
node_id_t source_id; // 48-bit Node ID of sender
node_id_t dest_id; // 48-bit Node ID of recipient (0 = global)
payload_type_enum payload_type; // BASIC, DATAGRAM, SNIP, or STREAM
uint16_t payload_count; // Number of valid bytes in payload
openlcb_payload_t *payload; // Pointer to payload buffer
openlcb_msg_timer_t timer; // Timer/retry union
uint8_t reference_count; // Number of active references
} openlcb_msg_t;
The source_alias and dest_alias fields are CAN-specific concepts (12-bit alias for the 48-bit Node ID). They ride along in the core openlcb_msg_t structure for convenience, avoiding the need for a separate CAN-specific message wrapper. On a non-CAN transport these fields would simply be zero.
1.5 Directory Structure
src/
+-- openlcb/ # Transport-independent core
| +-- openlcb_types.h # All type definitions
| +-- openlcb_defines.h # All protocol constants (MTI, errors, etc.)
| +-- openlcb_buffer_store.h/.c # Segregated payload buffer pool
| +-- openlcb_buffer_fifo.h/.c # Circular FIFO for message pointers
| +-- openlcb_buffer_list.h/.c # Random-access list for multi-frame assembly
| +-- openlcb_node.h/.c # Node pool and enumeration
| +-- openlcb_main_statemachine.h/.c
| +-- openlcb_login_statemachine.h/.c
| +-- openlcb_message_network.h/.c
| +-- openlcb_event_transport.h/.c
| +-- openlcb_datagram.h/.c
| +-- openlcb_snip.h/.c
| +-- openlcb_config.h/.c # Configuration memory protocol
| +-- openlcb_application.h/.c # Application-level callbacks
| +-- openlcb_application_train.h/.c
| +-- openlcb_application_broadcast_time.h/.c
| +-- openlcb_utilities.h/.c # Payload insert/extract helpers
| +-- openlcb_float16.h/.c # IEEE 754 half-precision speed encoding
| +-- openlcb_gridconnect.h/.c # GridConnect ASCII format conversion
| +-- *_Test.cxx # Google Test files
|
+-- drivers/
| +-- canbus/ # CAN transport layer
| +-- can_types.h # CAN frame types and constants
| +-- can_buffer_store.h/.c # CAN frame buffer pool
| +-- can_buffer_fifo.h/.c # CAN frame FIFO
| +-- can_main_statemachine.h/.c
| +-- can_login_statemachine.h/.c
| +-- can_rx_statemachine.h/.c
| +-- can_tx_statemachine.h/.c
| +-- alias_mappings.h/.c # Global alias-to-NodeID table
| +-- listener_alias_table.h/.c
| +-- *_Test.cxx
|
+-- applications/ # Platform-specific demo projects
| +-- arduino/ # ESP32, RP2040
| +-- dspic/ # Microchip dsPIC
| +-- platformio/ # PlatformIO builds (ESP32, macOS)
| +-- stm32_cubeide/ # STM32 CubeIDE
| +-- ti_thiea/ # TI MSPM0
| +-- xcode/ # macOS Xcode
|
+-- utilities/ # Shared utility code
+-- test/ # Test infrastructure
+-- build/ # Build output
1.6 Target Platforms
The library has been tested or has demo projects for the following platforms:
| Platform | Toolchain | Notes |
|---|---|---|
| Microchip dsPIC | MPLAB X / XC16 | 16-bit, bare-metal, hardware CAN |
| STM32F407 | STM32CubeIDE | ARM Cortex-M4, hardware CAN |
| ESP32 | Arduino / PlatformIO | WiFi or hardware CAN via MCP2515 |
| Raspberry Pi Pico (RP2040) | Arduino | CAN via MCP2515 SPI adapter |
| TI MSPM0 | TI THEIA IDE | ARM Cortex-M0+ |
| macOS / Linux | Xcode / PlatformIO / CMake | Desktop simulation via GridConnect over TCP or serial |
Because the library is pure C with no OS dependencies in the core, porting to a new platform requires only providing the platform-specific callbacks (CAN driver send/receive, timer tick, lock/unlock) and a suitable openlcb_user_config.h.