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))); + } + } +}