Developer Notes
This document captures design decisions, trade-offs, and historical context for CRUMBS internals. It’s intended for contributors and future maintainers.
Handler Dispatch: O(1) vs O(n) Trade-off
Original Design (v0.8.x): O(1) Direct Indexing
The initial handler dispatch used a 256-entry lookup table for O(1) constant-time dispatch:
struct crumbs_context_s {
// ...
crumbs_handler_fn handlers[256]; // 512 bytes on AVR
void *handler_userdata[256]; // 512 bytes on AVR
};
// O(1) dispatch
crumbs_handler_fn fn = ctx->handlers[command_type];
if (fn) fn(ctx, cmd, data, len, ctx->handler_userdata[cmd]);
Memory cost: ~1KB on AVR (2-byte pointers), ~2KB on 32-bit platforms.
Problem discovered: The LED peripheral example compiled with 70% RAM usage on Arduino Nano (ATmega328P), leaving only 613 bytes for stack, buffers, and user state. This was unacceptable for a simple LED controller.
Current Design (v0.9.x): O(n) Sparse Table
Switched to a configurable sparse table with linear search:
struct crumbs_context_s {
// ...
uint8_t handler_count;
uint8_t handler_cmd[CRUMBS_MAX_HANDLERS];
crumbs_handler_fn handlers[CRUMBS_MAX_HANDLERS];
void *handler_userdata[CRUMBS_MAX_HANDLERS];
};
// O(n) dispatch
for (uint8_t i = 0; i < ctx->handler_count; i++) {
if (ctx->handler_cmd[i] == cmd) {
ctx->handlers[i](ctx, cmd, data, len, ctx->handler_userdata[i]);
break;
}
}
Default: CRUMBS_MAX_HANDLERS=16 (~68 bytes on AVR)
Trade-off analysis:
| Handlers | O(n) Overhead @ 16MHz | Impact on I²C (100kHz) |
|---|---|---|
| 4 | ~0.5 µs | Negligible |
| 16 | ~2 µs | Negligible |
| 64 | ~8 µs | Still negligible |
Conclusion: O(n) is acceptable for I²C applications. O(1) would only matter for:
- High-speed SPI messaging (5000+ msg/s)
- Internal software dispatch (non-I²C)
- Real-time systems with µs-level timing budgets
Future Option: Restore O(1) for Power Users
If O(1) dispatch is needed in the future, options include:
- Automatic mode switching: Use O(1) when
CRUMBS_MAX_HANDLERS == 256 - Separate handler table: Externalize handler storage so context stays fixed-size
- Compile-time mode flag:
CRUMBS_HANDLER_MODE_SPARSEvsCRUMBS_HANDLER_MODE_DIRECT
See _reference/06_handler_configuration_and_performance.md for detailed design options.
Arduino Separate Compilation Bug
The Problem
Arduino and PlatformIO compile library source files separately from sketch files. This causes a critical ABI mismatch when users define CRUMBS_MAX_HANDLERS in their sketch:
// ❌ THIS DOES NOT WORK
#define CRUMBS_MAX_HANDLERS 8 // Sketch sees this
#include <crumbs.h> // But library was compiled with default (16)
| Component | CRUMBS_MAX_HANDLERS | sizeof(crumbs_context_t) |
|---|---|---|
| Library | 16 (default) | ~68 bytes |
| Sketch | 8 (user-defined) | ~36 bytes |
The Crash
When crumbs_init() is called, the library executes:
memset(ctx, 0, sizeof(*ctx)); // Library thinks ctx is 68 bytes
But the user’s ctx variable is only 36 bytes. The library writes zeros 32 bytes past the end of the actual struct, corrupting:
- Stack variables
- Return addresses
- Saved registers
Symptom: Crash on startup with garbled serial output (“Sta” then reset).
The Solution
Users MUST set CRUMBS_MAX_HANDLERS via build flags, not sketch defines:
# platformio.ini
build_flags = -DCRUMBS_MAX_HANDLERS=8
This ensures both library and sketch are compiled with the same value.
Runtime Detection
Added crumbs_context_size() to detect mismatches at runtime:
void setup() {
if (sizeof(crumbs_context_t) != crumbs_context_size()) {
Serial.println("ERROR: CRUMBS_MAX_HANDLERS mismatch!");
while(1); // Halt — don't corrupt memory
}
}
This catches the bug before crumbs_init() can corrupt memory.
Variable-Length Payload Migration (v0.7.x)
Old Format (Fixed 31 Bytes)
type_id 1 byte
command_type 1 byte
data[7] 28 bytes (float32 Ă— 7)
crc8 1 byte
Problems:
- Always transmitted 7 floats, even for simple commands
- Inefficient for small messages
- Hard-coded interpretation prevented evolution
New Format (Variable 4–31 Bytes)
type_id 1 byte
command_type 1 byte
data_len 1 byte (0–27)
data[] 0–27 bytes (opaque)
crc8 1 byte
Benefits:
- Minimal overhead for small commands (4 bytes for empty payload)
- Opaque bytes allow any encoding (floats, ints, structs, strings)
- Future-proof (typed payloads can be layered on top)
Breaking Changes
| Old | New |
|---|---|
float data[7] |
uint8_t data[27] + data_len |
CRUMBS_DATA_LENGTH (7) |
CRUMBS_MAX_PAYLOAD (27) |
CRUMBS_MESSAGE_SIZE (31) |
CRUMBS_MESSAGE_MAX_SIZE (31) |
| Fixed encode length | Variable encode length |
CRC Implementation Selection
Evaluated multiple CRC-8 variants from AceCRC:
| Variant | Flash | RAM | Speed |
|---|---|---|---|
crc8_bit |
~50 B | 0 | Slowest |
crc8_nibble |
~90 B | 0 | Good |
crc8_nibblem |
~130 B | 16 B | Faster |
crc8_byte |
~280 B | 256 B | Fastest |
Selected: crc8_nibble — optimal balance of flash size, speed, and zero RAM for AVR targets.
See docs/crc.md for full comparison and validation.
API Design Decisions (v0.9.x Analysis)
During v0.9.x development, several API “improvements” were evaluated and rejected:
Rejected: Sequential Payload Reader
Proposal: Add crumbs_reader_t with auto-advancing offset:
crumbs_reader_t r;
crumbs_reader_init(&r, data, len);
crumbs_reader_u8(&r, &value); // auto-advances offset
Why rejected: For typical 2-4 byte payloads, the setup overhead equals or exceeds the savings. Current offset-based reading is:
- Explicit about message layout (documents the wire format)
- No additional state to manage
- Supports random access if needed
Rejected: Batch Handler Registration
Proposal: Register multiple handlers in one call:
static const crumbs_handler_entry_t handlers[] = {
{ CMD_SET_ALL, handle_set_all, NULL },
{ CMD_SET_ONE, handle_set_one, NULL },
};
crumbs_register_handlers(&ctx, handlers, 2);
Why rejected: Same line count as individual calls, adds a type definition, requires counting entries. Individual registration is clearer at the call site.
Confirmed Correct: Full Handler Signature
Minimal signatures like void (*fn)(const uint8_t *data, uint8_t len) were considered. The full signature provides:
- Access to
ctx->user_datafor shared state - Command type for handlers that process multiple commands
- Per-handler user data pointer
The (void)param; casts are a one-time cost per handler.
Design Principles
-
C99 core, C++ HALs allowed — Core must compile as pure C; HALs may use C++ where platform bindings require it (Arduino Wire).
-
No dynamic allocation — All storage is static or stack-allocated. Safe for embedded.
-
Pay for what you use — Features like handler dispatch can be disabled entirely (
CRUMBS_MAX_HANDLERS=0). -
Platform guards — Linux HAL files are guarded so Arduino toolchain can compile the repo without linux-wire dependency.
-
ABI stability — Context size is validated at runtime to catch configuration mismatches.