# `ad9913` - Encapsulates control for the AD9913 DDS.
#
# Copyright (C) 2020 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 (
List, Dict, Tuple, Optional
)
import math
from fractions import Fraction
from decimal import Decimal
from enum import IntFlag
[docs]class ModulusConstraintException(Exception):
"""Raised when a specified frequency cannot be exactly synthesized
using the programmable modulus facility.
"""
pass
[docs]class ad9913(object):
"""Encapsulates 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>`_
"""
MAX_LEVEL = (1 << 10) - 1
"The AD9913 has a 10-bit DAC."
MAX_PHOFFSET = (1 << 14) - 1
"The AD9913 has a 14-bit phase offset word."
DEFAULT_CFR1 = 0x00000000
[docs] class SweepType(IntFlag):
"""An `enum` containing possible sweep type settings.
(``FREQUENCY``, ``PHASE``)
"""
FREQUENCY = 0b0
PHASE = 0b1 << 12
[docs] class SweepRampType(IntFlag):
"""An `enum` containing possible sweep ramp type settings.
(``SWEEP_OFF``, ``RAMP_UP``, ``RAMP_DOWN``, ``RAMP_BIDIR``)
"""
SWEEP_OFF = 0
RAMP_UP = 0b01 << 20
RAMP_DOWN = 0b10 << 20
RAMP_BIDIR = 0b11 << 20
[docs] class SweepTriggerSource(IntFlag):
"""An `enum` containing possible sweep trigger source settings.
(``PROFILE_PINS``, ``REGISTER``)
"""
PROFILE_PINS = 0
REGISTER = 0b1 << 27
[docs] class SweepTriggerType(IntFlag):
"""An `enum` containing possible sweep trigger type settings.
(``EDGE_TRIGGER``, ``STATE_TRIGGER``)
"""
EDGE_TRIGGER = 0
STATE_TRIGGER = 0b1 << 9
FREQ_SWEEP_FULL_SCALE = (1 << 32) - 1
PHASE_SWEEP_FULL_SCALE = (1 << 14) - 1
POWER_DOWN = 0
POWER_UP = 1
[docs] class Subsystem(IntFlag):
"""An `enum` containing the device subsystem selectors.
(``DIGITAL``, ``DAC``, ``INPUT_CLOCK``, ``ALL``)
"""
DIGITAL = 0b1000000,
DAC = 0b0100000,
INPUT_CLOCK = 0b0010000,
ALL = 0b1110000
[docs] class ProfileControl(IntFlag):
"""An `enum` representing the profile control setting types.
(``INTERNAL``, ``PINS``)
"""
PINS = 0x0000_0000
INTERNAL = 0x0800_0000
#
DEFAULT_BOARD_MODEL = "20dB"
CAL_DATA = {
# 'Unsplit' configuration with no output amplifier
"0dB": {
0.0: [0.05341072761742072, 0.14856329999808282],
5.0: [0.05341072761742072, 0.14856329999808282],
10.0: [0.05215161401447688, 0.1289853499981325],
15.0: [0.05077914635103887, 0.16034107179900572],
20.0: [0.05009405613303852, 0.15987164999814596],
25.0: [0.048559963279174556, 0.16714872499820932],
30.0: [0.04814379772310773, 0.15315397499813166],
35.0: [0.04767926639219087, 0.16227664375993245],
40.0: [0.047286982765811586, 0.17457562556813194],
45.0: [0.04699643434994327, 0.16125302499810712],
50.0: [0.046642030762040446, 0.15801582882825418],
55.0: [0.04601056578403445, 0.1624665263261262],
60.0: [0.04488505546666688, 0.19749582499816387],
65.0: [0.043419924446442826, 0.17925284999818258],
70.0: [0.04161471271581862, 0.16927828488525404],
75.0: [0.039755977456545244, 0.20351457499817638],
80.0: [0.038203555788343335, 0.18695239999819624],
85.0: [0.03681936273430552, 0.18207842185241524],
90.0: [0.035090286509843915, 0.19098782499829225],
95.0: [0.033104491542008674, 0.18411009999832217],
100.0: [0.03156599765413759, 0.18552427499823843]
},
# 'Unsplit' configuration with 14dB gain output amplifier
"14dB": {
0.0: [0.27761635133114493, 0.8556567499996949],
5.0: [0.27761635133114493, 0.8556567499996949],
10.0: [0.2716392398881172, 0.8594159999996933],
15.0: [0.2636231571675115, 0.8133949999995956],
20.0: [0.2587522984818391, 0.8273814999995684],
25.0: [0.25015311350939395, 0.7649162499994157],
30.0: [0.2473772894253821, 0.850585818124398],
35.0: [0.24610280353695857, 0.8238179999995725],
40.0: [0.2465760190700479, 0.8418099999996267],
45.0: [0.24824423850938981, 0.9020542499998401],
50.0: [0.250217390853144, 0.9427587499998781],
55.0: [0.2495826631417456, 0.9701349999999351],
60.0: [0.2437891992238056, 1.038502496408651],
65.0: [0.23274105951119406, 1.0315142500000374],
70.0: [0.21896243175381114, 0.9903327499999979],
75.0: [0.2053787651637079, 1.024231750000051],
80.0: [0.19457087683647856, 0.9966567499998937],
85.0: [0.18701011006256485, 0.9967657500000023],
90.0: [0.1813981672790654, 1.0252095001884176],
95.0: [0.1763368361654828, 1.0440242500001313],
100.0: [0.17229462092089085, 1.082986210020929]
},
"20dB": {
# 'Unsplit' configuration with 20dB gain output amplifier
0.0: [0.5973246884030837, 1.6099550999671581],
5.0: [0.5973246884030837, 1.6099550999671581],
10.0: [0.5830919816167377, 1.7779857500016947],
15.0: [0.5645657565515982, 1.759589667593269],
20.0: [0.5533635326277022, 1.5749557500012796],
25.0: [0.5338074646129538, 1.444020500000936],
30.0: [0.5255274556515386, 1.7664722500016392],
35.0: [0.5200621716441738, 1.9416642500019743],
40.0: [0.518543044116597, 2.115762750002453],
45.0: [0.5197223375449083, 2.095591500002397],
50.0: [0.5215814944842507, 2.1686870000025347],
55.0: [0.5173105480228442, 2.584131250003476],
60.0: [0.5025146849713414, 2.9346482500041384],
65.0: [0.4775791739418752, 2.950062500004246],
70.0: [0.44587339246202673, 3.0741420000044846],
75.0: [0.4140794278479868, 3.0884137500044924],
80.0: [0.3877716879582235, 3.054623000004465],
85.0: [0.3682465746769308, 3.0999927500045734],
90.0: [0.3530905514824368, 3.387161243554389],
95.0: [0.3405744404856941, 3.502685000005458],
100.0: [0.3300431888772152, 3.67671525000578]
},
"20dB-DC": {
0.0: [0.5164459076298327, -0.43232000133546994],
5.0: [0.5164459076298327, -0.43232000133546994],
10.0: [0.5081601939327507, -0.5556855000033538],
15.0: [0.49671542991618167, -0.4116807500030937],
20.0: [0.49028764039410877, -0.2505032500027975],
25.0: [0.47624479021025456, -0.31170025000284873],
30.0: [0.47173548621208305, -0.3851582500030446],
35.0: [0.4666712516072925, -0.14981275000256455],
40.0: [0.46271273621206344, -0.21235850000268264],
45.0: [0.457483599493302, 0.18211124999821193],
50.0: [0.451555439337039, 0.17781849999821797],
55.0: [0.44126486127883, 0.4762729535604122],
60.0: [0.42538534663113353, 0.6666489134960794],
65.0: [0.40656982123032326, 0.6201222499991663],
70.0: [0.3865953938427936, 0.9546507493343078],
75.0: [0.36963493313281615, 1.1006545000002332],
80.0: [0.35694534788462673, 1.218254500000442],
85.0: [0.34588162637724973, 1.3803990000007813],
90.0: [0.33074026608309903, 1.5936252500012533],
95.0: [0.3100244517367698, 1.742534506609399],
100.0: [0.2843669533532185, 1.527594250001112]
},
# 'Split' configuration with 20dB gain output amplifier
"20dB-RF": {
0.0: [0.29843161833486676, 0.9497827499998844],
5.0: [0.29843161833486676, 0.9497827499998844],
10.0: [0.2954292408072867, 0.8963354999997812],
15.0: [0.2898524641528628, 1.0380145000001055],
20.0: [0.288091132121609, 0.9315334999998103],
25.0: [0.28062153814181334, 0.8928532499997506],
30.0: [0.27788184489732204, 0.9226357499998044],
35.0: [0.27439817394143207, 0.9478427499998815],
40.0: [0.27072234857377697, 0.9971642499999629],
45.0: [0.26687048599970004, 0.9896312385126531],
50.0: [0.2629753759175101, 1.0504742500000472],
55.0: [0.25759932444691014, 1.1231250000002806],
60.0: [0.25024252504432054, 1.159518250000302],
65.0: [0.2407359942538586, 1.2090610000004014],
70.0: [0.2301683715516296, 1.2377840000004743],
75.0: [0.21963268933653324, 1.2827857500005346],
80.0: [0.2100551084541593, 1.2721460000005431],
85.0: [0.2013353570754639, 1.3197920000006356],
90.0: [0.19262088357933807, 1.3904199455549628],
95.0: [0.18215177803763202, 1.4092102467238283],
100.0: [0.1648446272959725, 1.3531790000007144]
},
# 'Split' configuration with 27dB gain output amplifier
"27dB-RF": {
0.0: [0.6290444988502939, 1.8355982500018238],
5.0: [0.6290444988502939, 1.8355982500018238],
10.0: [0.6162244820763687, 1.813328000001766],
15.0: [0.5986628207711835, 1.924673000001964],
20.0: [0.5872994032619674, 1.902532750001955],
25.0: [0.5622986822141188, 1.9695312500021578],
30.0: [0.546359205651584, 1.9074327500019312],
35.0: [0.5272527635559541, 2.0725035000023513],
40.0: [0.5067273124989241, 2.490848250003234],
45.0: [0.4852820838683627, 2.7583545000038576],
50.0: [0.4644882011804289, 2.9444523254740442],
55.0: [0.4410411856605456, 3.338426250005034],
60.0: [0.4165059317542421, 3.3043175000049714],
65.0: [0.3893919363497711, 3.585993500005552],
70.0: [0.3632623088221406, 3.4532177500053955],
75.0: [0.33847455767318946, 3.5242287500054768],
80.0: [0.3161691355503496, 3.391745925001626],
85.0: [0.2955542488495664, 3.2943355000049874],
90.0: [0.27351591268223896, 3.181136000004802],
95.0: [0.24862670588071412, 2.7484420000038305],
100.0: [0.21727926240638096, 2.4840450000031797]
}
}
[docs] @classmethod
def board_models(cls) -> List:
"""Returns a list containing the board model names for which
calibration data is available.
:classmethod:
"""
return list(ad9913.CAL_DATA.keys())
def __init__(self,
cs: str = None,
io_update: str = None,
reset: str = None,
ps0: str = None,
ps1: str = None,
ps2: str = None,
sysclk: float = 250.0,
board_model: str = DEFAULT_BOARD_MODEL) -> None:
"""
:param cs: The AD9913 chip select (``~CS``) controller pin.
:type cs: str
:param io_update: The AD9913 IO update (``IO_UPDATE``) controller pin.
:type io_update: str
:param reset: The AD9913 reset (``MASTER_RESET``) controller pin.
:type reset: str
:param ps0: The AD9913 profile select 0 (``PS0``) controller pin.
:type ps0: str
:param ps1: The AD9913 profile select 1 (``PS1``) controller pin.
:type ps1: str
:param ps2: The AD9913 profile select 2 (``PS2``) controller pin.
:type ps2: str
:param sysclk: The system clock input frequency in MHz.
:type sysclk: float
"""
self.cs = cs.upper() if cs else cs
self.io_update = io_update.upper() if io_update else io_update
self.reset = reset.upper() if reset else reset
self.ps0 = ps0.upper() if ps0 else ps0
self.ps1 = ps1.upper() if ps1 else ps1
self.ps2 = ps2.upper() if ps2 else ps2
self.sysclk = sysclk
self.board_model = board_model
self._ps_port: Optional[str] = None
self._ps_mask: Optional[int] = None
self._ps_pins: Optional[List[int]] = None
ps_pins = [p for p in [self.ps0, self.ps1, self.ps2] if p]
if len(ps_pins):
port_set = set([p[0] for p in ps_pins])
if len(port_set) == 1:
# All ps pins are on the same port
self._ps_port = list(port_set)[0]
self._ps_pins = [int(p[1]) for p in ps_pins]
mask_str = '0b'
for p in range(7, -1, -1):
if p in self._ps_pins:
mask_str += '1'
else:
mask_str += '0'
self._ps_mask = int(mask_str, 2)
# Set the stored value of CFR1 to be the power on default.
# (Since we don't generally have read access to the on chip
# CFR1 register we maintain a copy of the changes we make to
# that register.)
self.cfr1 = ad9913.DEFAULT_CFR1
# The factory default for CFR2 ensures that the DDS clock input
# is differential with PLL disabled.
@property
def ps_port(self) -> Optional[str]:
"""The controller port which the ``PS0``, ``PS1``, ``PS2`` device pins
are assigned. This will be ``None`` if one of more of the pins are
either unassigned to the controller or if the pins are assigned
across multiple controller ports.
Note that this property is read-only.
"""
return self._ps_port
@property
def ps_pins(self) -> Optional[List[int]]:
"""The controller pins within the controller ``ps_port`` to which the
``PS0``, ``PS1``, ``PS2`` device pins are assigned. This will
be ``None`` if one of more of the pins are either unassigned to
the controller or if the pins are assigned across multiple
controller ports.
Note that this property is read-only.
"""
return self._ps_pins
@property
def ps_mask(self) -> Optional[int]:
"""
"""
return self._ps_mask
[docs] def ps_bits(self, sym: int) -> Optional[int]:
"""Convert a symbol to profile select pin specification.
>>> dds = ad9913('C5', 'B7', 'C4', ps0='D0', ps1='D1', ps2='D2',
... board_model='20dB')
>>> hex(dds.ps_bits(1))
'0x1'
>>> hex(dds.ps_bits(7))
'0x7'
>>> dds2 = ad9913('C6', 'B6', 'C4', ps0='D4', ps1='D5', ps2='D6',
... board_model='27dB-RF')
>>> hex(dds2.ps_bits(1))
'0x10'
>>> hex(dds2.ps_bits(7))
'0x70'
"""
pins = self.ps_pins
if pins is None:
return None
b = (sym & 0b0000_0001) << pins[0]
if len(pins) > 1:
b |= (sym & 0b0000_0010) << (pins[1] - 1)
if len(pins) > 2:
b |= (sym & 0b0000_0100) << (pins[2] - 2)
return b
@property
def model(self) -> str:
return self.board_model
@model.setter
def model(self, model: str) -> None:
self.board_model = model
@property
def cal_data(self) -> Dict[float, List[float]]:
"""The calibration data for the DDS board associated with this
AD9913 instance.
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.
"""
return ad9913.CAL_DATA[self.board_model]
def __repr__(self) -> str:
return "{}({!r})".format(self.__class__.__name__, vars(self))
[docs] def pin_config(self) -> str:
"""Initialize controller pin configuration.
:return: A string specifying the commands required to initialize the
connected controller pins.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.pin_config()
'OD1:HD1:OD2:LD2:'
>>> dds2 = ad9913('d3', 'd4', ps0='c5', ps1='c6', ps2='c7', reset='b1')
>>> dds2.pin_config()
'OD3:HD3:OD4:LD4:OB1:LB1:OC5:LC5:OC6:LC6:OC7:LC7:'
"""
cmd = ''
if self.cs:
# Chip select is active low.
cmd += 'O{}:H{}:'.format(self.cs, self.cs)
if self.io_update:
cmd += 'O{}:L{}:'.format(self.io_update, self.io_update)
if self.reset:
cmd += 'O{}:L{}:'.format(self.reset, self.reset)
if self.ps0:
cmd += 'O{}:L{}:'.format(self.ps0, self.ps0)
if self.ps1:
cmd += 'O{}:L{}:'.format(self.ps1, self.ps1)
if self.ps2:
cmd += 'O{}:L{}:'.format(self.ps2, self.ps2)
return cmd
[docs] def chip_reset(self) -> str:
"""Reset the chip internal logic to default states.
:return: A string containing the controller commands required to reset
the chip.
>>> dds2 = ad9913('d3', 'd4', ps0='c5', ps1='c6', ps2='c7', reset='b1')
>>> dds2.chip_reset()
'HB1:LB1:'
"""
if self.reset:
self.cfr1 = ad9913.DEFAULT_CFR1
return 'H{}:L{}:'.format(self.reset, self.reset)
else:
return ''
[docs] def power_control(self,
mode: int,
subsystem: Subsystem) -> str:
"""Power down some or all of the DDS circuitry.
:param mode: Specify `ad9913.POWER_DOWN` or `ad9913.POWER_UP`.
:type mode: int
:param subsystem: Specify which DDS functions to power down:
- ``Subsystem.DIGITAL`` (``CFR1[6]``)
- ``Subsystem.DAC`` (``CFR1[5]``)
- ``Subsystem.INPUT_CLOCK`` (``CFR1[4]``)
- ``Subsystem.ALL`` (``CFR1[6..4]``)
:type subsystem: ad9913.Subsystem
:return: A string containing the commands required to power down/up the
specified DDS functions.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds2 = ad9913('d3', 'd4', ps0='c5', ps1='c6', ps2='c7', reset='b1')
>>> dds1.power_control(ad9913.POWER_DOWN, ad9913.Subsystem.DAC)
'LD1:W00,00,00,00,20:HD1:HD2:LD2:'
>>> dds1.power_control(ad9913.POWER_DOWN, ad9913.Subsystem.DIGITAL)
'LD1:W00,00,00,00,60:HD1:HD2:LD2:'
>>> dds1.power_control(ad9913.POWER_DOWN, ad9913.Subsystem.INPUT_CLOCK)
'LD1:W00,00,00,00,70:HD1:HD2:LD2:'
>>> dds1.power_control(ad9913.POWER_UP, ad9913.Subsystem.ALL)
'LD1:W00,00,00,00,00:HD1:HD2:LD2:'
>>> dds1.power_control(ad9913.POWER_DOWN,ad9913.Subsystem.DAC)
'LD1:W00,00,00,00,20:HD1:HD2:LD2:'
>>> dds1.power_control(ad9913.POWER_UP, ad9913.Subsystem.DAC)
'LD1:W00,00,00,00,00:HD1:HD2:LD2:'
>>> dds2.power_control(ad9913.POWER_UP, ad9913.Subsystem.ALL)
'LD3:W00,00,00,00,00:HD3:HD4:LD4:'
"""
# CFR1 is 32 bits
if mode == ad9913.POWER_DOWN:
self.cfr1 |= subsystem
else:
self.cfr1 &= (~subsystem)
return self.config_cfr1()
[docs] def tuning_word(self,
fout: float) -> int:
"""Calculate the DDS tuning word for a specified output frequency.
:param fout: The desired output frequency in MHz.
:type fout: float
:return: An integer containing the 32-bit tuning word.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds2 = ad9913('d1', 'd2', sysclk=200.0)
>>> hex(dds1.tuning_word(50.0))
'0x33333333'
>>> hex(dds2.tuning_word(50.0))
'0x40000000'
>>> hex(dds1.tuning_word(10.0))
'0xa3d70a4'
>>> hex(dds1.tuning_word(5.0))
'0x51eb852'
>>> hex(dds1.tuning_word(1.0))
'0x10624dd'
>>> hex(dds1.tuning_word(0.00001))
'0xac'
>>> hex(dds1.tuning_word(0.0005821))
'0x2710'
>>> hex(dds1.tuning_word(0.00058211))
'0x2711'
"""
return round((1 << 32) * (fout / self.sysclk))
[docs] def phase_offset_word(self,
ph_degrees: float) -> int:
"""Calculate the DDS phase offset word for a specified phase offset.
:param ph_degrees: The desired phase offset in degrees.
:type ph_degrees: float
:return: An integer containing the 14-bit phase offset word.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> hex(dds1.phase_offset_word(180.0))
'0x2000'
>>> hex(dds1.phase_offset_word(90.0))
'0x1000'
>>> hex(dds1.phase_offset_word(45.0))
'0x800'
>>> hex(dds1.phase_offset_word(1.0))
'0x2d'
>>> hex(dds1.phase_offset_word(0.1))
'0x4'
>>> hex(dds1.phase_offset_word(359))
'0x3fd2'
>>> hex(dds1.phase_offset_word(359.9))
'0x3ffb'
>>> hex(dds1.phase_offset_word(900.0))
'0x2000'
"""
return int(math.modf(ph_degrees / 360.0)[0] * (1 << 14))
[docs] def modulus_parameters(self,
fout: float) -> Tuple[int, int, int]:
"""Calculate the DDS modulus parameters for a specified output frequency.
:param fout: The desired output frequency in MHz.
:type fout: float
:return: A tuple containing the modulus parameters: `(A, B, X)` or
`None` if the specified output frequency cannot be produced.
:raises: ModulusConstraintException
If the specified frequency cannot be exactly synthesized
.. note::
Note that if `A == 0` then the output frequency can be produced
using the more usual accumulator based method (that is, the
modulus feature is unnecessary).
See Analog Devices application note AN-953 *Direct Digital
Synthesis (DDS) with a Programmable Modulus*
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.modulus_parameters(25.0)
(3, 5, 429496729)
>>> dds1.modulus_parameters(5.0)
(23, 25, 85899345)
>>> dds1.modulus_parameters(5.01)
(1912, 3125, 86071144)
>>> dds1.modulus_parameters(5.00001)
(280739, 390625, 85899517)
>>> dds1.modulus_parameters(5.00000001)
Traceback (most recent call last):
...
ModulusConstraintException
>>> dds1.modulus_parameters(0.0005821)
(3924336, 9765625, 10000)
"""
numerator = Decimal(str(fout))
denominator = Decimal(str(self.sysclk))
while True:
if numerator % 10 == 0 and denominator % 10 == 0:
break
else:
numerator *= 10
denominator *= 10
m = int(numerator)
n = int(denominator)
divisor = math.gcd(m, n)
m = round(m / divisor)
n = round(n / divisor)
if n > (1 << 32):
frem, _ = math.modf(n / (1 << 32))
print(" frem, _: {}, {}".format(frem, _))
if frem != 0.0:
raise ModulusConstraintException
_, fx = math.modf((m * (1 << 32)) / n)
x = int(fx)
y = int((m * (1 << 32)) - x*n)
divisor = math.gcd(int(n), y)
a = int(y / divisor)
b = int(n / divisor)
if (b <= 0) or (a >= b):
raise ModulusConstraintException
return a, b, int(x)
[docs] def config_tuning_word(self,
tuning_word: int,
update: bool = True) -> str:
"""Configure and update the current device tuning word.
:param tuning_word: The DDS tuning word for the desired output
frequency.
:type tuning_word: 32-bit integer
:return: The command string required to configure the current device
tuning word and update the associated register.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.config_tuning_word(dds1.tuning_word(50.0))
'LD1:W03,33,33,33,33:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(10.0))
'LD1:W03,0A,3D,70,A4:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(5.0))
'LD1:W03,05,1E,B8,52:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(1.0))
'LD1:W03,01,06,24,DD:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(0.1))
'LD1:W03,00,1A,36,E3:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(0.001))
'LD1:W03,00,00,43,1C:HD1:HD2:LD2:'
>>> dds1.config_tuning_word(dds1.tuning_word(0.00001))
'LD1:W03,00,00,00,AC:HD1:HD2:LD2:'
"""
tstr = "{:08X}".format(tuning_word)
# The frequency tuning word register is register 3.
cmd = 'L{}:W03,{},{},{},{}:H{}:'.format(
self.cs, tstr[0:2], tstr[2:4], tstr[4:6], tstr[6:8], self.cs)
if update:
cmd += 'H{}:L{}:'.format(self.io_update, self.io_update)
return cmd
[docs] def config_phase_offset(self,
phoffset_word: int,
update: bool = True) -> str:
"""Configure and update the current device phase offset.
:param phoffset_word: The DDS phase offset word for the desired phase
offset.
:type phoffset_word: 14-bit integer
:return: The command string required to configure the current device
phase offset and update the associated register.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.config_phase_offset(dds1.phase_offset_word(180.0))
'LD1:W04,20,00:HD1:HD2:LD2:'
>>> dds1.config_phase_offset(dds1.phase_offset_word(90.0))
'LD1:W04,10,00:HD1:HD2:LD2:'
>>> dds1.config_phase_offset(dds1.phase_offset_word(45.0))
'LD1:W04,08,00:HD1:HD2:LD2:'
>>> dds1.config_phase_offset(dds1.phase_offset_word(1.0))
'LD1:W04,00,2D:HD1:HD2:LD2:'
>>> dds1.config_phase_offset(dds1.phase_offset_word(0.1))
'LD1:W04,00,04:HD1:HD2:LD2:'
"""
if phoffset_word > ad9913.MAX_PHOFFSET:
phoffset_word = ad9913.MAX_PHOFFSET
if phoffset_word < 0:
phoffset_word = 0
phstr = "{:04X}".format(phoffset_word)
# The phase offset register is register 4.
cmd = 'L{}:W04,{},{}:H{}:'.format(
self.cs, phstr[0:2], phstr[2:4], self.cs)
if update:
cmd += 'H{}:L{}:'.format(self.io_update, self.io_update)
return cmd
[docs] def config_output_level(self,
level: int,
update: bool = True) -> str:
"""Configure and update the current device output level.
:param level: A 10 bit value specifying the full scale DAC output
current.
:type level: integer
:return: The command string required to configure and update the
current device full scale DAC output current.
:rtype: str
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.config_output_level(1023)
'LD1:W02,00,7f,13,FF:HD1:HD2:LD2:'
>>> dds1.config_output_level(511)
'LD1:W02,00,7f,11,FF:HD1:HD2:LD2:'
>>> dds1.config_output_level(15)
'LD1:W02,00,7f,10,0F:HD1:HD2:LD2:'
>>> dds1.config_output_level(4095)
'LD1:W02,00,7f,13,FF:HD1:HD2:LD2:'
>>> dds1.config_output_level(-20)
'LD1:W02,00,7f,10,01:HD1:HD2:LD2:'
"""
#
# Typical output full scale current is:
#
# Iout(x, Rset) = 0.0206/Rset * (1+x)
#
# In the present design Rset is 4.64k and the full scale
# current (x=1023) is:
#
# Iout = 4.55 mA
#
# Depending on the DAC output circuit topology, the
# full scale output current is therefore changed in steps
# of 4.55/1024 = 4.44 uA.
#
if level > ad9913.MAX_LEVEL:
level = ad9913.MAX_LEVEL
if level < 1:
level = 1
lstr = "{:04X}".format(0x1000 + level)
# The DAC control register is register 2.
# It has default, reserved values in the upper 22 bits.
# They are 0b0000_0000_0111_1111_0001_00.
cmd = 'L{}:W02,00,7f,{},{}:H{}:'.format(
self.cs, lstr[0:2], lstr[2:4], self.cs)
if update:
cmd += 'H{}:L{}:'.format(self.io_update, self.io_update)
return cmd
[docs] def config_profile(self,
prof: int,
tuning_word: int,
phoffset_word: int = 0x0) -> str:
"""Configure and update the tuning word for the specified DDS profile.
:param prof: The number of the DDS profile to update.
(valid profiles are 0..7)
:type prof: integer
:param tuning_word: The DDS tuning word for the desired profile output
frequency.
:type tuning_word: 32-bit integer
:param phoffset_word: The phase offset word for the desired profile
phase offset.
:type phoffset_word: 14-bit integer
:return: The command string required to configure the tuning
word and phase offset for the specified profile and update
the associated register.
>>> dds2 = ad9913('d3', 'd4', ps0='c5', ps1='c6', ps2='c7', sysclk=250.0)
>>> baseFreq = 10.0
>>> for prof in range(7):
... outFreq = baseFreq + prof
... phOffset = prof * 15.0
... dds2.config_profile(prof, dds2.tuning_word(outFreq))
...
'LD3:W09,00,00,0A,3D,70,A4:HD3:HD4:LD4:'
'LD3:W0A,00,00,0B,43,95,81:HD3:HD4:LD4:'
'LD3:W0B,00,00,0C,49,BA,5E:HD3:HD4:LD4:'
'LD3:W0C,00,00,0D,4F,DF,3B:HD3:HD4:LD4:'
'LD3:W0D,00,00,0E,56,04,19:HD3:HD4:LD4:'
'LD3:W0E,00,00,0F,5C,28,F6:HD3:HD4:LD4:'
'LD3:W0F,00,00,10,62,4D,D3:HD3:HD4:LD4:'
"""
preg = self._profile_reg(prof)
tstr = "{:08X}".format(tuning_word)
if phoffset_word > ad9913.MAX_PHOFFSET:
phoffset_word = ad9913.MAX_PHOFFSET
if phoffset_word < 0:
phoffset_word = 0
phstr = "{:04X}".format(phoffset_word)
cmd = 'L{}:W{:02X},{},{},{},{},{},{}:H{}:H{}:L{}:'.format(
self.cs, preg,
phstr[0:2], phstr[2:4],
tstr[0:2], tstr[2:4], tstr[4:6], tstr[6:8],
self.cs, self.io_update, self.io_update)
return cmd
[docs] def set_active_profile(self,
prof: int) -> None:
"""Set the active profile.
:param prof: The number of the DDS profile to update.
(valid profiles are 0..7)
:type prof: integer
"""
# Clear any previous profile setting
self.cfr1 &= 0xff8f_ffff # ~(0b11 << 20)
# Set the specified sweep type
self.cfr1 |= (prof << 20)
[docs] def set_profile_control(self,
ctl: ProfileControl) -> None:
"""Set direct switch profile control method.
:param ctl: Whether profile is controlled by pins or internally
(via SPI)
:type ctl: ad9913.ProfileControl
"""
# Clear any previous profile control setting
self.cfr1 &= 0xf7ff_ffff # ~(0b1 << 27)
# Set the specified profile control type
self.cfr1 |= ctl
[docs] def config_modulus_params(self,
a: int,
b: int,
x: int) -> str:
"""Configure the modulus parameter registers, update the output frequency.
:param a: The A value as calculated by modulus_parameters.
:type a: 32-bit integer
:param b: The B value as calculated by modulus_parameters.
:type b: 32-bit integer
:param x: The X value as calculated by modulus_parameters.
:type x: 32-bit integer
:return: The command string required to configure and update the
modulus parameter registers.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> dds1.config_modulus_params(*dds1.modulus_parameters(25.0))
'LD1:W06,00,00,00,05,19,99,99,99:HD1:LD1:W07,00,00,00,00,00,00,00,03:HD1:HD2:LD2:'
>>> dds1.config_modulus_params(*dds1.modulus_parameters(0.5820761))
'LD1:W06,00,95,02,F9,00,98,96,77:HD1:LD1:W07,00,00,00,00,00,25,B8,41:HD1:HD2:LD2:'
"""
astr = "{:08X}".format(a)
bstr = "{:08X}".format(b)
xstr = "{:08X}".format(x)
cmd = 'L{}:W06,{},{},{},{},{},{},{},{}:H{}:'.format(
self.cs, bstr[0:2], bstr[2:4], bstr[4:6], bstr[6:8],
xstr[0:2], xstr[2:4], xstr[4:6], xstr[6:8], self.cs)
cmd += 'L{}:W07,00,00,00,00,{},{},{},{}:H{}:H{}:L{}:'.format(
self.cs, astr[0:2], astr[2:4], astr[4:6], astr[6:8], self.cs,
self.io_update, self.io_update)
return cmd
[docs] def config_sweep_limits(self,
sweep_type: SweepType,
start: float,
end: float,
falling_delta: float,
rising_delta: float) -> str:
"""Configure sweep limits and deltas.
:param sweep_type: The sweep type. Either `SweepType.FREQUENCY` or
`SweepType.PHASE`
:type sweep_type: ad9913.SweepType
:param start: The starting point of the sweep. This is specified
as MHz for a `FREQUENCY` sweep or as degrees for
a `PHASE` sweep.
:type start: float
:param end: The end point of the sweep. This is specified
as MHz for a `FREQUENCY` sweep or as degrees for
a `PHASE` sweep.
:type end: float
:param falling_delta: The falling delta tuning step. This is specified
as MHz for a `FREQUENCY` sweep or as degrees for
a `PHASE` sweep.
:type falling_delta: float
:param rising_delta: The rising delta tuning step. This is specified
as MHz for a `FREQUENCY` sweep or as degrees for
a `PHASE` sweep.
:type rising_delta: float
:return: The command string required to configure and update the
sweep limit and delta registers.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> sweepStart = 1.0
>>> sweepEnd = 20.0
>>> sweepDelta = 0.1
>>> dds1.config_sweep_limits(
... ad9913.SweepType.FREQUENCY,
... sweepStart, sweepEnd,
... sweepDelta, sweepDelta)
'LD1:W06,14,7A,E1,48,01,06,24,DD:HD1:LD1:W07,00,1A,36,E3,00,1A,36,E3:HD1:HD2:LD2:'
>>> sweepStart = 0.0
>>> sweepEnd = 115.0
>>> sweepDelta = 1.0
>>> dds1.config_sweep_limits(
... ad9913.SweepType.PHASE,
... sweepStart, sweepEnd,
... sweepDelta, sweepDelta)
'LD1:W06,51,C4,00,00,00,00,00,00:HD1:LD1:W07,00,00,00,2E,00,00,00,2E:HD1:HD2:LD2:'
"""
if start > end:
start, end = end, start
if sweep_type == ad9913.SweepType.FREQUENCY:
s0 = self.tuning_word(start)
e0 = self.tuning_word(end)
max_delta = ad9913.FREQ_SWEEP_FULL_SCALE - e0
fdw = round((falling_delta/self.sysclk) * (1 << 32))
rdw = round((rising_delta/self.sysclk) * (1 << 32))
if fdw > max_delta:
fdw = max_delta - 1
if rdw > max_delta:
rdw = max_delta - 1
else:
s0 = self.phase_offset_word(start)
e0 = self.phase_offset_word(end)
max_delta = ad9913.PHASE_SWEEP_FULL_SCALE - e0
fdw = round((falling_delta/45.0) * (1 << 11))
rdw = round((rising_delta/45.0) * (1 << 11))
if fdw > max_delta:
fdw = max_delta - 1
if rdw > max_delta:
rdw = max_delta - 1
# For PHASE sweeps the start and end phase offset words
# must be MSB-aligned
s0 <<= 18
e0 <<= 18
s0str = "{:08X}".format(s0)
e0str = "{:08X}".format(e0)
cmd = 'L{}:W06,{},{},{},{},{},{},{},{}:H{}:'.format(
self.cs, e0str[0:2], e0str[2:4], e0str[4:6], e0str[6:8],
s0str[0:2], s0str[2:4], s0str[4:6], s0str[6:8],
self.cs)
fdw_str = "{:08X}".format(fdw)
rdw_str = "{:08X}".format(rdw)
cmd += 'L{}:W07,{},{},{},{},{},{},{},{}:H{}:H{}:L{}:'.format(
self.cs, fdw_str[0:2], fdw_str[2:4], fdw_str[4:6], fdw_str[6:8],
rdw_str[0:2], rdw_str[2:4], rdw_str[4:6], rdw_str[6:8],
self.cs, self.io_update, self.io_update)
return cmd
[docs] def config_sweep_rates(self,
falling: float,
rising: float) -> str:
"""Configure the sweep ramp rates.
:param falling: The falling sweep ramp rate in seconds.
:type falling: float
:param rising: The rising sweep ramp rate in seconds.
:type rising: float
:return: The command string required to configure and update the
sweep rate registers.
.. 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.
>>> dds1 = ad9913('d1', 'd2', sysclk=250.0)
>>> risingSweepRate = 1e-6
>>> fallingSweepRate = 2e-6
>>> dds1.config_sweep_rates(fallingSweepRate, risingSweepRate)
'LD1:W08,01,F4,00,FA:HD1:HD2:LD2:'
>>> risingSweepRate = 10e-9
>>> fallingSweepRate = 20e-9
>>> dds1.config_sweep_rates(fallingSweepRate, risingSweepRate)
'LD1:W08,00,05,00,02:HD1:HD2:LD2:'
"""
fsrr = round(falling * self.sysclk * 1e6)
rsrr = round(rising * self.sysclk * 1e6)
fsrr_str = "{:04X}".format(fsrr)
rsrr_str = "{:04X}".format(rsrr)
cmd = 'L{}:W08,{},{},{},{}:H{}:H{}:L{}:'.format(
self.cs, fsrr_str[0:2], fsrr_str[2:4],
rsrr_str[0:2], rsrr_str[2:4],
self.cs, self.io_update, self.io_update)
return cmd
[docs] def set_sweep_type(self,
sweep_type: SweepType) -> None:
"""Set the sweep type.
:param sweep_type: The sweep type to configure. This will be one of:
- `SweepType.FREQUENCY`
- `SweepType.PHASE`
:type sweep_type: ad9913.SweepType
"""
# Clear any previous sweep type setting
self.cfr1 &= 0xffff_cfff # ~(0b11 << 12)
# Set the specified sweep type
self.cfr1 |= int(sweep_type)
[docs] def set_sweep_mode(self,
sweep_mode: SweepRampType) -> None:
"""Set the sweep ramp type.
:param sweep_mode: The sweep ramp type to configure.
This will be one of:
- ``SweepRampType.SWEEP_OFF``
- ``SweepRampType.RAMP_UP``
- ``SweepRampType.RAMP_DOWN``
- ``SweepRampType.RAMP_BIDIR``
:type sweep_mode: ad9913.SweepRampType
>>> dds1 = ad9913('d1', 'd2')
>>> dds1.set_sweep_mode(ad9913.SweepRampType.RAMP_UP)
>>> dds1.config_cfr1()
'LD1:W00,00,10,00,00:HD1:HD2:LD2:'
"""
# Clear any previous ramp type setting
self.cfr1 &= 0xffcf_ffff # ~(0b11 << 20) or 0x0030_0000
# Set the specified ramp type
self.cfr1 |= int(sweep_mode)
[docs] def set_sweep_dwell(self,
dwell_active: bool = False) -> None:
"""Set the sweep dwell state.
:param dwell_active: If set to False dwell is de-activated and
the device reverts to it's initial state after the
completion of a sweep. If set to True, the dwell is
active and the device will hold at the final state after
completion of the sweep.
:type dwell_active: bool
"""
if dwell_active is True:
self.cfr1 &= 0xffff_feff # ~(1<<8) or ~0x0000_0100
else:
self.cfr1 |= (1 << 8)
[docs] def set_sweep_trigger_source(self,
trigger_source: SweepTriggerSource) -> None:
"""Set the sweep trigger source.
:param trigger_source: Set the device to trigger sweep using either the
profile pins or the ``CFR1[22:20]`` register bits.
:type trigger_source: ad9913.SweepTriggerSource
>>> dds1 = ad9913('d1', 'd2')
>>> dds1.set_sweep_trigger_source(ad9913.SweepTriggerSource.PROFILE_PINS)
>>> dds1.config_cfr1()
'LD1:W00,00,00,00,00:HD1:HD2:LD2:'
>>> dds1.set_sweep_trigger_source(ad9913.SweepTriggerSource.REGISTER)
>>> dds1.config_cfr1()
'LD1:W00,08,00,00,00:HD1:HD2:LD2:'
"""
if trigger_source == ad9913.SweepTriggerSource.REGISTER:
self.cfr1 |= int(trigger_source)
else:
self.cfr1 &= 0xf7ff_ffff # ~(0b1<<27) or ~0x0800_0000
[docs] def set_sweep_trigger_type(self,
trigger_type: SweepTriggerType) -> None:
"""Set the sweep trigger type.
:param trigger_type:
:type trigger_type: ad9913.SweepTriggerType
"""
if trigger_type == ad9913.SweepTriggerType.EDGE_TRIGGER:
self.cfr1 &= 0xffff_fdff # ~(0b1<<9) or ~0x0000_0200
else:
self.cfr1 |= int(ad9913.SweepTriggerType.STATE_TRIGGER)
[docs] def set_aux_accumulator_enable(self,
enable: bool) -> None:
"""
>>> dds1 = ad9913('d1', 'd2')
>>> dds1.set_aux_accumulator_enable(True)
>>> dds1.config_cfr1()
'LD1:W00,00,00,08,00:HD1:HD2:LD2:'
>>> dds1.set_aux_accumulator_enable(False)
>>> dds1.config_cfr1()
'LD1:W00,00,00,00,00:HD1:HD2:LD2:'
"""
if enable:
self.cfr1 |= (1 << 11)
else:
self.cfr1 &= 0xffff_f7ff # ~(1<<11) or ~0x0000_0x0800
def set_modulus_enable(self,
enable: bool) -> None:
if enable:
self.cfr1 |= (1 << 28)
self.cfr1 &= 0xffff_cfff # clear CFR1 [13:12]
else:
self.cfr1 &= 0xefff_ffff # ~(1<<28)
def set_direct_switch_mode_enable(self,
enable: bool) -> None:
if enable:
self.cfr1 |= (1 << 16)
else:
self.cfr1 &= 0xfffe_ffff # ~(1<<16)
def set_clear_phase_accumulator(self,
enable: bool) -> None:
if enable:
self.cfr1 |= (1 << 14)
else:
self.cfr1 &= 0xffff_bfff # ~(1<<14)
[docs] def config_sweep_params(
self,
sweep_type: SweepType,
start: float,
end: float,
falling_delta: float,
rising_delta: float,
falling_rate: float,
rising_rate: float,
dwell: bool = False,
trigger_source: SweepTriggerSource = SweepTriggerSource.REGISTER,
trigger_type: SweepTriggerType = SweepTriggerType.STATE_TRIGGER
) -> str:
"""Configure sweep parameters.
:param sweep_type: The sweep type. Either ``SweepType.FREQUENCY`` or
``SweepType.PHASE``
:type sweep_type: ad9913.SweepType
:param start: The starting point of the sweep. This is specified
as MHz for a ``FREQUENCY`` sweep or as degrees for
a ``PHASE`` sweep.
:type start: float
:param end: The end point of the sweep. This is specified
as MHz for a ``FREQUENCY`` sweep or as degrees for
a ``PHASE`` sweep.
:type end: float
:param falling_delta: The falling delta tuning step. This is specified
as MHz for a ``FREQUENCY`` sweep or as degrees for
a ``PHASE`` sweep.
:type falling_delta: float
:param rising_delta: The rising delta tuning step. This is specified
as MHz for a ``FREQUENCY`` sweep or as degrees for
a ``PHASE`` sweep.
:type rising_delta: float
:param falling_rate: The falling sweep ramp rate in seconds.
:type falling_rate: float
:param rising_rate: The rising sweep ramp rate in seconds.
:type rising_rate: float
:param dwell: If set to False dwell is de-activated and the device
reverts to it's initial state after the completion
of a sweep. If set to True, the dwell is active
and the device will hold at the final state after
completion of the sweep.
:type dwell: bool
:param trigger_source: Set the device to trigger sweep using either the
profile pins or the ``CFR1[22:20]`` register bits.
The default is to use the ``CFR1[22:20]`` register bits
as the trigger source.
:type trigger_source: ad9913.SweepTriggerSource
:param trigger_type: Set the device to trigger either on an EDGE
transition or when the profile pins/CFR1[22:20] bits are in
a specified STATE.
The default is to use STATE_TRIGGER.
:type trigger_type: ad9913.SweepTriggerType
:return: The command string required to configure the sweep parameter
registers.
>>> dds1 = ad9913('d1', 'd2')
>>> sweepStart = 1.0
>>> sweepEnd = 2.0
>>> sweepDelta = 0.1
>>> sweepRate = 1e-6
>>> dds1.config_sweep_params(
... ad9913.SweepType.FREQUENCY,
... sweepStart, sweepEnd,
... sweepDelta, sweepDelta,
... sweepRate, sweepRate)
'LD1:W06,02,0C,49,BA,01,06,24,DD:HD1:LD1:W07,00,1A,36,E3,00,1A,36,E3:HD1:HD2:LD2:LD1:W08,00,FA,00,FA:HD1:HD2:LD2:LD1:W00,08,00,03,00:HD1:HD2:LD2:'
"""
cmd = self.config_sweep_limits(sweep_type, start, end,
falling_delta, rising_delta)
cmd += self.config_sweep_rates(falling_rate, rising_rate)
self.set_sweep_dwell(dwell)
self.set_sweep_trigger_source(trigger_source)
self.set_sweep_trigger_type(trigger_type)
cmd += self.config_cfr1()
return cmd
[docs] def config_cfr1(self,
update: bool = True) -> str:
"""Configure the CFR1 register value.
:param update: Set to True if the CFR1 should be updated immediately,
False if the value should be buffered only.
:type update: bool
This will configure and optionally update the device CFR1 register using
the current value stored in `self.cfr1`.
:return: The command string required to set the device CFR1 register.
"""
sval = '{:08X}'.format(self.cfr1)
cmd = 'L{}:W00,{},{},{},{}:H{}:'.format(
self.cs, sval[0:2], sval[2:4], sval[4:6], sval[6:8], self.cs)
if update:
cmd += 'H{}:L{}:'.format(self.io_update, self.io_update)
return cmd
def _profile_reg(self,
prof: int) -> int:
"""Calculate the DDS profile register for a specified profile.
:return: The profile register number for the specified profile.
:rtype: int
"""
if prof in range(8):
return 0x09 + prof
else:
return 0x09
if __name__ == '__main__':
import doctest
doctest.testmod()