Source code for rfblocks.ad9913

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