Source code for rfblocks.ad9552

# `ad9552` - Encapsulates control of the AD9552 clock generator.
#
#    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 (
    Tuple
)
import math
from enum import IntFlag


class DividerRangeException(Exception):
    """Raised when a requested output frequency is out of range.
    """
    pass


[docs]class ad9552(object): """Encapsulates control of the AD9552 clock generator. Documentation for the clock generator rfblocks module which uses the AD9552 can be found here: `A Low Jitter Variable Rate Clock Generator to 1000 MHz <../boards/AD9552-ClockSource.html>`_ """ VCO_MIN = 3350 "The minimum internal VCO frequency" VCO_MAX = 4050 "The maximum internal VCO frequency" P0_VALUES = [n for n in range(4, 12)] "Legal P0 output divider values" P1_VALUES = [n for n in range(1, 64)] "Legal P1 output divider values" OUT1 = 1 "Clock channel 1 identifier" OUT2 = 2 "Clock channel 2 identifier"
[docs] class ReferenceSource(IntFlag): """An `enum` containing the possible settings for the reference source. (``ad9552.ReferenceSource.INTERNAL``, ``ad9552.ReferenceSource.EXTERNAL``) """ INTERNAL = 0b00 EXTERNAL = 0b01
[docs] class OutputState(IntFlag): """An ``enum`` containing the possible clock output states. (``ad9552.OutputState.ACTIVE``, ``ad9552.OutputState.POWERED_DOWN``) """ ACTIVE = 0b0, POWERED_DOWN = 0b1
[docs] class OutputMode(IntFlag): """An ``enum`` containing the possible clock output modes. (``ad9552.OutputMode.CMOS_BOTH_ACTIVE``, ``ad9552.OutputMode.CMOS_POS_ACTIVE``, ``ad9552.OutputMode.CMOS_NEG_ACTIVE``, ``ad9552.OutputMode.CMOS_TRISTATE``, ``ad9552.OutputMode.LVDS``, ``ad9552.OutputMode.LVPECL``) """ CMOS_BOTH_ACTIVE = 0b000, CMOS_POS_ACTIVE = 0b001, CMOS_NEG_ACTIVE = 0b010, CMOS_TRISTATE = 0b011, LVDS = 0b100, LVPECL = 0b101
[docs] class DriveStrength(IntFlag): """An `enum` containing the possible clock drive strengths. (``ad9552.DriveStrength.WEAK``, ``ad9552.DriveStrength.STRONG``) """ WEAK = 0b0, STRONG = 0b1
[docs] class CmosPolarity(IntFlag): """An `enum` containing the possible CMOS clock mode polarities. (``ad9552.CmosPolarity.DIFF_POS``, ``ad9552.CmosPolarity.COMM_POS``, ``ad9552.CmosPolarity.COMM_NEG``, ``ad9552.CmosPolarity.DIFF_NEG``) """ DIFF_POS = 0b00, COMM_POS = 0b01, COMM_NEG = 0b10, DIFF_NEG = 0b11
[docs] class OutputModeControl(IntFlag): """An `enum` containing the possible clock output mode control types. (``ad9552.OutputModeControl.PIN``, ``ad9552.OutputModeControl.SPI``) """ PIN = 0b0, SPI = 0b1
[docs] class SourceControl(IntFlag): PLL = 0b0, REF = 0b1
def __init__(self, cs: str = None, lockdetect: str = None, reset: str = None, fref: float = 20.0, refselect: str = None, refsrc: ReferenceSource = ReferenceSource.INTERNAL) -> None: """ :param cs: The AD9552 chip select (`~CS`) controller pin. :type cs: str, optional :param lockdetect: The AD9552 lock detect (`LOCKED`) controller pin. :type lockdetect: str, optional :param reset: The AD9552 reset (`RESET`) controller pin. :type reset: str, optional :param fref: The input reference frequency in MHz. :type fref: float :param refselect: Select either external or on board reference source. This facility is only available on version 3 (or greater) of the board hardware. :type refselect: str, optional """ self.cs = cs.upper() if cs else cs self.lockdetect = lockdetect.upper() if lockdetect else lockdetect self.reset = reset.upper() if reset else reset self.fref = fref self.refselect = refselect.upper() if refselect else refselect self._refsrc = refsrc def __repr__(self) -> str: return "{}({!r})".format(self.__class__.__name__, vars(self))
[docs] def pin_config(self) -> str: """Initialize controller pin configuration. :return: The command string required to configure the device controller pins. >>> clk1 = ad9552('d0', 'c4', 'c5', fref=26.0) >>> clk1.pin_config() 'OD0:HD0:IC4:OC5:LC5:' """ cmd = '' if self.cs: cmd += 'O{}:H{}:'.format(self.cs, self.cs) if self.lockdetect: cmd += 'I{}:'.format(self.lockdetect) if self.reset: cmd += 'O{}:L{}:'.format(self.reset, self.reset) cmd += self.config_refsrc() return cmd
[docs] def chip_reset(self) -> str: """Reset the chip internal logic to default states. :return: The command string required to reset the device or the empty string if there is no device reset pin connected to the controller. >>> clk2 = ad9552('d2', 'c6', 'c7', fref=20.0) >>> clk2.chip_reset() 'HC7:LC7:' """ if self.reset: return 'H{}:L{}:'.format(self.reset, self.reset) else: return ''
@property def refsrc(self) -> ReferenceSource: """The current reference source. """ return self._refsrc @refsrc.setter def refsrc(self, src: ReferenceSource) -> None: self._refsrc = src
[docs] def config_refsrc(self) -> str: """Set the state of the reference select controller pin based on whether an external reference source is being used. :return: The command string required to set the state of the reference select controller pin. >>> clk1 = ad9552('d0', 'c4', 'c5', refselect='b1') >>> clk1.config_refsrc() 'OB1:HB1:' >>> clk1.refsrc = ad9552.ReferenceSource.EXTERNAL >>> clk1.config_refsrc() 'OB1:LB1:' """ cmd = "" if self.refselect is not None: if self.refsrc == ad9552.ReferenceSource.EXTERNAL: cmd = 'O{}:L{}:'.format(self.refselect, self.refselect) else: cmd = 'O{}:H{}:'.format(self.refselect, self.refselect) return cmd
[docs] def divider_values( self, fout: float) -> Tuple[int, int, float, float, int, int]: """Calculate the required device divider values. :param fout: The desired output frequency (in MHz) :type fout: float :return: A tuple containing: ``(K, N, FRAC, MOD, P0, P1)`` or ``None`` if there was no divider solution obtained for the specified output frequency. See the sections *Output/Input Frequency Relationship* and *Calculating Divider Values* in the AD9552 datasheet (Rev E., pp17-18). ``K`` : Reference frequency multiplier (either 1 or 2) ``N`` : The 8-bit integer divide value for the SDM. Note that operational limitations impose a lower boundary of 64 (0x40) on N. ``FRAC`` : The 20-bit fractional part of the SDM. ``MOD`` : The 20-bit modulus of the SDM. ``P0, P1`` : Output divider values >>> clk1 = ad9552('d0', 'c4', 'c5') >>> clk1.divider_values(50.0) (1, 170, 0, 1048575, 4, 17) >>> clk1.divider_values(125.0) (1, 168, 786429, 1048572, 9, 3) >>> clk1.divider_values(250.0) (1, 175, 0, 1048575, 7, 2) >>> clk1.divider_values(644.53125) (1, 193, 376809, 1048512, 6, 1) >>> clk2 = ad9552('d2', 'c6', 'c7', fref=10.0) >>> clk2.divider_values(50.0) (2, 170, 0, 1048575, 4, 17) >>> clk2.divider_values(11.0) (2, 167, 786429, 1048572, 5, 61) >>> clk2.divider_values(12.0) Traceback (most recent call last): ... DividerRangeException: No valid P1 value """ from math import ceil k = 1 if self.fref < 20.0: k = 2 odf_min = ad9552.VCO_MIN / fout odf_max = ad9552.VCO_MAX / fout odf_values = [n for n in range(int(ceil(odf_min)), int(odf_max)+1)] if len(odf_values) == 0: raise DividerRangeException( "No valid output divide factors: {:.2f}, {:.2f}".format( odf_min, odf_max )) for odf in odf_values: div0 = [n for n in ad9552.P0_VALUES if odf % n == 0] if len(div0): break if len(div0) == 0: raise DividerRangeException("No valid P0 value") p0 = div0[0] p1 = int(odf/p0) if p1 not in ad9552.P1_VALUES: raise DividerRangeException("No valid P1 value") fout_mod = 1 while (fout * fout_mod) % 1 != 0: fout_mod *= 10 fout_frac = int(fout * fout_mod) numer = odf * fout_frac denom = int(k * fout_mod * self.fref) divisor = math.gcd(numer, denom) fnumer = numer / divisor fdenom = denom / divisor n = int(fnumer / fdenom) frac = int(fnumer % fdenom) mod = int(fdenom) # Generally, the largest possible MOD value yields the # smallest spurs. Thus, it is desirable to scale MOD and FRAC by # the integer part of (2^20 - 1) divided by the value of # MOD obtained previously. scaling = int(((1 << 20) - 1) / mod) frac *= scaling mod *= scaling return (k, n, frac, mod, p0, p1)
[docs] def register_ioupdate(self) -> str: """Perform a device register update. :return: The command string required to perform a device register update. .. note:: Data from a write sequence is stored in a buffer register (data inactive). An active register exists for every buffer register. The I/O update signal is used to transfer the contents from the buffer register into the active register. This method is used internally by the various ``ad9552`` class methods and may be useful if sub-classing to add extra functionality. >>> clk1 = ad9552('d0', 'c4', 'c5') >>> clk1.register_ioupdate() 'LD0:W00,05,01:HD0:' """ return 'L{}:W00,05,01:H{}:'.format(self.cs, self.cs)
[docs] def enable_vco_calibration(self) -> str: """Enable SPI control of VCO calibration. :return: The command string required to enable SPI control of VCO calibration. .. note:: The process of enabling SPI control of VCO calibration must be carried out before each update of the clock output frequency. Note that this must be done *before* updating the PLL control registers. This method is used internally by the various ``ad9552`` class methods and may be useful if sub-classing to add extra functionality. >>> clk1 = ad9552('d0', 'c4', 'c5') >>> clk1.enable_vco_calibration() 'LD0:W00,0E,74:HD0:LD0:W00,05,01:HD0:' """ cmd = 'L{}:W00,0E,74:H{}:'.format(self.cs, self.cs) cmd += self.register_ioupdate() return cmd
[docs] def calibrate_vco(self) -> str: """Request VCO calibration. :return: The command string required to initiate VCO calibration. .. note:: The process calibrating the VCO must be carried out after each update of the clock output frequency. Note that this must be done *after* updating the PLL control registers. SPI control of VCO calibration must have been enabled prior to issuing this command. This method is used internally by the various ``ad9552`` class methods and may be useful if sub-classing to add extra functionality. >>> clk1 = ad9552('d0', 'c4', 'c5') >>> clk1.calibrate_vco() 'LD0:W00,0E,F4:HD0:LD0:W00,05,01:HD0:' """ cmd = 'L{}:W00,0E,F4:H{}:'.format(self.cs, self.cs) cmd += self.register_ioupdate() return cmd
[docs] def config_reference_multiplier(self) -> str: """Enable the input reference multiplier. :return: The command string required to set the input reference multiplier. >>> clk1 = ad9552('d0', 'c4', 'c5') >>> clk1.config_reference_multiplier() 'LD0:W00,1D,04:HD0:' """ cmd = 'L{}:W00,1D,04:H{}:'.format(self.cs, self.cs) return cmd
[docs] def config_output_frequency( self, pll_values: Tuple[int, int, float, float, int, int]) -> str: """Configure and update the output frequency. :param pll_values: The PLL control values required to produce the desired output frequency. :type pll_values: tuple :return: The string command required to configure the current device PLL control registers and update the output frequency. The required command sequence runs as follows: 1. Enable SPI control of VCO calibration. 2. Update the PLL control registers 3. Request VCO calibration. Note that 'streaming' mode is used to convey the PLL control register values to the device. See the section *Operation of the Serial Control Port* in the AD9552 datasheet (Rev E., pp20-22). >>> clk1 = ad9552('d2', 'c6', 'c7', fref=10.0) >>> clk1.config_output_frequency(clk1.divider_values(644.53125)) 'LD2:W00,1D,04:HD2:LD2:W00,0E,74:HD2:LD2:W00,05,01:HD2:LD2:W60,19,80,0A,90,FE,5B,08,FC,FF,C1:HD2:LD2:W00,05,01:HD2:LD2:W00,0E,F4:HD2:LD2:W00,05,01:HD2:' >>> clk1.config_output_frequency(clk1.divider_values(100)) 'LD2:W00,1D,04:HD2:LD2:W00,0E,74:HD2:LD2:W00,05,01:HD2:LD2:W60,19,80,39,00,00,00,FE,FF,FF,AF:HD2:LD2:W00,05,01:HD2:LD2:W00,0E,F4:HD2:LD2:W00,05,01:HD2:' """ k, n, frac, mod, p0, p1 = pll_values cmd = '' if k > 1: cmd = self.config_reference_multiplier() # Configure registers 0x11 to 0x19 # Reg_0x11[7:0] = N[7:0] # Reg_0x12[7:0] = MOD[19:12] # Reg_0x13[7:0] = MOD[11:4] # Reg_0x14[7:4] = MOD[3:0] # If FRAC is 0: # Reg_0x14[3:0] = 0b1110 = 0xE # else: # Reg_0x14[3:0] = 0b1000 = 0x8 # Reg_0x15[7:0] = FRAC[19:12] # Reg_0x16[7:0] = FRAC[11:4] # Reg_0x17[7:4] = FRAC[3:0] # Reg_0x17[3:1] = 0b000 These bits are unused # Reg_0x17[0] = P1[5] # Reg_0x18[7:3] = P1[4:0] # Reg_0x18[2:0] = P0[2:0] # Reg_0x19[7:0] = 0b1000_0000 pll_control_values = [] pll_control_values.append('{:02X}'.format(n)) modStr = '{:05X}'.format(int(mod)) pll_control_values.append(modStr[0:2]) pll_control_values.append(modStr[2:4]) if frac == 0: # Disable and bypass SDM if FRAC == 0 pll_control_values.append(modStr[4] + 'E') else: pll_control_values.append(modStr[4] + '8') fracStr = '{:05X}'.format(int(frac)) pll_control_values.append(fracStr[0:2]) pll_control_values.append(fracStr[2:4]) if p1 & 0b100000: pll_control_values.append(fracStr[4] + '1') else: pll_control_values.append(fracStr[4] + '0') pll_control_values.append('{:02X}'.format( ((p1 & 0b11111) << 3) + ((p0 - 4) & 0b111))) pll_control_values.append('80') # In streaming mode the register number is automatically # decremented. The initial register is 0x19. # Therefore we reverse the order of the PLL control register # values. pll_control_values.reverse() cmd += self.enable_vco_calibration() cmd += 'L{}:W60,19,{}:H{}:'.format( self.cs, ','.join(pll_control_values), self.cs) cmd += self.register_ioupdate() cmd += self.calibrate_vco() return cmd
[docs] def config_output_mode( self, chan: int, mode: OutputMode, state: OutputState, strength: DriveStrength = DriveStrength.STRONG, polarity: CmosPolarity = CmosPolarity.DIFF_POS, enabled: OutputModeControl = OutputModeControl.SPI) -> str: """Configure the clock output mode for a specified clock channel. :param chan: The specified clock channel. This must be one of ``ad9552.OUT1`` or ``ad9552.OUT2``. :type chan: int :param mode: :type mode: ad9552.OutputMode :param state: :type state: ad9552.OutputState :param strength: :type strength: ad9552.DriveStrength, optional :param polarity: :type polarity: ad9552.CmosPolarity, optional :param enabled: :type enabled: ad9552.OutputModeControl, optional :return: The command string required to configure the specified output mode for the specified clock channel. If an illegal clock channel is specified, and empty command string is returned. >>> clk1 = ad9552('d2', 'c6', 'c7', fref=20.0) >>> clk1.config_output_mode( ... ad9552.OUT1, ... ad9552.OutputMode.LVDS, ... ad9552.OutputState.ACTIVE, ... ad9552.DriveStrength.STRONG, ... ad9552.CmosPolarity.DIFF_POS) 'LD2:W00,32,A1:HD2:LD2:W00,05,01:HD2:' >>> clk1.config_output_mode( ... ad9552.OUT2, ... ad9552.OutputMode.LVPECL, ... ad9552.OutputState.ACTIVE, ... ad9552.DriveStrength.STRONG, ... ad9552.CmosPolarity.DIFF_POS) 'LD2:W00,34,A9:HD2:LD2:W00,05,01:HD2:' >>> clk1.config_output_mode( ... ad9552.OUT1, ... ad9552.OutputMode.LVDS, ... ad9552.OutputState.POWERED_DOWN, ... ad9552.DriveStrength.STRONG, ... ad9552.CmosPolarity.DIFF_POS) 'LD2:W00,32,E1:HD2:LD2:W00,05,01:HD2:' """ cmd = '' if chan in [1, 2]: reg_value = ((strength << 7) | (state << 6) | (mode << 3) | (polarity << 1) | enabled) & 0xff reg = 0x30 + (chan * 2) cmd += 'L{}:W00,{},{}:H{}:'.format( self.cs, '{:02X}'.format(reg), '{:02X}'.format(reg_value), self.cs) cmd += self.register_ioupdate() return cmd
[docs] def config_src_control(self, src: SourceControl) -> str: """Configure the source for channel 2. :param src: :type src: ad9552.SourceControl :return: The string command required to configure the specified source for channel 2. >>> clk1 = ad9552('d2', 'c6', 'c7', fref=20.0) >>> clk1.config_src_control(ad9552.SourceControl.PLL) 'LD2:W00,33,00:HD2:LD2:W00,05,01:HD2:' >>> clk1.config_src_control(ad9552.SourceControl.REF) 'LD2:W00,33,08:HD2:LD2:W00,05,01:HD2:' """ if src == ad9552.SourceControl.PLL: cmd = 'L{}:W00,33,00:H{}:'.format(self.cs, self.cs) else: cmd = 'L{}:W00,33,08:H{}:'.format(self.cs, self.cs) cmd += self.register_ioupdate() return cmd
[docs] def check_is_locked(self) -> str: """Check if the device PLL is locked. :return: The command string for checking the device PLL lock status. >>> clk1 = ad9552('d2', 'c6', 'c7') >>> clk1.check_is_locked() 'PC6:' """ return 'P{}:'.format(self.lockdetect)
if __name__ == '__main__': import doctest doctest.testmod()