When we started designing the Ruten SDK in late 2023, the first question was not what the software should do. It was where the boundaries should go.
Closed-loop BMI firmware is full of boundary decisions that look arbitrary at first glance but encode real engineering tradeoffs. The line between signal acquisition and decoding is not just an abstraction for tidiness. It determines which half of the stack runs inside an interrupt service routine and which half can tolerate scheduler jitter. Get it wrong and you build a system that works on the bench and saturates the CPU under realistic electrode counts and sample rates.
This post is a walkthrough of the four layers in the Ruten SDK and the reasoning behind each boundary decision.
Layer 1: Signal Acquisition
The acquisition layer is the narrowest and most time-sensitive part of the stack. Its job is to receive raw ADC samples from the electrode interface, timestamp each sample, and hand the data to a ring buffer without dropping packets. Everything else happens downstream.
We model every hardware source through the same abstract interface, regardless of whether the physical device connects over SPI, I2C, USB-FS, or a proprietary parallel bus. The interface exposes two methods that matter in the critical path: acquire_frame() and get_timestamp(). An adapter for a specific amplifier maps those two calls onto whatever protocol the hardware speaks.
The design goal is zero conditional logic inside the hot path. By the time the acquisition layer delivers a frame to the decoder, it should carry no knowledge of where the signal came from or what electrode geometry produced it. That abstraction is what lets the same decoder code run against a 32-channel intralaminar array and a 256-channel high-density ECoG grid without modification.
One tradeoff worth naming directly: this abstraction adds a thin latency cost. A direct SPI read into a fixed buffer is faster than going through an adapter dispatch. On a Cortex-M7 running at 480 MHz, the adapter overhead measures under 2 microseconds per frame. For a 30 kHz sampling rate that is within budget. For any application requiring sub-100-microsecond end-to-end latency, the cost deserves explicit accounting before adoption.
Layer 2: Real-Time Decoder
The decoder layer is where most of the interesting firmware work lives. It receives a stream of frames from the acquisition layer and converts them into one of two signal classes: discrete spike events (for single-unit or multi-unit activity) or continuous field potential features (for LFP, EEG power bands, or ECoG spatial patterns).
For spike detection, we use a threshold-crossing detector with a configurable dead-time. The threshold is set as a multiple of the RMS noise floor, estimated from a rolling 50 ms baseline window. When a sample crosses the threshold, the decoder reads out a 1.5 ms window centered on the crossing event and passes it to the feature extractor.
Feature extraction varies by hardware class. On a host-side processor (x86, ARM Cortex-A), we run a full principal component projection against a pre-computed basis, stored in a compact format. On a Cortex-M4, we fall back to a three-feature vector: peak-to-valley amplitude, half-width at half-maximum, and energy in the spike window. That reduced feature set is enough to separate two or three well-isolated units without a matrix multiply on every event.
The decoder output is not a waveform. It is a structured event record: channel index, timestamp, feature vector, and a unit assignment if a classifier has been configured. Nothing downstream of the decoder ever sees raw ADC counts.
Latency Versus Classification Accuracy
One thing we learned from building earlier neural interface prototypes: the pressure to minimize decode latency and the pressure to maximize classification accuracy pull in opposite directions. A spike sorting algorithm that completes in 50 microseconds produces worse unit separation than one given 5 milliseconds, given the same hardware.
The Ruten decoder exposes a mode flag for this tradeoff. In DETECT_ONLY mode, the decoder emits crossing events immediately with no classification delay. In CLASSIFY mode, it buffers events until the feature extractor completes. Most closed-loop rehabilitation applications we have seen run in DETECT_ONLY mode during the real-time control loop and batch-process a richer classification offline for adaptation and model recalibration.
Layer 3: Stimulation Command Bus
The stimulation command bus sits between the decoder and the hardware stimulator driver. Its job is to receive decoded intent signals, validate them against the current safety envelope, and dispatch pulse parameter records to the stimulator adapter.
The bus enforces three categories of constraints. First, waveform parameters: charge per phase, pulse width, amplitude, and waveform shape (monophasic, biphasic symmetric, biphasic asymmetric). Second, timing constraints: minimum inter-pulse interval, maximum burst duration, and a refractory window following any artifact-triggering event. Third, channel-level state: the bus maintains a per-channel inhibit flag that the safety monitor can set when a hardware fault is detected.
None of these checks live in the stimulator adapter itself. The adapter is a thin driver that translates structured command records into hardware register writes. Safety logic in the adapter would mean reimplementing the same checks for every new stimulator platform. Centralizing the safety envelope in the command bus means writing the validation logic once, covering every adapter in the registry.
Layer 4: Device Adapter Registry
The registry is the outermost boundary of the SDK. It is a lookup table mapping a device identifier to a pair of adapter implementations: one acquisition adapter and one stimulation adapter.
A device maker porting Ruten to a new hardware platform writes two adapter classes, registers them with a macro at startup, and the rest of the SDK routes correctly from that point forward. The registry handles multiplexing if multiple device types are present simultaneously, which is common in research benches that combine a research-grade amplifier with a commercially sourced stimulator.
The registry pattern was a deliberate choice against a unified driver model. A unified driver model requires every new device to fit into a common data contract at the driver level. That works well when devices are similar. Neural recording hardware is not similar: a 128-channel wireless headstage has different timing constraints, packet formats, and error recovery behaviors than a wired rack-mount amplifier. The adapter pattern accepts that diversity rather than flattening it.
A Concrete Runtime Example
To make the layer interactions concrete, consider a motor imagery task. A subject imagines flexing their right hand. The acquisition layer is streaming 128-channel ECoG data at 1 kHz from a flexible subdural array over USB-FS. The decoder detects a mu-rhythm power increase in the contralateral sensorimotor band and emits a continuous feature event: channel cluster 34 through 41, beta desynchronization, 800 ms sustained.
The application layer receives that event and decides to deliver a 30 Hz FES burst to the forearm extensor muscles via a peripheral stimulator. It constructs a command record with charge per phase set to 8 nC and submits it to the stimulation command bus. The bus checks: 8 nC is within the per-channel limit of 50 nC. Inter-pulse interval since the last command is 180 ms, above the 100 ms minimum. The inhibit flag is clear. The bus passes the command to the stimulator adapter, which writes the pulse parameters to the stimulator's SPI register map.
Total latency from feature detection to stimulation command dispatch in this configuration: under 800 microseconds. The acquisition-to-feature path accounts for roughly 400 microseconds. The command bus validation adds under 30 microseconds. The remainder is adapter overhead and USB round-trip time.
What the Stack Does Not Handle
It is worth being explicit about the scope boundary. The Ruten SDK does not make clinical decisions. It does not decide which decoded signal corresponds to a motor intention. It does not implement the control policy mapping neural features to stimulation parameters. Those layers live above the SDK, in the device maker's application code.
The middleware's job is to ensure that by the time the application layer receives a decoded feature and dispatches a stimulation command, both operations have happened with the timing precision and safety constraints that the application layer would otherwise have to build from scratch on every platform. That is a narrower scope than a complete closed-loop BMI system. The narrowness is intentional.
The two pieces actively in development for the next SDK release are the feedback path and an adaptation interface. Right now there is no first-class concept of feedback from stimulator back to decoder. If stimulation modifies neural activity and the decoder should account for that modification, the device maker handles it in application code. We think that feedback path belongs inside the middleware, with artifact blanking and re-entrancy handling built in. The adaptation interface work aims to give device makers clean hooks for long-timescale parameter updates without requiring changes to the core control loop architecture.