Chapter 6 — Thread Safety
6.1 Why It Matters
Even on bare-metal systems without an OS, the library operates in two concurrent contexts:
- Interrupt context (ISR): The CAN receive interrupt fires when a frame arrives. It allocates a CAN buffer, copies the frame data, and pushes it to the CAN FIFO. On some platforms, the CAN TX complete interrupt also runs here.
- Main loop context: All state machines, protocol handlers, buffer operations, and node management run in the main loop.
Without synchronization, the ISR could interrupt the main loop in the middle of a buffer pool operation, corrupting the data structure.
6.2 The Lock/Unlock Contract
The library defines a lock/unlock contract through callback function pointers that the application must provide:
// Application provides these functions:
void lock_shared_resources(void); // Enter critical section
void unlock_shared_resources(void); // Leave critical section
The library calls lock_shared_resources() before accessing any shared data structure and unlock_shared_resources() immediately after. The critical section must be as short as possible to minimize interrupt latency.
6.3 What Must Be Protected
(allocate/free)"] B["CAN FIFO
(push/pop)"] C["Alias Mapping Table
(is_duplicate flag)"] D["Listener Alias Table
(alias resolution)"] E["OpenLCB Buffer List
(timeout check)"] end subgraph ISR["ISR Context (writes)"] I1["CAN RX: allocate + push"] I2["CAN RX: set is_duplicate"] I3["CAN RX: update listener alias"] end subgraph MAIN["Main Loop (reads/writes)"] M1["CAN RX SM: pop + free"] M2["CAN TX SM: allocate"] M3["Main SM: check duplicates"] M4["Main SM: resolve aliases"] M5["Main SM: check timeouts"] end I1 --> A I1 --> B I2 --> C I3 --> D M1 --> A M1 --> B M2 --> A M3 --> C M4 --> D M5 --> E style SHARED fill:#fff3e0,stroke:#e65100 style ISR fill:#ffcdd2,stroke:#c62828 style MAIN fill:#c8e6c9,stroke:#2e7d32
| Resource | ISR Access | Main Loop Access | Why Lock Needed |
|---|---|---|---|
| CAN Buffer Store | allocate_buffer() | allocate_buffer(), free_buffer() | Concurrent allocation could return same slot twice |
| CAN FIFO | push() | pop() | Concurrent push/pop could corrupt head/tail pointers |
| Alias Mapping Table | Set is_duplicate flag | Read and clear is_duplicate | Flag read-modify-write is not atomic |
| Listener Alias Table | Write alias on AMD arrival | Read alias for TX resolution | Partial write visible to reader |
| OpenLCB Buffer List | None | check_timeouts() | Documentation requires lock (future-proofing) |
6.4 Bare-Metal Implementation
On bare-metal systems, the simplest and most reliable approach is to disable and re-enable interrupts:
// dsPIC example
void lock_shared_resources(void) {
__builtin_disi(0x3FFF); // Disable interrupts
}
void unlock_shared_resources(void) {
__builtin_disi(0x0000); // Re-enable interrupts
}
// ARM Cortex-M example
void lock_shared_resources(void) {
__disable_irq();
}
void unlock_shared_resources(void) {
__enable_irq();
}
// ESP32 (FreeRTOS) example
static portMUX_TYPE spinlock = portMUX_INITIALIZER_UNLOCKED;
void lock_shared_resources(void) {
portENTER_CRITICAL(&spinlock);
}
void unlock_shared_resources(void) {
portEXIT_CRITICAL(&spinlock);
}
6.5 RTOS Implementation
On an RTOS, use a mutex or critical section:
// FreeRTOS example with mutex
static SemaphoreHandle_t shared_mutex;
void lock_shared_resources(void) {
xSemaphoreTake(shared_mutex, portMAX_DELAY);
}
void unlock_shared_resources(void) {
xSemaphoreGive(shared_mutex);
}
If the lock function is called from both ISR and main loop context, a standard mutex will not work. Use a critical section (disable interrupts) or an ISR-safe mutex. On FreeRTOS, portENTER_CRITICAL() / portEXIT_CRITICAL() is the appropriate mechanism when ISR and task contexts share resources.
6.6 Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Forgetting to lock around CAN FIFO push in ISR | Corrupted FIFO, random crashes, lost messages | Wrap ISR push in lock/unlock |
| Holding lock for too long | Increased interrupt latency, missed CAN frames at high bus load | Keep critical sections minimal -- only the shared access |
| Using a non-ISR-safe mutex from ISR context | Deadlock or undefined behavior | Use disable-interrupts or ISR-safe primitives |
| Nested locks without nesting support | Premature unlock on inner unlock call | Ensure lock implementation supports nesting, or restructure code |