Source code for FIREQ_LL_API.acquisition_driver

"""Low-level driver for the FIREQ acquisition IP."""

from typing import Any

from ._utils import _FIREQDriver, _set_bit, _set_bits

__all__ = ["AcquisitionDriver"]


[docs] class AcquisitionDriver(_FIREQDriver): """Driver class that controls the FIREQ Acquisition IP. Provides methods to define acquisition behaviour and output type (when applicable). """ bindto = ["user.org:user:axisAcquisitionIP:1.0"] # Register offset definitions _ctrl = 0 _readout_off_l = 1 _readout_off_h = 2 _readout_inc_l = 3 _readout_inc_h = 4 _trigger_mask_l = 5 # Bit position definitions _manual_trigger_pos = 31 _raw_enable_pos = 27 _accumulated_decimated_enable_pos = 26 _accumulate_select_pos = 25 _duration_pos = 0 _trigger_mask_pos = 0 # Output interfaces mapping the acquisition mode to the interface responsable for the output _output_interfaces = { "raw": "m00_axis", "decimated": "m01_axis", "accumulated": "m01_axis", } # Formats of the raw bytes coming from the buffers _formats = { "raw": (("imag", "<i2"), ("real", "<i2")), "decimated": (("imag", "<i2"), ("real", "<i2")), "accumulated": (("imag", "<i4"), ("real", "<i4")), } # Port name of the fabric clock fabric_clock_port = "HS_axi_clock" def __init__(self, description: dict[str, Any]) -> None: """Initialize the AcquisitionDriver with the given description. :param description: Dictionary containing IP parameters and configuration :type description: dict """ super().__init__(description=description) # maximum acquisition duration in clock cycles self.duration_width = int(description["parameters"]["DurationWidth"]) self.maximum_duration = pow(2, self.duration_width) # size of the samples in bits self.sample_size = int(description["parameters"]["SampleSize"]) # parallelism of the acquisition in number of samples self.log_number_of_channels = int(description["parameters"]["LogNsamplesClock"]) self.number_of_channels = pow(2, self.log_number_of_channels) # depth of the phase increment and offset in bits self.phase_depth = int(description["parameters"]["PhaseDepth"]) # number of triggers on the input trigger channel self.trigger_channels = int(description["parameters"]["TriggerWordWidth"]) # maximum time of flight delay in clock cycles self.time_of_flight_width = int(description["parameters"]["TimeOfFlightCounterWidth"]) self.time_of_flight_max = pow(2, self.time_of_flight_width) # not decimated output width in bits self.non_decimated_output_width = int(description["parameters"]["C_M00_AXIS_TDATA_WIDTH"]) # decimated output width in bits self.decimated_output_width = int(description["parameters"]["C_M01_AXIS_TDATA_WIDTH"]) # payload of the acquisition, calculated depending on the parameters set # should be recalculated any time that the duration, output mode and trigger channel are set self._cache = {} self.payload = {} self.reset_state() # ratio between dac and adc sampling rates - 1, gives a mask of bits to truncate self.dac_to_adc_ratio_mask = 2 - 1
[docs] def reset_state(self) -> None: """Reset the cache and payload to the init value.""" self._cache = {"active": False, "duration": 0, "output_mode": None} self.payload = {}
[docs] def reset_memory_and_registers(self) -> None: """Reset all memory and registers to 0.""" for offset in range(0, self._axi_lite_interface_mmio.length, 4): self._axi_lite_interface_mmio.write(offset, 0)
[docs] def set_dac_to_adc_ratio(self, ratio: int) -> None: """Set the ratio between the sampling rate of the dac and the adc.""" if ratio < 1: self.log.error("DAC to ADC cannot be smaller than 1") raise RuntimeError("DAC to ADC cannot be smaller than 1") self.dac_to_adc_ratio_mask = ratio - 1 self.log.debug(f"Set the DAC to ADC ratio to {ratio}, with mask {bin(self.dac_to_adc_ratio_mask)}")
def _calculate_payload(self) -> None: """Calculate the payload of the acquisition for a single shot.""" if self._cache["active"] and self._cache["duration"] > 0 and self._cache["output_mode"] is not None: if self._cache["output_mode"] == "raw": self.payload["size"] = self._cache["duration"] * self.non_decimated_output_width // 8 elif self._cache["output_mode"] == "decimated": self.payload["size"] = ((self._cache["duration"] + 1) // 2) * self.decimated_output_width // 8 elif self._cache["output_mode"] == "accumulated": self.payload["size"] = self.decimated_output_width // 8 self.payload["on_interface"] = self._output_interfaces[self._cache["output_mode"]] self.payload["format"] = self._formats[self._cache["output_mode"]] else: self.payload = {}
[docs] def print_description(self, printer_func: callable) -> None: """Print a detailed description of the acquisition IP configuration parameters. :param printer_func: Function to use to print the description :type printer_func: callable """ printer_func( "maximum_duration: " + str(self.maximum_duration) + ", maximum duration of acquisition in clock cycles" ) printer_func("sample_size: " + str(self.sample_size) + ", width of samples (bits)") printer_func( "number_of_channels: " + str(self.number_of_channels) + ", parallelism of the acquisition (samples/clock cycle)" ) printer_func("phase_depth: " + str(self.phase_depth) + ", width of phases (bits)") printer_func( "trigger_channels: " + str(self.trigger_channels) + ", number of trigger channels for readout and drive (bits)" ) printer_func( "time_of_flight_width: " + str(self.time_of_flight_width) + ", width of the time of flight timer (bits)" )
[docs] def init_axi_lite_interface(self, base_address: int, axi_depth: int) -> None: """Initialize the AXI Lite interface for register access. :param base_address: Base address of the AXI Lite interface :type base_address: int :param axi_depth: Depth of the AXI Lite address space :type axi_depth: int """ super().init_axi_lite_interface(base_address, axi_depth) # delete the mmio object created by PYNQ del self.mmio
[docs] def set_demodulation_frequency(self, frequency: float) -> int: """Set acquisition demodulation frequency. :param frequency: Frequency of the demodulation signal, normalized to the ADC sampling frequency :type frequency: float :return: Error code (0 on success) :rtype: int """ normalized_frequency = frequency % 1.0 # compute the phase increment pinc = int(2**self.phase_depth * normalized_frequency) # masking off the LSB of the phases. This is done in an effort to keep the generation and acquisition in phase. # Generation is done (at the DAC) at a frequency that is double the one used for the ADC. As a result, the phase # increment for the acquisition (at equal modulation and demodulation frequencies) is equal to double the one # of the generation. Masking the LSB accounts for the truncation done in the generation portion, and keeps the # two systems in sync. pinc = pinc & (2**self.phase_depth - 1) & (~self.dac_to_adc_ratio_mask) # write inc LOW self._axi_lite_interface_mmio.write(self._readout_inc_l * 4, pinc & 0xFFFFFFFF) # write inc HIGH self._axi_lite_interface_mmio.write(self._readout_inc_h * 4, pinc >> 32) # log with deferred formatting string arguments to avoid eager evaluation self.log.debug("set frequency to %s and phase increment to %s", frequency, pinc) return 0
[docs] def set_demodulation_initial_phase(self, normalized_phase: float) -> int: """Set acquisition demodulation frequency. :param normalized_phase: Phase offset of the demodulation signal, normalized to 2pi :type normalized_phase: float :return: Error code (0 on success) :rtype: int """ # compute the phase increment poff = int(2**self.phase_depth * normalized_phase) # TODO: check that this is ok even when moving away from the first nyquist zone. # I am unsure if I have to invert the phase or not when in the second nyquist zone. poff = poff & (2**self.phase_depth - 1) # write inc LOW self._axi_lite_interface_mmio.write(self._readout_off_l * 4, poff & 0xFFFFFFFF) # write inc HIGH self._axi_lite_interface_mmio.write(self._readout_off_h * 4, poff >> 32) self.log.debug("set initial phase to %s and phase offset to %s", normalized_phase, poff) return 0
[docs] def trigger_manually(self) -> int: """Trigger the acquisition manually.""" manual_trigger_mask = 1 << self._manual_trigger_pos control_register = self._axi_lite_interface_mmio.read(0) | manual_trigger_mask self._axi_lite_interface_mmio.write(self._ctrl * 4, control_register) self.log.debug("acquisition triggered manually") return 0
[docs] def set_acquisition_duration(self, duration: int) -> int: """Set the acquisition duration. :param duration: Duration in fabric clock cycles :type duration: int :return: Error code (0 on success) :rtype: int """ if duration < 1 or duration > self.maximum_duration: self.log.error("acquisition duration: %s out of range", duration) return -3 control_register = self._axi_lite_interface_mmio.read(self._ctrl * 4) control_register = _set_bits(control_register, self._duration_pos, self.duration_width, duration - 1) self._axi_lite_interface_mmio.write(self._ctrl * 4, control_register) self.log.debug("set the acquisition duration to %s fabric clock cycles", duration) self._cache["duration"] = duration self._calculate_payload() return 0
[docs] def set_trigger_channel(self, channel: int) -> int: """Set the readout trigger channel. :param channel: Channel selection, set to 0 to deactivate external triggers :type channel: int :return: Error code (0 on success) :rtype: int """ if channel < 0 or channel > self.trigger_channels: self.log.error("channel choice: %s out of range", channel) return -3 channel_mask = (1 << channel) >> 1 trigger_mask_reg = self._axi_lite_interface_mmio.read(self._trigger_mask_l * 4) trigger_mask_reg = _set_bits(trigger_mask_reg, self._trigger_mask_pos, self.trigger_channels, channel_mask) self._axi_lite_interface_mmio.write(self._trigger_mask_l * 4, trigger_mask_reg) self.log.debug("set the acquisition trigger channel to %s", channel) self._cache["active"] = channel > 0 self._calculate_payload() return 0
[docs] def set_time_of_flight(self, time_of_flight: int) -> int: """Set time of flight. :param time_of_flight: Time of flight in fabric clock cycles :type time_of_flight: int :return: Error code (0 on success) :rtype: int """ if time_of_flight < 1 or time_of_flight > self.time_of_flight_max: self.log.error("time of flight: %s out of range", time_of_flight) return -3 control_register = self._axi_lite_interface_mmio.read(self._ctrl * 4) control_register = _set_bits( control_register, self._duration_pos + self.duration_width, self.time_of_flight_width, time_of_flight - 1, ) self._axi_lite_interface_mmio.write(self._ctrl * 4, control_register) self.log.debug("set the time of flight to %s fabric clock cycles", time_of_flight) return 0
[docs] def set_output_mode(self, output_mode: str) -> int: """Set the type of output data of the decimated stream. Can be set to output the decimated samples or the accumulated values. :param output_type: Selection, allowed values are 'raw', 'decimated' and 'accumulated' :type output_type: str :return: Error code (0 on success) :rtype: int """ if output_mode == "raw": self._cache["output_mode"] = "raw" raw_enable = 1 acc_dec_enable = 0 acc_dec_mode = 0 elif output_mode == "decimated": self._cache["output_mode"] = "decimated" raw_enable = 0 acc_dec_enable = 1 acc_dec_mode = 0 elif output_mode == "accumulated": self._cache["output_mode"] = "accumulated" raw_enable = 0 acc_dec_enable = 1 acc_dec_mode = 1 else: self.log.error("output_type: %s not recognized", output_mode) return -3 # set the bits relative to the interface enable and decimated output mode ctl = _set_bit( self._axi_lite_interface_mmio.read(self._ctrl * 4), self._accumulate_select_pos, acc_dec_mode, ) ctl = _set_bit( ctl, self._accumulated_decimated_enable_pos, acc_dec_enable, ) ctl = _set_bit( ctl, self._raw_enable_pos, raw_enable, ) # write the control register self._axi_lite_interface_mmio.write(self._ctrl * 4, ctl) self.log.debug("set the output mode to %s, ctl: %s", output_mode, hex(ctl)) self._calculate_payload() return 0