# `HMC833Controller` - Control for the HMC833 wideband PLO
#
# 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 (
Dict, Optional
)
import serial
from PyQt5.QtCore import (
pyqtSignal, QObject
)
from rfblocks import (
hmc833, write_cmd, query_cmd
)
[docs]class HMC833Controller(QObject):
"""Higher level control for the HMC833 phase locked oscillator.
Documentation for the HMC833 frequency synthesizer rfblocks module
can be found here: `An HMC833 Frequency Synthesizer <../boards/HMC833-Synth.html>`_
"""
DEFAULT_PLO_FREQ: float = 1000.0
DEFAULT_REF_FREQ: float = 50.0
DEFAULT_DEVICE_CONFIG: Dict = {
'freq': DEFAULT_PLO_FREQ,
'ref_freq': DEFAULT_REF_FREQ,
'refsrc': hmc833.DEFAULT_REFSRC,
'bufgain': hmc833.OutputBufferGain.MAXGAIN_MINUS_9DB,
'divgain': hmc833.DividerGain.MAXGAIN_MINUS_3DB,
'vco_mute': False
}
freq_changed = pyqtSignal(float)
ref_freq_changed = pyqtSignal(float)
refsrc_changed = pyqtSignal(int)
bufgain_changed = pyqtSignal(int)
divgain_changed = pyqtSignal(int)
vco_mute_changed = pyqtSignal(bool)
lock_status_changed = pyqtSignal(bool)
hardware_updated = pyqtSignal()
def __init__(self,
controller_id: str,
device: hmc833,
config: Optional[Dict] = None) -> None:
"""
:param controller_id: The controller name.
:type controller_id: str
:param device: An instance of :py:class:`rfblocks.hmc833`.
:type device: :py:class:`rfblocks.hmc833`
:param config: Initial configuration for the HMC833 PLO
board. If this is None the default configuration will be used.
See :py:attr:`HMC833Controller.DEFAULT_DEVICE_CONFIG` for a
brief description of the structure for :py:`config`.
:py:`HMC833Controller` maintains the state configuration for an
HMC833 PLO board. The following signals are defined:
- :py:`freq_changed(float)`
- :py:`ref_freq_changed(float)`
- :py:`refsrc_changed(hmc833.ReferenceSource)`
- :py:`bufgain_changed(hmc833.OutputBufferGain)`
- :py:`divgain_changed(hmc833.DividerGain)`
- :py:`vco_mute_changed(bool)`
- :py:`lock_status_changed(bool)`
"""
super().__init__()
self._controller_id: str = controller_id
self._hmc833: hmc833 = device
self._initial_config: Dict = config
if self._initial_config is None:
self._initial_config = {**HMC833Controller.DEFAULT_DEVICE_CONFIG}
self._freq: float = self._initial_config['freq']
self._hmc833.buf_gain: hmc833.OutputBufferGain = \
self._initial_config['bufgain']
self._hmc833.div_gain: hmc833.DividerGain = \
self._initial_config['divgain']
self._hmc833.mute_vco: bool = self._initial_config['vco_mute']
self._lock_status: bool = False
@property
def plo(self) -> hmc833:
return self._hmc833
@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 PLO 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 cp_gain(self) -> float:
"""Returns the current charge pump gain (mA)
"""
return self.plo.cp_gain * hmc833.CP_GAIN_STEP
@cp_gain.setter
def cp_gain(self, gain: float) -> None:
self.plo.cp_gain = round(gain / hmc833.CP_GAIN_STEP)
@property
def ref_freq(self) -> float:
"""The current reference frequency (in MHz).
"""
return self.plo.fref
@ref_freq.setter
def ref_freq(self, f: float) -> None:
"""Set the reference frequency.
:param f: The desired reference frequency (in MHz).
:type f: float
Note that this will set the value of the :py:attr:`ref_freq`
property only. Updating the PLO hardware should be
done separately. See, for example, :py:meth:`configure`.
"""
if f != self.plo.fref:
self.plo.fref = f
self.ref_freq_changed.emit(f)
@property
def ref_div(self) -> int:
"""The current PLO reference divider value.
"""
return self.plo.refdiv
@ref_div.setter
def ref_div(self, d: int) -> None:
"""Set the PLO reference divider value.
:param d: The desired reference divider value
:type d: int
Note that this will set the value of the :py:attr:`ref_div`
property only. Updating the PLO hardware should be
done separately. See, for example, :py:meth:`configure_refsrc`.
"""
if d != self.plo.refdiv:
self.plo.refdiv = d
@property
def refsrc(self) -> hmc833.ReferenceSource:
"""The reference source for the module.
"""
return self.plo.refsrc
@refsrc.setter
def refsrc(self, src: hmc833.ReferenceSource) -> None:
"""Set the module reference source.
:param src: The new reference source. This should be either
:py:`hmc833.ReferenceSource.INTERNAL` for the on board reference
or :py:`hmc833.ReferenceSource.EXTERNAL` for an external
reference connected to the 'Ref' board input.
:type src: hmc833.ReferenceSource
Note that this will set the value of the :py:attr:`refsrc`
property only. Updating the PLO hardware should be
done separately. See, for example, :py:meth:`configure`.
"""
if src != self.plo.refsrc:
self.plo.refsrc = src
self.refsrc_changed.emit(self.plo.refsrc)
@property
def buffer_gain(self) -> hmc833.OutputBufferGain:
"""The PLO current output buffer gain setting.
"""
return self.plo.buf_gain
@property
def divider_gain(self) -> hmc833.DividerGain:
"""The PLO current divider output stage gain setting.
"""
return self.plo.div_gain
@buffer_gain.setter
def buffer_gain(self, gain: hmc833.OutputBufferGain) -> None:
"""Set the PLO current output buffer gain.
:param gain: The VCO output buffer gain setting.
:type gain: :py:class:`rfblocks.hmc833.OutputBufferGain`
Note that this will set the value of the :py:attr:`buffer_gain`
property only. Updating the PLO hardware should be
done separately. See, for example,
:py:meth:`configure_gains`.
"""
if gain != self.plo.buf_gain:
self.plo.buf_gain = gain
self.bufgain_changed.emit(gain)
@divider_gain.setter
def divider_gain(self, gain: hmc833.DividerGain) -> None:
"""Set the PLO current divider output stage gain.
:param gain: The divider output stage gain setting.
:type gain: :py:class:`rfblocks.hmc833.DividerGain`
Note that this will set the value of the :py:attr:`divider_gain`
property only. Updating the PLO hardware should be
done separately. See, for example,
:py:meth:`configure_gains`.
"""
if gain != self.plo.div_gain:
self.plo.div_gain = gain
self.divgain_changed.emit(gain)
@property
def vco_mute(self) -> bool:
"""Mute status of the PLO VCO output.
"""
return self.plo.mute_vco
@vco_mute.setter
def vco_mute(self, mute: bool) -> None:
"""Set the mute status of the PLO VCO output.
Note that this will set the value of the :py:attr:`mute_vco`
property only. Updating the PLO hardware should be
done separately. See, for example,
:py:meth:`configure_vco_mute`.
"""
if mute is not self.plo.mute_vco:
self.plo.mute_vco = mute
self.vco_mute_changed.emit(mute)
@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 synthesizer configuration.
:return: A dictionary containing the current synthesizer
configuration:
:freq: synthesizer output frequency
:ref_freq: synthesizer reference frequency
:refsrc: reference source
:bufgain: output buffer gain setting
:divgain: frequency divider buffer gain setting
:vco_mute: output mute status
"""
config = {
'freq': self.freq,
'ref_freq': self.ref_freq,
'refsrc': self.refsrc,
'bufgain': self.buffer_gain,
'divgain': self.divider_gain,
'vco_mute': self.vco_mute
}
return config
[docs] def load_config(self, config: Dict) -> None:
"""Set the current synthesizer configuration.
:param config: A dictionary containing the synthesizer
configuration to be set.
:type config: Dict
Note that in order to update the synthesizer hardware
:py:meth:`configure` should be called.
"""
self.freq = config['freq']
self.ref_freq = config['ref_freq']
self.refsrc = config['refsrc']
self.buffer_gain = config['bufgain']
self.divider_gain = config['divgain']
self.vco_mute = config['vco_mute']
[docs] def initialize(self, ser: serial.Serial) -> bool:
"""Initialize the PLO board.
:param ser: Device update commands will be sent via this serial
device.
:type ser: serial.Serial
"""
write_cmd(ser, self.plo.pin_config())
write_cmd(ser, self.plo.device_initialize(self.freq))
return self.check_plo_lock(ser)
[docs] def check_plo_lock(self, ser: serial.Serial) -> bool:
"""Check the current PLL/VCO lock status.
:return: A boolean value indicating the PLO lock status.
``True`` for locked.
As a side effect, this method may update the value of the
``lock_status`` property which in turn may emit
the ``lock_status_changed`` signal.
"""
cmd = self.plo.check_is_locked()
resp = query_cmd(ser, cmd)
locked: bool = False
try:
locked: bool = bool(int(resp[0]))
except (ValueError, IndexError):
pass
self.lock_status = locked
return locked