Chapter 4 — OpenLCB Buffer System

The buffer system is the memory backbone of the library. It provides three cooperating modules: a buffer store (pool allocator), a buffer FIFO (ordered message queue), and a buffer list (random-access assembly area). All memory is statically allocated at compile time.

4.1 The message_buffer_t Master Structure

A single message_buffer_t instance holds all message structures and payload pools:

typedef struct {
    openlcb_msg_array_t messages;            // Array of openlcb_msg_t[LEN_MESSAGE_BUFFER]
    openlcb_basic_data_buffer_t basic;       // Pool of BASIC payloads (16 bytes each)
    openlcb_datagram_data_buffer_t datagram; // Pool of DATAGRAM payloads (72 bytes each)
    openlcb_snip_data_buffer_t snip;         // Pool of SNIP payloads (256 bytes each)
    openlcb_stream_data_buffer_t stream;     // Pool of STREAM payloads (512 bytes each)
} message_buffer_t;

The total number of message structures equals the sum of all four pool depths:

#define LEN_MESSAGE_BUFFER (USER_DEFINED_BASIC_BUFFER_DEPTH \
    + USER_DEFINED_DATAGRAM_BUFFER_DEPTH \
    + USER_DEFINED_SNIP_BUFFER_DEPTH \
    + USER_DEFINED_STREAM_BUFFER_DEPTH)

4.2 Four Segregated Payload Pools

Each message structure is permanently paired with one payload buffer from its pool. During initialization, each openlcb_msg_t.payload pointer is wired to its corresponding payload slot. This pairing never changes.

PoolPayload SizeDepth MacroTypical DepthUse Cases
BASIC16 bytesUSER_DEFINED_BASIC_BUFFER_DEPTH32Events, Verify Node ID, Protocol Support, OIR
DATAGRAM72 bytesUSER_DEFINED_DATAGRAM_BUFFER_DEPTH4Config memory read/write, datagram protocol
SNIP256 bytesUSER_DEFINED_SNIP_BUFFER_DEPTH4SNIP replies, events with payload
STREAM512 bytesUSER_DEFINED_STREAM_BUFFER_DEPTH1Stream data transfers
flowchart LR subgraph Store["message_buffer_t"] direction TB MSG["openlcb_msg_t array
[LEN_MESSAGE_BUFFER]"] B["BASIC pool
16 bytes x N"] D["DATAGRAM pool
72 bytes x N"] S["SNIP pool
256 bytes x N"] ST["STREAM pool
512 bytes x N"] end MSG -->|"payload ptr"| B MSG -->|"payload ptr"| D MSG -->|"payload ptr"| S MSG -->|"payload ptr"| ST style Store fill:#e3f2fd,stroke:#1565c0 style B fill:#c8e6c9,stroke:#2e7d32 style D fill:#fff9c4,stroke:#f57f17 style S fill:#ffe0b2,stroke:#e65100 style ST fill:#e1bee7,stroke:#7b1fa2

4.3 Buffer Store API

4.3.1 Initialization

void OpenLcbBufferStore_initialize(void);

Clears all message structures, wires payload pointers, and resets counters. Must be called once at startup before any other buffer operation.

4.3.2 Allocation and Deallocation

openlcb_msg_t *OpenLcbBufferStore_allocate_buffer(payload_type_enum payload_type);
void OpenLcbBufferStore_free_buffer(openlcb_msg_t *msg);
void OpenLcbBufferStore_inc_reference_count(openlcb_msg_t *msg);

4.3.3 Reference Counting Lifecycle

stateDiagram-v2 [*] --> Free : Pool initialized Free --> Allocated : allocate_buffer()
ref_count = 1 Allocated --> Shared : inc_reference_count()
ref_count++ Shared --> Shared : inc_reference_count()
ref_count++ Shared --> Allocated : free_buffer()
ref_count-- (still > 0) Allocated --> Free : free_buffer()
ref_count = 0 Shared --> Free : free_buffer()
ref_count = 0
Critical: Reference Count Correctness

Every code path that receives a buffer pointer must eventually call free_buffer() exactly once. Forgetting to free causes a pool leak; double-freeing corrupts the pool. When using inc_reference_count(), each holder must independently call free_buffer().

4.3.4 Peak Allocation Counters

The buffer store tracks both current and peak allocation counts for each pool type:

uint16_t OpenLcbBufferStore_basic_messages_allocated(void);
uint16_t OpenLcbBufferStore_basic_messages_max_allocated(void);
uint16_t OpenLcbBufferStore_datagram_messages_allocated(void);
uint16_t OpenLcbBufferStore_datagram_messages_max_allocated(void);
uint16_t OpenLcbBufferStore_snip_messages_allocated(void);
uint16_t OpenLcbBufferStore_snip_messages_max_allocated(void);
uint16_t OpenLcbBufferStore_stream_messages_allocated(void);
uint16_t OpenLcbBufferStore_stream_messages_max_allocated(void);
void OpenLcbBufferStore_clear_max_allocated(void);

Use the *_max_allocated() functions during development to determine if your pool depths are adequate. If the peak matches the pool depth, the pool was at capacity and messages may have been dropped.

4.4 Buffer FIFO

The FIFO is a circular buffer of openlcb_msg_t pointers. It serves as the primary message queue between the transport layer (producer) and the main state machine (consumer).

4.4.1 Circular Buffer Design

Uses one extra slot so that head == tail always means empty (no separate count or full flag needed).

4.4.2 API

void OpenLcbBufferFifo_initialize(void);
openlcb_msg_t *OpenLcbBufferFifo_push(openlcb_msg_t *new_msg);
openlcb_msg_t *OpenLcbBufferFifo_pop(void);
bool OpenLcbBufferFifo_is_empty(void);
uint16_t OpenLcbBufferFifo_get_allocated_count(void);
void OpenLcbBufferFifo_check_and_invalidate_messages_by_source_alias(uint16_t alias);
openlcb_msg_t *OpenLcbBufferFifo_push_to_head(openlcb_msg_t *new_msg);

4.5 Buffer List

The buffer list is a fixed-size array of openlcb_msg_t pointers supporting random access and attribute-based search. It is used primarily for multi-frame message assembly where incoming CAN frames must be matched to an in-progress message by source alias, dest alias, and MTI.

4.5.1 API

void OpenLcbBufferList_initialize(void);
openlcb_msg_t *OpenLcbBufferList_add(openlcb_msg_t *new_msg);
openlcb_msg_t *OpenLcbBufferList_find(uint16_t source_alias, uint16_t dest_alias, uint16_t mti);
openlcb_msg_t *OpenLcbBufferList_release(openlcb_msg_t *msg);
openlcb_msg_t *OpenLcbBufferList_index_of(uint16_t index);
bool OpenLcbBufferList_is_empty(void);
void OpenLcbBufferList_check_timeouts(uint8_t current_tick);
sequenceDiagram participant CAN_RX as CAN RX Handler participant List as Buffer List participant Store as Buffer Store participant FIFO as Buffer FIFO Note over CAN_RX: First frame arrives CAN_RX->>Store: allocate_buffer(SNIP) Store-->>CAN_RX: msg_ptr (ref_count=1) CAN_RX->>CAN_RX: Copy data, set inprocess=1 CAN_RX->>List: add(msg_ptr) Note over CAN_RX: Middle frame arrives CAN_RX->>List: find(src_alias, dst_alias, mti) List-->>CAN_RX: msg_ptr CAN_RX->>CAN_RX: Append data to payload Note over CAN_RX: Final frame arrives CAN_RX->>List: find(src_alias, dst_alias, mti) List-->>CAN_RX: msg_ptr CAN_RX->>CAN_RX: Append data, set inprocess=0 CAN_RX->>List: release(msg_ptr) CAN_RX->>FIFO: push(msg_ptr)

4.6 Pool Exhaustion and Tuning

When a pool is exhausted, allocate_buffer() returns NULL. The consequences depend on context:

ContextSymptomRecovery
CAN RX (incoming message) Frame is dropped silently. Sender may retry (datagrams) or the message is lost (events). Increase the relevant pool depth.
Protocol handler (outgoing reply) Reply cannot be constructed. Addressed messages may trigger an OIR from the remote node after timeout. Increase the relevant pool depth.
Sibling dispatch (loopback copy) Loopback copy for sibling nodes is dropped. The sibling never sees the message. Increase BASIC pool depth.
Tuning Strategy

Run your application under expected peak load and monitor the *_max_allocated() counters. If any peak value equals the pool depth, increase that depth. A good rule of thumb is to add 25-50% headroom above the observed peak.

← Previous: Ch 3b — CAN-Specific Types Next: Ch 4b — CAN Buffer System →