diff --git a/aidatlu/constellation/README.md b/aidatlu/constellation/README.md index d634c24..c0ec89d 100644 --- a/aidatlu/constellation/README.md +++ b/aidatlu/constellation/README.md @@ -1,8 +1,9 @@ --- title: "AidaTLU" -description: "Satellite for the AIDA-2020 TLU using a Python based control software" +description: "Satellite for the AIDA-2020 TLU using a Python-based control software" +category: "Readout Systems" language: "Python" -category: "External" +parent_class: "TransmitterSatellite" --- ## Description @@ -11,13 +12,13 @@ The AIDA-2020 Trigger Logic Unit is designed to provide flexible trigger configu The Python-based control software for the AIDA-2020 TLU provides a comprehensive interface for controlling the TLU. The software establishes a connection to the hardware and allows for easy configuration of different trigger setups. -Information over each individual trigger signal is saved in a compressed and human-readable HDF5 format. +Information about each individual trigger signal is saved in a compressed (blosc) and human-readable HDF5 format. The satellite connects the AIDA-2020 TLU to the [Constellation](https://constellation.pages.desy.de/) control and data acquisition framework. ## Building -After installing [IPbus](https://ipbus.web.cern.ch/doc/user/html/software/install/compile.html), with Python bindings (`uhal`), install the [Aida-TLU](https://github.com/SiLab-Bonn/aidatlu) package with the constellation requirement. +After installing [IPbus](https://ipbus.web.cern.ch/doc/user/html/software/install/compile.html) with Python bindings (`uhal`), install the [Aida-TLU](https://github.com/SiLab-Bonn/aidatlu) package with the constellation requirement. ```bash pip install .[constellation] @@ -27,7 +28,7 @@ A more detailed description of the prerequisites can also be found [here](https: ## Usage -Add the chosen cactus library path, where the default installation location is `/opt/cactus/`: +Add the chosen cactus library path, where the default install location is `/opt/cactus/`: ```sh export LD_LIBRARY_PATH=/lib @@ -54,16 +55,16 @@ This means DUT interface signals (e.g. clock signals) are disrupted during these | Configuration | Description | Type | Default Value | |-----------|-------------|------| ------| -| `internal_trigger_rate` | (Optional) Generates internal triggers with a given frequency given in Hz | Integer | 0 | -| `dut_interfaces` | (Required) Specify the operation mode of the DUT interface (`aida`, `eudet`, `aidatrig`, `off`) given as list with a required length of 4. Where `aida` and `eudet` corresponds to the classic AIDA and EUDET mode respectively and `aidatrig` to the AIDA-mode with handshake. Disable a DUT interface with `off`. | List | None | -| `trigger_threshold` | (Required) Threshold setting of each individual trigger input channel given in V | List | None | -| `trigger_inputs_logic` | (Required) Trigger Logic configuration accept a Python expression for the trigger inputs. The logic is set by using the variables for the input channels `CH1`, `CH2`, `CH3`, `CH4`, `CH5` and `CH6` and the Python logic operators `and`, `or`, `not` and so on. Don't forget to use brackets... | String | None | +| `internal_trigger_rate` | (Optional) Generates internal triggers with a given frequency given in Hz. | Integer | 0 | +| `dut_interfaces` | (Required) Specify the operation mode of the DUT interface (`aida`, `eudet`, `aidatrig`, `off`), given as list with a required length of 4. `aida` and `eudet` correspond to the classic AIDA and EUDET mode respectively and `aidatrig` to the AIDA-mode with handshake. Disable a DUT interface with `off`. | List | None | +| `trigger_threshold` | (Required) Threshold setting of each individual trigger input channel given in V. | List | None | +| `trigger_inputs_logic` | (Required) Trigger Logic configuration accepts a Python expression for the trigger inputs. The logic is set by using the variables for the input channels `CH1`, `CH2`, `CH3`, `CH4`, `CH5` and `CH6` and the Python logic operators `and`, `or`, `not` and so on. Don't forget to use brackets... | String | None | | `trigger_polarity` | (Optional) TLU can trigger on a rising or falling edge. Set to `rising` or `falling` | String | `falling` | | `trigger_signal_stretch` | (Required) Stretches each individual trigger input by a given number of clock cycles (corresponds to `6.25ns` steps) | List | None | | `trigger_signal_delay` | (Required) Delays each individual trigger input by a given number of clock cycles (corresponds to `6.25ns` steps) | List | None | | `enable_clock_lemo_output` | (Optional) Enable the LEMO clock output. | String | False | | `pmt_power` | (Required) Sets the four PMT control voltages in V | List | None | -| `clock_config` | (Optional) Specify a custom clock configuration. If no path is provided the TLU uses the default configuration. | String | None | +| `clock_config` | (Optional) Specify a custom clock configuration. If no path is provided, the TLU uses the default configuration. | String | None | The default clock configuration can be found in [`aidatlu/misc/aida_tlu_clk_config.txt`](https://github.com/SiLab-Bonn/aidatlu/blob/main/aidatlu/misc/aida_tlu_clk_config.txt). diff --git a/aidatlu/constellation/__main__.py b/aidatlu/constellation/__main__.py index 2f9766d..0f13310 100644 --- a/aidatlu/constellation/__main__.py +++ b/aidatlu/constellation/__main__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from constellation.core.logging import setup_cli_logging -from constellation.core.datasender import DataSenderArgumentParser +from constellation.core.transmitter_satellite import TransmitterSatelliteArgumentParser from .aidatlu_satellite import AidaTLU @@ -8,7 +8,7 @@ def main(args=None): """Controlling of a Satellite for the AIDA-2020 TLU""" - parser = DataSenderArgumentParser(description=main.__doc__) + parser = TransmitterSatelliteArgumentParser(description=main.__doc__) args = vars(parser.parse_args(args)) # set up logging diff --git a/aidatlu/constellation/aidatlu_satellite.py b/aidatlu/constellation/aidatlu_satellite.py index 5adf1bb..0363f24 100644 --- a/aidatlu/constellation/aidatlu_satellite.py +++ b/aidatlu/constellation/aidatlu_satellite.py @@ -12,16 +12,16 @@ from constellation.core.configuration import Configuration from constellation.core.message.cscp1 import SatelliteState from constellation.core.monitoring import schedule_metric -from constellation.core.datasender import DataSender +from constellation.core.transmitter_satellite import TransmitterSatellite from aidatlu import logger from aidatlu.hardware.i2c import I2CCore -from aidatlu.main.config_parser import toml_parser -from aidatlu.main.tlu import AidaTLU as TLU +from aidatlu.main.config_parser import Configure, toml_parser +from aidatlu.hardware.tlu_controller import TLUControl as TLU from aidatlu.test.utils import MockI2C -class AidaTLU(DataSender): +class AidaTLU(TransmitterSatellite): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -53,7 +53,11 @@ def do_initializing(self, config: Configuration) -> str: return "Initializing complete" def do_launching(self, payload: Any = None) -> str: - self.aidatlu.configure() + self.config_parser.configure() + self.conf_list = self.config_parser.get_configuration_table() + self.aidatlu.get_event_fifo_fill_level() + self.aidatlu.get_event_fifo_csr() + self.aidatlu.get_scalers() return "Do launching complete" def do_landing(self) -> str: @@ -82,19 +86,26 @@ def _pull_fifo_event(self): func_type = type(self.aidatlu.pull_fifo_event) self.aidatlu.pull_fifo_event = func_type(_pull_fifo_event, self.aidatlu) - self.aidatlu.start_run_configuration() + self.aidatlu.start_run() + self.aidatlu.get_fw_version() + self.aidatlu.get_device_id() + # reset starting parameter + self.start_time = self.aidatlu.get_timestamp() + self.last_time = 0 + self.last_post_veto_trigger = self.aidatlu.trigger_logic.get_post_veto_trigger() + self.last_pre_veto_trigger = self.aidatlu.trigger_logic.get_pre_veto_trigger() # Set Begin-of-run tags - self.BOR["BoardID"] = self.aidatlu.get_device_id() + self.bor["BoardID"] = self.aidatlu.get_device_id() # For EudaqNativeWriter compatibility - self.BOR["eudaq_event"] = "TluRawDataEvent" - self.BOR["frames_as_blocks"] = True + self.bor["eudaq_event"] = "TluRawDataEvent" + self.bor["write_as_blocks"] = True return "Do starting complete" def do_run(self, run_identifier: str) -> str: - t = threading.Thread(target=self.aidatlu.handle_status) + t = threading.Thread(target=self.handle_status) t.start() # We ideally pull 6 uint32s, but we might pull more or less @@ -112,7 +123,8 @@ def do_run(self, run_identifier: str) -> str: return "Do running complete" def do_stopping(self) -> str: - self.aidatlu.stop_run_configuration() + self.aidatlu.pull_fifo_event() + self.log.info("Run finished") return "Do stopping complete" def _init_tlu(self, config: Configuration) -> None: @@ -123,10 +135,11 @@ def _init_tlu(self, config: Configuration) -> None: self.clock_file = str(self.file_path) + "/../misc/aida_tlu_clk_config.txt" else: self.clock_file = self.config_file["clock_config"] - self.aidatlu = TLU( - self.hw, self.config_file, self.clock_file, i2c=self.i2c_method - ) + self.aidatlu = TLU(self.hw, i2c=self.i2c_method) + self.aidatlu.write_clock_config(self.clock_file) + self.config_parser = Configure(self.aidatlu, self.config_file) + self.aidatlu.reset_configuration() # Resets aidatlu loggers and replaces them with constellation loggers logger._reset_all_loggers() self.aidatlu.log = self.log @@ -135,7 +148,7 @@ def _init_tlu(self, config: Configuration) -> None: self.aidatlu.clock_controller.log = self.log self.aidatlu.trigger_logic.log = self.log self.aidatlu.dut_logic.log = self.log - self.aidatlu.config_parser.log = self.log + self.config_parser.log = self.log def _handle_event(self, evt: list) -> None: # Calculate timestamp in nanoseconds from TLU 40MHz clock: @@ -149,41 +162,85 @@ def _handle_event(self, evt: list) -> None: } # New data format: store 6 uint32 as bytes in little-endian payload = np.array(evt, dtype=" None: + """Status message handling in separate thread. Calculates run time and obtain trigger information and sent it out every second.""" + t = threading.current_thread() + while getattr(t, "do_run", True): + time.sleep(0.5) + last_time = self.aidatlu.get_timestamp() + current_time = (last_time - self.start_time) * 25 / 1000000000 + # Logs and poss. sends status every 1s. + if current_time - self.last_time > 1: + self.log_status(current_time) + + def log_status(self, time: int) -> None: + """Logs the status of the TLU run with trigger number, runtime usw. + Also calculates the mean trigger frequency between function calls. + + Args: + time (int): current runtime of the TLU + """ + self.post_veto_rate = ( + self.aidatlu.get_post_veto_trigger_number() - self.last_post_veto_trigger + ) / (time - self.last_time) + self.pre_veto_rate = ( + self.aidatlu.get_pre_veto_trigger_number() - self.last_pre_veto_trigger + ) / (time - self.last_time) + self.run_time = time + self.total_post_veto = self.aidatlu.get_post_veto_trigger_number() + self.total_pre_veto = self.aidatlu.get_pre_veto_trigger_number() + s0, s1, s2, s3, s4, s5 = self.aidatlu.get_scalers() + + self.last_time = time + self.last_post_veto_trigger = self.aidatlu.get_post_veto_trigger_number() + self.last_pre_veto_trigger = self.aidatlu.get_pre_veto_trigger_number() + + self.log.info( + "Run time: %.1f s, Pre veto: %s, Post veto: %s, Pre veto rate: %.f Hz, Post veto rate.: %.f Hz" + % ( + self.run_time, + self.total_pre_veto, + self.total_post_veto, + self.pre_veto_rate, + self.post_veto_rate, + ) + ) + if self.aidatlu.get_event_fifo_csr() == 0x10: + self.log.warning("FIFO is full") @schedule_metric("Hz", MetricsType.LAST_VALUE, 1) def pre_veto_rate(self) -> Any: if self.fsm.current_state_value == SatelliteState.RUN and hasattr( - self.aidatlu, "pre_veto_rate" + self, "pre_veto_rate" ): - return self.aidatlu.pre_veto_rate + return self.pre_veto_rate else: return None @schedule_metric("Hz", MetricsType.LAST_VALUE, 1) def post_veto_rate(self) -> Any: if self.fsm.current_state_value == SatelliteState.RUN and hasattr( - self.aidatlu, "post_veto_rate" + self, "post_veto_rate" ): - return self.aidatlu.post_veto_rate + return self.post_veto_rate else: return None @schedule_metric("", MetricsType.LAST_VALUE, 1) def post_veto(self) -> Any: - if self.fsm.current_state_value == SatelliteState.RUN and hasattr( - self.aidatlu, "total_post_veto" - ): - return self.aidatlu.total_post_veto + if self.fsm.current_state_value == SatelliteState.RUN: + return self.aidatlu.get_post_veto_trigger_number() else: return None @schedule_metric("", MetricsType.LAST_VALUE, 1) def pre_veto(self) -> Any: - if self.fsm.current_state_value == SatelliteState.RUN and hasattr( - self.aidatlu, "total_pre_veto" - ): - return self.aidatlu.total_pre_veto + if self.fsm.current_state_value == SatelliteState.RUN: + return self.aidatlu.get_pre_veto_trigger_number() else: return None diff --git a/aidatlu/constellation/example_tlu_config.toml b/aidatlu/constellation/example_tlu_config.toml index ecbf417..d8ec986 100644 --- a/aidatlu/constellation/example_tlu_config.toml +++ b/aidatlu/constellation/example_tlu_config.toml @@ -12,3 +12,4 @@ trigger_signal_delay = [0, 0, 0, 0, 0, 0] enable_clock_lemo_output = false pmt_power = [0.8, 0.8, 0.0, 0.0] +clock_config = false diff --git a/aidatlu/hardware/tlu_controller.py b/aidatlu/hardware/tlu_controller.py new file mode 100644 index 0000000..4ed52a3 --- /dev/null +++ b/aidatlu/hardware/tlu_controller.py @@ -0,0 +1,214 @@ +import numpy as np + +from aidatlu import logger +from aidatlu.hardware.clock_controller import ClockControl +from aidatlu.hardware.dac_controller import DacControl +from aidatlu.hardware.dut_controller import DUTLogic +from aidatlu.hardware.i2c import I2CCore +from aidatlu.hardware.ioexpander_controller import IOControl +from aidatlu.hardware.trigger_controller import TriggerLogic + + +class TLUControl: + """Controls general TLU functionalities.""" + + def __init__(self, hw, i2c=I2CCore) -> None: + self.log = logger.setup_main_logger(__class__.__name__) + self.i2c = i2c(hw) + self.i2c_hw = hw + self.log.info("Initializing IPbus interface") + self.i2c.init() + + if self.i2c.modules["eeprom"]: + self.log.info("Found device with ID %s" % hex(self.get_device_id())) + + # TODO some configuration also sends out ~70 triggers. + self.io_controller = IOControl(self.i2c) + self.dac_controller = DacControl(self.i2c) + self.clock_controller = ClockControl(self.i2c, self.io_controller) + self.trigger_logic = TriggerLogic(self.i2c) + self.dut_logic = DUTLogic(self.i2c) + + ### General TLU Functions ### + + def reset_configuration(self) -> None: + # Disable all outputs + self.io_controller.clock_lemo_output(False) + for i in range(4): + self.io_controller.configure_hdmi(i + 1, 1) + self.dac_controller.set_voltage(5, 0) + self.io_controller.all_off() + # sets all thresholds to 1.2 V + for i in range(6): + self.dac_controller.set_threshold(i + 1, 0) + # Resets all internal counters and raise the trigger veto. + self.set_run_active(False) + self.reset_status() + self.reset_counters() + self.trigger_logic.set_trigger_veto(True) + self.reset_fifo() + self.reset_timestamp() + + def start_run(self) -> None: + """Start run configurations""" + self.reset_counters() + self.reset_fifo() + self.reset_timestamp() + self.set_run_active(True) + self.trigger_logic.set_trigger_veto(False) + + def stop_run(self) -> None: + """Stop run configurations""" + self.trigger_logic.set_trigger_veto(True) + self.set_run_active(False) + + ### Basic TLU Control Functions ### + + def write_clock_config(self, clock_config_path): + self.clock_controller.write_clock_conf(clock_config_path) + + def get_device_id(self) -> int: + """Read back board id. Consists of six blocks of hex data + + Returns: + int: Board id as 48 bits integer + """ + id = [] + for addr in range(6): + id.append(self.i2c.read(self.i2c.modules["eeprom"], 0xFA + addr) & 0xFF) + return int("0x" + "".join(["{:x}".format(i) for i in id]), 16) & 0xFFFFFFFFFFFF + + def get_fw_version(self) -> int: + return self.i2c.read_register("version") + + def reset_timestamp(self) -> None: + """Sets bit to 'ResetTimestampW' register to reset the time stamp.""" + self.i2c.write_register("Event_Formatter.ResetTimestampW", 1) + + def reset_counters(self) -> None: + """Resets the trigger counters.""" + self.write_status(0x2) + self.write_status(0x0) + + def reset_status(self) -> None: + """Resets the complete status and all counters.""" + self.write_status(0x3) + self.write_status(0x0) + self.write_status(0x4) + self.write_status(0x0) + + def reset_fifo(self) -> None: + """Sets 0 to 'EventFifoCSR' this resets the FIFO.""" + self.set_event_fifo_csr(0x0) + + def set_event_fifo_csr(self, value: int) -> None: + """Sets value to the EventFifoCSR register. + + Args: + value (int): 0 resets the FIFO. #TODO can do other stuff that is not implemented + + """ + self.i2c.write_register("eventBuffer.EventFifoCSR", value) + + def write_status(self, value: int) -> None: + """Sets value to the 'SerdesRstW' register. + + Args: + value (int): Bit 0 resets the status, bit 1 resets trigger counters and bit 2 calibrates IDELAY. + """ + self.i2c.write_register("triggerInputs.SerdesRstW", value) + + def set_run_active(self, state: bool) -> None: + """Raises internal run active signal. + + Args: + state (bool): True sets run active, False disables it. + """ + if type(state) != bool: + raise TypeError("State has to be bool") + self.i2c.write_register("Shutter.RunActiveRW", int(state)) + self.log.info("Run active: %s" % self.get_run_active()) + + def get_run_active(self) -> bool: + """Reads register 'RunActiveRW' + + Returns: + bool: Returns bool of the run active register. + """ + return bool(self.i2c.read_register("Shutter.RunActiveRW")) + + def set_enable_record_data(self, value: int) -> None: + """#TODO not sure what this does. Looks like a separate internal event buffer to the FIFO. + + Args: + value (int): #TODO I think this does not work + """ + self.i2c.write_register("Event_Formatter.Enable_Record_Data", value) + + def get_event_fifo_csr(self) -> int: + """Reads value from 'EventFifoCSR', corresponds to status flags of the FIFO. + + Returns: + int: number of events + """ + return self.i2c.read_register("eventBuffer.EventFifoCSR") + + def get_event_fifo_fill_level(self) -> int: + """Reads value from 'EventFifoFillLevel' + Returns the number of words written in + the FIFO. The lowest 14-bits are the actual data. + + Returns: + int: buffer level of the fifi + """ + return self.i2c.read_register("eventBuffer.EventFifoFillLevel") + + def get_timestamp(self) -> int: + """Get current time stamp. + + Returns: + int: Time stamp is not formatted. + """ + time = self.i2c.read_register("Event_Formatter.CurrentTimestampHR") + time = time << 32 + time = time + self.i2c.read_register("Event_Formatter.CurrentTimestampLR") + return time + + def pull_fifo_event(self) -> list: + """Pulls event from the FIFO. This is needed in the run loop to prevent the buffer to get stuck. + if this register is full the fifo needs to be reset or new triggers are generated but not sent out. + #TODO check here if the FIFO is full and reset it if needed would prob. make sense. + + Returns: + list: 6 element long vector containing bitwords of the data. + """ + event_numb = self.get_event_fifo_fill_level() + if event_numb: + fifo_content = self.i2c_hw.getNode("eventBuffer.EventFifoData").readBlock( + event_numb + ) + self.i2c_hw.dispatch() + return np.array(fifo_content) + pass + + def get_scaler(self, channel: int) -> int: + """reads current scaler value from register""" + if channel < 0 or channel > 5: + raise ValueError("Only channels 0 to 5 are valid") + return self.i2c.read_register(f"triggerInputs.ThrCount{channel:d}R") + + def get_scalers(self) -> list: + """reads current sc values from registers + + Returns: + list: all 6 trigger sc values + """ + return [self.get_scaler(n) for n in range(6)] + + def get_pre_veto_trigger_number(self) -> int: + """Obtains the number of triggers recorded in the TLU before the veto is applied from the trigger logic register""" + return self.trigger_logic.get_pre_veto_trigger() + + def get_post_veto_trigger_number(self) -> int: + """Obtains the number of triggers recorded in the TLU after the veto is applied from the trigger logic register""" + return self.trigger_logic.get_post_veto_trigger() diff --git a/aidatlu/main/config_parser.py b/aidatlu/main/config_parser.py index bd3dd66..8b7ca19 100644 --- a/aidatlu/main/config_parser.py +++ b/aidatlu/main/config_parser.py @@ -20,11 +20,11 @@ def configure(self) -> None: self.tlu.set_enable_record_data(1) self.log.info("TLU configured") - def get_data_handling(self) -> tuple: + def get_data_handling(self) -> bool: """Information about data handling. Returns: - tuple: two bools, save and interpret data. + bool: save and interpret data. """ return self.conf["save_data"] @@ -76,18 +76,18 @@ def conf_dut(self) -> None: dut = [0, 0, 0, 0] dut_mode = [0, 0, 0, 0] for i in range(4): - if self.tlu.config_parser.conf["DUT_%s" % (i + 1)] == "eudet": + if self.conf["DUT_%s" % (i + 1)] == "eudet": self.tlu.io_controller.switch_led(i + 1, "g") dut[i] = 2**i # Clock output needs to be disabled for EUDET mode. self.tlu.io_controller.clock_hdmi_output(i + 1, "off") - if self.tlu.config_parser.conf["DUT_%s" % (i + 1)] == "aidatrig": + if self.conf["DUT_%s" % (i + 1)] == "aidatrig": self.tlu.io_controller.switch_led(i + 1, "w") dut[i] = 2**i dut_mode[i] = 2 ** (2 * i) # In AIDA mode the clock output is needed. self.tlu.io_controller.clock_hdmi_output(i + 1, "chip") - if self.tlu.config_parser.conf["DUT_%s" % (i + 1)] == "aida": + if self.conf["DUT_%s" % (i + 1)] == "aida": self.tlu.io_controller.switch_led(i + 1, "b") dut[i] = 2**i dut_mode[i] = 3 * (2) ** (2 * i) @@ -99,7 +99,7 @@ def conf_dut(self) -> None: "DUT %i configured in %s" % ( (i + 1), - self.tlu.config_parser.conf["DUT_%s" % (i + 1)], + self.conf["DUT_%s" % (i + 1)], ) ) for i in range(4) diff --git a/aidatlu/main/tlu.py b/aidatlu/main/tlu.py index ddd3d78..e2607ab 100644 --- a/aidatlu/main/tlu.py +++ b/aidatlu/main/tlu.py @@ -8,12 +8,8 @@ import zmq from aidatlu import logger -from aidatlu.hardware.clock_controller import ClockControl -from aidatlu.hardware.dac_controller import DacControl -from aidatlu.hardware.dut_controller import DUTLogic +from aidatlu.hardware.tlu_controller import TLUControl from aidatlu.hardware.i2c import I2CCore -from aidatlu.hardware.ioexpander_controller import IOControl -from aidatlu.hardware.trigger_controller import TriggerLogic from aidatlu.main.config_parser import Configure, yaml_parser from aidatlu.main.data_parser import interpret_data @@ -22,24 +18,11 @@ class AidaTLU: def __init__(self, hw, config_dict, clock_config_path, i2c=I2CCore) -> None: self.log = logger.setup_main_logger(__class__.__name__) - self.i2c = i2c(hw) - self.i2c_hw = hw - self.log.info("Initializing IPbus interface") - self.i2c.init() - - if self.i2c.modules["eeprom"]: - self.log.info("Found device with ID %s" % hex(self.get_device_id())) - - # TODO some configuration also sends out ~70 triggers. - self.io_controller = IOControl(self.i2c) - self.dac_controller = DacControl(self.i2c) - self.clock_controller = ClockControl(self.i2c, self.io_controller) - self.clock_controller.write_clock_conf(clock_config_path) - self.trigger_logic = TriggerLogic(self.i2c) - self.dut_logic = DUTLogic(self.i2c) + self.tlu_controller = TLUControl(hw=hw, i2c=i2c) + self.tlu_controller.write_clock_config(clock_config_path) self.reset_configuration() - self.config_parser = Configure(self, config_dict) + self.config_parser = Configure(self.tlu_controller, config_dict) self.log.success("TLU initialized") @@ -47,186 +30,22 @@ def configure(self) -> None: """loads the conf.yaml and configures the TLU accordingly.""" self.config_parser.configure() self.conf_list = self.config_parser.get_configuration_table() - self.get_event_fifo_fill_level() - self.get_event_fifo_csr() - self.get_scalers() + self.tlu_controller.get_event_fifo_fill_level() + self.tlu_controller.get_event_fifo_csr() + self.tlu_controller.get_scalers() def reset_configuration(self) -> None: - """Switch off all outputs, reset all counters and set threshold to 1.2V""" - # Disable all outputs - self.io_controller.clock_lemo_output(False) - for i in range(4): - self.io_controller.configure_hdmi(i + 1, 1) - self.dac_controller.set_voltage(5, 0) - self.io_controller.all_off() - # sets all thresholds to 1.2 V - for i in range(6): - self.dac_controller.set_threshold(i + 1, 0) - # Resets all internal counters and raise the trigger veto. - self.set_run_active(False) - self.reset_status() - self.reset_counters() - self.trigger_logic.set_trigger_veto(True) - self.reset_fifo() - self.reset_timestamp() + self.tlu_controller.reset_configuration() self.run_number = 0 try: self.h5_file.close() except: pass - def get_device_id(self) -> int: - """Read back board id. Consists of six blocks of hex data - - Returns: - int: Board id as 48 bits integer - """ - id = [] - for addr in range(6): - id.append(self.i2c.read(self.i2c.modules["eeprom"], 0xFA + addr) & 0xFF) - return int("0x" + "".join(["{:x}".format(i) for i in id]), 16) & 0xFFFFFFFFFFFF - - def get_fw_version(self) -> int: - return self.i2c.read_register("version") - - def reset_timestamp(self) -> None: - """Sets bit to 'ResetTimestampW' register to reset the time stamp.""" - self.i2c.write_register("Event_Formatter.ResetTimestampW", 1) - - def reset_counters(self) -> None: - """Resets the trigger counters.""" - self.write_status(0x2) - self.write_status(0x0) - - def reset_status(self) -> None: - """Resets the complete status and all counters.""" - self.write_status(0x3) - self.write_status(0x0) - self.write_status(0x4) - self.write_status(0x0) - - def reset_fifo(self) -> None: - """Sets 0 to 'EventFifoCSR' this resets the FIFO.""" - self.set_event_fifo_csr(0x0) - - def set_event_fifo_csr(self, value: int) -> None: - """Sets value to the EventFifoCSR register. - - Args: - value (int): 0 resets the FIFO. #TODO can do other stuff that is not implemented - - """ - self.i2c.write_register("eventBuffer.EventFifoCSR", value) - - def write_status(self, value: int) -> None: - """Sets value to the 'SerdesRstW' register. - - Args: - value (int): Bit 0 resets the status, bit 1 resets trigger counters and bit 2 calibrates IDELAY. - """ - self.i2c.write_register("triggerInputs.SerdesRstW", value) - - def set_run_active(self, state: bool) -> None: - """Raises internal run active signal. - - Args: - state (bool): True sets run active, False disables it. - """ - if type(state) != bool: - raise TypeError("State has to be bool") - self.i2c.write_register("Shutter.RunActiveRW", int(state)) - self.log.info("Run active: %s" % self.get_run_active()) - - def get_run_active(self) -> bool: - """Reads register 'RunActiveRW' - - Returns: - bool: Returns bool of the run active register. - """ - return bool(self.i2c.read_register("Shutter.RunActiveRW")) - - def start_run(self) -> None: - """Start run configurations""" - self.reset_counters() - self.reset_fifo() - self.reset_timestamp() - self.set_run_active(True) - self.trigger_logic.set_trigger_veto(False) - def stop_run(self) -> None: - """Stop run configurations""" - self.trigger_logic.set_trigger_veto(True) - self.set_run_active(False) + self.tlu_controller.stop_run() self.run_number += 1 - def set_enable_record_data(self, value: int) -> None: - """#TODO not sure what this does. Looks like a separate internal event buffer to the FIFO. - - Args: - value (int): #TODO I think this does not work - """ - self.i2c.write_register("Event_Formatter.Enable_Record_Data", value) - - def get_event_fifo_csr(self) -> int: - """Reads value from 'EventFifoCSR', corresponds to status flags of the FIFO. - - Returns: - int: number of events - """ - return self.i2c.read_register("eventBuffer.EventFifoCSR") - - def get_event_fifo_fill_level(self) -> int: - """Reads value from 'EventFifoFillLevel' - Returns the number of words written in - the FIFO. The lowest 14-bits are the actual data. - - Returns: - int: buffer level of the fifi - """ - return self.i2c.read_register("eventBuffer.EventFifoFillLevel") - - def get_timestamp(self) -> int: - """Get current time stamp. - - Returns: - int: Time stamp is not formatted. - """ - time = self.i2c.read_register("Event_Formatter.CurrentTimestampHR") - time = time << 32 - time = time + self.i2c.read_register("Event_Formatter.CurrentTimestampLR") - return time - - def pull_fifo_event(self) -> list: - """Pulls event from the FIFO. This is needed in the run loop to prevent the buffer to get stuck. - if this register is full the fifo needs to be reset or new triggers are generated but not sent out. - #TODO check here if the FIFO is full and reset it if needed would prob. make sense. - - Returns: - list: 6 element long vector containing bitwords of the data. - """ - event_numb = self.get_event_fifo_fill_level() - if event_numb: - fifo_content = self.i2c_hw.getNode("eventBuffer.EventFifoData").readBlock( - event_numb - ) - self.i2c_hw.dispatch() - return np.array(fifo_content) - pass - - def get_scaler(self, channel: int) -> int: - """reads current scaler value from register""" - if channel < 0 or channel > 5: - raise ValueError("Only channels 0 to 5 are valid") - return self.i2c.read_register(f"triggerInputs.ThrCount{channel:d}R") - - def get_scalers(self) -> list: - """reads current sc values from registers - - Returns: - list: all 6 trigger sc values - """ - return [self.get_scaler(n) for n in range(6)] - def init_raw_data_table(self) -> None: """Initializes the raw data table, where the raw FIFO data is found.""" data_dtype = np.dtype([("raw", "u4")]) @@ -257,7 +76,7 @@ def handle_status(self) -> None: t = threading.current_thread() while getattr(t, "do_run", True): time.sleep(0.5) - last_time = self.get_timestamp() + last_time = self.tlu_controller.get_timestamp() current_time = (last_time - self.start_time) * 25 / 1000000000 # Logs and poss. sends status every 1s. if current_time - self.last_time > 1: @@ -267,7 +86,10 @@ def handle_status(self) -> None: if current_time > self.timeout: self.stop_condition = True if self.max_trigger != None: - if self.trigger_logic.get_post_veto_trigger() > self.max_trigger: + if ( + self.tlu_controller.trigger_logic.get_post_veto_trigger() + > self.max_trigger + ): self.stop_condition = True def log_sent_status(self, time: int) -> None: @@ -278,14 +100,16 @@ def log_sent_status(self, time: int) -> None: time (int): current runtime of the TLU """ self.post_veto_rate = ( - self.trigger_logic.get_post_veto_trigger() - self.last_post_veto_trigger + self.tlu_controller.trigger_logic.get_post_veto_trigger() + - self.last_post_veto_trigger ) / (time - self.last_time) self.pre_veto_rate = ( - self.trigger_logic.get_pre_veto_trigger() - self.last_pre_veto_trigger + self.tlu_controller.trigger_logic.get_pre_veto_trigger() + - self.last_pre_veto_trigger ) / (time - self.last_time) self.run_time = time - self.total_post_veto = self.trigger_logic.get_post_veto_trigger() - self.total_pre_veto = self.trigger_logic.get_pre_veto_trigger() + self.total_post_veto = self.tlu_controller.trigger_logic.get_post_veto_trigger() + self.total_pre_veto = self.tlu_controller.trigger_logic.get_pre_veto_trigger() if self.zmq_address: self.socket.send_string( @@ -302,8 +126,12 @@ def log_sent_status(self, time: int) -> None: ) self.last_time = time - self.last_post_veto_trigger = self.trigger_logic.get_post_veto_trigger() - self.last_pre_veto_trigger = self.trigger_logic.get_pre_veto_trigger() + self.last_post_veto_trigger = ( + self.tlu_controller.trigger_logic.get_post_veto_trigger() + ) + self.last_pre_veto_trigger = ( + self.tlu_controller.trigger_logic.get_pre_veto_trigger() + ) self.log.info( "Run time: %.1f s, Pre veto: %s, Post veto: %s, Pre veto rate: %.f Hz, Post veto rate.: %.f Hz" @@ -316,7 +144,7 @@ def log_sent_status(self, time: int) -> None: ) ) - if self.get_event_fifo_csr() == 0x10: + if self.tlu_controller.get_event_fifo_csr() == 0x10: self.log.warning("FIFO is full") def log_trigger_inputs(self, event_vector: list) -> None: @@ -368,14 +196,18 @@ def run(self) -> None: def start_run_configuration(self) -> None: """Start of the run configurations, consists of timestamp resets, data preparations and zmq connections initialization.""" - self.start_run() - self.get_fw_version() - self.get_device_id() + self.tlu_controller.start_run() + self.tlu_controller.get_fw_version() + self.tlu_controller.get_device_id() # reset starting parameter - self.start_time = self.get_timestamp() + self.start_time = self.tlu_controller.get_timestamp() self.last_time = 0 - self.last_post_veto_trigger = self.trigger_logic.get_post_veto_trigger() - self.last_pre_veto_trigger = self.trigger_logic.get_pre_veto_trigger() + self.last_post_veto_trigger = ( + self.tlu_controller.trigger_logic.get_post_veto_trigger() + ) + self.last_pre_veto_trigger = ( + self.tlu_controller.trigger_logic.get_pre_veto_trigger() + ) self.stop_condition = False # prepare data handling and zmq connection self.save_data = self.config_parser.get_data_handling() @@ -408,7 +240,7 @@ def run_loop(self) -> None: KeyboardInterrupt: The run loop can be interrupted when raising a KeyboardInterrupt. """ try: - current_event = self.pull_fifo_event() + current_event = self.tlu_controller.pull_fifo_event() try: if self.save_data and np.size(current_event) > 1: self.data_table.append(current_event) @@ -427,7 +259,7 @@ def run_loop(self) -> None: def stop_run_configuration(self) -> None: """Cleans remaining FIFO data and closes data files and zmq connections after a run.""" # Cleanup of FIFO - self.pull_fifo_event() + self.tlu_controller.pull_fifo_event() if self.zmq_address: self.socket.close() diff --git a/aidatlu/test/fixtures/tlu_test_configuration.toml b/aidatlu/test/fixtures/tlu_test_configuration.toml index dd71348..e58b2a0 100644 --- a/aidatlu/test/fixtures/tlu_test_configuration.toml +++ b/aidatlu/test/fixtures/tlu_test_configuration.toml @@ -10,7 +10,7 @@ trigger_signal_delay = [0, 1, 0, 0, 3, 0] enable_clock_lemo_output = 'True' pmt_power = [0.8, 0.8, 0, -0.2] save_data = 'True' -output_data_path = 'aidatlu/test/fixtures/test_output_data/' +output_data_path = 'aidatlu/test/fixtures/test_output_data' zmq_connection = 'False' # Optional stop conditions can also be added to the configuration. diff --git a/aidatlu/test/fixtures/tlu_test_configuration.yaml b/aidatlu/test/fixtures/tlu_test_configuration.yaml index 4ba3c5a..b693ba3 100644 --- a/aidatlu/test/fixtures/tlu_test_configuration.yaml +++ b/aidatlu/test/fixtures/tlu_test_configuration.yaml @@ -60,7 +60,7 @@ pmt_control: # Save and generate interpreted data from the raw data set. Set to 'True' or 'False'. # If no specific output path is provided, the data is saved in the default output data path (aidatlu/aidatlu/tlu_data). save_data: True -output_data_path: 'aidatlu/test/fixtures/test_output_data/' +output_data_path: 'aidatlu/test/fixtures/test_output_data' # zmq connection for status messages, leave it blank or set to off if not needed zmq_connection: off #"tcp://:7500" diff --git a/aidatlu/test/test_configuration.py b/aidatlu/test/test_configuration.py index e616a73..cfc97ec 100644 --- a/aidatlu/test/test_configuration.py +++ b/aidatlu/test/test_configuration.py @@ -5,6 +5,7 @@ from aidatlu.hardware.ioexpander_controller import IOControl from aidatlu.main.tlu import AidaTLU from aidatlu.hardware.i2c import I2CCore +from aidatlu.hardware.tlu_controller import TLUControl from aidatlu.test.utils import MockI2C FILEPATH = Path(__file__).parent @@ -29,10 +30,8 @@ HW = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) I2CMETHOD = I2CCore -TLU = AidaTLU( +TLU = TLUControl( HW, - CONFIG_FILE, - FILEPATH / "../misc/aida_tlu_clk_config.txt", i2c=I2CMETHOD, ) diff --git a/aidatlu/test/test_tlu.py b/aidatlu/test/test_tlu.py index afb6c02..24c780b 100644 --- a/aidatlu/test/test_tlu.py +++ b/aidatlu/test/test_tlu.py @@ -4,6 +4,7 @@ import pytest from aidatlu.main.tlu import AidaTLU from aidatlu.hardware.i2c import I2CCore +from aidatlu.hardware.tlu_controller import TLUControl from aidatlu.test.utils import MockI2C from aidatlu.main.config_parser import yaml_parser @@ -35,26 +36,32 @@ i2c=I2CMETHOD, ) +TLUCONTROL = TLUControl( + HW, + i2c=I2CMETHOD, +) + def test_check_ups(): """Test read write TLU configurations""" + TLUCONTROL.reset_configuration() if MOCK: - TLU.set_event_fifo_csr(0) - assert TLU.get_event_fifo_csr() == 0 - assert TLU.get_device_id() == 0xFFFFFFFFFFFF - assert TLU.get_fw_version() == -1 - assert TLU.get_run_active() == 0 - assert TLU.get_event_fifo_fill_level() == -1 - assert TLU.get_timestamp() == -0x100000001 - assert TLU.get_scalers() == [-1, -1, -1, -1, -1, -1] + TLUCONTROL.set_event_fifo_csr(0) + assert TLUCONTROL.get_event_fifo_csr() == 0 + assert TLUCONTROL.get_device_id() == 0xFFFFFFFFFFFF + assert TLUCONTROL.get_fw_version() == -1 + assert TLUCONTROL.get_run_active() == 0 + assert TLUCONTROL.get_event_fifo_fill_level() == -1 + assert TLUCONTROL.get_timestamp() == -0x100000001 + assert TLUCONTROL.get_scalers() == [-1, -1, -1, -1, -1, -1] with pytest.raises(ValueError): - TLU.get_scaler(6) + TLUCONTROL.get_scaler(6) else: - TLU.set_event_fifo_csr(0) - assert TLU.get_event_fifo_csr() == 3 - assert TLU.get_run_active() == 0 - assert TLU.get_event_fifo_fill_level() == 0 + TLUCONTROL.set_event_fifo_csr(0) + assert TLUCONTROL.get_event_fifo_csr() == 3 + assert TLUCONTROL.get_run_active() == 0 + assert TLUCONTROL.get_event_fifo_fill_level() == 0 def test_configuration(): @@ -78,10 +85,10 @@ def _pull_fifo_event(self): return 0 # Overwrite TLU methods needed for run loop - func_type = type(TLU.get_timestamp) - TLU.get_timestamp = func_type(_get_timestamp, TLU) - func_type = type(TLU.pull_fifo_event) - TLU.pull_fifo_event = func_type(_pull_fifo_event, TLU) + func_type = type(TLUCONTROL.get_timestamp) + TLU.tlu_controller.get_timestamp = func_type(_get_timestamp, TLUCONTROL) + func_type = type(TLUCONTROL.pull_fifo_event) + TLU.tlu_controller.pull_fifo_event = func_type(_pull_fifo_event, TLUCONTROL) TLU.configure() TLU.run()