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

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_modeset_sweep_trigger_sourceset_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.
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_limitsconfig_sweep_rates

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].

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)