diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1eDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1eDialog.cs
index e831865..72ac286 100644
--- a/OpenEphys.Onix1.Design/NeuropixelsV1eDialog.cs
+++ b/OpenEphys.Onix1.Design/NeuropixelsV1eDialog.cs
@@ -269,7 +269,8 @@ private void CheckStatus()
{
gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(ConfigureNode.GainCalibrationFile,
ConfigureNode.ProbeConfiguration.SpikeAmplifierGain,
- ConfigureNode.ProbeConfiguration.LfpAmplifierGain);
+ ConfigureNode.ProbeConfiguration.LfpAmplifierGain,
+ 960);
}
catch (IOException ex)
{
diff --git a/OpenEphys.Onix1/ConfigureHeadstageNric1384.cs b/OpenEphys.Onix1/ConfigureHeadstageNric1384.cs
new file mode 100644
index 0000000..0d053d6
--- /dev/null
+++ b/OpenEphys.Onix1/ConfigureHeadstageNric1384.cs
@@ -0,0 +1,151 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Configures a Nric1384 headstage on the specified port.
+ ///
+ ///
+ /// The Nric1384 Headstage is a 2.5g serialized, multifunction headstage for small animals built around the
+ /// IMEC Nric1384 bioacquisition chip. This headstage is designed to function with passive probes of the
+ /// user's choosing (e.g. silicon probe arrays, high-density tetrode drives, etc). It provides the
+ /// following features:
+ ///
+ /// - 384 analog ephys channels sampled at 30 kHz per channel and exposed via an array of
+ /// 12x ultra-high density Molex 203390-0323 quad-row connectors.
+ /// - A BNO055 9-axis IMU for real-time, 3D orientation tracking at 100
+ /// Hz.
+ /// - Two TS4231 light to digital converters for real-time, 3D position tracking with HTC
+ /// Vive base stations.
+ /// - A single electrical stimulator (current controlled, +/-15V compliance, automatic
+ /// electrode discharge).
+ ///
+ ///
+ [Description("Configures a Nric1384 Headstage headstage.")]
+ public class ConfigureHeadstageNric1384 : MultiDeviceFactory
+ {
+ PortName port;
+ readonly ConfigureHeadstageNric1384PortController PortControl = new();
+
+ ///
+ /// Initialize a new instance of a class.
+ ///
+ public ConfigureHeadstageNric1384()
+ {
+ Port = PortName.PortA;
+ PortControl.HubConfiguration = HubConfiguration.Standard;
+ }
+
+ ///
+ /// Gets or sets the Nric1384 bioacquisition chip configuration.
+ ///
+ [Category(DevicesCategory)]
+ [TypeConverter(typeof(SingleDeviceFactoryConverter))]
+ [Description("Specifies the configuration for the Nric1384 bioacquisition device.")]
+ public ConfigureNric1384 Nric1384 { get; set; } = new();
+
+ ///
+ /// Gets or sets the BNO055 9-axis inertial measurement unit configuration.
+ ///
+ [Category(DevicesCategory)]
+ [TypeConverter(typeof(SingleDeviceFactoryConverter))]
+ [Description("Specifies the configuration for the Bno055 device.")]
+ public ConfigureBno055 Bno055 { get; set; } = new();
+
+ ///
+ /// Gets or sets the port.
+ ///
+ ///
+ /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation.
+ ///
+ [Description("Specifies the physical connection of the headstage to the ONIX breakout board.")]
+ [Category(ConfigurationCategory)]
+ public PortName Port
+ {
+ get { return port; }
+ set
+ {
+ port = value;
+ var offset = (uint)port << 8;
+ PortControl.DeviceAddress = (uint)port;
+ Nric1384.DeviceAddress = offset + 0;
+ Bno055.DeviceAddress = offset + 1;
+ }
+ }
+
+ ///
+ /// Gets or sets the port voltage.
+ ///
+ ///
+ ///
+ /// If defined, it will override automated voltage discovery and apply the specified voltage to the headstage.
+ /// If left blank, an automated headstage detection algorithm will attempt to communicate with the headstage and
+ /// apply an appropriate voltage for stable operation. Because ONIX allows any coaxial tether to be used, some of
+ /// which are thin enough to result in a significant voltage drop, its may be required to manually specify the
+ /// port voltage.
+ ///
+ ///
+ /// Warning: This device requires 3.8V to 5.5V for proper operation. Voltages higher than 5.5V can
+ /// damage the headstage.
+ ///
+ ///
+ [Description("If defined, overrides automated voltage discovery and applies " +
+ "the specified voltage to the headstage. Warning: this device requires 3.8V to 5.5V " +
+ "for proper operation. Higher voltages can damage the headstage.")]
+ [Category(ConfigurationCategory)]
+ public double? PortVoltage
+ {
+ get => PortControl.PortVoltage;
+ set => PortControl.PortVoltage = value;
+ }
+
+ internal override IEnumerable GetDevices()
+ {
+ yield return PortControl;
+ yield return Nric1384;
+ yield return Bno055;
+ }
+
+ class ConfigureHeadstageNric1384PortController : ConfigurePortController
+ {
+ protected override bool ConfigurePortVoltage(DeviceContext device)
+ {
+ const double MinVoltage = 3.8;
+ const double MaxVoltage = 5.5;
+ const double VoltageOffset = 0.7;
+ const double VoltageIncrement = 0.2;
+
+ for (var voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement)
+ {
+ SetPortVoltage(device, voltage);
+ if (base.CheckLinkState(device))
+ {
+ SetPortVoltage(device, voltage + VoltageOffset);
+ return CheckLinkState(device);
+ }
+ }
+
+ return false;
+ }
+
+ private void SetPortVoltage(DeviceContext device, double voltage)
+ {
+ device.WriteRegister(PortController.PORTVOLTAGE, 0);
+ Thread.Sleep(500);
+ device.WriteRegister(PortController.PORTVOLTAGE, (uint)(10 * voltage));
+ Thread.Sleep(500);
+ }
+
+ protected override bool CheckLinkState(DeviceContext device)
+ {
+ // NB: needs an additional reset after power on to provide its device table.
+ device.Context.Reset();
+ var linkState = device.ReadRegister(PortController.LINKSTATE);
+ return (linkState & PortController.LINKSTATE_SL) != 0;
+ }
+
+ }
+ }
+}
diff --git a/OpenEphys.Onix1/ConfigureNric1384.cs b/OpenEphys.Onix1/ConfigureNric1384.cs
new file mode 100644
index 0000000..6b195a1
--- /dev/null
+++ b/OpenEphys.Onix1/ConfigureNric1384.cs
@@ -0,0 +1,204 @@
+using System;
+using System.ComponentModel;
+using Bonsai;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Configures a Nric184 bioacquisition chip.
+ ///
+ public class ConfigureNric1384 : SingleDeviceFactory
+ {
+ ///
+ /// Initialize a new instance of a class.
+ ///
+ public ConfigureNric1384()
+ : base(typeof(Nric1384))
+ {
+ }
+
+ ///
+ /// Gets or sets the device enable state.
+ ///
+ ///
+ /// If set to true, will produce data. If set to false,
+ /// will not produce data.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Specifies whether the Nric1384 data stream is enabled.")]
+ public bool Enable { get; set; } = true;
+
+ ///
+ /// Gets or sets the amplifier gain for the spike-band.
+ ///
+ ///
+ /// The spike-band is from DC to 10 kHz if is set to false, while the
+ /// spike-band is from 300 Hz to 10 kHz if is set to true.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Amplifier gain for spike-band.")]
+ public NeuropixelsV1Gain SpikeAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain1000;
+
+ ///
+ /// Gets or sets the amplifier gain for the LFP-band.
+ ///
+ ///
+ /// The LFP band is from 0.5 to 500 Hz.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Amplifier gain for LFP-band.")]
+ public NeuropixelsV1Gain LfpAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain50;
+
+ ///
+ /// Gets or sets the state of the spike-band filter.
+ ///
+ ///
+ /// If set to true, the spike-band has a 300 Hz high-pass filter which will be activated. If set to
+ /// false, the high-pass filter will not to be activated.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("If true, activates a 300 Hz high-pass in the spike-band data stream.")]
+ public bool SpikeFilter { get; set; } = true;
+
+ ///
+ /// Gets or sets the path to the gain calibration file.
+ ///
+ ///
+ ///
+ /// Each chip is linked to a gain calibration file that contains gain adjustments determined by IMEC during
+ /// factory testing. Electrode voltages are scaled using these values to ensure they can be accurately compared
+ /// across chips. Therefore, using the correct gain calibration file is mandatory to create standardized recordings.
+ ///
+ ///
+ /// Calibration files are chip-specific and not interchangeable across chips. Calibration files must contain the
+ /// serial number of the corresponding chip on their first line of text. If you have lost track of a calibration
+ /// file for your chip, email IMEC at neuropixels.info@imec.be with the chip serial number to retrieve a new copy.
+ ///
+ ///
+ [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")]
+ [Description("Path to the Nric1384 gain calibraiton file.")]
+ [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
+ public string GainCalibrationFile { get; set; }
+
+ ///
+ /// Gets or sets the path to the ADC calibration file.
+ ///
+ ///
+ ///
+ /// Each chip must be provided with an ADC calibration file that contains chip-specific hardware settings that is
+ /// created by IMEC during factory calibration. These files are used to set internal bias currents, correct for ADC
+ /// nonlinearities, correct ADC-zero crossing non-monotonicities, etc. Using the correct calibration file is mandatory
+ /// for the chip to operate correctly.
+ ///
+ ///
+ /// Calibration files are chip-specific and not interchangeable across chips. Calibration files must contain the
+ /// serial number of the corresponding chip on their first line of text. If you have lost track of a calibration
+ /// file for your chip, email IMEC at neuropixels.info@imec.be with the chip serial number to retrieve a new copy.
+ ///
+ ///
+ [FileNameFilter("ADC calibration files (*_ADCCalibration.csv)|*_ADCCalibration.csv")]
+ [Description("Path to the Nric1384 ADC calibraiton file.")]
+ [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
+ public string AdcCalibrationFile { get; set; }
+
+ ///
+ /// Configures a Nric1384 bioacquisition device.
+ ///
+ ///
+ /// This will schedule configuration actions to be applied by a node
+ /// prior to data acquisition.
+ ///
+ /// A sequence of that holds all configuration actions.
+ ///
+ /// The original sequence with the side effect of an additional configuration action to configure
+ /// a Nric1384 device.
+ ///
+ public override IObservable Process(IObservable source)
+ {
+ var enable = Enable;
+ var deviceName = DeviceName;
+ var deviceAddress = DeviceAddress;
+ return source.ConfigureDevice(context =>
+ {
+ var device = context.GetDeviceContext(deviceAddress, typeof(Nric1384));
+ device.WriteRegister(Nric1384.ENABLE, enable ? 1u : 0);
+
+ if (enable)
+ {
+ var probeControl = new Nric1384RegisterContext(device, SpikeAmplifierGain, LfpAmplifierGain, SpikeFilter, GainCalibrationFile, AdcCalibrationFile);
+ probeControl.InitializeChip();
+ probeControl.WriteShiftRegisters();
+ }
+
+ return DeviceManager.RegisterDevice(deviceName, device, DeviceType);
+
+ });
+ }
+ }
+
+ static class Nric1384
+ {
+ public const int ID = 33;
+
+ public const int I2cAddress = 0x70;
+ public const int ChannelCount = 384;
+ public const int ElectrodeCount = 384;
+
+ // managed registers
+ public const uint ENABLE = 0x8000; // Enable or disable the data output stream
+ public const uint ADC00_OFF_THRESH = 0x8001; // ADC 0 offset and threshold parameters: [6-bit ADC 00 Offset, 10-bit ADC 00 Threshold]
+ public const uint ADC01_OFF_THRESH = 0x8002;
+ public const uint ADC02_OFF_THRESH = 0x8003;
+ public const uint ADC03_OFF_THRESH = 0x8004;
+ public const uint ADC04_OFF_THRESH = 0x8005;
+ public const uint ADC05_OFF_THRESH = 0x8006;
+ public const uint ADC06_OFF_THRESH = 0x8007;
+ public const uint ADC07_OFF_THRESH = 0x8008;
+ public const uint ADC08_OFF_THRESH = 0x8009;
+ public const uint ADC09_OFF_THRESH = 0x800a;
+ public const uint ADC10_OFF_THRESH = 0x800b;
+ public const uint ADC11_OFF_THRESH = 0x800c;
+ public const uint ADC12_OFF_THRESH = 0x800d;
+ public const uint ADC13_OFF_THRESH = 0x800e;
+ public const uint ADC14_OFF_THRESH = 0x800f;
+ public const uint ADC15_OFF_THRESH = 0x8010;
+ public const uint ADC16_OFF_THRESH = 0x8011;
+ public const uint ADC17_OFF_THRESH = 0x8012;
+ public const uint ADC18_OFF_THRESH = 0x8013;
+ public const uint ADC19_OFF_THRESH = 0x8014;
+ public const uint ADC20_OFF_THRESH = 0x8015;
+ public const uint ADC21_OFF_THRESH = 0x8016;
+ public const uint ADC22_OFF_THRESH = 0x8017;
+ public const uint ADC23_OFF_THRESH = 0x8018;
+ public const uint ADC24_OFF_THRESH = 0x8019;
+ public const uint ADC25_OFF_THRESH = 0x801a;
+ public const uint ADC26_OFF_THRESH = 0x801b;
+ public const uint ADC27_OFF_THRESH = 0x801c;
+ public const uint ADC28_OFF_THRESH = 0x801d;
+ public const uint ADC29_OFF_THRESH = 0x801e;
+ public const uint ADC30_OFF_THRESH = 0x801f;
+ public const uint ADC31_OFF_THRESH = 0x8020; // ADC 31 offset and threshold parameters: [6-bit ADC 31 Offset , 10-bit ADC 31 Threshold]
+ public const uint LFP_GAIN = 0x8021; // LFP gain correction parameter: [X Q1.14]
+ public const uint AP_GAIN = 0x8022; // AP gain correction parameter: [X Q1.14]
+
+ // unmanaged regiseters
+ public const uint OP_MODE = 0X00;
+ public const uint REC_MOD = 0X01;
+ public const uint CAL_MOD = 0X02;
+ public const uint STATUS = 0X08;
+ public const uint SYNC = 0X09;
+ public const uint SR_CHAIN3 = 0X0C; // Odd channels
+ public const uint SR_CHAIN2 = 0X0D; // Even channels
+ public const uint SR_LENGTH2 = 0X0F;
+ public const uint SR_LENGTH1 = 0X10;
+ public const uint SOFT_RESET = 0X11;
+
+ internal class NameConverter : DeviceNameConverter
+ {
+ public NameConverter()
+ : base(typeof(Nric1384))
+ {
+ }
+ }
+ }
+}
diff --git a/OpenEphys.Onix1/ContextTask.cs b/OpenEphys.Onix1/ContextTask.cs
index 1cd7ace..b8c8dc8 100644
--- a/OpenEphys.Onix1/ContextTask.cs
+++ b/OpenEphys.Onix1/ContextTask.cs
@@ -106,7 +106,7 @@ private void Initialize()
DeviceTable = ctx.DeviceTable;
}
- private void Reset()
+ internal void Reset()
{
lock (disposeLock)
lock (regLock)
diff --git a/OpenEphys.Onix1/NeuropixelsV1Helper.cs b/OpenEphys.Onix1/NeuropixelsV1Helper.cs
index 7344e2d..7da1bf6 100644
--- a/OpenEphys.Onix1/NeuropixelsV1Helper.cs
+++ b/OpenEphys.Onix1/NeuropixelsV1Helper.cs
@@ -63,7 +63,7 @@ public static class NeuropixelsV1Helper
.Where(l => l.Ok)
.Select(l => l.Param);
- return calibrationValues.Count() == NumberOfAdcParameters
+ return calibrationValues.Count() != NumberOfAdcParameters
? null
: new NeuropixelsV1eAdc {
CompP = calibrationValues.ElementAt(0),
@@ -99,13 +99,13 @@ public static class NeuropixelsV1Helper
/// Current for the AP data.
/// Current for the LFP data.
/// object that contains the AP and LFP gain correction values. This object is null if the file was not successfully parsed.
- public static NeuropixelsV1eGainCorrection? TryParseGainCalibrationFile(string gainCalibrationFile, NeuropixelsV1Gain apGain, NeuropixelsV1Gain lfpGain)
+ public static NeuropixelsV1eGainCorrection? TryParseGainCalibrationFile(string gainCalibrationFile, NeuropixelsV1Gain apGain, NeuropixelsV1Gain lfpGain, int electrodeCount)
{
if (!File.Exists(gainCalibrationFile)) return null;
var lines = File.ReadLines(gainCalibrationFile);
- if (lines.Count() != NeuropixelsV1e.ElectrodeCount + 1) return null;
+ if (lines.Count() != electrodeCount + 1) return null;
if (!ulong.TryParse(lines.ElementAt(0), out var serialNumber)) return null;
if (!lines
@@ -117,7 +117,7 @@ public static class NeuropixelsV1Helper
})
.Where(l => l.Ok)
.Select(l => l.Channel)
- .SequenceEqual(Enumerable.Range(0, NeuropixelsV1e.ElectrodeCount))) return null;
+ .SequenceEqual(Enumerable.Range(0, electrodeCount))) return null;
var apIndex = Array.IndexOf(Enum.GetValues(typeof(NeuropixelsV1Gain)), apGain);
var apGainCorrections = lines
diff --git a/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs b/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs
index 2941d4f..d7d4565 100644
--- a/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs
+++ b/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs
@@ -28,7 +28,7 @@ public NeuropixelsV1eDataFrame(ulong[] clock, ulong[] hubClock, int[] frameCount
/// Gets the frame count value array.
///
///
- /// Frame count is a 20-bit counter on the probe that increments its value for every frame produced.
+ /// A 20-bit counter on the probe that increments its value for every frame produced.
/// The value ranges from 0 to 1048575 (2^20-1), and should always increment by 1 until it wraps around back to 0.
/// This can be used to detect dropped frames.
///
@@ -39,7 +39,7 @@ public NeuropixelsV1eDataFrame(ulong[] clock, ulong[] hubClock, int[] frameCount
///
///
/// Spike-band data has 384 rows (channels) with columns representing the samples acquired at 30 kHz. Each sample is a
- /// 10-bit offset binary encoded as an unsigned short value.
+ /// 10-bit, offset binary value encoded as a .
///
public Mat SpikeData { get; }
@@ -48,7 +48,7 @@ public NeuropixelsV1eDataFrame(ulong[] clock, ulong[] hubClock, int[] frameCount
///
///
/// LFP data has 32 rows (channels) with columns representing the samples acquired at 2.5 kHz. Each sample is a
- /// 10-bit offset binary encoded as an unsigned short value.
+ /// 10-bit, offset binary value encoded as a .
///
public Mat LfpData { get; }
@@ -69,7 +69,6 @@ internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, int[] fra
lfpBuffer[RawToChannel[k, lfpFrameIndex], lfpBufferIndex] = (ushort)(lfpGainCorrection * (a > thresholds[k] ? a - offsets[k] : a));
}
-
// Loop over 12 AP frames within each "super-frame"
for (int i = 0; i < NeuropixelsV1e.FramesPerRoundRobin; i++)
{
diff --git a/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs
index d44d51f..f51b490 100644
--- a/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs
+++ b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs
@@ -51,7 +51,8 @@ public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddres
$"match the ADC calibration file serial number: {adcCalibration.Value.SerialNumber}.");
}
- var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(gainCalibrationFile, probeConfiguration.SpikeAmplifierGain, probeConfiguration.LfpAmplifierGain);
+ var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(gainCalibrationFile,
+ probeConfiguration.SpikeAmplifierGain, probeConfiguration.LfpAmplifierGain, NeuropixelsV1e.ElectrodeCount);
if (!gainCorrection.HasValue)
{
diff --git a/OpenEphys.Onix1/Nric1384Data.cs b/OpenEphys.Onix1/Nric1384Data.cs
new file mode 100644
index 0000000..208c990
--- /dev/null
+++ b/OpenEphys.Onix1/Nric1384Data.cs
@@ -0,0 +1,91 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Bonsai;
+using OpenCV.Net;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Produces a sequence of objects from a Nric1384 bioacquisition device.
+ ///
+ public class Nric1384Data : Source
+ {
+ ///
+ [Description(SingleDeviceFactory.DeviceNameDescription)]
+ [Category(DeviceFactory.ConfigurationCategory)]
+ [TypeConverter(typeof(Nric1384.NameConverter))]
+ public string DeviceName { get; set; }
+
+ int bufferSize = 36;
+
+ ///
+ /// Gets or sets the buffer size.
+ ///
+ ///
+ /// Buffer size sets the number of super frames that are buffered before propagating data.
+ /// A super frame consists of 384 channels from the spike-band and 32 channels from the LFP band.
+ /// The buffer size must be a multiple of 12.
+ ///
+ [Description("Number of super-frames (384 channels from spike band and 32 channels from " +
+ "LFP band) to buffer before propagating data. Must be a multiple of 12.")]
+ [Category(DeviceFactory.ConfigurationCategory)]
+ public int BufferSize
+ {
+ get => bufferSize;
+ set => bufferSize = (int)(Math.Ceiling((double)value / NeuropixelsV1e.FramesPerRoundRobin) * NeuropixelsV1e.FramesPerRoundRobin);
+ }
+
+ ///
+ /// Generates a sequence of objects.
+ ///
+ /// A sequence of objects.
+ public unsafe override IObservable Generate()
+ {
+ var spikeBufferSize = BufferSize;
+ var lfpBufferSize = spikeBufferSize / NeuropixelsV1e.FramesPerRoundRobin;
+
+ return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo =>
+ {
+ var device = deviceInfo.GetDeviceContext(typeof(Nric1384));
+
+ return Observable.Create(observer =>
+ {
+ var sampleIndex = 0;
+ var spikeBuffer = new short[NeuropixelsV1e.ChannelCount * spikeBufferSize];
+ var lfpBuffer = new short[NeuropixelsV1e.ChannelCount * lfpBufferSize];
+ var frameCountBuffer = new int[spikeBufferSize];
+ var hubClockBuffer = new ulong[spikeBufferSize];
+ var clockBuffer = new ulong[spikeBufferSize];
+
+ var frameObserver = Observer.Create(frame =>
+ {
+ var payload = (Nric1384Payload*)frame.Data.ToPointer();
+ Marshal.Copy(new IntPtr(payload->LfpData), lfpBuffer, sampleIndex * NeuropixelsV1e.AdcCount, NeuropixelsV1e.AdcCount);
+ Marshal.Copy(new IntPtr(payload->ApData), spikeBuffer, sampleIndex * NeuropixelsV1e.ChannelCount, NeuropixelsV1e.ChannelCount);
+ frameCountBuffer[sampleIndex] = payload->FrameCount;
+ hubClockBuffer[sampleIndex] = payload->HubClock;
+ clockBuffer[sampleIndex] = frame.Clock;
+ if (++sampleIndex >= spikeBufferSize)
+ {
+ var lfpData = BufferHelper.CopyTranspose(lfpBuffer, lfpBufferSize, NeuropixelsV1e.ChannelCount, Depth.U16);
+ var apData = BufferHelper.CopyTranspose(spikeBuffer, spikeBufferSize, NeuropixelsV1e.ChannelCount, Depth.U16);
+ observer.OnNext(new Nric1384DataFrame(clockBuffer, hubClockBuffer, frameCountBuffer, apData, lfpData));
+ frameCountBuffer = new int[spikeBufferSize];
+ hubClockBuffer = new ulong[spikeBufferSize];
+ clockBuffer = new ulong[spikeBufferSize];
+ sampleIndex = 0;
+ }
+ },
+ observer.OnError,
+ observer.OnCompleted);
+
+ return device.Context.GetDeviceFrames(device.Address).SubscribeSafe(frameObserver);
+ });
+ });
+ }
+ }
+}
diff --git a/OpenEphys.Onix1/Nric1384DataFrame.cs b/OpenEphys.Onix1/Nric1384DataFrame.cs
new file mode 100644
index 0000000..a102f02
--- /dev/null
+++ b/OpenEphys.Onix1/Nric1384DataFrame.cs
@@ -0,0 +1,66 @@
+using System.Runtime.InteropServices;
+using OpenCV.Net;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Buffered data from a Nric1384 bioacquisition device.
+ ///
+ public class Nric1384DataFrame : BufferedDataFrame
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An array of values.
+ /// An array of hub clock counter values.
+ /// An array of frame count values.
+ /// An array of multi-channel spike data as a object.
+ /// An array of multi-channel LFP data as a object.
+ public Nric1384DataFrame(ulong[] clock, ulong[] hubClock, int[] frameCount, Mat spikeData, Mat lfpData)
+ : base(clock, hubClock)
+ {
+ FrameCount = frameCount;
+ SpikeData = spikeData;
+ LfpData = lfpData;
+ }
+
+ ///
+ /// Gets the frame count value array.
+ ///
+ ///
+ /// A 20-bit counter on the chip that increments its value for every frame produced. The value ranges from 0 to
+ /// 1048575 (2^20-1), and should always increment by 13 (one count is taken per super-frame and there are 13 frames
+ /// in a super frame) until it wraps around back to 0. This can be used to detect dropped frames.
+ ///
+ public int[] FrameCount { get; }
+
+
+ ///
+ /// Gets the spike-band data as a object.
+ ///
+ ///
+ /// Spike-band data has 384 rows (channels) with columns representing the samples acquired at 30 kHz. Each sample is a
+ /// 10-bit, offset binary value encoded as a .
+ ///
+ public Mat SpikeData { get; }
+
+
+ ///
+ /// Gets the LFP band data as a object.
+ ///
+ ///
+ /// LFP data has 32 rows (channels) with columns representing the samples acquired at 2.5 kHz. Each sample is a
+ /// 10-bit, offset binary value encoded as a .
+ ///
+ public Mat LfpData { get; }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ unsafe struct Nric1384Payload
+ {
+ public ulong HubClock;
+ public fixed ushort LfpData[NeuropixelsV1e.AdcCount];
+ public fixed ushort ApData[NeuropixelsV1e.ChannelCount];
+ public int FrameCount;
+ }
+}
diff --git a/OpenEphys.Onix1/Nric1384RegisterContext.cs b/OpenEphys.Onix1/Nric1384RegisterContext.cs
new file mode 100644
index 0000000..b04eba7
--- /dev/null
+++ b/OpenEphys.Onix1/Nric1384RegisterContext.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections;
+using System.IO;
+
+namespace OpenEphys.Onix1
+{
+ class Nric1384RegisterContext : I2CRegisterContext
+ {
+ readonly double ApGainCorrection;
+ readonly double LfpGainCorrection;
+ readonly NeuropixelsV1eAdc[] Adcs = new NeuropixelsV1eAdc[NeuropixelsV1e.AdcCount];
+
+ const byte ReferenceSource = 0b001; // All, hardcoded
+ const int BaseConfigurationBitCount = 2448;
+ const int BaseConfigurationConfigOffset = 576;
+ const uint ShiftRegisterSuccess = 1 << 7;
+
+ readonly DeviceContext device;
+
+ readonly BitArray[] BaseConfigs = { new(BaseConfigurationBitCount, false), // Ch 0, 2, 4, ...
+ new(BaseConfigurationBitCount, false) }; // Ch 1, 3, 5, ...
+
+ public Nric1384RegisterContext(DeviceContext deviceContext, NeuropixelsV1Gain apGain, NeuropixelsV1Gain lfpGain, bool apFilter, string gainCalibrationFile, string adcCalibrationFile)
+ : base(deviceContext, Nric1384.I2cAddress)
+ {
+
+ device = deviceContext;
+
+ if (!File.Exists(gainCalibrationFile))
+ {
+ throw new ArgumentException("A gain calibration file must be specified for the Nric1384 chip.");
+ }
+
+ if (!File.Exists(adcCalibrationFile))
+ {
+ throw new ArgumentException("An ADC calibration file must be specified for the Nric1384 chip.");
+ }
+
+ var adcCalibration = NeuropixelsV1Helper.TryParseAdcCalibrationFile(adcCalibrationFile);
+
+ if (!adcCalibration.HasValue)
+ {
+ throw new ArgumentException($"The calibration file \"{adcCalibrationFile}\" is invalid.");
+ }
+
+ var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(gainCalibrationFile,apGain, lfpGain, Nric1384.ElectrodeCount);
+
+ if (!gainCorrection.HasValue)
+ {
+ throw new ArgumentException($"The calibration file \"{gainCalibrationFile}\" is invalid.");
+ }
+
+ if (adcCalibration.Value.SerialNumber != gainCorrection.Value.SerialNumber)
+ {
+ throw new ArgumentException($"The ADC calibration file's serial number ({adcCalibration.Value.SerialNumber}) " +
+ $"does not match the gain calibration file's serial number ({gainCorrection.Value.SerialNumber}).");
+ }
+
+ ApGainCorrection = gainCorrection.Value.ApGainCorrectionFactor;
+ LfpGainCorrection = gainCorrection.Value.LfpGainCorrectionFactor;
+
+ // create shift-register bit arrays
+ for (int i = 0; i < NeuropixelsV1e.ChannelCount; i++)
+ {
+ var configIdx = i % 2;
+
+ // References
+ var refIdx = configIdx == 0 ?
+ (382 - i) / 2 * 3 :
+ (383 - i) / 2 * 3;
+
+ BaseConfigs[configIdx][refIdx + 0] = (ReferenceSource >> 0 & 0x1) == 1;
+ BaseConfigs[configIdx][refIdx + 1] = (ReferenceSource >> 1 & 0x1) == 1;
+ BaseConfigs[configIdx][refIdx + 2] = (ReferenceSource >> 2 & 0x1) == 1;
+
+ var chanOptsIdx = BaseConfigurationConfigOffset + ((i - configIdx) * 4);
+
+ // MSB [Full, standby, LFPGain(3 downto 0), APGain(3 downto0)] LSB
+
+ BaseConfigs[configIdx][chanOptsIdx + 0] = ((byte)apGain >> 0 & 0x1) == 1;
+ BaseConfigs[configIdx][chanOptsIdx + 1] = ((byte)apGain >> 1 & 0x1) == 1;
+ BaseConfigs[configIdx][chanOptsIdx + 2] = ((byte)apGain >> 2 & 0x1) == 1;
+
+ BaseConfigs[configIdx][chanOptsIdx + 3] = ((byte)lfpGain >> 0 & 0x1) == 1;
+ BaseConfigs[configIdx][chanOptsIdx + 4] = ((byte)lfpGain >> 1 & 0x1) == 1;
+ BaseConfigs[configIdx][chanOptsIdx + 5] = ((byte)lfpGain >> 2 & 0x1) == 1;
+
+ BaseConfigs[configIdx][chanOptsIdx + 6] = false;
+ BaseConfigs[configIdx][chanOptsIdx + 7] = !apFilter; // Full bandwidth = 1, filter on = 0
+
+ }
+
+ Adcs = adcCalibration.Value.Adcs;
+
+ int k = 0;
+ foreach (var adc in Adcs)
+ {
+ if (adc.CompP < 0 || adc.CompP > 0x1F)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter CompP value of {adc.CompP} is invalid.");
+ }
+
+ if (adc.CompN < 0 || adc.CompN > 0x1F)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter CompN value of {adc.CompN} is invalid.");
+ }
+
+ if (adc.Cfix < 0 || adc.Cfix > 0xF)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter Cfix value of {adc.Cfix} is invalid.");
+ }
+
+ if (adc.Slope < 0 || adc.Slope > 0x7)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter Slope value of {adc.Slope} is invalid.");
+ }
+
+ if (adc.Coarse < 0 || adc.Coarse > 0x3)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter Coarse value of {adc.Coarse} is invalid.");
+ }
+
+ if (adc.Fine < 0 || adc.Fine > 0x3)
+ {
+ throw new ArgumentOutOfRangeException($"ADC calibration parameter Fine value of {adc.Fine} is invalid.");
+ }
+
+ var configIdx = k % 2;
+ int d = k++ / 2;
+
+ int compOffset = 2406 - 42 * (d / 2) + (d % 2) * 10;
+ int slopeOffset = compOffset + 20 + (d % 2);
+
+ var compP = new BitArray(new byte[] { (byte)adc.CompP });
+ var compN = new BitArray(new byte[] { (byte)adc.CompN });
+ var cfix = new BitArray(new byte[] { (byte)adc.Cfix });
+ var slope = new BitArray(new byte[] { (byte)adc.Slope });
+ var coarse = (new BitArray(new byte[] { (byte)adc.Coarse }));
+ var fine = new BitArray(new byte[] { (byte)adc.Fine });
+
+ BaseConfigs[configIdx][compOffset + 0] = compP[0];
+ BaseConfigs[configIdx][compOffset + 1] = compP[1];
+ BaseConfigs[configIdx][compOffset + 2] = compP[2];
+ BaseConfigs[configIdx][compOffset + 3] = compP[3];
+ BaseConfigs[configIdx][compOffset + 4] = compP[4];
+
+ BaseConfigs[configIdx][compOffset + 5] = compN[0];
+ BaseConfigs[configIdx][compOffset + 6] = compN[1];
+ BaseConfigs[configIdx][compOffset + 7] = compN[2];
+ BaseConfigs[configIdx][compOffset + 8] = compN[3];
+ BaseConfigs[configIdx][compOffset + 9] = compN[4];
+
+ BaseConfigs[configIdx][slopeOffset + 0] = slope[0];
+ BaseConfigs[configIdx][slopeOffset + 1] = slope[1];
+ BaseConfigs[configIdx][slopeOffset + 2] = slope[2];
+
+ BaseConfigs[configIdx][slopeOffset + 3] = fine[0];
+ BaseConfigs[configIdx][slopeOffset + 4] = fine[1];
+
+ BaseConfigs[configIdx][slopeOffset + 5] = coarse[0];
+ BaseConfigs[configIdx][slopeOffset + 6] = coarse[1];
+
+ BaseConfigs[configIdx][slopeOffset + 7] = cfix[0];
+ BaseConfigs[configIdx][slopeOffset + 8] = cfix[1];
+ BaseConfigs[configIdx][slopeOffset + 9] = cfix[2];
+ BaseConfigs[configIdx][slopeOffset + 10] = cfix[3];
+ }
+ }
+
+ internal void InitializeChip()
+ {
+ // turn off calibration mode
+ WriteByte(Nric1384.CAL_MOD, (uint)NeuropixelsV1CalibrationRegisterValues.CAL_OFF);
+ WriteByte(Nric1384.SYNC, 0);
+
+ // perform digital and channel reset
+ WriteByte(Nric1384.REC_MOD, (uint)NeuropixelsV1RecordRegisterValues.DIG_CH_RESET);
+
+ // change operation state to Recording
+ WriteByte(Nric1384.OP_MODE, (uint)NeuropixelsV1OperationRegisterValues.RECORD);
+
+ // start acquisition
+ WriteByte(Nric1384.REC_MOD, (uint)NeuropixelsV1RecordRegisterValues.ACTIVE);
+ }
+
+ public void WriteShiftRegisters()
+ {
+ // base
+ for (int i = 0; i < BaseConfigs.Length; i++)
+ {
+ var srAddress = i == 0 ? Nric1384.SR_CHAIN2 : Nric1384.SR_CHAIN3;
+
+ for (int j = 0; j < 2; j++)
+ {
+ var baseBytes = BitHelper.ToBitReversedBytes(BaseConfigs[i]);
+
+ WriteByte(Nric1384.SR_LENGTH1, (uint)baseBytes.Length % 0x100);
+ WriteByte(Nric1384.SR_LENGTH2, (uint)baseBytes.Length / 0x100);
+
+ foreach (var b in baseBytes)
+ {
+ WriteByte(srAddress, b);
+ }
+ }
+
+ if (ReadByte(Nric1384.STATUS) != ShiftRegisterSuccess)
+ {
+ throw new InvalidOperationException($"Shift register {srAddress} status check failed.");
+ }
+ }
+
+ // write adc thresholds and offsets
+ for (uint i = 0; i < Adcs.Length; i++)
+ {
+ var thresh = (uint)Adcs[i].Threshold;
+ var offset = (uint)Adcs[i].Offset;
+ device.WriteRegister(Nric1384.ADC00_OFF_THRESH + i, offset << 10 | thresh);
+ }
+
+ // gain corrections
+ device.WriteRegister(Nric1384.LFP_GAIN, (uint)(LfpGainCorrection * (1 << 14)));
+ device.WriteRegister(Nric1384.AP_GAIN, (uint)(ApGainCorrection * (1 << 14)));
+ }
+ }
+}