Skip to content

Commit 070dccd

Browse files
authored
Merge pull request #295 from open-ephys/issue-277
Implement the fmc card's output clock
2 parents 282b23c + 83df7a2 commit 070dccd

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed

OpenEphys.Onix1/ConfigureBreakoutBoard.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public class ConfigureBreakoutBoard : MultiDeviceFactory
4747
public ConfigureBreakoutDigitalIO DigitalIO { get; set; } = new();
4848

4949
/// <summary>
50+
/// Gets or sets the breakout board's output clock configuration.
51+
/// </summary>
52+
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
53+
[Description("Specifies the configuration for the clock output in the ONIX breakout board.")]
54+
[Category(DevicesCategory)]
55+
public ConfigureOutputClock ClockOutput { get; set; } = new();
56+
5057
/// Gets or sets the the Harp synchronization input configuration.
5158
/// </summary>
5259
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
@@ -67,6 +74,7 @@ internal override IEnumerable<IDeviceConfiguration> GetDevices()
6774
yield return Heartbeat;
6875
yield return AnalogIO;
6976
yield return DigitalIO;
77+
yield return ClockOutput;
7078
yield return HarpInput;
7179
yield return MemoryMonitor;
7280
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Drawing.Design;
4+
using System.Reactive.Disposables;
5+
using System.Reactive.Subjects;
6+
using Bonsai;
7+
8+
namespace OpenEphys.Onix1
9+
{
10+
/// <summary>
11+
/// Configures the ONIX breakout board's output clock.
12+
/// </summary>
13+
/// <remarks>
14+
/// The output clock provides a 3.3V logic level, 50 Ohm output impedance, frequency divided copy
15+
/// of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> that is used to generate
16+
/// <see cref="DataFrame.Clock"/> values for all data streams within an ONIX system. This clock runs at a
17+
/// user defined rate, duty cycle, and start delay. It can be used to drive external hardware or can be
18+
/// logged by external recording systems for post-hoc synchronization with ONIX data.
19+
/// </remarks>
20+
[Description("Configures the ONIX breakout board's output clock.")]
21+
public class ConfigureOutputClock : SingleDeviceFactory
22+
{
23+
readonly BehaviorSubject<bool> gate = new(false);
24+
double frequencyHz = 1e6;
25+
double dutyCycle = 50;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="ConfigureOutputClock"/> class.
29+
/// </summary>
30+
public ConfigureOutputClock()
31+
: base(typeof(OutputClock))
32+
{
33+
DeviceAddress = 5;
34+
}
35+
36+
/// <summary>
37+
/// Gets or sets a value specifying if the output clock is active.
38+
/// </summary>
39+
/// <remarks>
40+
/// If set to true, the clock output will be connected to the clock output line. If set to false, the
41+
/// clock output line will be held low. This value can be toggled in real time to gate acquisition of
42+
/// external hardware.
43+
/// </remarks>
44+
[Category(AcquisitionCategory)]
45+
[Description("Clock gate control signal.")]
46+
public bool ClockGate
47+
{
48+
get => gate.Value;
49+
set => gate.OnNext(value);
50+
}
51+
52+
/// <summary>
53+
/// Gets or sets the output clock frequency in Hz.
54+
/// </summary>
55+
/// <remarks>
56+
/// Valid values are between 0.1 Hz and 10 MHz. The output clock high and low times must each be an
57+
/// integer multiple of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>
58+
/// frequency. Therefore, the true clock frequency will be set to a value that is as close as possible
59+
/// to the requested setting while respecting this constraint. The value as actualized in hardware is
60+
/// reported by <see cref="OutputClockData"/>.
61+
/// </remarks>
62+
[Range(0.1, 10e6)]
63+
[Category(ConfigurationCategory)]
64+
[Description("Frequency of the output clock (Hz).")]
65+
public double Frequency
66+
{
67+
get => frequencyHz;
68+
set => frequencyHz = value >= 0.1 && value <= 10e6
69+
? value
70+
: throw new ArgumentOutOfRangeException(nameof(Frequency), value,
71+
$"{nameof(Frequency)} must be between 0.1 Hz and 10 MHz.");
72+
}
73+
74+
/// <summary>
75+
/// Gets or sets the output clock duty cycle in percent.
76+
/// </summary>
77+
/// <remarks>
78+
/// Valid values are between 10% and 90%. The output clock high and low times must each be an integer
79+
/// multiple of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> frequency.
80+
/// Therefore, the true duty cycle will be set to a value that is as close as possible to the
81+
/// requested setting while respecting this constraint. The value as actualized in hardware is
82+
/// reported by <see cref="OutputClockData"/>.
83+
/// </remarks>
84+
[Range(10, 90)]
85+
[Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
86+
[Category(ConfigurationCategory)]
87+
[Precision(1, 1)]
88+
[Description("Duty cycle of output clock (%).")]
89+
public double DutyCycle
90+
{
91+
get => dutyCycle;
92+
set => dutyCycle = value >= 10 && value <= 90
93+
? value
94+
: throw new ArgumentOutOfRangeException(nameof(DutyCycle), value,
95+
$"{nameof(DutyCycle)} must be between 10% and 90%.");
96+
}
97+
98+
/// <summary>
99+
/// Gets or sets the delay following acquisition commencement before the clock becomes active in
100+
/// seconds.
101+
/// </summary>
102+
/// <remarks>
103+
/// <para>
104+
/// Valid values are between 0 and and 3600 seconds. Setting to a value greater than 0 can be useful
105+
/// for ensuring data sources that are driven by the output clock start significantly after ONIX has
106+
/// begun acquisition for the purposes of ordering acquisition start times.
107+
/// </para>
108+
/// <para>
109+
/// The delay must be an integer multiple of the <see
110+
/// cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see> frequency. Therefore, the true delay
111+
/// cycle will be set to a value that is as close as possible to the requested setting while
112+
/// respecting this constraint. The value as actualized in hardware is reported by <see
113+
/// cref="OutputClockData"/>.
114+
/// </para>
115+
/// </remarks>
116+
[Category(ConfigurationCategory)]
117+
[Description("Specifies a delay following acquisition start before the clock becomes active (sec).")]
118+
[Range(0, 3600)]
119+
public double Delay { get; set; } = 0;
120+
121+
/// <summary>
122+
/// Configures a clock output.
123+
/// </summary>
124+
/// <remarks>
125+
/// This will schedule configuration actions to be applied by a <see cref="StartAcquisition"/>
126+
/// instance prior to data acquisition.
127+
/// </remarks>
128+
/// <param name="source">A sequence of <see cref="ContextTask"/> instances that holds configuration
129+
/// actions.</param>
130+
/// <returns>The original sequence modified by adding additional configuration actions required to
131+
/// configure a clock output device./></returns>
132+
public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
133+
{
134+
var clkFreqHz = Frequency;
135+
var dutyCycle = DutyCycle;
136+
var delaySeconds = Delay;
137+
var deviceName = DeviceName;
138+
var deviceAddress = DeviceAddress;
139+
140+
return source.ConfigureDevice((context, observer) =>
141+
{
142+
var device = context.GetDeviceContext(deviceAddress, DeviceType);
143+
144+
var baseFreqHz = device.ReadRegister(OutputClock.BASE_FREQ_HZ);
145+
var periodTicks = (uint)(baseFreqHz / clkFreqHz);
146+
var h = (uint)(periodTicks * (dutyCycle / 100));
147+
var l = periodTicks - h;
148+
var delayTicks = (uint)(delaySeconds * baseFreqHz);
149+
device.WriteRegister(OutputClock.HIGH_CYCLES, h);
150+
device.WriteRegister(OutputClock.LOW_CYCLES, l);
151+
device.WriteRegister(OutputClock.DELAY_CYCLES, delayTicks);
152+
153+
var deviceInfo = new OutputClockDeviceInfo(device, DeviceType,
154+
new((double)baseFreqHz / periodTicks, 100.0 * h / periodTicks, delaySeconds, h + l, h, l, delayTicks));
155+
156+
var shutdown = Disposable.Create(() =>
157+
{
158+
device.WriteRegister(OutputClock.CLOCK_GATE, 0u);
159+
});
160+
161+
return new CompositeDisposable(
162+
DeviceManager.RegisterDevice(deviceName, deviceInfo),
163+
gate.SubscribeSafe(observer, value => device.WriteRegister(OutputClock.CLOCK_GATE, value ? 1u : 0u)),
164+
shutdown
165+
);
166+
});
167+
}
168+
}
169+
170+
static class OutputClock
171+
{
172+
public const int ID = 20;
173+
174+
public const uint NULL = 0; // No command
175+
public const uint CLOCK_GATE = 1; // Output gate. Bit 0 = 0 is disabled, Bit 0 = 1 is enabled.
176+
public const uint HIGH_CYCLES = 2; // Number of input clock cycles output clock should be high. Valid values are 1 or greater.
177+
public const uint LOW_CYCLES = 3; // Number of input clock cycles output clock should be low. Valid values are 1 or greater.
178+
public const uint DELAY_CYCLES = 4; // Delay, in input clock cycles, following reset before clock becomes active.
179+
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.
180+
public const uint BASE_FREQ_HZ = 6; // Frequency of the input clock in Hz.
181+
182+
internal class NameConverter : DeviceNameConverter
183+
{
184+
public NameConverter()
185+
: base(typeof(OutputClock))
186+
{
187+
}
188+
}
189+
}
190+
191+
/// <summary>
192+
/// Hardware-verified output clock parameters.
193+
/// </summary>
194+
/// <param name="Frequency">Gets the exact clock frequency as actualized by the clock synthesizer in
195+
/// Hz.</param>
196+
/// <param name="DutyCycle">Gets the exact clock duty cycle as actualized by the clock synthesizer
197+
/// in percent.</param>
198+
/// <param name="Delay">Gets the exact clock delay as actualized by the clock synthesizer in
199+
/// seconds.</param>
200+
/// <param name="PeriodTicks">Gets the exact clock period as actualized by the clock synthesizer in units
201+
/// of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>.</param>
202+
/// <param name="HighTicks">Gets the exact clock high time per period as actualized by the clock
203+
/// synthesizer in units of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition
204+
/// Clock</see>.</param>
205+
/// <param name="LowTicks">Gets the exact clock low time per period as actualized by the clock synthesizer
206+
/// in units of ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition
207+
/// Clock</see>.</param>
208+
/// <param name="DelayTicks">Gets the exact clock delay as actualized by the clock synthesizer in units of
209+
/// ticks of the <see cref="ContextTask.AcquisitionClockHz">Acquisition Clock</see>.</param>
210+
public readonly record struct OutputClockParameters(double Frequency,
211+
double DutyCycle, double Delay, uint PeriodTicks, uint HighTicks, uint LowTicks, uint DelayTicks);
212+
213+
class OutputClockDeviceInfo : DeviceInfo
214+
{
215+
public OutputClockDeviceInfo(DeviceContext device, Type deviceType, OutputClockParameters parameters)
216+
: base(device, deviceType)
217+
{
218+
Parameters = parameters;
219+
}
220+
221+
public OutputClockParameters Parameters { get; }
222+
}
223+
}

OpenEphys.Onix1/OutputClockData.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Linq;
4+
using System.Reactive.Linq;
5+
using Bonsai;
6+
7+
namespace OpenEphys.Onix1
8+
{
9+
/// <summary>
10+
/// Produces a sequence with a single element containing the output clock's exact hardware parameters for each subscription.
11+
/// </summary>
12+
/// <remarks>
13+
/// This data IO operator must be linked to an appropriate configuration, such as a <see
14+
/// cref="ConfigureOutputClock"/>, using a shared <c>DeviceName</c>.
15+
/// </remarks>
16+
[Description("Produces a sequence with a single element containing the output clock's hardware parameters for each subscription.")]
17+
public class OutputClockData : Source<OutputClockParameters>
18+
{
19+
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
20+
[TypeConverter(typeof(OutputClock.NameConverter))]
21+
[Description(SingleDeviceFactory.DeviceNameDescription)]
22+
[Category(DeviceFactory.ConfigurationCategory)]
23+
public string DeviceName { get; set; }
24+
25+
/// <summary>
26+
/// Generates a sequence containing a single <see cref="OutputClockParameters"/> structure.
27+
/// </summary>
28+
/// <returns>A sequence containing a single <see cref="OutputClockParameters"/></returns> structure.
29+
public override IObservable<OutputClockParameters> Generate()
30+
{
31+
return DeviceManager.GetDevice(DeviceName).SelectMany(
32+
deviceInfo =>
33+
{
34+
var clockOutDeviceInfo = (OutputClockDeviceInfo)deviceInfo;
35+
return Observable.Defer(() => Observable.Return(clockOutDeviceInfo.Parameters));
36+
});
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)