# `AD9552Controller` - Control for the AD9552 clock generator.
#
# Copyright (C) 2021 Dyadic Pty Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import (
Optional, Dict
)
import serial
from PyQt5.QtCore import (
pyqtSignal, QObject
)
from rfblocks import (
ad9552, write_cmd, query_cmd
)
[docs]class AD9552Channel(QObject):
"""Higher level control for an AD9552 clock generator channel.
:py:`AD9552Channel` maintains the state configuration for a single
ad9552 clock generator channel. The following signals are
defined:
- :py:`state_changed(int, int)`
- :py:`mode_changed(int, int)`
- :py:`drive_changed(int, int)`
- :py:`polarity_changed(int, int)`
- :py:`source_changed(int, int)`
"""
DEFAULT_CHAN_MODE = ad9552.OutputMode.LVPECL
DEFAULT_CHAN_STATE = ad9552.OutputState.POWERED_DOWN
DEFAULT_CHAN_DRIVE = ad9552.DriveStrength.STRONG
DEFAULT_CHAN_POLARIY = ad9552.CmosPolarity.DIFF_POS
DEFAULT_CHAN_SOURCE = ad9552.SourceControl.PLL
DEFAULT_CHAN_CONFIG = {
'mode': DEFAULT_CHAN_MODE,
'state': DEFAULT_CHAN_STATE,
'drive': DEFAULT_CHAN_DRIVE,
'polarity': DEFAULT_CHAN_POLARIY,
'source': DEFAULT_CHAN_SOURCE
}
state_changed = pyqtSignal(int, int)
mode_changed = pyqtSignal(int, int)
drive_changed = pyqtSignal(int, int)
polarity_changed = pyqtSignal(int, int)
source_changed = pyqtSignal(int, int)
def __init__(self,
chan_id: str,
chan_config: Optional[Dict] = None) -> None:
"""
:param chan_id: Channel identifier (for example: '1').
:type chan_id: str
:param chan_config: Initial configuration for the clock channel.
If this is None the default configuration will be used.
See :py:attr:`AD9552Channel.DEFAULT_CHAN_CONFIG` for a
brief description of the structure for :py:`chan_config`.
:type chan_config: Dict
"""
super().__init__()
self._chan_id: int = int(chan_id)
self._initial_config: Dict = chan_config
if self._initial_config is None:
self._initial_config = AD9552Channel.DEFAULT_CHAN_CONFIG
self._label: str = self._initial_config['label']
self._mode: ad9552.OutputMode = self._initial_config['mode']
self._state: ad9552.OutputState = self._initial_config['state']
self._drive: ad9552.DriveStrength = self._initial_config['drive']
self._polarity: ad9552.CmosPolarity = self._initial_config['polarity']
self._source: ad9552.SourceControl = self._initial_config['source']
def __str__(self) -> str:
s = (f'{self.__class__.__name__}: chan_id: {self.chan_id}, '
f'label: {self.label}, mode: {self.mode.name}, '
f'state: {self.state.name}, drive: {self.drive.name}, '
f'polarity: {self.polarity.name}, source: {self.source.name}')
return s
def __repr__(self) -> str:
return "{}({!r})".format(self.__class__.__name__, vars(self))
@property
def chan_id(self) -> int:
return self._chan_id
@property
def label(self) -> str:
return self._label
@property
def initial_config(self) -> Dict:
"""Return a dictionary containing the initial channel configuration.
"""
return self._initial_config
@property
def state(self) -> ad9552.OutputState:
"""The state of the clock channel.
This should be either :py:`ad9552.OutputState.ACTIVE` or
:py:`ad9552.OutputState.POWERED_DOWN`.
:type: :class:`rfblocks.ad9552.OutputState`
"""
return self._state
@state.setter
def state(self, state: ad9552.OutputState) -> None:
if state != self._state:
self._state = state
self.state_changed.emit(self.chan_id, self._state)
@property
def mode(self) -> ad9552.OutputMode:
"""The channel output mode.
:type: :class:`rfblocks.ad9552.OutputMode`
Note that this will set the value of the :py:attr:`mode`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`AD9552Controller.configure`.
"""
return self._mode
@mode.setter
def mode(self, mode: ad9552.OutputMode) -> None:
if mode != self._mode:
self._mode = mode
self.mode_changed.emit(self.chan_id, self._mode)
@property
def drive(self) -> ad9552.DriveStrength:
"""The channel drive strength.
:type: :class:`rfblocks.ad9552.DriveStrength`
Note that this will set the value of the :py:attr:`drive`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`AD9552Controller.configure`.
"""
return self._drive
@drive.setter
def drive(self, drive: ad9552.DriveStrength) -> None:
if drive != self._drive:
self._drive = drive
self.drive_changed.emit(self.chan_id, self._drive)
@property
def polarity(self) -> ad9552.CmosPolarity:
"""The channel CMOS polarity
:type: :class:`rfblocks.ad9552.CmosPolarity`
Note that this will set the value of the :py:attr:`polarity`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`AD9552Controller.configure`.
"""
return self._polarity
@polarity.setter
def polarity(self, polarity: ad9552.CmosPolarity) -> None:
if polarity != self._polarity:
self._polarity = polarity
self.polarity_changed.emit(self.chan_id, self._polarity)
@property
def source(self) -> ad9552.SourceControl:
"""The channel source control setting
:type: :class:`rfblocks.ad9552.SourceControl`
Note that this will set the value of the :py:attr:`source`
property only. Updating the clock channel hardware should be
done separately. See, for example,
:py:meth:`AD9552Controller.configure`.
"""
return self._source
@source.setter
def source(self, source: ad9552.SourceControl) -> None:
if source != self._source:
self._source = source
self.source_changed.emit(self.chan_id, self._source)
[docs] def dump_config(self) -> Dict:
"""Return the current configuration for this channel.
:return: A dictionary containing the current clock channel
configuration:
:mode: channel mode (:py:class:`rfblocks.ad9552.OutputMode`)
:state: channel output state
(:py:class:`rfblocks.ad9552.OutputState`)
:drive: channel drive strength
(:py:class:`rfblocks.ad9552.DriveStrength`)
:polarity: channel CMOS polarity
(:py:class:`rfblocks.ad9552.CmosPolarity`)
:source: channel source
(:py:class:`rfblocks.ad9552.SourceControl`)
>>> clk = ad9552('d0', 'c4', 'c5')
>>> clk_ctl = AD9552Controller('Clk 1', clk)
>>> chan1 = clk_ctl.channels['1']
>>> from pprint import pprint as pp
>>> pp(chan1.dump_config())
{'drive': <DriveStrength.STRONG: 1>,
'mode': <OutputMode.LVPECL: 5>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>}
"""
config = {
'mode': self.mode,
'state': self.state,
'drive': self.drive,
'polarity': self.polarity,
'source': self.source
}
return config
[docs] def load_config(self, config: Dict) -> None:
"""Set the current configuration for this channel.
:param config: A dictionary containing the channel configuration
to be set.
:type config: Dict
>>> clk = ad9552('d0', 'c4', 'c5')
>>> clk_ctl = AD9552Controller('Clk 1', clk)
>>> chan1 = clk_ctl.channels['1']
>>> import copy
>>> config = copy.deepcopy(chan1.dump_config())
>>> config['mode'] = ad9552.OutputMode.LVDS
>>> config['drive'] = ad9552.DriveStrength.WEAK
>>> chan1.load_config(config)
>>> pp(chan1.dump_config())
{'drive': <DriveStrength.WEAK: 0>,
'mode': <OutputMode.LVDS: 4>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>}
"""
self.mode = config['mode']
self.state = config['state']
self.drive = config['drive']
self.polarity = config['polarity']
self.source = config['source']
[docs]class AD9552Controller(QObject):
"""Higher level control for 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>`_
"""
DEFAULT_CLKOUT_FREQ: float = 250.0
DEFAULT_REFSRC: ad9552.ReferenceSource = ad9552.ReferenceSource.INTERNAL
MIN_FREQUENCY: float = 10.0
MAX_FREQUENCY: float = 1100.0
DEFAULT_DEVICE_CONFIG: Dict = {
'freq': DEFAULT_CLKOUT_FREQ,
'channels': {
'1': {'label': '1', **AD9552Channel.DEFAULT_CHAN_CONFIG},
'2': {'label': '2', **AD9552Channel.DEFAULT_CHAN_CONFIG}
}
}
freq_changed = pyqtSignal(float)
refsrc_changed = pyqtSignal(int)
lock_status_changed = pyqtSignal(bool)
def __init__(self,
controller_id: str,
device: ad9552,
config: Optional[Dict] = None) -> None:
"""
:param controller_id: The controller name.
:type controller_id: str
:param device: An instance of :py:class:`rfblocks.ad9552`.
:type device: :py:class:`rfblocks.ad9552`
:param config: Initial configuration for the AD9552 clock generator
board. If this is None the default configuration will be used.
See :py:attr:`AD9552Controller.DEFAULT_DEVICE_CONFIG` for a
brief description of the structure for :py:`config`.
:py:`AD9552Controller` maintains the state configuration for an
AD9552 clock generator board. The following signals are
defined:
- :py:`freq_changed(float)`
- :py:`refsrc_changed(str)`
"""
super().__init__()
self._controller_id: str = controller_id
self._ad9552: ad9552 = device
self._initial_config: Dict = config
if self._initial_config is None:
self._initial_config = AD9552Controller.DEFAULT_DEVICE_CONFIG
self._freq: float = self._initial_config['freq']
self._min_freq: float = AD9552Controller.MIN_FREQUENCY
self._max_freq: float = AD9552Controller.MAX_FREQUENCY
self._ref_src = AD9552Controller.DEFAULT_REFSRC
self._lock_status = False
self._channels: Dict = {
chan_id: AD9552Channel(chan_id, chan_config)
for chan_id, chan_config in
self._initial_config['channels'].items()}
def __str__(self) -> str:
s = (f'{self.__class__.__name__}: '
f'controller_id: {self.controller_id}, '
f'freq: {self.freq:.3f}, ref_src: {self.refsrc}, '
f'lock_status: {self.lock_status}, '
f'channels: {", ".join([f"[{ch}]" for ch_id, ch in self.channels.items()])}')
return s
def __repr__(self) -> str:
return "{}({!r})".format(self.__class__.__name__, vars(self))
@property
def controller_id(self) -> str:
return self._controller_id
@property
def channels(self) -> Dict:
return self._channels
@property
def freq(self) -> float:
"""Returns the current output frequency (in MHz)
"""
return self._freq
@freq.setter
def freq(self, f: float) -> None:
"""Set the current output frequency.
:param f: The new output frequency (in MHz)
:type f: float
Note that this will set the value of the :py:attr:`freq`
property only. Updating the clock module hardware should be
done separately. See, for example,
:py:meth:`configure_freq`.
"""
if f != self._freq:
self._freq = f
self.freq_changed.emit(self._freq)
@property
def channels(self) -> Dict:
return self._channels
@property
def refsrc(self) -> str:
"""The reference source for the module.
"""
return self._ref_src
@refsrc.setter
def refsrc(self, src: ad9552.ReferenceSource) -> None:
"""Set the module reference source.
:param src: The new reference source. This should be either
:py:`ad9552.ReferenceSource.INTERNAL` for the on board reference
or :py:`ad9552.ReferenceSource.EXTERNAL` for an external
reference connected to the 'Ref' board input.
:type src: ad9552.ReferenceSource
Note that this will set the value of the :py:attr:`refsrc`
property only. Updating the clock module hardware should be
done separately. See, for example, :py:meth:`configure`.
"""
if src != self._ref_src:
self._ref_src = src
self._ad9552.refsrc = src
self.refsrc_changed.emit(self._ref_src)
@property
def min_freq(self) -> float:
return self._min_freq
@min_freq.setter
def min_freq(self, f: float) -> None:
self._min_freq = f
@property
def max_freq(self) -> float:
return self._max_freq
@max_freq.setter
def max_freq(self, f: float) -> None:
self._max_freq = f
@property
def lock_status(self) -> bool:
return self._lock_status
@lock_status.setter
def lock_status(self, status: bool) -> None:
if status is not self._lock_status:
self._lock_status = status
self.lock_status_changed.emit(status)
[docs] def dump_config(self) -> Dict:
"""Return the current configuration for this clock module.
:return: A dictionary containing the current clock modules
configuration:
:freq: The currently set output frequency (in MHz).
:channels: A dictionary containing the channel configurations
keyed using the channel id.
>>> clk = ad9552('d0', 'c4', 'c5')
>>> clk_ctl = AD9552Controller('Clk 1', clk)
>>> from pprint import pprint as pp
>>> pp(clk_ctl.dump_config())
{'channels': {'1': {'drive': <DriveStrength.STRONG: 1>,
'mode': <OutputMode.LVPECL: 5>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>},
'2': {'drive': <DriveStrength.STRONG: 1>,
'mode': <OutputMode.LVPECL: 5>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>}},
'freq': 250.0}
"""
config = {
'freq': self._freq,
'channels': {
chan_id: channel.dump_config()
for chan_id, channel in self._channels.items()
}
}
return config
[docs] def load_config(self, config: Dict) -> None:
"""Set the current configuration for this clock module.
:param config: A dictionary containing the module configuration
to be set.
:type config: Dict
>>> clk = ad9552('d0', 'c4', 'c5')
>>> clk_ctl = AD9552Controller('Clk 1', clk)
>>> chan1 = clk_ctl.channels['1']
>>> import copy
>>> ch_config = copy.deepcopy(chan1.dump_config())
>>> ch_config['mode'] = ad9552.OutputMode.LVDS
>>> ch_config['drive'] = ad9552.DriveStrength.WEAK
>>> config = copy.deepcopy(clk_ctl.dump_config())
>>> config['channels']['1'] = ch_config
>>> config['freq'] = 644.1
>>> clk_ctl.load_config(config)
>>> pp(clk_ctl.dump_config())
{'channels': {'1': {'drive': <DriveStrength.WEAK: 0>,
'mode': <OutputMode.LVDS: 4>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>},
'2': {'drive': <DriveStrength.STRONG: 1>,
'mode': <OutputMode.LVPECL: 5>,
'polarity': <CmosPolarity.DIFF_POS: 0>,
'source': <SourceControl.PLL: 0>,
'state': <OutputState.POWERED_DOWN: 1>}},
'freq': 644.1}
"""
self.freq = config['freq']
for chan_id, channel in self._channels.items():
channel.load_config(config['channels'][chan_id])
[docs] def initialize(self, ser: serial.Serial) -> bool:
"""Initialize the clock module.
"""
write_cmd(ser, self._ad9552.pin_config())
return self.configure(ser)