Source code for FIREQ_SYSTEM.dma_node

"""DMA Node class for FIREQ system node representation."""

from __future__ import annotations

import copy
import logging
import queue
import struct
from dataclasses import dataclass

import msgpack
import numpy as np
from pynq import allocate
from pynq.lib import DMA

from ._generic_node import _GenericNode
from ._utils import _MutableRef

logger = logging.getLogger(__name__)


@dataclass
class DMAPayload:
    """Object that holds the payload for a DMA transfer."""

    source: str
    shots: int
    dtype: str
    data: bytes


[docs] class DMANode(_GenericNode): """Object representing the DMA IP. Configuration dictionary keys: - ``_name`` (str): Name of the DMA node instance. - ``_ll_handler`` (DMA): Low-level DMA driver handler. """ nodetype = "dma" wraps = [DMA.__name__] def __init__(self, name: str, parent: _GenericNode, _ll_handler: DMA) -> None: """Initialize the DMA node. :param name: Name of the node :type name: str :param parent: Parent node in the system tree :type parent: _GenericNode :param _ll_handler: Low-level DMA driver handler :type _ll_handler: DMA """ super().__init__(name=name, parent=parent) self._ll_handler = _ll_handler # get the interface mapping for the node self._if_map = self.root.get_axi_stream_interface_map(self.name) self._input_interface = self._if_map["S_AXIS_S2MM"] self._transferring: bool = False # these will be initialized later by _build_dependencies self._input_payload: _MutableRef | list[_MutableRef] = None self._max_payload_size: _MutableRef | None = None # buffer and other self._buffer: np.ndarray | None = None self._is_switch_input: bool = False self._switch_func: callable | None = None self._current_payload_index: int | None = None # class that holds the data of the dma self.dma_payload_interface_class = DMAPayload def _build_dependencies(self) -> None: """Build the dependencies for this node. Resolves input payload references and allocates the receive buffer. """ # fetch the input payload and the maximum size of the input buffers self._input_payload = self.root.get_reference(f"{self._input_interface}/payload") self._max_payload_size = self.root.get_reference(f"{self._input_interface}/max_payload_size") # fetch the hw shots reference self._hw_shots = self.root.get_reference(self.root.make_func_label(self.root, "hw_shots")) # Do not allocate the buffer yet, since we do not know if the max payload size has a valid value # try to get the input switch node if isinstance(self._input_payload, list): self._switch_func = self.root.get_reference(f"{self._input_interface}/payload_switch_func") self._is_switch_input = True
[docs] def init_dma(self) -> None: """Initialize the DMA transfer. Will start the transfer in the receive channel and optionally set the input of the switch if the input of the DMA is a switch. This also means that, for implementation reasons, this function must be called before any data is being sent to the input of the switch. Failure to do so may lead to unaligned payloads and DMA errors. :raises RuntimeError: If the DMA is already transferring """ if self._buffer is None: if not self._max_payload_size: raise RuntimeError() self._buffer = allocate(shape=(self._max_payload_size["value"],), dtype=np.uint8) if self._transferring: self.log.error("DMA already transferring, cannot initialize") raise RuntimeError("DMA already transferring, cannot initialize") if not self._input_payload: self.log.warning("No payload to transfer for DMA node %s", self.name) raise RuntimeError("No payload to transfer for DMA node %s", self.name) # further checks and set the current payload if self._is_switch_input: for i, payload in enumerate(self._input_payload): if payload: self._current_payload_index = i # NOTE: adding one because the switch expects the first slave to be 1 not 0 self._switch_func(i + 1) break else: # loop exhausted without break -> no valid payload found self.log.warning("No payload to transfer for DMA node %s", self.name) raise RuntimeError("No payload to transfer for DMA node %s", self.name) self._transferring = True # start the transfer self._ll_handler.recvchannel.transfer(self._buffer)
[docs] def save_variables(self) -> None: """Save the variables needed for the transfer. This function must be called after the dma init_dma method and before the transfer_all method. After running this function, non-hw parameters in the dependency orchestrator can be changed. """ self._saved_payload = copy.deepcopy(self._input_payload) self._saved_hw_shots = copy.deepcopy(self._hw_shots)
[docs] def transfer_all(self, data_queue: queue.Queue) -> bool: """Transfer all available data from the DMA into the provided queue. This function expects saved variables to be set. This is done by the save_variables method. :param data_queue: Queue in which to put ``(source_name, data_array)`` tuples :type data_queue: queue.Queue :return: ``True`` if all transfers completed successfully, ``False`` on error or if no transfer was started :rtype: bool """ if not self._transferring: return False # get the current payload current_payload = ( self._saved_payload if self._current_payload_index is None else self._saved_payload[self._current_payload_index] ) while True: # check for errors if self._ll_handler.recvchannel.error: self.log.error("DMA transfer error for node %s", self.name) return False # wait for the transfer to complete and put the data in the queue self._ll_handler.recvchannel.wait() data_queue.put( self.dma_payload_interface_class( source=current_payload["source"], shots=self._saved_hw_shots["value"], dtype=current_payload["format"], data=self._buffer[: current_payload["size"]].tobytes(), ) ) # break the loop if the input is not a switch or if the current payload is the last if not self._is_switch_input or self._current_payload_index >= len(self._saved_payload) - 1: break # find the next valid input payload to transfer for i in range(self._current_payload_index + 1, len(self._saved_payload)): if self._saved_payload[i]: self._current_payload_index = i # NOTE: adding one because the switch expects the first slave to be 1 not 0 self._switch_func(i + 1) current_payload = self._saved_payload[i] break else: # no more valid payloads break self._ll_handler.recvchannel.transfer(self._buffer) # end of transfer self._current_payload_index = None self._transferring = False return True