The driver is a C++ shared library (.so) the headstage loads at startup. It configures your chip, receives the frames your gateware sends, and presents the result as packets to be parsed by the software.
Plugin structure
A plugin consists of three key components: a class that inherits the SDK base template, a limits declaration, and a registration shim in a separate file.
#include "scifi-peripheral-sdk/record_plugin.h"
namespace axon_test_source {
class AxonTestSourcePeripheral
: public scifi::plugin::RecordPluginWithLimits<AxonTestSourcePeripheral> {
public:
SCIFI_RECORD_PLUGIN_LIMITS(MAX_SAMPLE_RATE, MAX_BIT_WIDTH, MAX_GAIN, MAX_CHANNEL_COUNT);
using RecordPluginWithLimits::RecordPluginWithLimits; // inherit the 5-arg ctor
// overrides follow
};
} // namespace axon_test_sourceRecordPluginWithLimits<T> takes your class as its template argument.
Hardware limits
SCIFI_RECORD_PLUGIN_LIMITS declares your ASIC's ceilings. The SDK reads them at construction and rejects any recording configuration that exceeds them before your code runs.
SCIFI_RECORD_PLUGIN_LIMITS(
/*max_sample_rate =*/ MAX_SAMPLE_RATE, // e.g. 1000000 (Hz)
/*max_bit_width =*/ MAX_BIT_WIDTH, // e.g. 16
/*max_gain =*/ MAX_GAIN, // e.g. 1
/*max_channel_count=*/ MAX_CHANNEL_COUNT); // e.g. 256Define these as constants for your peripheral. The example sets 256 channels, 1 MHz, 16-bit, gain 1.
Registration
SCIFI_REGISTER_PERIPHERAL registers your plugin with the device — the name, version, and peripheral ID it needs to load and drive your peripheral. Ensure the ID matches the one you chose for the gateware implementation.
#include "scifi-peripheral-sdk/plugin.h"
#include "axon_test_source_peripheral.h"
SCIFI_REGISTER_PERIPHERAL(
axon_test_source::AxonTestSourcePeripheral, // the class you implement
"axon_test_source", // name shown in device logs
"0.1.0", // plugin .deb version
static_cast<uint32_t>(0xF001)); // peripheral IDIncludes and namespaces
Your headers pull the SDK surface from the scifi-peripheral-sdk/ include root, and the message types your overrides use from api/:
#include "scifi-peripheral-sdk/record_plugin.h"
#include "scifi-peripheral-sdk/plugin.h"
#include "scifi-peripheral-sdk/scifi/status.h"
#include "api/synapse.pb.h"
#include "api/channel.pb.h"Function Implementation
In order for the headstage to correctly use your peripheral, you must implement a set of standard functions that describe Broadband Source peripherals. There are mandatory functions, as well as optional functions to implement.
The must-implement functions include:
| Override | Called when |
|---|---|
start_recording_impl / stop_recording_impl | A recording session opens or closes — set up and tear down your chip's acquisition |
parse_frame_payload | Each frame arrives from the gateware — turn payload words into samples |
configure_channels / configure_bit_width / configure_sample_rate | The host changes acquisition parameters |
to_proto, get_lsb, self_test, validate_ephys_config, get_impedance | The host queries capabilities, validates a config, or runs diagnostics |
The sections below cover each group in turn.
Recording Lifecycle overrides
The recording stack opens and closes a session using these functions. They are protected and return scifi::Status.
The following function is called when the signal chain is started:
scifi::Status start_recording_impl(uint32_t sample_rate, uint32_t bit_width,
std::vector<synapse::Channel> channels,
float gain, float hp_corner, float lp_corner,
uint32_t& actual_sample_rate) override;This function sets acquisition: it likely will program your chip's registers, start its sample loop, and write the achieved rate back into actual_sample_rate (it may differ from the requested sample_rate if your chip quantizes it). Return a non-OK scifi::Status to abort the session.
The following function is called when the signal chain is stopped:
scifi::Status stop_recording_impl() override;The function typically stops the chip's acquisition and releases any per-session state. The SDK owns the sockets and closes them itself.
Data path overrides
Each frame your gateware emits arrives here as its payload words. This function's role is to return the samples it contains.
std::span<const uint16_t> parse_frame_payload(
std::span<const uint32_t> payload_words) override;payload_words is the frame payload — the beats after the header, in order. Typically, you will decode them into 16-bit samples and return a span over your sample buffer. Each response packet is one frame; each payload word carries a 16-bit sample in its low half (0x0000_RRRR), so the method walks channels_enabled_ words into a fixed buffer and returns a span over it.
Note: Return a span over storage that outlives the call. The example uses a member buffer (frame_buffer_[MAX_CHANNEL_COUNT]) written once per packet and read by the SDK before the next packet arrives. This is single-threaded by contract, so a member buffer works well — a span over a local variable would not.
This is the mirror of the gateware's frame layout: your RTL packs samples into payload words on tx_axis, and parse_frame_payload unpacks them. The gateware and driver must agree on the packing.
Configuration overrides
The host changes acquisition parameters through these functions.
scifi::Status configure_channels(const std::vector<synapse::Channel>& channels) override;
scifi::Status configure_bit_width(uint16_t bit_width) override;
scifi::Status configure_sample_rate(double desired_sample_rate, double& actual_sample_rate) override;configure_channels— enable the requested channels on your chip.configure_bit_width— set the sample width.configure_sample_rate— set the rate, writing the achieved value intoactual_sample_rate.
Capability and diagnostic overrides
The host queries capabilities and runs diagnostics through these public methods.
synapse::Peripheral to_proto() const override;
float get_lsb(float hp_corner_hz, float lp_corner_hz) const override;
synapse::QueryResponse self_test(const synapse::SelfTestQuery& query) override;
const std::optional<std::string> validate_ephys_config(
const synapse::BroadbandSourceConfig& config) const override;
scifi::Status get_impedance(uint32_t electrode_id, float stim_freq, float& mag, float& phase) override;to_proto— describe your peripheral as asynapse::Peripheralmessage.get_lsb— the volts-per-count for the given filter corners, so the host can scale samples to physical units.self_test— answer a diagnostic query.validate_ephys_config— returnstd::nulloptif the config is acceptable, or an error string explaining why it is not.get_impedance— measure electrode impedance, writing magnitude and phase through the out-parameters.
Packaging and building
The plugin's metadata declares what the device loads, and the build cross-compiles and packages it.
manifest.json
The plugin's metadata. The device reads it to check the ABI version, and synapsectl peripherals build both reads install to place the artifacts.
{
"name": "scifi-axon-test-source",
"kind": "peripheral_plugin",
"version": "0.1.0",
"abi_version": 1,
"description": "Axon test source — streams synthetic dummy data over the SDK data path",
"peripheral_ids": ["0xF001"],
"install": {
"type": "shared_library",
"target": "/usr/lib/scifi/plugins/axon_test_source.so",
"gateware_target": "/usr/lib/scifi/gateware/axon_test_source.bit"
}
}| Field | Notes |
|---|---|
name | The .deb package name. |
kind | peripheral_plugin. |
version | Package version. Match the SCIFI_REGISTER_PERIPHERAL version. |
abi_version | Plugin ABI the host checks at load. |
peripheral_ids | Array of IDs this plugin claims. Must contain your ID. |
install.target | Where the driver .so installs on the device. |
install.gateware_target | Where the bitstream installs. If unset, derived from install.target (.so → .bit). Set it explicitly when several peripherals share one devkit bitstream path. |
Building and deploying the driver
The driver cross-compiles to aarch64 with CMake and vcpkg, inside the driver container, when you run synapsectl peripherals build both . (or build driver). To point the build at your own driver:
- Set
OUTPUT_NAMEto your.sobasename (nolibprefix). - List your new
src/driver/*.cppfiles as sources. - Keep the vcpkg dependencies your code needs (see the example's
vcpkg.json).
The .deb itself is not produced by CMake — synapsectl peripherals build both . stages the compiled .so and the bitstream into one package with fpm.
Deploy drivers by running synapsectl peripherals deploy both .