Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the fmc card's output clock #295

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions OpenEphys.Onix1/ConfigureBreakoutBoard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@
public ConfigureBreakoutDigitalIO DigitalIO { get; set; } = new();

/// <summary>
/// Gets or sets the breakout board's output clock configuration.
/// </summary>
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
[Description("Specifies the configuration for the clock output in the ONIX breakout board.")]
[Category(DevicesCategory)]
public ConfigureOutputClock ClockOutput { get; set; } = new();

/// Gets or sets the the Harp synchronization input configuration.
/// </summary>

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (debug, ubuntu-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (debug, ubuntu-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (debug, windows-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (debug, windows-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (release, ubuntu-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (release, ubuntu-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (release, windows-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'

Check warning on line 58 in OpenEphys.Onix1/ConfigureBreakoutBoard.cs

View workflow job for this annotation

GitHub Actions / build (release, windows-latest)

XML comment has badly formed XML -- 'End tag was not expected at this location.'
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
[Description("Specifies the configuration for the Harp synchronization input on the ONIX breakout board.")]
[Category(DevicesCategory)]
Expand All @@ -67,6 +74,7 @@
yield return Heartbeat;
yield return AnalogIO;
yield return DigitalIO;
yield return ClockOutput;
yield return HarpInput;
yield return MemoryMonitor;
}
Expand Down
223 changes: 223 additions & 0 deletions OpenEphys.Onix1/ConfigureOutputClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Bonsai;

namespace OpenEphys.Onix1
{
/// <summary>
/// Configures the ONIX breakout board's output clock.
/// </summary>
/// <remarks>
/// The output clock provides a 3.3V logic level, 50 Ohm output impedance, frequency divided copy
/// of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> that is used to generate
/// <see cref="DataFrame.Clock"/> values for all data streams within an ONIX system. This clock runs at a
/// user defined rate, duty cycle, and start delay. It can be used to drive external hardware or can be
/// logged by external recording systems for post-hoc synchronization with ONIX data.
/// </remarks>
[Description("Configures the ONIX breakout board's output clock.")]
public class ConfigureOutputClock : SingleDeviceFactory
{
readonly BehaviorSubject<bool> gate = new(false);
double frequencyHz = 1e6;
double dutyCycle = 50;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigureOutputClock"/> class.
/// </summary>
public ConfigureOutputClock()
: base(typeof(OutputClock))
{
DeviceAddress = 5;
}

/// <summary>
/// Gets or sets a value specifying if the output clock is active.
/// </summary>
/// <remarks>
/// If set to true, the clock output will be connected to the clock output line. If set to false, the
/// clock output line will be held low. This value can be toggled in real time to gate acquisition of
/// external hardware.
/// </remarks>
[Category(AcquisitionCategory)]
[Description("Clock gate control signal.")]
public bool ClockGate
{
get => gate.Value;
set => gate.OnNext(value);
}

/// <summary>
/// Gets or sets the output clock frequency in Hz.
/// </summary>
/// <remarks>
/// Valid values are between 0.1 Hz and 10 MHz. The output clock high and low times must each be an
/// integer multiple of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>
/// frequency. Therefore, the true clock frequency will be set to a value that is as close as possible
/// to the requested setting while respecting this constraint. The value as actualized in hardware is
/// reported by <see cref="OutputClockData"/>.
/// </remarks>
[Range(0.1, 10e6)]
[Category(ConfigurationCategory)]
[Description("Frequency of the output clock (Hz).")]
public double Frequency
{
get => frequencyHz;
set => frequencyHz = value >= 0.1 && value <= 10e6
? value
: throw new ArgumentOutOfRangeException(nameof(Frequency), value,
$"{nameof(Frequency)} must be between 0.1 Hz and 10 MHz.");
}

/// <summary>
/// Gets or sets the output clock duty cycle in percent.
/// </summary>
/// <remarks>
/// Valid values are between 10% and 90%. The output clock high and low times must each be an integer
/// multiple of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> frequency.
/// Therefore, the true duty cycle will be set to a value that is as close as possible to the
/// requested setting while respecting this constraint. The value as actualized in hardware is
/// reported by <see cref="OutputClockData"/>.
/// </remarks>
[Range(10, 90)]
[Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
[Category(ConfigurationCategory)]
[Precision(1, 1)]
[Description("Duty cycle of output clock (%).")]
public double DutyCycle
{
get => dutyCycle;
set => dutyCycle = value >= 10 && value <= 90
? value
: throw new ArgumentOutOfRangeException(nameof(DutyCycle), value,
$"{nameof(DutyCycle)} must be between 10% and 90%.");
}

/// <summary>
/// Gets or sets the delay following acquisition commencement before the clock becomes active in
/// seconds.
/// </summary>
/// <remarks>
/// <para>
/// Valid values are between 0 and and 3600 seconds. Setting to a value greater than 0 can be useful
/// for ensuring data sources that are driven by the output clock start significantly after ONIX has
/// begun acquisition for the purposes of ordering acquisition start times.
/// </para>
/// <para>
/// The delay must be an integer multiple of the <see
/// cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> frequency. Therefore, the true delay
/// cycle will be set to a value that is as close as possible to the requested setting while
/// respecting this constraint. The value as actualized in hardware is reported by <see
/// cref="OutputClockData"/>.
/// </para>
/// </remarks>
[Category(ConfigurationCategory)]
[Description("Specifies a delay following acquisition start before the clock becomes active (sec).")]
[Range(0, 3600)]
public double Delay { get; set; } = 0;

/// <summary>
/// Configures a clock output.
/// </summary>
/// <remarks>
/// This will schedule configuration actions to be applied by a <see cref="StartAcquisition"/>
/// instance prior to data acquisition.
/// </remarks>
/// <param name="source">A sequence of <see cref="ContextTask"/> instances that holds configuration
/// actions.</param>
/// <returns>The original sequence modified by adding additional configuration actions required to
/// configure a clock output device./></returns>
public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
{
var clkFreqHz = Frequency;
var dutyCycle = DutyCycle;
var delaySeconds = Delay;
var deviceName = DeviceName;
var deviceAddress = DeviceAddress;

return source.ConfigureDevice((context, observer) =>
{
var device = context.GetDeviceContext(deviceAddress, DeviceType);

var baseFreqHz = device.ReadRegister(OutputClock.BASE_FREQ_HZ);
var periodTicks = (uint)(baseFreqHz / clkFreqHz);
var h = (uint)(periodTicks * (dutyCycle / 100));
var l = periodTicks - h;
var delayTicks = (uint)(delaySeconds * baseFreqHz);
device.WriteRegister(OutputClock.HIGH_CYCLES, h);
device.WriteRegister(OutputClock.LOW_CYCLES, l);
device.WriteRegister(OutputClock.DELAY_CYCLES, delayTicks);

var deviceInfo = new OutputClockDeviceInfo(device, DeviceType,
new((double)baseFreqHz / periodTicks, 100.0 * h / periodTicks, delaySeconds, h + l, h, l, delayTicks));

var shutdown = Disposable.Create(() =>
{
device.WriteRegister(OutputClock.CLOCK_GATE, 0u);
});

return new CompositeDisposable(
DeviceManager.RegisterDevice(deviceName, deviceInfo),
gate.SubscribeSafe(observer, value => device.WriteRegister(OutputClock.CLOCK_GATE, value ? 1u : 0u)),
shutdown
);
});
}
}

static class OutputClock
{
public const int ID = 20;

public const uint NULL = 0; // No command
public const uint CLOCK_GATE = 1; // Output gate. Bit 0 = 0 is disabled, Bit 0 = 1 is enabled.
public const uint HIGH_CYCLES = 2; // Number of input clock cycles output clock should be high. Valid values are 1 or greater.
public const uint LOW_CYCLES = 3; // Number of input clock cycles output clock should be low. Valid values are 1 or greater.
public const uint DELAY_CYCLES = 4; // Delay, in input clock cycles, following reset before clock becomes active.
public const uint GATE_RUN = 5; // LSB sets the gate using run status. Bit 0 = 0: Clock runs whenever CLOCK_GATE(0) is 1. Bit 0 = 1: Clock runs only when acquisition is in RUNNING state.
public const uint BASE_FREQ_HZ = 6; // Frequency of the input clock in Hz.

internal class NameConverter : DeviceNameConverter
{
public NameConverter()
: base(typeof(OutputClock))
{
}
}
}

/// <summary>
/// Hardware-verified output clock parameters.
/// </summary>
/// <param name="Frequency">Gets the exact clock frequency as actualized by the clock synthesizer in
/// Hz.</param>
/// <param name="DutyCycle">Gets the exact clock duty cycle as actualized by the clock synthesizer
/// in percent.</param>
/// <param name="Delay">Gets the exact clock delay as actualized by the clock synthesizer in
/// seconds.</param>
/// <param name="PeriodTicks">Gets the exact clock period as actualized by the clock synthesizer in units
/// of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>.</param>
/// <param name="HighTicks">Gets the exact clock high time per period as actualized by the clock
/// synthesizer in units of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition
/// Clock</see>.</param>
/// <param name="LowTicks">Gets the exact clock low time per period as actualized by the clock synthesizer
/// in units of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition
/// Clock</see>.</param>
/// <param name="DelayTicks">Gets the exact clock delay as actualized by the clock synthesizer in units of
/// ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>.</param>
public readonly record struct OutputClockParameters(double Frequency,
double DutyCycle, double Delay, uint PeriodTicks, uint HighTicks, uint LowTicks, uint DelayTicks);

class OutputClockDeviceInfo : DeviceInfo
{
public OutputClockDeviceInfo(DeviceContext device, Type deviceType, OutputClockParameters parameters)
: base(device, deviceType)
{
Parameters = parameters;
}

public OutputClockParameters Parameters { get; }
}
}
39 changes: 39 additions & 0 deletions OpenEphys.Onix1/OutputClockData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Bonsai;

namespace OpenEphys.Onix1
{
/// <summary>
/// Produces a sequence with a single element containing the output clock's exact hardware parameters for each subscription.
/// </summary>
/// <remarks>
/// This data IO operator must be linked to an appropriate configuration, such as a <see
/// cref="ConfigureOutputClock"/>, using a shared <c>DeviceName</c>.
/// </remarks>
[Description("Produces a sequence with a single element containing the output clock's hardware parameters for each subscription.")]
public class OutputClockData : Source<OutputClockParameters>
{
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
[TypeConverter(typeof(OutputClock.NameConverter))]
[Description(SingleDeviceFactory.DeviceNameDescription)]
[Category(DeviceFactory.ConfigurationCategory)]
public string DeviceName { get; set; }

/// <summary>
/// Generates a sequence containing a single <see cref="OutputClockParameters"/> structure.
/// </summary>
/// <returns>A sequence containing a single <see cref="OutputClockParameters"/></returns> structure.
public override IObservable<OutputClockParameters> Generate()
{
return DeviceManager.GetDevice(DeviceName).SelectMany(
deviceInfo =>
{
var clockOutDeviceInfo = (OutputClockDeviceInfo)deviceInfo;
return Observable.Defer(() => Observable.Return(clockOutDeviceInfo.Parameters));
});
}
}
}