Loading...

Driver

Driver

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_source

RecordPluginWithLimits<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. 256

Define 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 ID

Includes 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:

OverrideCalled when
start_recording_impl / stop_recording_implA recording session opens or closes — set up and tear down your chip's acquisition
parse_frame_payloadEach frame arrives from the gateware — turn payload words into samples
configure_channels / configure_bit_width / configure_sample_rateThe host changes acquisition parameters
to_proto, get_lsb, self_test, validate_ephys_config, get_impedanceThe 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 into actual_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 a synapse::Peripheral message.
  • 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 — return std::nullopt if 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"
  }
}
FieldNotes
nameThe .deb package name.
kindperipheral_plugin.
versionPackage version. Match the SCIFI_REGISTER_PERIPHERAL version.
abi_versionPlugin ABI the host checks at load.
peripheral_idsArray of IDs this plugin claims. Must contain your ID.
install.targetWhere the driver .so installs on the device.
install.gateware_targetWhere 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_NAME to your .so basename (no lib prefix).
  • List your new src/driver/*.cpp files 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 .