Three-Tier Memory: PSRAM, Flash, and CAN FD

Three-Tier Memory: PSRAM, Flash, and CAN FD
Three-tier memory pipeline moving camera and thermal bursts from PSRAM through flash to CAN FD
Three tiers, three distinct jobs — fast capture in PSRAM, persistent storage in flash, controlled transport over CAN FD. No tier blocks another.

When you're building a sensor module that must capture high-resolution images, store them reliably across power cycles, and ship them over a constrained CAN FD bus, all at the same time, you run into a fundamental problem. No single memory tier does all three jobs well. External PSRAM is fast but volatile. Flash is persistent but slow to write. CAN FD is transport, not storage. The moment one layer tries to carry the full load, something breaks: frames drop, storage corrupts, or the capture pipeline stalls waiting for a transmission to finish. This is the architecture problem we solved in our sensor modules, the multi-modal camera and thermal imaging boards at the heart of Hoomanely's physical intelligence platform.

The problem: one memory tier doesn't cut it

The modules capture synchronized bursts of camera images, 517KB of raw 12-bit Bayer data at 640x400, paired with thermal frames from a 32x24 array (768 float values). A typical burst sequence generates 3 to 10 of these paired captures in rapid succession, triggered by a proximity event detecting a pet at the monitoring station.

The naive approach, capture straight to flash, then transmit, hits a wall immediately. Streaming a 517KB image to our 32MB OCTOSPI flash in 4KB chunks, including LittleFS metadata commits, takes over three seconds per camera frame. If the flash write blocks the capture thread, the next burst window is missed entirely.

The inverse, skip flash, transmit over CAN FD immediately after capture, is equally fragile. A 517KB raw image across 64-byte CAN FD frames means approximately 8,700 individual frames on the bus. Any contention, retry, or bus-off event during that transmission loses the data permanently with no recovery path.

What the system needed was three tiers doing three distinct jobs, each handing off to the next only when a threshold condition is met, never blocking, never competing.

The approach: one tier, one responsibility

The design principle we built the entire pipeline around is deliberately simple: each memory tier owns exactly one job. PSRAM is the capture staging area, every burst frame lands here first with zero filesystem overhead and zero write latency. LittleFS on OCTOSPI flash is the persistence layer, data migrates here in streaming 4KB chunks, asynchronously, when PSRAM buffer slots fill up, acting as the safety net across power cycles and connectivity gaps. CAN FD is the transport layer, data only leaves the device when a transmission threshold is met, ensuring the bus is used efficiently rather than saturated one frame at a time.

The PSRAM buffer manager holds a fixed set of slots. Each slot carries one camera frame stored in PSRAM, its paired thermal buffer, a sequence ID, a UTC timestamp, and the command ID of the proximity event that triggered the capture. Slot occupancy is tracked continuously. When the count hits the configured offload threshold, the PSRAM-to-flash offload process begins, running asynchronously so the next burst capture can start without waiting.

Data moving through three tiers

Tier 1 to Tier 2: PSRAM to flash. The offload module streams each PSRAM buffer entry into the LittleFS data file (/storage_data.bin) in 4KB chunks, avoiding any large heap allocation. At the same time, a table entry gets written to /storage_table.bin with full metadata:

typedef struct {
    uint32_t sequence_id;
    uint32_t timestamp;
    file_type_t file_type;          // FILE_TYPE_THERMAL or FILE_TYPE_CAMERA
    transmission_status_t status;   // PENDING -> SENDING -> SENT / FAILED
    uint32_t content_size;
    uint32_t content_offset;        // Byte offset in storage_data.bin
    uint16_t command_id;
    uint8_t  retry_count;
} table_entry_t;

The table supports up to 5,000 entries and the data file is capped at 30MB in circular mode, space is reclaimed automatically after a successful transmission. Every entry tracks its own status and retry count independently, so a single failed transmission does not stall the rest of the queue. Once the flash entry count reaches 12 (FLASH_TRANSMIT_THRESHOLD), the storage manager signals the VBUS control layer to begin a transmission sweep.

Data flows left to right — each handoff is triggered by a threshold, not a timer, so no tier ever starves another of CPU time.

Tier 2 to Tier 3: flash to CAN FD. When the transmission sweep begins, the storage manager selects pending entries in a deliberate order: thermal data first, then camera images. Thermal frames are smaller and carry higher immediate diagnostic value for the Biosense AI Engine, they reach the CM4 orchestrator faster even when camera data is queued behind them.

Three distinct transmission strategies get applied based on file size. Small thermal files under 32KB use a direct buffer: a single malloc, read full, transmit, free. Large thermal files up to 1MB use static streaming with 2KB static chunks and zero heap allocation. Camera images at 517KB or more use a PSRAM-reload path: read flash back into PSRAM, then transmit from PSRAM.

Camera images use the PSRAM-reload path because transmitting 8,700 CAN FD frames directly from flash, where each read has filesystem overhead, would extend transmission time unpredictably. Loading the full image into PSRAM first gives the CAN FD layer consistent, low-latency byte access for the entire transmission window. Every entry walks PENDING to SENDING to SENT, with up to 3 retry attempts before being marked FAILED. Cleanup runs automatically after a successful send, freeing the data file offset for reuse.

The results: what the production logs show

Timing data from Sequence 17, a live device session where the PSRAM buffer hit its 12-entry offload limit mid-burst, illustrates exactly where time goes in the pipeline. Total burst pair time ran 12,806ms. Flash offload duration was 6,050ms. Net thermal store, excluding offload, was 88ms. Net camera store was 43ms. Camera capture duration was 6,386ms.

The key insight is that the net store time is only 131ms (88ms thermal plus 43ms camera). The 6-second offload isn't a fundamental bottleneck, it's a scheduling problem. When the pipeline coordinator correctly overlaps the flash offload with the next camera capture, that 6,050ms disappears from the critical path entirely. The tiers were already fast enough; the work was in ensuring they ran in parallel.

Sequence 17 timing — the 6,050ms flash offload only looks expensive. When overlapped with the next camera capture, the critical path shrinks to the 131ms net store.

Why it matters at Hoomanely

Our Physical Intelligence devices generate multi-modal health data continuously: visual body condition, thermal temperature signatures, weight trends, behavioral patterns, and environmental quality readings. All of this is captured and processed at the edge, with no cloud dependency on the critical capture path. That constraint is intentional. A pet's health event doesn't wait for a strong WiFi signal. The three-tier memory architecture is what makes our modules genuinely resilient: data captured during a network outage is preserved in flash, queued with full metadata, and transmitted the moment the CAN bus clears. Nothing is silently dropped.

For the Biosense AI Engine to build accurate longitudinal health baselines, it needs complete data with accurate timestamps. Every table entry carries a sequence_id, timestamp, and command_id that trace back to the exact proximity event that triggered the capture. The metadata chain is unbroken from sensor edge to cloud inference.

Key takeaways

Isolate tier responsibilities ruthlessly, PSRAM captures, flash persists, CAN FD transmits. The moment a tier tries to do two jobs, latency compounds and data loss risk rises. Threshold-based handoffs beat continuous streaming, batching flash writes in 4KB chunks and gating CAN transmissions at 12 entries reduces per-event overhead by an order of magnitude. Metadata is not optional, a table entry with status, retry count, byte offset, and command ID transforms a raw write into a recoverable, auditable pipeline. Async overlap is the real optimization, the 6-second flash offload is nearly invisible when correctly scheduled alongside the next capture. And transmission strategy must be size-aware, three distinct TX paths prevent the worst of both worlds, heap exhaustion for large files and unnecessary complexity for small ones.

Read more