Chapter 28 — Porting to a New Platform

This chapter describes what the application must provide when porting the OpenLCB C Library to a new hardware platform. The library is written in standard C with no OS dependencies -- all platform-specific behavior is injected through callback function pointers.

28.1 Platform Requirements

The application must provide four categories of platform support:

flowchart LR subgraph Platform["Application Platform Layer"] A["Transport Driver
(CAN, TCP/IP, ...)"] B["100ms Timer"] C["Persistent Storage
(EEPROM/Flash/File)"] D["Lock/Unlock
(Interrupt Control)"] end subgraph Library["OpenLCB C Library"] E["can_config_t /
openlcb_config_t"] end A --> E B --> E C --> E D --> E style Platform fill:#fff3e0,stroke:#e65100 style Library fill:#e8f5e9,stroke:#2e7d32
RequirementConfig StructFunction Pointers
Transport driver can_config_t transmit_raw_can_frame(), is_tx_buffer_clear()
100ms timer Direct call Call OpenLcb_100ms_timer_tick() from a hardware timer ISR
Persistent storage openlcb_config_t config_mem_read(), config_mem_write()
Lock/unlock Both config structs lock_shared_resources(), unlock_shared_resources()
Reboot openlcb_config_t reboot()

28.2 Bare-Metal vs. RTOS

ConcernBare-MetalRTOS
Main loop while(1) { OpenLcb_run(); } Dedicated task calling OpenLcb_run()
100ms timer Hardware timer interrupt calls OpenLcb_100ms_timer_tick() RTOS software timer or periodic task calls OpenLcb_100ms_timer_tick()
lock_shared_resources Disable global interrupts Mutex lock or critical section enter
unlock_shared_resources Enable global interrupts Mutex unlock or critical section exit
CAN RX ISR reads frame, pushes to CAN FIFO ISR or DMA fills FIFO; task processes
Persistent storage Direct EEPROM/Flash register access File I/O or flash driver with mutex

Bare-Metal Lock/Unlock Example

// PIC18 example (Microchip XC8)
void my_lock(void) {
    INTCONbits.GIE = 0;   // Disable global interrupts
}

void my_unlock(void) {
    INTCONbits.GIE = 1;   // Enable global interrupts
}

RTOS Lock/Unlock Example

// FreeRTOS example
static SemaphoreHandle_t openlcb_mutex;

void my_lock(void) {
    xSemaphoreTake(openlcb_mutex, portMAX_DELAY);
}

void my_unlock(void) {
    xSemaphoreGive(openlcb_mutex);
}

POSIX Lock/Unlock Example (Testing)

// Linux/macOS testing
static pthread_mutex_t openlcb_mutex = PTHREAD_MUTEX_INITIALIZER;

void my_lock(void) {
    pthread_mutex_lock(&openlcb_mutex);
}

void my_unlock(void) {
    pthread_mutex_unlock(&openlcb_mutex);
}

28.3 100ms Timer Implementation

The library requires a single timer tick at approximately 100ms intervals. This is the only action performed in interrupt/timer context -- all protocol work runs in the main loop.

// Timer ISR -- keep it minimal
void Timer_100ms_ISR(void) {
    OpenLcb_100ms_timer_tick();  // Increments volatile uint8_t counter
}

// The counter wraps at 255.
// Protocol timeouts compare (current - snapshot) which handles wrapping
// correctly for intervals up to 12.7 seconds.
Timer Accuracy

The library tolerates timer jitter. The 200ms CAN login wait actually measures "elapsed ticks > 2" which produces a 200-300ms window depending on when the timer started relative to the tick boundary. This is intentional and meets the OpenLCB specification requirement of "at least 200ms."

28.4 Persistent Storage

The application provides read/write functions for configuration memory. The library calls these when a remote node issues Memory Configuration Protocol read/write commands:

uint16_t my_config_mem_read(openlcb_node_t *node,
                           uint32_t address,
                           uint16_t count,
                           configuration_memory_buffer_t *buffer) {
    // Read 'count' bytes from EEPROM/Flash/file starting at 'address'
    // into 'buffer'. Return actual bytes read.
    // The 'node' parameter allows per-node storage partitioning.
    return eeprom_read(address, (uint8_t *)buffer, count);
}

uint16_t my_config_mem_write(openlcb_node_t *node,
                            uint32_t address,
                            uint16_t count,
                            configuration_memory_buffer_t *buffer) {
    // Write 'count' bytes from 'buffer' to persistent storage
    // starting at 'address'. Return actual bytes written.
    return eeprom_write(address, (uint8_t *)buffer, count);
}

28.5 CAN Driver Integration

For CAN-based platforms, the application must provide two hardware-level functions:

// Transmit a CAN frame
bool my_can_transmit(can_msg_t *can_msg) {
    // Copy can_msg->identifier (29-bit) and can_msg->data[0..7]
    // into CAN hardware TX buffer and trigger transmission.
    // Return true if the frame was accepted by hardware.
    CAN_TX_BUF.id = can_msg->identifier;
    memcpy(CAN_TX_BUF.data, can_msg->data, can_msg->payload_count);
    CAN_TX_BUF.dlc = can_msg->payload_count;
    CAN_TX_BUF.send = 1;
    return true;
}

// Check if CAN TX hardware is ready
bool my_can_is_tx_clear(void) {
    return (CAN_TX_BUF.send == 0);
}

The CAN RX interrupt pushes received frames into the library's CAN FIFO:

void CAN_RX_ISR(void) {
    can_msg_t *buffer = CanBufferStore_allocate();
    if (buffer) {
        buffer->identifier = CAN_RX_BUF.id;
        buffer->payload_count = CAN_RX_BUF.dlc;
        memcpy(buffer->data, CAN_RX_BUF.data, CAN_RX_BUF.dlc);
        CanBufferFifo_push(buffer);
    }
    // If allocate returns NULL, frame is dropped (buffer exhaustion)
    CAN_RX_BUF.clear_interrupt();
}

28.6 Memory Tuning for Constrained Targets

All buffer pool sizes are configured at compile time in openlcb_user_config.h. The total RAM footprint depends on pool depths and enabled features:

PoolPer-Buffer SizeTypical DepthRAM Cost
BASIC16 bytes + overhead5~160 bytes
DATAGRAM72 bytes + overhead2~200 bytes
SNIP256 bytes + overhead2~580 bytes
STREAM512 bytes + overhead1~570 bytes
CAN frames~16 bytes each10~160 bytes
Nodes~200 bytes each1~200 bytes
8-bit Target Limit

Total buffer count across all OpenLCB pools (BASIC + DATAGRAM + SNIP + STREAM) must not exceed 126. This is enforced at compile time by openlcb_config.h. The limit exists because buffer indices use 7-bit fields on 8-bit processors.

Minimal Configuration Profile

For the most constrained targets (8-bit MCU with 2-4KB RAM), use a minimal configuration:

// openlcb_user_config.h -- minimal profile

#define USER_DEFINED_BASIC_BUFFER_DEPTH       3
#define USER_DEFINED_DATAGRAM_BUFFER_DEPTH    1
#define USER_DEFINED_SNIP_BUFFER_DEPTH        1
#define USER_DEFINED_STREAM_BUFFER_DEPTH      0
#define USER_DEFINED_CAN_MSG_BUFFER_DEPTH     5
#define USER_DEFINED_NODE_BUFFER_DEPTH        1
#define USER_DEFINED_PRODUCER_COUNT           2
#define USER_DEFINED_CONSUMER_COUNT           2
#define USER_DEFINED_PRODUCER_RANGE_COUNT     1
#define USER_DEFINED_CONSUMER_RANGE_COUNT     1
#define USER_DEFINED_TRAIN_NODE_COUNT         0
#define USER_DEFINED_MAX_LISTENERS_PER_TRAIN  0
#define USER_DEFINED_MAX_TRAIN_FUNCTIONS      0
#define USER_DEFINED_CDI_LENGTH              256
#define USER_DEFINED_FDI_LENGTH                1

// Enable only essential features:
#define OPENLCB_COMPILE_EVENTS
// (no datagrams, no train, no broadcast time)

Full-Featured Configuration Profile

For 32-bit platforms with ample RAM (STM32, ESP32, Raspberry Pi Pico):

// openlcb_user_config.h -- full-featured profile

#define USER_DEFINED_BASIC_BUFFER_DEPTH      10
#define USER_DEFINED_DATAGRAM_BUFFER_DEPTH    5
#define USER_DEFINED_SNIP_BUFFER_DEPTH        3
#define USER_DEFINED_STREAM_BUFFER_DEPTH      2
#define USER_DEFINED_CAN_MSG_BUFFER_DEPTH    20
#define USER_DEFINED_NODE_BUFFER_DEPTH        5
#define USER_DEFINED_PRODUCER_COUNT          16
#define USER_DEFINED_CONSUMER_COUNT          16
#define USER_DEFINED_PRODUCER_RANGE_COUNT     4
#define USER_DEFINED_CONSUMER_RANGE_COUNT     4
#define USER_DEFINED_TRAIN_NODE_COUNT         8
#define USER_DEFINED_MAX_LISTENERS_PER_TRAIN  4
#define USER_DEFINED_MAX_TRAIN_FUNCTIONS     29
#define USER_DEFINED_CDI_LENGTH            2048
#define USER_DEFINED_FDI_LENGTH             512

// Enable all features:
#define OPENLCB_COMPILE_EVENTS
#define OPENLCB_COMPILE_DATAGRAMS
#define OPENLCB_COMPILE_MEMORY_CONFIGURATION
#define OPENLCB_COMPILE_FIRMWARE
#define OPENLCB_COMPILE_BROADCAST_TIME
#define OPENLCB_COMPILE_TRAIN
#define OPENLCB_COMPILE_TRAIN_SEARCH

28.7 Porting Checklist

ItemStatusNotes
Create openlcb_user_config.hCopy from templates/openlcb_user_config.h, adjust buffer depths and feature flags
Implement lock_shared_resources()Disable interrupts (bare-metal) or mutex lock (RTOS)
Implement unlock_shared_resources()Enable interrupts (bare-metal) or mutex unlock (RTOS)
Implement 100ms timerHardware timer ISR calling OpenLcb_100ms_timer_tick()
Implement config_mem_read()EEPROM, Flash, or file-backed storage
Implement config_mem_write()Same storage backend as read
Implement reboot()Software reset or watchdog trigger
Implement transmit_raw_can_frame()CAN hardware TX buffer write (CAN transport only)
Implement is_tx_buffer_clear()CAN hardware TX buffer status check (CAN transport only)
Implement CAN RX ISRAllocate CAN buffer, copy frame, push to FIFO
Populate can_config_tWire all function pointers
Populate openlcb_config_tWire all required + desired optional callbacks
Call CanConfig_initialize()Before OpenLcb_initialize()
Call OpenLcb_initialize()After CAN config, before creating nodes
Call OpenLcb_create_node()For each virtual node, with Node ID and parameters
Main loop calls OpenLcb_run()As fast as possible, non-blocking
Test on hardwareVerify login completes, respond to Verify Node ID
← Prev: Ch 27 — Adding a Transport Next: Appendix A — MTI Reference →