Open RF Prototyping

ad9913_qt.py

The ad9913_qt Module

from typing import (
    List, Dict, Optional
)
import copy
import time
import random
from math import sqrt, log10
from enum import Enum, IntFlag
import serial
import json
import asyncio

import numpy as np
from scipy import interpolate

from rfblocks import (
    ad9552, ad9913, AD9913Controller,
    ModulusConstraintException,
    list_available_serial_ports, create_serial,
    write_cmd, query_cmd, DEFAULT_SOCKET_URL
)

from qtrfblocks import (
    ClkModule
)

from qasync import (
    QEventLoop, QThreadExecutor, asyncSlot, asyncClose
)

from PyQt5.QtWidgets import (
    QWidget, QLabel, QAbstractSpinBox, QDoubleSpinBox, QVBoxLayout,
    QLineEdit, QHBoxLayout, QGroupBox, QMainWindow, QComboBox,
    QCheckBox, QPushButton, QRadioButton, QButtonGroup, QMessageBox,
    QFormLayout, QErrorMessage, QApplication, QScrollArea,
    QMenu, QFileDialog, QSpinBox, QDialog, QDialogButtonBox,
    QAction, QActionGroup
)

from PyQt5.QtCore import (
    Qt, QObject, pyqtSlot
)

<<dds-chan-class>>

DDS Channel Class

Note that the DDSChan class must be a subclass of the QObject class in order to be able to deal with the Qt signal/slot facility.

The modulate function is defined as a coroutine. This is done to allow write_chan_cmd to be invoked separately in the asyncio thread executor. This in turn allows serial commands from other DDSChan instances to be interleaved with the modulation commands.

class ModulationType(IntFlag):
    FREQUENCY = 0b0
    PHASE = 0b1


class DDSChan(QObject):

    MIN_FREQUENCY: float = 0.0
    MAX_FREQUENCY: float = 100.0

    MIN_RATE: float = 0.004
    MAX_RATE: float = 262.0

    MIN_PHASE: float = 0.0
    MAX_PHASE: float = 360.0

    FREQ_SUFFIX: str = ' MHz'
    RATE_SUFFIX: str = ' uS'
    PHASE_SUFFIX: str = u"\N{DEGREE SIGN}"
    FREQ_RANGE: List[float] = [MIN_FREQUENCY, MAX_FREQUENCY]
    PHASE_RANGE: List[float] = [MIN_PHASE, MAX_PHASE]
    RATE_RANGE: List[float] = [MIN_RATE, MAX_RATE]

    LVL_UNITS = Enum('Lvl_Units', 'DAC MV DBM')

    # The following number is derived from measurement of
    # the DDS_2 module output RMS voltages
    # DAC_TO_MILLIVOLTS = 510/1013

    DAC_CODE_RANGE: List[int] = [1, 1023]
    # MILLIVOLTS_RANGE = [DAC_TO_MILLIVOLTS * 1, DAC_TO_MILLIVOLTS * 1023]
    # DBM_RANGE = [10*log10( ((DAC_TO_MILLIVOLTS * 1e-3) * 1)**2 / 50e-3 ),
    #             10*log10( ((DAC_TO_MILLIVOLTS * 1e-3) * 1023)**2 / 50e-3 )]


    def __init__(self,
                 app: QWidget,
                 controller: AD9913Controller):
        """Create an instance of ``DDSChan``.

        :param app: The parent application for the channel display.
        :type app: QWidget
        :param controller: AD9913 DDS controller
        :type controller: :py:class:`rfblocks.AD9913Controller`
        """
        super().__init__(app)
        self._app: QWidget = app
        self._controller: AD9913Controller = controller

        self._min_freq: float = DDSChan.MIN_FREQUENCY
        self._max_freq: float = DDSChan.MAX_FREQUENCY

        self._level_units: Enum = DDSChan.LVL_UNITS.DBM
        self.label: str = self._controller.label

        self._modulation_type: ModulationType = ModulationType.FREQUENCY
        self._modulation_tones: int = 2
        self._modulating: bool = False
        self._group_box: Optional[QGroupBox] = None

    @classmethod
    def level_units_control(cls,
                            app: QWidget,
                            parent: QObject) -> QPushButton:
        level_units_btn: QPushButton = QPushButton("Units")
        level_units_menu: QMenu = QMenu()
        units_group: QActionGroup = QActionGroup(app)
        dac_units_action: QAction = QAction('DAC Code', units_group,
                                            triggered=parent.set_dac_units,
                                            checkable=True)
        mv_units_action: QAction = QAction('Millivolts', units_group,
                                           triggered=parent.set_millivolt_units,
                                           checkable=True)
        dbm_units_action: QAction = QAction('dBm', units_group,
                                            triggered=parent.set_dbm_units,
                                            checkable=True)
        dbm_units_action.setChecked(True)
        level_units_menu.addAction(dac_units_action)
        level_units_menu.addAction(mv_units_action)
        level_units_menu.addAction(dbm_units_action)
        level_units_btn.setMenu(level_units_menu)
        return level_units_btn

    @property
    def ctl(self) -> AD9913Controller:
        """Return a reference to the :py:class:`AD9913Controller`.
        """
        return self._controller

    <<build-chan-ui>>

    @property
    def millivolts_range(self) -> List[float]:
        return [self.ctl.level_to_millivolts(l) for l in DDSChan.DAC_CODE_RANGE]

    @property
    def dbm_range(self) -> List[float]:
        return [self.ctl.level_to_dbm(l) for l in DDSChan.DAC_CODE_RANGE]

    @property
    def enabled(self) -> bool:
        """Enable/disable the on screen clock module UI components.
        """
        if self._group_box:
            return self._group_box.isEnabled()
        else:
            return False

    @enabled.setter
    def enabled(self, en: bool) -> None:
        if self._group_box:
            self._group_box.setEnabled(en)

    @property
    def state(self) -> int:
        """The state of the DDS channel.  Either powered up or
        powered down.
        """
        return self.ctl.state

    def set_state(self, flag: int):
        """Set the DDS channel state.

        :param flag: This should be either :py:`ad9913.POWER_UP` or
            :py:`ad9913.POWER_DOWN`.
        :type flag: int
        """
        self.ctl.state = flag

    @pyqtSlot(int)
    def update_chan_state(self, state: int) -> None:
        """Update the displayed DDS channel state.
        """
        if state == ad9913.POWER_UP:
            self._state_box.setChecked(True)
        else:
            self._state_box.setChecked(False)

    @property
    def chan_id(self) -> str:
        """The channel identifier.
        """
        return self.ctl._controller_id

    def write_chan_cmd(self, cmd: str) -> None:
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                write_cmd(ser, cmd)
        except serial.serialutil.SerialException as se:
            print(str(se))

    @property
    def freq(self) -> float:
        """Return the DDS output frequency (in MHz).
        """
        return self.ctl.freq

    def set_freq(self, f: float) -> None:
        """Set the DDS output frequency (in MHz).

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.freq = f
        # If necessary, recalculate the output millivolts or dBm
        if self._level_units == DDSChan.LVL_UNITS.MV:
            self._lvl_box.setValue(self.ctl.millivolts)
            self._lvl_box.setRange(*self.millivolts_range)
        elif self._level_units == DDSChan.LVL_UNITS.DBM:
            self._lvl_box.setValue(self.ctl.dbm)
            self._lvl_box.setRange(*self.dbm_range)

    @pyqtSlot(float)
    def update_chan_freq(self, f: float) -> None:
        """Update the displayed DDS channel frequency.
        """
        self._freq_box.setValue(f)

    @property
    def phase(self) -> float:
        """Return the DDS phase offset (in degrees).
        """
        return self.ctl.phase

    def set_phase(self, p: float) -> None:
        """Set the DDS phase offset (in degrees).

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.phase = p

    @pyqtSlot(float)
    def update_chan_phase(self, p: float) -> None:
        """Update the displayed DDS channel phase offset.
        """
        self._ph_box.setValue(p)

    @property
    def level_units(self) -> Enum:
        return self._level_units

    @property
    def level(self) -> float:
        """Return the DDS output level (in DDS DAC units).
        """
        return self.ctl.level

    def set_level(self, l: float) -> None:
        """Set the DDS output level (in DDS DAC units).

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.level = l

    @property
    def millivolts(self) -> float:
        """Return the DDS output level (in millivolts RMS)
        """
        return self.ctl.millivolts

    def set_millivolts(self, mv: float):
        """Set the DDS output level (in millivolts).

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.millivolts = mv

    @property
    def dbm(self) -> float:
        """Return the DDS output level (in dBm).
        """
        return self.ctl.dbm

    def set_dbm(self, d: float):
        """Set the DDS output level (in dBm)

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.dbm = d

    @pyqtSlot(float)
    def update_chan_level(self, l: float) -> None:
        """Update the displayed DDS channel output level.
        """
        if self._level_units == DDSChan.LVL_UNITS.MV:
            self._lvl_box.setValue(self.ctl.level_to_millivolts(l))
        elif self._level_units == DDSChan.LVL_UNITS.DBM:
            self._lvl_box.setValue(self.ctl.level_to_dbm(l))
        else:
            self._lvl_box.setValue(l)

    @property
    def min_freq(self) -> float:
        return self._min_freq

    @min_freq.setter
    def min_freq(self, f: float) -> None:
        self._min_freq = f

    @property
    def max_freq(self) -> float:
        return self._max_freq

    @max_freq.setter
    def max_freq(self, f: float) -> None:
        self._max_freq = f

    def set_pmod(self, state: bool) -> None:
        """Set the DDS programmable modulus state.

        Note that :py:meth:`configure` must be invoked in order to
        update the DDS hardware
        """
        self.ctl.pmod = state
        if state:
            # Disable direct switch mode when enabling
            # programmable modulus
            self.set_direct_switch_enabled(False)

    @pyqtSlot(bool)
    def update_chan_pmod(self, state: bool) -> None:
        """Update the displayed prog. modulus checkbox.
        """
        self._pmod_box.setChecked(state)

    @property
    def direct_switch_enabled(self) -> bool:
        return self.ctl.direct_switch_enabled

    def set_direct_switch_enabled(self, enable: bool) -> None:
        self.ctl.direct_switch_enabled = enable

    @property
    def selected_profile(self) -> int:
        return self.ctl.selected_profile

    def set_selected_profile(self,
                             state: bool,
                             profnum: int) -> None:
        if state:
            self.ctl.selected_profile = profnum

    @property
    def profile_type(self) -> ad9913.SweepType:
        return self.ctl.profile_type

    def set_profile_type(self,
                         state: bool,
                         ptype: ad9913.SweepType) -> None:
        if state:
            self.ctl.profile_type = ptype

    @property
    def modulation_type(self) -> ModulationType:
        return self._modulation_type

    def set_modulation_type(self,
                            state: bool,
                            mtype: ModulationType) -> None:
        if state:
            self._modulation_type = mtype

    @property
    def modulation_tones(self) -> int:
        return self._modulation_tones

    def set_modulation_tones(self,
                             state: bool,
                             mtones: int) -> None:
        if state:
            self._modulation_tones = mtones

    @property
    def modulating(self) -> bool:
        return self._modulating

    def set_modulating(self, m: bool) -> None:
        self._modulating = m

    def show_sweep(self) -> None:
        sweep_dialog = SweepDialog(self._app, self)
        sweep_dialog.exec_()

    def show_profiles(self) -> None:
        profiles_dialog = ProfilesDialog(self._app,
                                         self, self.ctl.profiles)
        profiles_dialog.exec_()

    def dump_config(self) -> Dict:
        """Return the current configuration for this clock module.

        :return: A dictionary containing the current clock modules
            configuration:

            :freq: The currently set output frequency (in MHz).
            :channels: A dictionary containing the channel configurations
                keyed using the channel id.
        """
        return self.ctl.dump_config()

    def load_config(self, config: Dict) -> None:
        """Set the current configuration for this clock module.

        :param config: A dictionary containing the module configuration
            to be set.
        :type config: Dict
        """
        self.ctl.load_config(config)

    @asyncSlot()
    async def configure(self):
        """Update the DDS hardware using the currently set configuration.
        """
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, self.configure_hw, ser)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self._app)
            error_dialog.showMessage(str(se))

    def initialize_hw(self, ser: serial.Serial) -> None:
        """Initialize the DDS board hardware.
        """
        self.ctl.initialize(ser)

    def configure_hw(self, ser: serial.Serial) -> None:
        self.ctl.configure(ser)

    def initialize_ui(self) -> None:
        """Initialize the user interface.
        """
        self.configure_ui(self.ctl._initial_config)

    def configure_ui(self, config: Dict) -> None:
        self._freq_box.setValue(config['freq'])
        self._ph_box.setValue(config['ph'])
        self.set_level(config['lvl'])
        if self._level_units == DDSChan.LVL_UNITS.DAC:
            self._lvl_box.setValue(self.ctl.level)
        elif self._level_units == DDSChan.LVL_UNITS.MV:
            self._lvl_box.setValue(self.ctl.millivolts)
        else:
            self._lvl_box.setValue(self.ctl.dbm)
        self._pmod_box.setChecked(config['pmod'])
        self._sweep = copy.deepcopy(config['sweep'])

    <<modulate>>

# ----------------------------------------------------------
#  DDS channel dialogs

<<sweep-dialog>>

<<profiles-dialog>>
async def modulate(self) -> None:
    #
    # Use up to 8 profiles to generate the required modulation
    # tones.  We assume that the profile frequencies and/or
    # phases have been set appropriately.
    #  The profiles are used as follows:
    #
    #  Tones    Profile numbers
    #    2        00, 01
    #    4        00, 01, 10, 11
    #    8        000, 001, 010, 011, 100, 101, 110, 111
    #
    port = self.ctl._ad9913.ps_port
    if not port:
        print("Channel is not configured for modulation")
        return
    loop = asyncio.get_event_loop()
    mask_str = '{:02X}'.format(self.ctl._ad9913.ps_mask)
    tones = self.modulation_tones
    rgen = random.Random()
    self.ctl._ad9913.set_profile_control(ad9913.ProfileControl.PINS)
    self.ctl._ad9913.set_direct_switch_mode_enable(True)
    if self.modulation_type == ModulationType.FREQUENCY:
        self.ctl._ad9913.set_sweep_type(ad9913.SweepType.FREQUENCY)
    else:
        self.ctl._ad9913.set_sweep_type(ad9913.SweepType.PHASE)
    cmd = self.ctl._ad9913.config_cfr1()
    await loop.run_in_executor(None, self.write_chan_cmd, cmd)
    while self.modulating:
        symbols = [round(rgen.random() * (tones-1)) for i in range(100)]
        signal_str = ','.join(['{:02X}'.format(b) for b in
            [self.ctl._ad9913.ps_bits(s) for s in symbols]])
        cmd = 'M{},{},{}:'.format(port, mask_str, signal_str)
        await loop.run_in_executor(None, self.write_chan_cmd, cmd)

Channel user interface

DDSChan UI

The DDSChan UI.

@classmethod
def level_units_control(cls,
                        app: QWidget,
                        parent: QObject) -> QPushButton:
    level_units_btn: QPushButton = QPushButton("Units")
    level_units_menu: QMenu = QMenu()
    units_group: QActionGroup = QActionGroup(app)
    dac_units_action: QAction = QAction('DAC Code', units_group,
                                        triggered=parent.set_dac_units,
                                        checkable=True)
    mv_units_action: QAction = QAction('Millivolts', units_group,
                                       triggered=parent.set_millivolt_units,
                                       checkable=True)
    dbm_units_action: QAction = QAction('dBm', units_group,
                                        triggered=parent.set_dbm_units,
                                        checkable=True)
    dbm_units_action.setChecked(True)
    level_units_menu.addAction(dac_units_action)
    level_units_menu.addAction(mv_units_action)
    level_units_menu.addAction(dbm_units_action)
    level_units_btn.setMenu(level_units_menu)
    return level_units_btn

def build_ui(self) -> QGroupBox:
    """Build on-screen UI components for a DDS_2 channel.
    """
    self._group_box = QGroupBox("{} ({})".format(
        self.label, self.ctl._ad9913.model))
    vbox: QVBoxLayout = QVBoxLayout()

    fbox: QFormLayout = QFormLayout()
    self._freq_box = QDoubleSpinBox()
    self._freq_box.setRange(self.min_freq, self.max_freq)
    self._freq_box.setDecimals(5)
    self._freq_box.setStyleSheet("""QDoubleSpinBox {
             font-size: 25pt; }""")
    self._freq_box.setValue(self.freq)
    self._freq_box.setSuffix(DDSChan.FREQ_SUFFIX)
    self._freq_box.valueChanged.connect(self.set_freq)
    fbox.addRow(QLabel("Freq.:"), self._freq_box)

    self._ph_box = QDoubleSpinBox()
    self._ph_box.setRange(*DDSChan.PHASE_RANGE)
    self._ph_box.setDecimals(2)
    self._ph_box.setStyleSheet("""QDoubleSpinBox {
             font-size: 25pt; }""")
    self._ph_box.setValue(self.phase)
    self._ph_box.setSuffix(DDSChan.PHASE_SUFFIX)
    self._ph_box.valueChanged.connect(self.set_phase)
    fbox.addRow(QLabel("Phase:"), self._ph_box)

    hbox3: QHBoxLayout = QHBoxLayout()
    self._lvl_box = QDoubleSpinBox()
    self._lvl_box.setStyleSheet("""QDoubleSpinBox {
             font-size: 25pt; }""")
    self._lvl_box.setRange(*self.dbm_range)
    self._lvl_box.setDecimals(1)
    self._lvl_box.setSuffix(' dBm')
    self._lvl_box.setValue(self.dbm)
    self._lvl_box.valueChanged.connect(self.set_dbm)
    hbox3.addWidget(self._lvl_box)

    level_units_btn: QPushButton = DDSChan.level_units_control(
        self._app, self)
    hbox3.addWidget(level_units_btn)
    fbox.addRow(QLabel("Level:"), hbox3)

    self._pmod_box: QCheckBox = QCheckBox("")
    self._pmod_box.setChecked(False)
    self._pmod_box.stateChanged.connect(self.set_pmod)
    lbl: QLabel = QLabel("Programmable:\nModulus")
    lbl.setWordWrap(True)
    fbox.addRow(lbl, self._pmod_box)

    self._state_box: QCheckBox = QCheckBox("")
    if self.set_state == ad9913.POWER_UP:
        self._state_box.setChecked(True)
    else:
        self._state_box.setChecked(False)
    self._state_box.stateChanged.connect(self.set_state)
    fbox.addRow(QLabel("Active:"), self._state_box)

    vbox.addLayout(fbox)

    # ----------------------------------------------------
    # Module hardware configure
    #
    hbox3 = QHBoxLayout()
    self._config_btn = QPushButton("Configure")
    self._config_btn.clicked.connect(self.configure)
    hbox3.addWidget(self._config_btn)
    hbox3.addStretch()
    vbox.addLayout(hbox3)

    hbox2: QHBoxLayout = QHBoxLayout()
    self._sweep_btn: QPushButton = QPushButton("Sweep...")
    self._sweep_btn.clicked.connect(self.show_sweep)
    hbox2.addWidget(self._sweep_btn)
    self._profile_btn = QPushButton("Profiles...")
    self._profile_btn.clicked.connect(self.show_profiles)
    hbox2.addWidget(self._profile_btn)
    hbox2.addStretch()
    vbox.addLayout(hbox2)

    self.ctl.state_changed.connect(self.update_chan_state)
    self.ctl.freq_changed.connect(self.update_chan_freq)
    self.ctl.phase_changed.connect(self.update_chan_phase)
    self.ctl.level_changed.connect(self.update_chan_level)
    self.ctl.pmod_changed.connect(self.update_chan_pmod)

    self._group_box.setLayout(vbox)
    return self._group_box

def set_dac_units(self) -> None:
    self._level_units = DDSChan.LVL_UNITS.DAC
    self._lvl_box.disconnect()
    self._lvl_box.setRange(*DDSChan.DAC_CODE_RANGE)
    self._lvl_box.setDecimals(0)
    self._lvl_box.setSuffix('')
    self._lvl_box.setValue(self.level)
    self._lvl_box.valueChanged.connect(self.set_level)

def set_millivolt_units(self) -> None:
    self._level_units = DDSChan.LVL_UNITS.MV
    self._lvl_box.disconnect()
    self._lvl_box.setRange(*self.millivolts_range)
    self._lvl_box.setDecimals(1)
    self._lvl_box.setSuffix(' mV')
    self._lvl_box.setValue(self.millivolts)
    self._lvl_box.valueChanged.connect(self.set_millivolts)

def set_dbm_units(self) -> None:
    self._level_units = DDSChan.LVL_UNITS.DBM
    self._lvl_box.disconnect()
    self._lvl_box.setRange(*self.dbm_range)
    self._lvl_box.setDecimals(1)
    self._lvl_box.setSuffix(' dBm')
    self._lvl_box.setValue(self.dbm)
    self._lvl_box.valueChanged.connect(self.set_dbm)

Linear sweep uses the auxiliary accumulator to sweep frequency or phase from starting point (S0) to ending point (E0). A frequency or phase sweep is determined by the destination bits in CFR1 [13:12].

The trigger to initiate the sweep can be edge or level triggered. This is determined by Register CFR1 [9]. Note that, in level triggered mode, the sweep automatically repeats as long as the appropriate profile pin is held high. The profile pins or the internal profile bits trigger and control the direction (up/down) of the linear sweep for frequency or phase.

Profile Pins [2:0] or CFR1 Bits [22:20]

Linear Sweep Mode

x00

Sweep Off

x01

Ramp Up

x10

Ramp Down

x11

Bidirectional Ramp

The rfblocks ad9913 class provides the following methods for control of the sweep mode and triggering:

  • set_sweep_mode

  • set_sweep_trigger_source

  • set_sweep_trigger_type

The sweep trigger source and type is currently set for this application to be SweepTriggerSource.REGISTER and SweepTriggerType.STATE_TRIGGER respectively.

The slope of the linear sweep is set by the intermediate step size (delta tuning word) between S0 and E0 (see Figure 1) and the time spent (sweep ramp rate word) at each step. The resolution of the delta tuning word is 32 bits for frequency and 14 bits for phase. The resolution for the delta ramp rate word is 16 bits.

Linear sweep mode

Figure 1: Linear Sweep Mode (from the AD9913 data sheet)

In linear sweep mode, the user specifies the start and end points (S0 and E0), a rising step (RDW) and a rising rate (RSRR). These settings apply when sweeping from S0 to E0. The falling step (FDW) and falling rate (FSRR) apply when sweeping from E0 to S0. The ad9913 class provides the following methods for settings these sweep parameters:

  • config_sweep_limits

  • config_sweep_rates

Sweep dialog UI

The DDSChan sweep dialog UI.

class SweepDialog(QDialog):
    """
    """
    def __init__(self, ddsApp, ddsChan, parent=None):
        QDialog.__init__(self, parent)

        self._app = ddsApp
        self._chan = ddsChan
        self._start_box = None
        self._sweep_config = ddsChan.ctl.sweep_config
        self._limit_suffix = DDSChan.FREQ_SUFFIX
        self._limit_range = DDSChan.FREQ_RANGE
        self._step_suffix = DDSChan.FREQ_SUFFIX
        self._step_range = DDSChan.FREQ_RANGE
        self._rate_suffix = DDSChan.RATE_SUFFIX
        self._rate_range = DDSChan.RATE_RANGE

        self.setWindowTitle("{} Sweep Settings".format(ddsChan.label))
        self.build_ui()

    @property
    def ctl(self):
        return self._chan.ctl

    @property
    def start(self):
        return self._sweep_config['start']

    def set_start(self, s):
        self._sweep_config['start'] = s

    @property
    def end(self):
        return self._sweep_config['end']

    def set_end(self, e):
        self._sweep_config['end'] = e

    @property
    def sweep_type(self):
        return self._sweep_config['type']

    def set_sweep_type(self, t):
        self._sweep_config['type'] = t
        self.set_sweep_range()

    def set_sweep_range(self):
        """Set the sweep range and suffix.
        """
        t = self._sweep_config['type']
        if t in [ad9913.SweepType.FREQUENCY]:
            self._limit_suffix = self._step_suffix = DDSChan.FREQ_SUFFIX
            self._limit_range = self._step_range = DDSChan.FREQ_RANGE
        else:
            self._limit_suffix = self._step_suffix = DDSChan.PHASE_SUFFIX
            self._limit_range = self._step_range = DDSChan.PHASE_RANGE
        if self._start_box:
            self._start_box.setSuffix(self._limit_suffix)
            self._start_box.setRange(*self._limit_range)
            self._end_box.setSuffix(self._limit_suffix)
            self._end_box.setRange(*self._limit_range)
            self._rising_step_box.setSuffix(self._limit_suffix)
            self._rising_step_box.setRange(*self._step_range)
            self._falling_step_box.setSuffix(self._limit_suffix)
            self._falling_step_box.setRange(*self._step_range)

    @property
    def ramp_type(self):
        return self._sweep_config['ramp']

    def set_ramp_type(self, r):
        self._sweep_config['ramp'] = r

    @property
    def rising_step(self):
        return self._sweep_config['delta'][0]

    def set_rising_step(self, r):
        self._sweep_config['delta'][0] = r

    @property
    def falling_step(self):
        return self._sweep_config['delta'][1]

    def set_falling_step(self, r):
        self._sweep_config['delta'][1] = r

    @property
    def rising_rate(self):
        return self._sweep_config['rate'][0]

    def set_rising_rate(self, r):
        self._sweep_config['rate'][0] = r

    @property
    def falling_rate(self):
        return self._sweep_config['rate'][1]

    def set_falling_rate(self, r):
        self._sweep_config['rate'][1] = r

    def build_ui(self):
        outer_vbox = QVBoxLayout()
        outer_hbox = QHBoxLayout()
        outer_vbox.addLayout(outer_hbox)
        vbox1 = QVBoxLayout()
        vbox2 = QVBoxLayout()
        outer_hbox.addLayout(vbox1)
        outer_hbox.addLayout(vbox2)

        self._sweep_gbox = QGroupBox("Sweep Parameters")
        fbox1 = QFormLayout()

        # ----------------------------------------------------
        # Sweep type
        #
        hbox1 = QHBoxLayout()
        self._sweep_type_widgets = {}
        type_group = QButtonGroup(hbox1)
        rb = QRadioButton("Freq.")
        type_group.addButton(rb)
        rb.sweep_type = ad9913.SweepType.FREQUENCY
        rb.toggled.connect(lambda state, w=rb: self.set_sweep_type(w.sweep_type))
        hbox1.addWidget(rb)
        self._sweep_type_widgets[rb.sweep_type] = rb
        rb = QRadioButton("Phase")
        type_group.addButton(rb)
        rb.sweep_type = ad9913.SweepType.PHASE
        rb.toggled.connect(lambda state, w=rb: self.set_sweep_type(w.sweep_type))
        hbox1.addWidget(rb)
        self._sweep_type_widgets[rb.sweep_type] = rb
        self._sweep_type_widgets[self.sweep_type].setChecked(True)

        fbox1.addRow(QLabel("Type:"), hbox1)

        # ----------------------------------------------------
        # Start and end
        #
        self._start_box = QDoubleSpinBox()
        self._start_box.setRange(*self._limit_range)
        self._start_box.setDecimals(5)
        self._start_box.setSuffix(self._limit_suffix)
        self._start_box.setValue(self.start)
        self._start_box.valueChanged.connect(self.set_start)
        fbox1.addRow(QLabel("Start:"), self._start_box)

        self._end_box = QDoubleSpinBox()
        self._end_box.setRange(*self._limit_range)
        self._end_box.setDecimals(5)
        self._end_box.setSuffix(self._limit_suffix)
        self._end_box.setValue(self.end)
        self._end_box.valueChanged.connect(self.set_end)
        fbox1.addRow(QLabel("End:"), self._end_box)

        # ----------------------------------------------------
        # Ramp type
        #
        hbox2 = QHBoxLayout()
        self._ramp_type_widgets = {}
        ramp_group = QButtonGroup(hbox2)
        rb = QRadioButton("Up")
        ramp_group.addButton(rb)
        rb.ramp_type = ad9913.SweepRampType.RAMP_UP
        rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
        hbox2.addWidget(rb)
        self._ramp_type_widgets[rb.ramp_type] = rb
        rb = QRadioButton("Down")
        ramp_group.addButton(rb)
        rb.ramp_type = ad9913.SweepRampType.RAMP_DOWN
        rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
        hbox2.addWidget(rb)
        self._ramp_type_widgets[rb.ramp_type] = rb
        rb = QRadioButton("BiDir.")
        ramp_group.addButton(rb)
        rb.ramp_type = ad9913.SweepRampType.RAMP_BIDIR
        rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
        hbox2.addWidget(rb)
        self._ramp_type_widgets[rb.ramp_type] = rb
        rb = QRadioButton("Sweep Off")
        ramp_group.addButton(rb)
        rb.ramp_type = ad9913.SweepRampType.SWEEP_OFF
        rb.toggled.connect(lambda state, w=rb: self.set_ramp_type(w.ramp_type))
        hbox2.addWidget(rb)
        self._ramp_type_widgets[rb.ramp_type] = rb
        self._ramp_type_widgets[self.ramp_type].setChecked(True)

        fbox1.addRow(QLabel("Ramp:"), hbox2)

        # ----------------------------------------------------
        # Step
        #
        self._rising_step_box = QDoubleSpinBox()
        self._rising_step_box.setRange(*self._step_range)
        self._rising_step_box.setDecimals(5)
        self._rising_step_box.setSuffix(self._limit_suffix)
        self._rising_step_box.setValue(self.rising_step)
        self._rising_step_box.valueChanged.connect(self.set_rising_step)
        fbox1.addRow(QLabel("Rising Step:"), self._rising_step_box)

        self._falling_step_box = QDoubleSpinBox()
        self._falling_step_box.setRange(*self._step_range)
        self._falling_step_box.setDecimals(5)
        self._falling_step_box.setSuffix(self._limit_suffix)
        self._falling_step_box.setValue(self.falling_step)
        self._falling_step_box.valueChanged.connect(self.set_falling_step)
        fbox1.addRow(QLabel("Falling Step:"), self._falling_step_box)

        # ----------------------------------------------------
        # Rate
        #
        self._rise_rate_box = QDoubleSpinBox()
        self._rise_rate_box.setRange(*self._rate_range)
        self._rise_rate_box.setDecimals(3)
        self._rise_rate_box.setSuffix(self._rate_suffix)
        self._rise_rate_box.setValue(self.rising_rate)
        self._rise_rate_box.valueChanged.connect(self.set_rising_rate)
        fbox1.addRow(QLabel("Rise Rate:"), self._rise_rate_box)

        self._fall_rate_box = QDoubleSpinBox()
        self._fall_rate_box.setRange(*self._rate_range)
        self._fall_rate_box.setDecimals(3)
        self._fall_rate_box.setSuffix(self._rate_suffix)
        self._fall_rate_box.setValue(self.falling_rate)
        self._fall_rate_box.valueChanged.connect(self.set_falling_rate)
        fbox1.addRow(QLabel("Fall Rate:"), self._fall_rate_box)

        hbox3 = QHBoxLayout()
        config_btn = QPushButton("Configure")
        start_btn = QPushButton("Start Sweep")
        stop_btn = QPushButton("Stop Sweep")
        config_btn.clicked.connect(self.configure)
        start_btn.clicked.connect(self.start_sweep)
        stop_btn.clicked.connect(self.stop_sweep)
        hbox3.addStretch()
        hbox3.addWidget(config_btn)
        hbox3.addWidget(start_btn)
        hbox3.addWidget(stop_btn)
        fbox1.addRow(hbox3)

        self._sweep_gbox.setLayout(fbox1)
        vbox1.addWidget(self._sweep_gbox)

        # ----------------------------------------------------
        # self._modulation_gbox = QGroupBox("Modulation")
        # vbox2.addWidget(self._modulation_gbox)

        bbox = QDialogButtonBox(QDialogButtonBox.Close)
        bbox.rejected.connect(self.reject)
        outer_vbox.addWidget(bbox)

        self.ctl.sweep_start_changed.connect(self.update_sweep_start)
        self.ctl.sweep_end_changed.connect(self.update_sweep_end)
        self.ctl.sweep_type_changed.connect(self.update_sweep_type)
        self.ctl.ramp_type_changed.connect(self.update_ramp_type)
        self.ctl.rising_step_changed.connect(self.update_rising_step)
        self.ctl.falling_step_changed.connect(self.update_falling_step)
        self.ctl.rising_rate_changed.connect(self.update_rising_rate)
        self.ctl.falling_rate_changed.connect(self.update_falling_rate)

        self.setLayout(outer_vbox)

    @pyqtSlot(float)
    def update_sweep_start(self, start):
        """Update the displayed sweep start value.
        """
        self._start_box.setValue(start)

    @pyqtSlot(float)
    def update_sweep_end(self, end):
        """Update the displayed sweep end value.
        """
        self._end_box.setValue(end)

    @pyqtSlot(ad9913.SweepType)
    def update_sweep_type(self, st):
        """Update the displayed sweep type.

        This also sets the sweep range and suffix to the
        values appropriate for the sweep type.
        """
        self._sweep_type_widgets[st].setChecked(True)
        self.set_sweep_range()

    @pyqtSlot(ad9913.SweepRampType)
    def update_ramp_type(self, rt):
        """Update the displayed sweep ramp type.
        """
        self._ramp_type_widgets[rt].setChecked(True)

    @pyqtSlot(float)
    def update_rising_step(self, v):
        """Update the displayed sweep rising step value.
        """
        self._rising_step_box.setValue(v)

    @pyqtSlot(float)
    def update_falling_step(self, v):
        """Update the displayed sweep falling step value.
        """
        self._falling_step_box.setValue(v)

    @pyqtSlot(float)
    def update_rising_rate(self, v):
        """Update the displayed sweep rising rate value.
        """
        self._rise_rate_box.setValue(v)

    @pyqtSlot(float)
    def update_falling_rate(self, v):
        """Update the displayed sweep falling rate value.
        """
        self._fall_rate_box.setValue(v)

    @asyncSlot()
    async def configure(self):
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, self.ctl.configure_sweep, ser)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self)
            error_dialog.showMessage(str(se))

    @asyncSlot()
    async def start_sweep(self):
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, self.ctl.start_sweep, ser)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self)
            error_dialog.showMessage(str(se))

    @asyncSlot()
    async def stop_sweep(self):
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, self.ctl.stop_sweep, ser)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self)
            error_dialog.showMessage(str(se))

    def reject(self):
        QDialog.reject(self)

Direct switch mode enables FSK or PSK modulation. This mode simply selects the frequency or phase value programmed into the profile registers. Frequency or phase is determined by the destination bits in CFR1 [13:12]. Direct switch mode is enabled using the direct switch mode active bit in register CFR1 [16].

Profiles dialog UI

The DDSChan profiles dialog UI.

class ProfilesDialog(QDialog):
    """
    """
    def __init__(self, ddsApp, ddsChan, profiles, parent=None):
        QDialog.__init__(self, parent)

        self._app = ddsApp
        self._chan = ddsChan
        self._profiles = profiles

        self.setWindowTitle("{} Profiles Settings".format(ddsChan.label))
        self.build_ui()

    @property
    def ctl(self):
        return self._chan.ctl

    def profile_freq(self, profnum):
        return self._profiles[profnum][0]

    def set_profile_freq(self, profnum, f):
        self._profiles[profnum][0] = f

    def profile_phase(self, profnum):
        return self._profiles[profnum][1]

    def set_profile_phase(self, profnum, p):
        self._profiles[profnum][1] = p

    def profiles_enable(self, enable):
        if enable:
            self._profile_select_gbox.setEnabled(True)
            self._profiles_gbox.setEnabled(True)
        else:
            self._profiles_gbox.setEnabled(False)
            self._profile_select_gbox.setEnabled(False)

    @asyncSlot()
    async def modulation_start(self):
        self.profiles_enable(False)
        self._chan.set_modulating(True)
        await self._chan.modulate()

    @asyncSlot()
    async def modulation_stop(self):
        self._chan.set_modulating(False)
        self.profiles_enable(True)

    def build_ui(self):
        outer_vbox = QVBoxLayout()
        outer_hbox = QHBoxLayout()
        outer_vbox.addLayout(outer_hbox)
        vbox1 = QVBoxLayout()
        vbox2 = QVBoxLayout()
        outer_hbox.addLayout(vbox1)
        outer_hbox.addLayout(vbox2)

        # --------------------------------------------------------------
        self._profiles_gbox = QGroupBox('Profiles')
        vbox1.addWidget(self._profiles_gbox)

        self._profile_widgets = []
        fbox = QFormLayout()

        for profnum in range(8):
            hbox = QHBoxLayout()
            fw = QDoubleSpinBox()
            fw.setRange(*DDSChan.FREQ_RANGE)
            fw.setDecimals(5)
            fw.setValue(self.profile_freq(profnum))
            fw.setSuffix(DDSChan.FREQ_SUFFIX)
            fw.valueChanged.connect(
                lambda fval, n=profnum: self.set_profile_freq(n, fval))
            hbox.addWidget(fw)
            pw = QDoubleSpinBox()
            pw.setRange(*DDSChan.PHASE_RANGE)
            pw.setDecimals(2)
            pw.setValue(self.profile_phase(profnum))
            pw.setSuffix(DDSChan.PHASE_SUFFIX)
            pw.valueChanged.connect(
                lambda pval, n=profnum: self.set_profile_phase(n, pval))
            hbox.addWidget(pw)
            fbox.addRow(QLabel("Profile {}:".format(profnum)), hbox)
            self._profile_widgets.append([fw, pw])

        hbox = QHBoxLayout()
        update_btn = QPushButton("Update")
        update_btn.clicked.connect(self.update)
        hbox.addStretch()
        hbox.addWidget(update_btn)
        fbox.addRow(hbox)
        self._profiles_gbox.setLayout(fbox)

        # --------------------------------------------------------------
        self._profile_select_gbox = QGroupBox("Profile Selection")
        vbox2.addWidget(self._profile_select_gbox)

        hbox = QHBoxLayout()
        fbox = QFormLayout()
        self._enable_box = QCheckBox("")
        self._enable_box.setChecked(self._chan.direct_switch_enabled)
        self._enable_box.stateChanged.connect(
            self._chan.set_direct_switch_enabled)
        fbox.addRow(QLabel("Enable:"), self._enable_box)

        hbox1 = QHBoxLayout()
        type_group = QButtonGroup(hbox1)
        rb = QRadioButton("Freq.")
        type_group.addButton(rb)
        rb.profile_type = ad9913.SweepType.FREQUENCY
        rb.toggled.connect(
            lambda state, w=rb: self._chan.set_profile_type(
                state, w.profile_type))
        self._freq_type_btn = rb
        hbox1.addWidget(rb)

        rb = QRadioButton("Phase")
        type_group.addButton(rb)
        rb.profile_type = ad9913.SweepType.PHASE
        rb.toggled.connect(
            lambda state, w=rb: self._chan.set_profile_type(
                state, w.profile_type))
        self._phase_type_btn = rb
        hbox1.addWidget(rb)
        fbox.addRow(QLabel("Type:"), hbox1)
        if self._chan.profile_type == ad9913.SweepType.FREQUENCY:
            self._freq_type_btn.setChecked(True)
        else:
            self._phase_type_btn.setChecked(True)

        hbox2 = QHBoxLayout()
        profGroup = QButtonGroup(hbox2)
        self._profile_select_btns = []
        for profnum in range(8):
            rb = QRadioButton("{}".format(profnum))
            rb.profile = profnum
            rb.toggled.connect(
                lambda state, w=rb: self._chan.set_selected_profile(
                    state, w.profile))
            profGroup.addButton(rb)
            self._profile_select_btns.append(rb)
            hbox2.addWidget(rb)
            self._profile_select_btns[self._chan.selected_profile].setChecked(True)
        fbox.addRow(QLabel("Profile:"), hbox2)

        select_btn = QPushButton("Select")
        select_btn.clicked.connect(self.configure)
        hbox.addStretch()
        hbox.addWidget(select_btn)
        fbox.addRow(hbox)

        self.ctl.selected_profile_changed.connect(self.update_selected_profile)
        self.ctl.profile_type_changed.connect(self.update_profile_type)
        self.ctl.profile_freq_changed.connect(self.update_profile_freq)
        self.ctl.profile_phase_changed.connect(self.update_profile_phase)

        self._profile_select_gbox.setLayout(fbox)

        # --------------------------------------------------------------
        self._modulation_gbox = QGroupBox("Modulation")
        vbox2.addWidget(self._modulation_gbox)

        fbox = QFormLayout()
        hbox1 = QHBoxLayout()
        mod_group = QButtonGroup(hbox1)

        rb = QRadioButton("PSK")
        mod_group.addButton(rb)
        rb.modulation_type = ModulationType.PHASE
        rb.toggled.connect(
            lambda state, w=rb: self._chan.set_modulation_type(
                state, w.modulation_type))
        self._psk_type_btn = rb
        hbox1.addWidget(rb)

        rb = QRadioButton("FSK")
        mod_group.addButton(rb)
        rb.modulation_type = ModulationType.FREQUENCY
        rb.toggled.connect(
            lambda state, w=rb: self._chan.set_modulation_type(
                state, w.modulation_type))
        self._fsk_type_btn = rb
        hbox1.addWidget(rb)
        fbox.addRow(QLabel("Modulation:"), hbox1)
        if self._chan.modulation_type == ModulationType.FREQUENCY:
            self._fsk_type_btn.setChecked(True)
        else:
            self._psk_type_btn.setChecked(True)

        hbox1 = QHBoxLayout()
        tones_group = QButtonGroup(hbox1)

        self._tone_btns = {}
        for tone in [2, 4, 8]:
            rb = QRadioButton("{}".format(tone))
            tones_group.addButton(rb)
            rb.mod_tones = tone
            rb.toggled.connect(
                lambda state, w=rb: self._chan.set_modulation_tones(
                    state, w.mod_tones))
            self._tone_btns[tone] = rb
            hbox1.addWidget(rb)
        fbox.addRow(QLabel("Tones:"), hbox1)
        self._tone_btns[self._chan.modulation_tones].setChecked(True)

        hbox = QHBoxLayout()
        mod_start_btn = QPushButton("Start")
        mod_start_btn.clicked.connect(self.modulation_start)
        mod_stop_btn = QPushButton("Stop")
        mod_stop_btn.clicked.connect(self.modulation_stop)
        hbox.addStretch()
        hbox.addWidget(mod_start_btn)
        hbox.addWidget(mod_stop_btn)
        fbox.addRow(hbox)

        self._modulation_gbox.setLayout(fbox)

        # --------------------------------------------------------------
        bbox = QDialogButtonBox(QDialogButtonBox.Close)
        outer_vbox.addWidget(bbox)
        bbox.rejected.connect(self.reject)

        # --------------------------------------------------------------
        # If the channel modulation is active disable profile
        # selections.
        if self._chan.modulating:
            self.profiles_enable(False)

        self.setLayout(outer_vbox)

    @pyqtSlot(int)
    def update_selected_profile(self, p):
        """Update the displayed selected profile checkboxes.
        """
        self._profile_select_btns[p].setChecked(True)

    @pyqtSlot(ad9913.SweepType)
    def update_profile_type(self, ptype):
        """Update the displayed profile type.
        """
        if ptype == ad9913.SweepType.FREQUENCY:
            self._freq_type_btn.setChecked(True)
        else:
            self._phase_type_btn.setChecked(True)

    @pyqtSlot(int, float)
    def update_profile_freq(self, profnum, f):
        """Update the displayed frequency for a profile.
        """
        self._profile_widgets[profnum][0].setValue(f)

    @pyqtSlot(int, float)
    def update_profile_phase(self, profnum, p):
        """Update the displayed phase for a profile.
        """
        self._profile_widgets[profnum][1].setValue(p)

    @asyncSlot()
    async def update(self):
        # Update frequency tuning and phase offset words for
        # each of the eight profile.
        try:
            for profnum in range(8):
                with create_serial(self._app.ctl_device,
                                   self._app.baudrate) as ser:
                    loop = asyncio.get_event_loop()
                    await loop.run_in_executor(
                        None, self.ctl.update_profile, ser, profnum)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self)
            error_dialog.showMessage(str(se))

    @asyncSlot()
    async def configure(self):
        try:
            with create_serial(self._app.ctl_device,
                               self._app.baudrate) as ser:
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, self.ctl.configure_profile, ser)
        except serial.serialutil.SerialException as se:
            error_dialog = QErrorMessage(self)
            error_dialog.showMessage(str(se))

    def reject(self):
        QDialog.reject(self)