Source code for csaxs_bec.devices.epics.mcs_card.mcs_card

"""
EPICS SIS38XX Multichannel Scaler (MCS) Interface

This module provides an interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS.
It focuses on the implementation for the SIS3820 model, as input/output modes differ between SIS3801
and SIS3820. It supports both MCS and scaler record operations, enabling configuration and control of
acquisition parameters such as dwell time, channel advance mode, and input/output settings.
The module facilitates data acquisition by managing FIFO buffers and simulating conventional
MCS behavior through memory buffers.

At cSAXS, the SIS3820 model is used, which supports 32 channels.

References:
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
"""

from __future__ import annotations

import enum

from ophyd import Component as Cpt
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind


[docs] class CHANNELADVANCE(int, enum.Enum): """Channel advance pixel mode for MCS card.""" INTERNAL = 0 EXTERNAL = 1
[docs] class ACQUIRING(int, enum.Enum): """Acquisition status for MCS card.""" DONE = 0 ACQUIRING = 1
[docs] class READMODE(int, enum.Enum): """Read mode for MCS channels.""" PASSIVE = 0 EVENT = 1 IO_INTR = 2 FREQ_0_1HZ = 3 FREQ_0_2HZ = 4 FREQ_0_5HZ = 5 FREQ_1HZ = 6 FREQ_2HZ = 7 FREQ_5HZ = 8 FREQ_10HZ = 9 FREQ_100HZ = 10
[docs] class CHANNEL1SOURCE(int, enum.Enum): """Source for first counter pulses.""" INTERNAL_CLOCK = 0 EXTERNAL = 1
[docs] class POLARITY(int, enum.Enum): """Polarity of input_polarity/output_polarity for MCS card.""" NORMAL = 0 INVERTED = 1
[docs] class ACQUIREMODE(int, enum.Enum): """Acquire mode for the card. Allowed modes are Scaler and MCS.""" MCS = 0 SCALER = 1
[docs] class MODELS(int, enum.Enum): SIS3801 = 0 SIS3820 = 1
[docs] class INPUTMODE(int, enum.Enum): """SIS3820 input mode definitions, in total there are 8 modes (0-7). Each mode defines the function of external inputs 1-4. Note: SIS3820 has extended input modes compared to SIS3801. Please check the EPICS documentation for details on the specific input modes supported by SIS3801. """ MODE_0 = 0 MODE_1 = 1 MODE_2 = 2 MODE_3 = 3 MODE_4 = 4 MODE_5 = 5 MODE_6 = 6 MODE_7 = 7
[docs] def describe(self) -> str: """Return a description of the input mode.""" descriptions = { self.MODE_0: "Inputs 1-4: No function (default idle mode)", self.MODE_1: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit next pulse", self.MODE_2: "Inputs 1-4: Next pulse, User bit 1, Inhibit counting, Inhibit next pulse", self.MODE_3: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit counting", self.MODE_4: "Inputs 1-4: Inhibit counting channels 1-8, 9-16, 17-24, 25-32", self.MODE_5: "Inputs 1-4: Next pulse, HISCAL_START, No function, No function", self.MODE_6: "Inputs 1-4: Next pulse, Inhibit counting, Clear counters, User bit 1", self.MODE_7: "Inputs 1-4: Encoder A, Encoder B, Encoder I, Inhibit counting", } return descriptions.get(self, "Unknown input mode")
[docs] class OUTPUTMODE(int, enum.Enum): """SIS3820 output mode definitions, in total there are 4 modes (0-3). Each mode configures output signals 5-8. Note: SIS3820 supports 4 output modes (0-3), SIS3801 supports only Mode 0 with differen functionality. Please check the EPICS documentation for details on the specific output modes supported by SIS3801. """ MODE_0 = 0 MODE_1 = 1 MODE_2 = 2 MODE_3 = 3
[docs] def describe(self) -> str: """Return a description of the output mode.""" descriptions = { self.MODE_0: "Outputs 5-8: LNE/CIP, SDRAM empty, SDRAM threshold, User LED", self.MODE_1: "Outputs 5-8: LNE/CIP, Enabled, 50 MHz, User LED", self.MODE_2: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), 10 MHz (20ns), User LED", self.MODE_3: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), MUX OUT channel, User LED (requires firmware ≥ 0x10A)", } return descriptions.get(self, "Unknown output mode")
def _create_mca_channels(num_channels: int) -> dict[str, tuple]: """ Create a dictionary of MCA channel definitions for the DynamicDeviceComponent. Starts from channel 1 to num_channels. Args: num_channels (int): The number of MCA channels to create. """ mcs_channels = {} for i in range(1, num_channels + 1): mcs_channels[f"mca{i}"] = ( EpicsSignalRO, f"mca{i}.VAL", {"kind": Kind.omitted, "auto_monitor": True, "doc": f"MCA channel {i}."}, ) return mcs_channels
[docs] class MCSCard(Device): """ Ophyd implementation for the interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS. This class provides signals to expose EPICS PVs of the MCS card. More details can be found in the documentation of the EPICS drivers for SIS3801 and SIS3820. References: - EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html """ WRITE_TIMEOUT = 4.0 # seconds snl_connected = Cpt( EpicsSignalRO, "SNL_Connected", kind=Kind.omitted, doc="Indicates whether the SNL program has connected to all PVs.", ) # NOTE: Please note that the erase_all command sends the mca or waveform records to process after erasing, potentially also values of 0. This logic needs to be considered when running callbacks on the mca channels. erase_all = Cpt( EpicsSignal, "EraseAll", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0. Please note that this operation sends the mca or waveform records to process after erasing, potentially also 0s.", ) erase_start = Cpt( EpicsSignal, "EraseStart", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records and starts acquisition.", ) start_all = Cpt( EpicsSignal, "StartAll", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Starts or resumes acquisition without erasing first.", ) acquiring = Cpt( EpicsSignalRO, "Acquiring", kind=Kind.omitted, auto_monitor=True, doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.", ) stop_all = Cpt( EpicsSignal, "StopAll", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Stops acquisition.", ) preset_real = Cpt( EpicsSignal, "PresetReal", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Preset real time. If non-zero then acquisition will stop when this time is reached.", ) elapsed_real = Cpt( EpicsSignalRO, "ElapsedReal", kind=Kind.omitted, doc="Elapsed time since acquisition started.", ) read_all = Cpt( EpicsSignal, "DoReadAll.VAL", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.", ) read_mode = Cpt( EpicsSignal, "ReadAll.SCAN", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.", ) num_use_all = Cpt( EpicsSignal, "NuseAll", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.", ) dwell = Cpt( EpicsSignal, "Dwell", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The dwell time per channel when using internal channel advance mode.", ) channel_advance = Cpt( EpicsSignal, "ChannelAdvance", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).", ) count_on_start = Cpt( EpicsSignal, "CountOnStart", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.", ) software_channel_advance = Cpt( EpicsSignal, "SoftwareChannelAdvance", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.", ) channel1_source = Cpt( EpicsSignal, "Channel1Source", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.", ) prescale = Cpt( EpicsSignal, "Prescale", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.", ) enable_client_wait = Cpt( EpicsSignal, "EnableClientWait", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.", ) client_wait = Cpt( EpicsSignal, "ClientWait", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.", ) acquire_mode = Cpt( EpicsSignal, "AcquireMode", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.", ) # NOTE: Setting mux_output programmatically results in occasional errors on the IOC; it is recommended to avoid using it. mux_output = Cpt( EpicsSignal, "MUXOutput", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3. NOTE: This settings seems to occasionally result in errors on the IOC; it is recommended to avoid using it.", ) user_led = Cpt( EpicsSignal, "UserLED", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="Toggles the user LED and also output signal 8 on the SIS3820.", ) input_mode = Cpt( EpicsSignal, "InputMode", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.", ) input_polarity = Cpt( EpicsSignal, "InputPolarity", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.", ) output_mode = Cpt( EpicsSignal, "OutputMode", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.", ) output_polarity = Cpt( EpicsSignal, "OutputPolarity", kind=Kind.omitted, write_timeout=WRITE_TIMEOUT, doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.", ) model = Cpt( EpicsSignalRO, "Model", kind=Kind.omitted, doc="The scaler model. Values are 'SIS3801' and 'SIS3820'.", ) firmware = Cpt(EpicsSignalRO, "Firmware", kind=Kind.omitted, doc="The firmware version.") max_channels = Cpt( EpicsSignalRO, "MaxChannels", kind=Kind.omitted, doc="The maximum number of channels." ) # Relevant counters current_channel = Cpt( EpicsSignalRO, "CurrentChannel", kind=Kind.omitted, auto_monitor=True, doc="The current channel number, i.e. the number of channel advances that have occurred minus 1.", ) counters = DynamicDeviceComponent( _create_mca_channels(32), kind=Kind.omitted, doc="Sub-device with the mca counters 1-32 for SIS3820.", )