# `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)
@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)
@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()