Source code for rfblocks.ad9913_controller

# `AD9913Controller` - Control for the AD9913 DDS.
#
#    Copyright (C) 2021 Dyadic Pty Ltd
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.

from typing import (
    Optional, Dict, List
)

from PyQt5.QtCore import (
    pyqtSignal, QObject
)

import copy
from math import sqrt, log10
import serial
import numpy as np
from scipy import interpolate

from rfblocks import (
    ad9913, write_cmd, ModulusConstraintException
)


[docs]class AD9913Controller(QObject): """Higher level control for the AD9913 DDS. Documentation for the AD9913 DDS signal source rfblocks module can be found here: `An AD9913 DDS Signal Source. <../boards/AD9913-DDS.html>`_ """ DEFAULT_DDS_FREQ: float = 10.0 DEFAULT_DDS_PHASE: float = 0.0 DEFAULT_DDS_LEVEL: int = 512 DEFAULT_DDS_CONFIG: Dict = { 'freq': DEFAULT_DDS_FREQ, 'ph': DEFAULT_DDS_PHASE, 'lvl': DEFAULT_DDS_LEVEL, 'pmod': False, 'state': ad9913.POWER_UP, 'sweep': { 'type': ad9913.SweepType.FREQUENCY, 'start': 0.5, 'end': 5.0, 'ramp': ad9913.SweepRampType.SWEEP_OFF, 'delta': [0.1, 0.1], # MHz per step 'rate': [10.0, 10.0], # microsecs per step 'dwell': False, 'trigsrc': ad9913.SweepTriggerSource.REGISTER, 'trigtype': ad9913.SweepTriggerType.EDGE_TRIGGER }, 'profiles': [[0.5, 0.0], [1.0, 0.0], [2.0, 0.0], [5.0, 0.0], [10.0, 0.0], [20.0, 0.0], [50.0, 0.0], [100.0, 0.0]], 'selected_profile': 0, 'profile_type': ad9913.SweepType.FREQUENCY } state_changed = pyqtSignal(int) freq_changed = pyqtSignal(float) phase_changed = pyqtSignal(float) level_changed = pyqtSignal(float) pmod_changed = pyqtSignal(bool) selected_profile_changed = pyqtSignal(int) profile_type_changed = pyqtSignal(ad9913.SweepType) sweep_start_changed = pyqtSignal(float) sweep_end_changed = pyqtSignal(float) sweep_type_changed = pyqtSignal(ad9913.SweepType) ramp_type_changed = pyqtSignal(ad9913.SweepRampType) rising_step_changed = pyqtSignal(float) falling_step_changed = pyqtSignal(float) rising_rate_changed = pyqtSignal(float) falling_rate_changed = pyqtSignal(float) profile_freq_changed = pyqtSignal(int, float) profile_phase_changed = pyqtSignal(int, float) def __init__(self, controller_id: str, device: ad9913, config: Optional[Dict] = None) -> None: """ :param controller_id: The controller name. :type controller_id: str :param device: An instance of :py:class:`rfblocks.ad9913`. :type device: :py:class:`rfblocks.ad9913` :param config: Initial configuration for the AD9913 DDS board. If this is None the default configuration will be used. See :py:attr:`AD9913Controller.DEFAULT_DDS_CONFIG` for a brief description of the structure for :py:`config`. :type config: Optional[Dict] :py:`AD9913Controller` maintains the state configuration for an AD9913 DDS board. The following signals are defined: - :py:`state_changed(int)` - :py:`freq_changed(float)` - :py:`phase_changed(float)` - :py:`level_changed(float)` - :py:`pmod_changed(bool)` - :py:`selected_profile_changed(int)` - :py:`profile_type_changed(ad9913.SweepType)` - :py:`sweep_start_changed(float)` - :py:`sweep_end_changed(float)` - :py:`sweep_type_changed(ad9913.SweepType)` - :py:`ramp_type_changed(ad9913.SweepRampType)` - :py:`rising_step_changed(float)` - :py:`falling_step_changed(float)` - :py:`rising_rate_changed(float)` - :py:`falling_rate_changed(float)` - :py:`profile_freq_changed(int, float)` - :py:`profile_phase_changed(int, float)` An example of creating a ``DDSChan`` instance: .. code:: python DEFAULT_DDS_FREQ = 10.0 DEFAULT_DDS_PHASE = 0.0 DEFAULT_DDS_LEVEL = 512 DEFAULT_DDS_CONFIG: Dict = { 'freq': DEFAULT_DDS_FREQ, 'ph': DEFAULT_DDS_PHASE, 'lvl': DEFAULT_DDS_LEVEL, 'lvl_units': DDSChan.LVL_UNITS.DBM, 'pmod': False, 'state': ad9913.POWER_UP, 'sweep': { 'type': ad9913.SweepType.FREQUENCY, 'start': 0.5, 'end': 5.0, 'ramp': ad9913.SweepRampType.SWEEP_OFF, 'delta': [0.1, 0.1], # MHz per step 'rate': [10.0, 10.0], # microsecs per step 'dwell': False, 'trigsrc': ad9913.SweepTriggerSource.REGISTER, 'trigtype': ad9913.SweepTriggerType.EDGE_TRIGGER }, 'profiles': [[0.5, 0.0], [1.0, 0.0], [2.0, 0.0], [5.0, 0.0], [10.0, 0.0], [20.0, 0.0], [50.0, 0.0], [100.0, 0.0]], 'selected_profile': 0, 'profile_type': ad9913.SweepType.FREQUENCY } DDSCH1_HWCONF: Dict = {'cs': 'C6', 'io_update': 'B6', 'reset': 'C4', 'ps0': 'D4', 'ps1': 'D5', 'ps2': 'D6', 'board_model': '27dB-RF'} device = ad9913(**DDSCH1_HWCONF) ch1_ctl = AD9913Controller('dds1', device, DEFAULT_DDS_CONFIG) """ super().__init__() self._controller_id: str = controller_id self._ad9913: ad9913 = device self._initial_config: Dict = config if self._initial_config is None: self._initial_config = AD9913Controller.DEFAULT_DDS_CONFIG self._config: Dict = copy.deepcopy(self._initial_config) self._sweep_config = self._config['sweep'] self._profiles = self._config['profiles'] self._selected_profile: int = 0 self._profile_type: ad9913.SweepType = ad9913.SweepType.FREQUENCY self._direct_switch_enabled: bool = False self.set_cal_data(self._ad9913.cal_data) @property def label(self): """Returns a label string. This is generally used when constructing a user interface for the controller. See, for example, :class:`qtrfblocks.DDSChan`. If no label has been specified when the controller instance was created the :py:`controller_id` will be used. >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.label 'dds_ctl' >>> ctl2 = AD9913Controller( ... 'dds_ctl', dds, ... { 'label': 'Chan X', **AD9913Controller.DEFAULT_DDS_CONFIG }) >>> ctl2.label 'Chan X' """ try: return self._config['label'] except KeyError: return self._controller_id
[docs] def set_cal_data(self, cal_data: Dict[float, List[float]]) -> None: """Set the calibration data for the board associated with the controller. :param cal_data: The new calibration data to be used. :type cal_data: Dict[float, List[float]] The format for the data is: .. code:: python { freq-0: [slope-0, intercept-0], freq-1: [slope-1, intercept-1], ... freq-n [slope-n, intercept-n] } where the frequencies run from 0 to 100MHz. The slope and intercept data are derived from calibration measurements using the procedure documented in `Calibration Procedure <https://www.rfblocks.org/boards/AD9913-DDS.html#calibration-procedure>`_. Note that calibration measurements are made down to 5MHz so the values measured at 5MHz are also used for 0MHz. This allows slope and intercept numbers to be interpolated for f < 5MHz. This will override any default calibration data as loaded during the initialization of the controller instance. """ freqs = np.array([float(f) for f in cal_data.keys()]) slopes = np.array([v[0] for v in cal_data.values()]) intercepts = np.array([v[1] for v in cal_data.values()]) self.slope_interp_fn = interpolate.interp1d(freqs, slopes) self.intercept_interp_fn = interpolate.interp1d(freqs, intercepts)
@property def sysclk(self) -> float: return self._ad9913.sysclk @sysclk.setter def sysclk(self, sysclk: float): self._ad9913.sysclk = sysclk @property def sweep_config(self): """Returns a dict containing the current DDS sweep parameters. >>> from pprint import pprint as pp >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> pp(ctl.sweep_config) {'delta': [0.1, 0.1], 'dwell': False, 'end': 5.0, 'ramp': <SweepRampType.SWEEP_OFF: 0>, 'rate': [10.0, 10.0], 'start': 0.5, 'trigsrc': <SweepTriggerSource.REGISTER: 134217728>, 'trigtype': <SweepTriggerType.EDGE_TRIGGER: 0>, 'type': <SweepType.FREQUENCY: 0>} """ return self._sweep_config @property def profiles(self): """Returns a list of the currently configured profiles. The list contains a sublist for each of the eight DDS profiles. Each sublist contains the profile frequency and phase. :type: List >>> from pprint import pprint as pp >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> pp(ctl.profiles) [[0.5, 0.0], [1.0, 0.0], [2.0, 0.0], [5.0, 0.0], [10.0, 0.0], [20.0, 0.0], [50.0, 0.0], [100.0, 0.0]] """ return self._profiles @property def state(self) -> int: """The state of the DDS channel. This should be either :py:`ad9913.POWER_UP` or :py:`ad9913.POWER_DOWN`. The :py:`state_changed(int)` signal is emitted if the :py:`state` changes. :type: int Note that :py:meth:`configure` must be invoked in order to update the DDS hardware """ return self._config['state'] @state.setter def state(self, flag: int) -> None: if flag: if self._config['state'] != ad9913.POWER_UP: self._config['state'] = ad9913.POWER_UP self.state_changed.emit(ad9913.POWER_UP) else: if self._config['state'] != ad9913.POWER_DOWN: self._config['state'] = ad9913.POWER_DOWN self.state_changed.emit(ad9913.POWER_DOWN) @property def freq(self) -> float: """The DDS output frequency in MHz. The :py:`freq_changed(float)` signal is emitted if the :py:`freq` value changes. :type: float Note that :py:meth:`configure` must be invoked in order to update the DDS hardware >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.freq 10.0 >>> ctl.freq = 25.0 >>> ctl.freq 25.0 """ return self._config['freq'] @freq.setter def freq(self, f: float) -> None: if self._config['freq'] != f: self._config['freq'] = f self.freq_changed.emit(f) @property def phase(self) -> float: """The DDS phase offset in degrees. The :py:`phase_changed(float)` signal is emitted if the :py:`phase` value changes. :type: float Note that :py:meth:`configure` must be invoked in order to update the DDS hardware >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.phase 0.0 >>> ctl.phase = 90.0 >>> ctl.phase 90.0 """ return self._config['ph'] @phase.setter def phase(self, p: float) -> None: if self._config['ph'] != p: self._config['ph'] = p self.phase_changed.emit(p) @property def level(self) -> float: """The DDS output level in DDS DAC units (DAC code). The AD9913 uses a 10-bit DAC so the level range is 0 to 1023. This emits the :py:`level_changed(float)` signal if the :py:`level` value changes. :type: float Note that :py:meth:`configure` must be invoked in order to update the DDS hardware >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.level 512 >>> ctl.level = 250 >>> ctl.level 250 """ return self._config['lvl'] @level.setter def level(self, lvl: float) -> None: if self._config['lvl'] != round(lvl): self._config['lvl'] = round(lvl) self.level_changed.emit(lvl) @property def level_range(self) -> List[float]: """Return the range of the level setting. The range is returned as a two element list: [min_level, max_level] """ return [1, ad9913.MAX_LEVEL] @property def millivolts(self) -> float: """The DDS output level in millivolts. This emits the :py:`level_changed(float)` signal if the :py:`level` value is changed. :type: float Note that :py:meth:`configure` must be invoked in order to update the DDS hardware >>> dds = ad9913('d1', 'd2', board_model="20dB") >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.level = 256 >>> f'{ctl.millivolts:.2f}' '151.05' >>> ctl.millivolts = 300 >>> ctl.level 511 """ return self.level_to_millivolts(self._config['lvl']) @millivolts.setter def millivolts(self, mv: float) -> None: self.level = self.millivolts_to_level(mv) @property def millivolts_range(self) -> List[float]: """Return the range of the millivolts setting. The range is returned as a two element list: [min_millivolts, max_millivolts] """ return [self.level_to_millivolts(0), self.level_to_millivolts(ad9913.MAX_LEVEL)] @property def dbm(self) -> float: """The DDS output level in dBm. This emits the :py:`level_changed(float)` signal if the :py:`level` value is changed. :type: float Note that :py:meth:`configure` must be invoked in order to update the DDS hardware >>> dds = ad9913('d1', 'd2', sysclk=250.0, board_model="20dB") >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.millivolts = 300 >>> f'{ctl.dbm:.2f}' '2.55' >>> ctl.dbm = -10.0 >>> ctl.level 118 """ return self.level_to_dbm(self._config['lvl']) @dbm.setter def dbm(self, d: float): self.level = self.dbm_to_level(d) @property def dbm_range(self) -> List[float]: """Return the range of the dbm (power) setting. The range is returned as a two element list: [min_dbm, max_dbm] """ return [self.level_to_dbm(0), self.level_to_dbm(ad9913.MAX_LEVEL)] @property def pmod(self) -> bool: """The current DDS programmable modulus state. This emits the :py:`pmod_changed(bool)` signal if the :py:`pmod` value changes. :type: bool This will be True if the programmable modulus facility is active and False otherwise. """ return self._config['pmod'] @pmod.setter def pmod(self, state: bool) -> None: if self._config['pmod'] != state: self._config['pmod'] = state self._ad9913.set_modulus_enable(state) if state: # Disable direct switch mode when enabling # programmable modulus self._ad9913.set_direct_switch_mode_enable(False) self.pmod_changed.emit(state) @property def selected_profile(self) -> int: """The currently selected DDS profile (0 to 7). This emits the :py:`selected_profile_changed(int)` signal if the :py:`selected_profile` value changes. :type: int """ return self._selected_profile @selected_profile.setter def selected_profile(self, profnum: int) -> None: if self._selected_profile != profnum: self._selected_profile = profnum self.selected_profile_changed.emit(profnum) @property def profile_type(self) -> ad9913.SweepType: """The current profile type. This emits a :py:`profile_type_changed(ad9913.SweepType)` signal if the :py:`profile_type` value changes. :type: :class:`rfblocks.ad9913.SweepType` """ return self._profile_type @profile_type.setter def profile_type(self, ptype: ad9913.SweepType) -> None: _ptype = ad9913.SweepType(ptype) if self._profile_type != _ptype: self._profile_type = _ptype self.profile_type_changed.emit(_ptype) @property def direct_switch_enabled(self) -> bool: """Enable or disable direct switch mode. Setting direct switch mode to :py:`True` enables profile switching. Profile switching may then be done using the internal mode, for example: .. code:: python dds_ctl.direct_switch_enabled = True dds_ctl.profile_type = ad9913.SweepType.FREQUENCY dds_ctl.selected_profile = 4 dds_ctl.configure_profile(ser) Alternatively, profile switching can be achieved using the profile select pins, for example: .. code:: python dds_ctl.direct_switch_enabled = True dds_ctl.profile_type = ad9913.SweepType.FREQUENCY dds_ctl._ad9913.set_profile_control( ad9913.ProfileControl.PINS) cmd = 'M{},{:02X},{:02X}:'.format(dds.ps_port, dds.ps_mask, 4) write_cmd(ser, cmd) """ return self._direct_switch_enabled @direct_switch_enabled.setter def direct_switch_enabled(self, enable: bool) -> None: self._direct_switch_enabled = enable
[docs] def dump_config(self) -> Dict: """Return the current configuration for this channel. :return: A dictionary containing the current DDS channel configuration: :freq: channel frequency :ph: phase offset :lvl: channel output level (in DAC units) :pmod: programmable modulus mode. True for active. :state: channel state (active or powered down) :sweep: sweep information :profiles: channel profile configuration :selected_profile: currently selected profile :profile_type: frequency or phase >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> from pprint import pprint as pp >>> pp(ctl.dump_config()) {'freq': 10.0, 'lvl': 512, 'ph': 0.0, 'pmod': False, 'profile_type': <SweepType.FREQUENCY: 0>, 'profiles': [[0.5, 0.0], [1.0, 0.0], [2.0, 0.0], [5.0, 0.0], [10.0, 0.0], [20.0, 0.0], [50.0, 0.0], [100.0, 0.0]], 'selected_profile': 0, 'state': 1, 'sweep': {'delta': [0.1, 0.1], 'dwell': False, 'end': 5.0, 'ramp': <SweepRampType.SWEEP_OFF: 0>, 'rate': [10.0, 10.0], 'start': 0.5, 'trigsrc': <SweepTriggerSource.REGISTER: 134217728>, 'trigtype': <SweepTriggerType.EDGE_TRIGGER: 0>, 'type': <SweepType.FREQUENCY: 0>}} """ config = { 'freq': self.freq, 'ph': self.phase, 'lvl': self.level, 'pmod': self.pmod, 'state': self.state, 'sweep': copy.deepcopy(self._sweep_config), 'profiles': copy.deepcopy(self._profiles), 'selected_profile': self._selected_profile, 'profile_type': self._profile_type } return config
[docs] def load_config(self, config: Dict) -> None: """Set the current configuration for this channel. :param config: A dictionary containing the channel configuration to be set. :type config: Dict Note that in order to update the DDS channel hardware :py:meth:`configure` should be called. >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> import copy >>> config = copy.deepcopy(AD9913Controller.DEFAULT_DDS_CONFIG) >>> config['freq'] = 45.0 >>> config['lvl'] = 100 >>> config['sweep']['type'] = ad9913.SweepType.PHASE >>> config['profiles'][0][0] = 10.0 >>> ctl.load_config(config) >>> from pprint import pprint as pp >>> pp(ctl.dump_config()) {'freq': 45.0, 'lvl': 100, 'ph': 0.0, 'pmod': False, 'profile_type': <SweepType.FREQUENCY: 0>, 'profiles': [[10.0, 0.0], [1.0, 0.0], [2.0, 0.0], [5.0, 0.0], [10.0, 0.0], [20.0, 0.0], [50.0, 0.0], [100.0, 0.0]], 'selected_profile': 0, 'state': 1, 'sweep': {'delta': [0.1, 0.1], 'dwell': False, 'end': 5.0, 'ramp': <SweepRampType.SWEEP_OFF: 0>, 'rate': [10.0, 10.0], 'start': 0.5, 'trigsrc': <SweepTriggerSource.REGISTER: 134217728>, 'trigtype': <SweepTriggerType.EDGE_TRIGGER: 0>, 'type': <SweepType.PHASE: 4096>}} """ self.freq = config['freq'] self.ph = config['ph'] self.level = config['lvl'] self.pmod = config['pmod'] self.state = config['state'] self._sweep_config = copy.deepcopy(config['sweep']) self._profiles = copy.deepcopy(config['profiles']) self._selected_profile = config['selected_profile'] self._profile_type = config['profile_type']
[docs] def initialize(self, ser: serial.Serial) -> None: """Initialize the DDS board. """ write_cmd(ser, self._ad9913.pin_config()) write_cmd(ser, self._ad9913.chip_reset()) self.configure(ser)
[docs] def configure(self, ser: serial.Serial) -> None: """Set the DDS hardware 'base' configuration. :param ser: :type ser: serial.Serial This will configure the DDS hardware for the following: - Output frequency - Phase offset - Output level - Active state - powered on or off If the programmable modulus flag is set to ``True``, the controller will also attempt to set the configured output frequency precisely. """ cmd = self._ad9913.config_cfr1() if self.pmod: try: a, b, x = self._ad9913.modulus_parameters(self.freq) except ModulusConstraintException: print( 'Specified frequency {} MHz cannot be synthesized exactly'.format( self.freq)) else: cmd += self._ad9913.config_modulus_params(a, b, x) else: cmd += self._ad9913.config_tuning_word( self._ad9913.tuning_word(self.freq)) cmd += self._ad9913.config_output_level(round(self.level)) cmd += self._ad9913.config_phase_offset( self._ad9913.phase_offset_word(self.phase)) cmd += self._ad9913.power_control(self.state, ad9913.Subsystem.DAC) write_cmd(ser, cmd)
[docs] def configure_sweep(self, ser: serial.Serial) -> None: """Update the DDS hardware registers with the current sweep parameters. """ self._ad9913.set_aux_accumulator_enable(False) self._ad9913.set_sweep_type(self.sweep_type) self._ad9913.set_sweep_trigger_source( ad9913.SweepTriggerSource.REGISTER) self._ad9913.set_sweep_trigger_type( ad9913.SweepTriggerType.STATE_TRIGGER) self._ad9913.set_sweep_dwell(False) cmd = self._ad9913.config_cfr1() cmd += self._ad9913.config_sweep_limits( self.sweep_type, self.sweep_start, self.sweep_end, self.sweep_falling_step, self.sweep_rising_step) cmd += self._ad9913.config_sweep_rates( self.sweep_falling_rate, self.sweep_rising_rate) write_cmd(ser, cmd)
@property def sweep_start(self) -> float: """The sweep start value. This will be the frequency if :attr:`sweep_type` is :py:`ad9913.SweepType.FREQUENCY` or the phase if :attr:`sweep_type` is :py:`ad9913.SweepType.PHASE`. This will emit a :py:`sweep_start_changed(float)` signal if the :py:`sweep_start` value changes. """ return self._sweep_config['start'] @sweep_start.setter def sweep_start(self, s: float) -> None: if self._sweep_config['start'] != s: self._sweep_config['start'] = s self.sweep_start_changed.emit(s) @property def sweep_end(self) -> float: """The sweep end value. This will be the frequency if :attr:`sweep_type` is :py:`ad9913.SweepType.FREQUENCY` or the phase if :attr:`sweep_type` is :py:`ad9913.SweepType.PHASE`. This will emit a :py:`sweep_end_changed(float)` signal if the :py:`sweep_end` value changes. """ return self._sweep_config['end'] @sweep_end.setter def sweep_end(self, e: float) -> None: if self._sweep_config['end'] != e: self._sweep_config['end'] = e self.sweep_end_changed.emit(e) @property def sweep_type(self) -> ad9913.SweepType: """The current sweep type. This is one of values defined in :class:`rfblocks.ad9913.SweepType`. A :py:`sweep_type_changed(ad9913.SweepType)` signal is emitted if the :py:`sweep_type` changes. """ return self._sweep_config['type'] @sweep_type.setter def sweep_type(self, t: ad9913.SweepType) -> None: _t = ad9913.SweepType(t) if self._sweep_config['type'] != _t: self._sweep_config['type'] = _t self.sweep_type_changed.emit(_t) @property def sweep_ramp_type(self) -> ad9913.SweepRampType: """The current sweep ramp type. This is one of values defined in :class:`rfblocks.ad9913.SweepRampType`. A :py:`ramp_type_changed(ad9913.SweepRampType)` signal is emitted if the :py:`sweep_ramp_type` changes. """ return self._sweep_config['ramp'] @sweep_ramp_type.setter def sweep_ramp_type(self, r: ad9913.SweepRampType) -> None: _r = ad9913.SweepRampType(r) if self._sweep_config['ramp'] != _r: self._sweep_config['ramp'] = _r self.ramp_type_changed.emit(_r) @property def sweep_rising_step(self) -> float: """The current sweep rising step value. This will be a frequency value if :attr:`sweep_type` is :py:`ad9913.SweepType.FREQUENCY` or a phase value if :attr:`sweep_type` is :py:`ad9913.SweepType.PHASE`. :type: float A :py:`rising_step_changed(float)` signal is emitted if the :py:`rising_step` value changes. """ return self._sweep_config['delta'][0] @sweep_rising_step.setter def sweep_rising_step(self, r: float) -> None: if self._sweep_config['delta'][0] != r: self._sweep_config['delta'][0] = r self.rising_step_changed.emit(r) @property def sweep_falling_step(self) -> float: """The current sweep falling step value. This will be a frequency value if :attr:`sweep_type` is :py:`ad9913.SweepType.FREQUENCY` or a phase value if :attr:`sweep_type` is :py:`ad9913.SweepType.PHASE`. :type: float A :py:`falling_step_changed(float)` signal is emitted if the :py:`falling_step` value changes. """ return self._sweep_config['delta'][1] @sweep_falling_step.setter def sweep_falling_step(self, r: float) -> None: if self._sweep_config['delta'][1] != r: self._sweep_config['delta'][1] = r self.falling_step_changed.emit(r) @property def sweep_rising_rate(self) -> float: """The current rising rate value in seconds. :type: float A :py:`rising_rate_changed(float)` signal is emitted if the :py:`sweep_rising_rate` value changes. .. note:: Note that there is granularity with the sweep ramp rates. This will depend on the system clock. With the default system clock of 250 MHz the smallest possible step is 4 nS. This will also be the smallest increment between step sizes. Since the ramp rate registers are 16 bits, the largest possible step size (with a 250 MHz system clock) will be 262 uS. """ return self._sweep_config['rate'][0] @sweep_rising_rate.setter def sweep_rising_rate(self, r: float) -> None: if self._sweep_config['rate'][0] != r: self._sweep_config['rate'][0] = r self.rising_rate_changed.emit(r) @property def sweep_falling_rate(self) -> float: """The current falling rate value in seconds. :type: float A :py:`falling_rate_changed(float)` signal is emitted if the :py:`sweep_falling_rate` value changes. .. note:: Note that there is granularity with the sweep ramp rates. This will depend on the system clock. With the default system clock of 250 MHz the smallest possible step is 4 nS. This will also be the smallest increment between step sizes. Since the ramp rate registers are 16 bits, the largest possible step size (with a 250 MHz system clock) will be 262 uS. """ return self._sweep_config['rate'][1] @sweep_falling_rate.setter def sweep_falling_rate(self, r: float) -> None: if self._sweep_config['rate'][1] != r: self._sweep_config['rate'][1] = r self.falling_rate_changed.emit(r)
[docs] def start_sweep(self, ser: serial.Serial) -> None: """Start the DDS sweep operation. """ self._ad9913.set_aux_accumulator_enable(True) self._ad9913.set_sweep_mode(self.sweep_ramp_type) cmd = self._ad9913.config_cfr1() write_cmd(ser, cmd)
[docs] def stop_sweep(self, ser: serial.Serial) -> None: """Stop the DDS sweep operation. """ self._ad9913.set_sweep_mode(ad9913.SweepRampType.SWEEP_OFF) self._ad9913.set_aux_accumulator_enable(False) cmd = self._ad9913.config_cfr1() write_cmd(ser, cmd)
[docs] def profile_freq(self, profnum: int) -> float: """The profile frequency (in MHz) for the specified profile. :param profnum: The target profile number. :type profnum: An integer in the range 0 to 7. :raise: IndexError if an invalid profile is specified. """ return self._profiles[profnum][0]
[docs] def set_profile_freq(self, profnum: int, f: float) -> None: """Set the frequency of the specified profile. :param profnum: The target profile number. :type profnum: An integer in the range 0 to 7. :param f: The frequency in MHz. :type f: float :raise: IndexError if an invalid profile is specified. """ if self._profiles[profnum][0] != f: self._profiles[profnum][0] = f self.profile_freq_changed.emit(profnum, f)
[docs] def profile_phase(self, profnum: int) -> float: """The profile phase (in degrees) for the specified profile. :param profnum: The target profile number. :type profnum: An integer in the range 0 to 7. :raise: IndexError if an invalid profile is specified. """ return self._profiles[profnum][1]
[docs] def set_profile_phase(self, profnum: int, p: float) -> None: """Set the phase of the specified profile. :param profnum: The target profile number. :type profnum: An integer in the range 0 to 7. :param p: The phase in degrees. :type p: float :raise: IndexError if an invalid profile is specified. """ if self._profiles[profnum][1] != p: self._profiles[profnum][1] = p self.profile_phase_changed.emit(profnum, p)
[docs] def update_profile(self, ser: serial.Serial, profnum: int) -> None: """Update the AD9913 profile registers for the specified profile. :param profnum: The profile registers to update. This should be in the range 0 to 7. :type profnum: int :raise: IndexError if an invalid profile number is specified. """ tuning_word = self._ad9913.tuning_word(self._profiles[profnum][0]) phoffset_word = self._ad9913.phase_offset_word( self._profiles[profnum][1]) cmd = self._ad9913.config_profile(profnum, tuning_word, phoffset_word) write_cmd(ser, cmd)
[docs] def configure_profile(self, ser: serial.Serial) -> None: """Update the AD9913 registers to use the currently selected profile. """ # 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] # Two approaches are designed for switching between profile # registers. The first is programming the internal profile # control bits, CFR1 [22:20], to the desired value and issuing # an IO_UPDATE. The second approach, with higher data # throughput, is achieved by changing the profile control pins # [2:0]. Control bit CFR1 [27] is for selection between the two # approaches. The default state uses the profile pins. self._ad9913.set_active_profile(self.selected_profile) self._ad9913.set_sweep_type(self.profile_type) self._ad9913.set_profile_control(ad9913.ProfileControl.INTERNAL) self._ad9913.set_direct_switch_mode_enable( self.direct_switch_enabled) cmd = self._ad9913.config_cfr1() write_cmd(ser, cmd)
@property def slope(self) -> float: """Return the slope of the DAC code/output mV relation at the currently set output frequency. :type: float >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> dds.freq = 20.0 >>> f'{ctl.slope:.2f}' '0.58' """ return float(self.slope_interp_fn(self._config['freq'])) @property def intercept(self) -> float: """Return the intercept of the DAC code/output mV relation at the currently set output frequency. :type: float >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> dds.freq = 20.0 >>> f'{ctl.intercept:.2f}' '1.78' """ return float(self.intercept_interp_fn(self._config['freq']))
[docs] def level_to_millivolts(self, lvl): """Return RMS output voltage for a given DAC code. >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.freq = 20.0 >>> f'{ctl.level_to_millivolts(100.0):.2f}' '56.91' >>> ctl.freq = 40.0 >>> f'{ctl.level_to_millivolts(100.0):.2f}' '53.97' """ return self.slope * lvl + self.intercept
[docs] def millivolts_to_level(self, mv) -> int: """Return the DAC code which produces a given RMS output voltage. >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.freq = 40.0 >>> f'{ctl.level_to_millivolts(100.0):.2f}' '53.97' >>> f'{ctl.millivolts_to_level(53.97):.2f}' '100.00' """ return round((mv - self.intercept) / self.slope)
[docs] def level_to_dbm(self, lvl) -> float: """Return the output power (in dBm) for a given DAC code. >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.freq = 40.0 >>> f'{ctl.level_to_dbm(100):.2f}' '-12.35' >>> f'{ctl.level_to_dbm(512):.2f}' '1.56' """ mvrms = self.level_to_millivolts(lvl) return 10*log10(((mvrms * 1e-3)**2 / 50e-3))
[docs] def dbm_to_level(self, d) -> float: """Return the DAC code for a given output power (in dBm). >>> dds = ad9913('d1', 'd2') >>> ctl = AD9913Controller('dds_ctl', dds) >>> ctl.freq = 40.0 >>> f'{ctl.level_to_dbm(100):.2f}' '-12.35' >>> f'{ctl.level_to_dbm(512):.2f}' '1.56' >>> f'{ctl.dbm_to_level(1.56):.2f}' '512.00' >>> f'{ctl.dbm_to_level(-12.35):.2f}' '100.00' """ # Calculate the RMS output voltage (in millivolts) mv = sqrt(10**(d/10) * 50e-3) * 1e3 return self.millivolts_to_level(mv)
if __name__ == '__main__': import doctest doctest.testmod()