Most of the literature on spike sorting assumes you have a workstation. You have gigabytes of RAM for your principal component analysis. You have disk space to buffer hours of raw waveforms. You can iterate on your clustering parameters after the recording session is done.
Embedded neural recording hardware exists in a different universe. A Cortex-M4 with 256 KB SRAM and 1 MB flash is not an unusual target platform for a closed-loop neuro-rehabilitation device. In that environment, the spike sorting question is not which algorithm is most accurate. It is which algorithm is accurate enough to drive a useful stimulation response and still fits inside the memory and compute budget.
This post documents what we have learned running the Ruten decoder on constrained embedded hardware, including the tradeoffs we accepted and the ones we rejected.
What You Actually Need from On-Device Spike Sorting
Before getting into implementation, it helps to be precise about the requirement. In a closed-loop rehabilitation application, the spike decoder is not producing a publishable electrophysiology dataset. It is producing a control signal. The question is: can the decoder reliably distinguish the signal states that matter for control?
For FES upper-limb rehabilitation driven by motor unit activity in residual muscle, that might mean distinguishing active versus resting discharge on two or three motor unit classes. The classifier does not need to be perfect across all firing rates and all units. It needs to produce a reliable control signal for the specific units that drive the FES output. That requirement is much less demanding than the full spike sorting problem, and it shapes every architecture decision downstream.
Threshold Crossing Detection
The first stage in the embedded pipeline is threshold-crossing detection. This is computationally cheap: for each sample, compare the filtered signal value against a threshold. If it crosses the threshold, record the event time and extract the waveform window.
The threshold is set adaptively using an estimate of the noise floor. The standard approach from the Quiroga 2004 threshold estimator: threshold equals 4 times the median of the absolute value of the signal divided by 0.6745. We compute this estimate from a 50 ms rolling window, updated once per 10 ms interval. The rolling update avoids a full pass over the buffer every sample and keeps the memory access pattern predictable.
On a Cortex-M4 with hardware floating-point enabled, threshold detection for a 16-channel recording running at 30 kHz costs roughly 1.2 microseconds per channel per sample. For 16 channels, that is about 20 microseconds per sample interval at 30 kHz. The sample interval at 30 kHz is 33 microseconds. There is meaningful headroom, but not infinite headroom. Adding more channels or a higher sample rate requires profiling carefully before committing to a platform.
Waveform Windowing and Feature Extraction
Once a threshold crossing is detected, the decoder extracts a waveform window centered on the event. Window size is a tradeoff. A longer window captures the full spike shape and is more discriminable. A shorter window uses less memory per event and allows more events per buffer.
We use a 48-sample window at 30 kHz, corresponding to 1.6 milliseconds. That captures the initial depolarization, peak, and the majority of the repolarization phase for typical cortical unit action potentials, while keeping the waveform buffer compact.
Feature extraction on embedded hardware is where the full principal component projection becomes impractical. A PCA projection onto the top three components of a 48-dimensional waveform requires a 3x48 matrix multiply per event. On a Cortex-M4 with CMSIS-DSP, that is feasible: the arm_mat_vec_mult_f32 routine handles a 3x48 multiply in roughly 3 microseconds. Whether that is within budget depends on the spike firing rate and the number of channels. At 200 spikes per second across 16 channels and a 1 ms deadline on feature delivery, the math fits.
When PCA Is Not an Option
For platforms with tighter compute budgets, we have implemented a three-feature extractor that avoids the matrix multiply entirely. The three features are: peak-to-valley amplitude, the sample index of the peak (a proxy for spike rise time), and the L1 norm of the waveform window (correlated with spike energy). These three numbers require only comparison and accumulation operations, run in under 500 nanoseconds per waveform, and produce feature vectors that separate well-isolated units with signal-to-noise ratios above about 6 dB.
The cost is unit separation quality. For a recording with three units firing at similar rates with similar amplitudes, the three-feature extractor may fail to resolve them. For a recording dominated by one or two high-SNR units with distinct amplitudes, it works well. Most closed-loop rehabilitation scenarios we have encountered fall into the second category: the device needs to track activity from a small number of units that are specifically selected for having high amplitude and low noise during the electrode placement procedure.
Clustering: K-Means on a Budget
Once features are extracted, the clustering step assigns each spike event to a unit class. On a workstation, you might use mixture of Gaussians with an expectation-maximization solver. On a Cortex-M4, the memory overhead of tracking a full covariance matrix for each cluster is prohibitive.
We use k-means with a maximum cluster count of four. The cluster centroids are initialized during a calibration period at session start: the device records 30 seconds of neural activity, computes feature vectors for all detected events, runs k-means on the collected feature cloud (this is the one moment when we can afford to be computationally generous since it is off the real-time path), and stores the resulting cluster centroids in a 4 by 3 float array that occupies 48 bytes.
During real-time operation, each new event is classified by computing its Euclidean distance to each centroid and assigning it to the nearest one. For four clusters and three features, this is 12 multiplications, 12 additions, and 4 comparisons per event. The operation fits inside a single interrupt service routine call with headroom to spare.
The fixed centroid approach does not adapt during the recording session. If the spike waveforms drift due to electrode movement or tissue inflammation, the centroids become stale and unit assignment degrades. On a deployment timescale of hours, this is usually acceptable. For longer sessions, the middleware exposes a recalibration call that can be triggered from the application layer when decode quality falls below a configurable threshold.
Memory Layout and Buffer Strategy
The tightest constraint on the embedded pipeline is often not compute but memory. With 256 KB SRAM, you need to account for the RTOS stack, the neural signal ring buffer, the waveform event buffer, and the feature extraction working space, plus any application-layer state.
In the Ruten embedded decoder, the ring buffer holds 50 ms of raw samples per channel, using 16-bit ADC values. For 16 channels at 30 kHz, that is 48,000 samples per channel per 50 ms interval, stored as uint16_t values: 16 channels times 1,500 samples times 2 bytes equals 48 KB. That is the largest single allocation in the system. On a 256 KB platform, it leaves 208 KB for everything else, which is workable but requires careful stack sizing.
The waveform event buffer holds up to 32 pending events, each storing a 48-sample window plus metadata: timestamp, channel index, and a classification result field. At 48 samples times 4 bytes per sample plus 12 bytes of metadata, each event record occupies 204 bytes. Thirty-two events occupy roughly 6.4 KB. At typical closed-loop firing rates, 32 pending events are more than enough to prevent buffer overruns in the decoder ISR.
What Matters Most for Getting It Working
The single most important factor in getting an embedded spike sorter to produce a useful control signal is electrode selection during placement. An algorithm that perfectly separates ten units on a noisy recording is less useful for closed-loop control than an algorithm that cleanly tracks two high-SNR units whose firing rate directly modulates with the intended movement direction.
We are not saying signal quality is a substitute for algorithm quality. We are saying that for embedded closed-loop applications, the device maker and the person responsible for electrode placement need to agree on what decode quality means before the system goes into use. A classifier that achieves 92 percent unit assignment accuracy in a bench test with clean recordings may produce noticeably worse real-time performance under realistic noise conditions. Designing the placement and calibration protocol with the decoder's known limitations in mind is more reliable than tuning the algorithm to mask those limitations.
The decoder configuration we ship as default in the Ruten SDK is the PCA-based extractor on Cortex-M4 and above, with k-means clustering at up to four units, and adaptive threshold detection. For Cortex-M0 class processors with no hardware floating-point, the three-feature extractor with fixed centroid classification is the default. Both configurations are deterministic in memory footprint and worst-case execution time, which is the property that matters most for embedding in a medical device firmware context.