Source code for rfblocks.hmc833_controller

# `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 configure(self, ser: serial.Serial) -> bool: """Update the PLO hardware using the currently set configuration. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial :return: A boolean value indicating the PLO lock status. ``True`` for locked. """ cmd = self.plo.config_gain() cmd += self.plo.config_vco_mute() cmd += self.plo.close_vco_config() write_cmd(ser, cmd) locked = self.configure_freq(ser) self.hardware_updated.emit() return locked
[docs] def configure_freq(self, ser: serial.Serial, retry_lock: bool = True) -> bool: """Update the PLO hardware with the currently set output frequency. Note that in some circumstances the PLO may not find lock. When this happens the retry_lock parameter determines what should happen next. If initial PLO lock isn't achieved and retry_lock is True the frequency configuration at a slightly different frequency is attempted followed by configuration at the specified frequency. In almost all cases this results in PLO lock. If retry_lock is False the frequency configuration is carried out only once even if initial PLO lock isn't achieved. In all cases the PLO lock status is returned. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial :param retry_lock: Whether to retry frequency configuration if initial PLO locked isn't achieved. :type bool: :return: A boolean value indicating the PLO lock status. ``True`` for locked. """ write_cmd(ser, self.plo.config_frequency(self.freq)) self.check_plo_lock(ser) if not self.lock_status and retry_lock: if self.freq < hmc833.MIN_FREQUENCY + 2.0: new_freq = self.freq + 1.0 else: new_freq = self.freq - 1.0 write_cmd(ser, self.plo.config_frequency(new_freq)) write_cmd(ser, self.plo.config_frequency(self.freq)) self.check_plo_lock(ser) return self.lock_status
[docs] def configure_gains(self, ser: serial.Serial) -> None: """Update the PLO hardware with the current gain settings. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial """ write_cmd(ser, self.plo.config_gain())
[docs] def configure_chargepump(self, ser: serial.Serial) -> None: """Update the PLO hardware with the current charge pump settings. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial """ n_int, n_frac, doubler, divide_ratio = self.plo.divider_values( self.freq) if n_frac == 0: cmd = self.plo.config_chargepump(False) else: cmd = self.plo.config_chargepump(True) write_cmd(ser, cmd)
[docs] def configure_vco_mute(self, ser: serial.Serial) -> None: """Update the PLO hardware with the current VCO mute status. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial """ write_cmd(ser, self.plo.config_vco_mute())
[docs] def configure_refdiv(self, ser: serial.Serial) -> None: """Update the PLO hardware with the current reference divider value. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial .. warning:: This method does not seem to work in 100% of cases when using spur data calculated from Analog's ADI SimFrequencyPlanner. At present it is unknown why. Use with caution. """ cmd = self.plo.config_reference_divider() cmd += self.plo.config_analog_enables() cmd += self.plo.config_autocal() cmd += self.plo.config_frequency(self.freq, full_reg_update=True) write_cmd(ser, cmd)
[docs] def configure_refsrc(self, ser: serial.Serial) -> None: """Update the PLO hardware with the current reference source status. :param ser: Device update commands will be sent via this serial device. :type ser: serial.Serial .. warning:: This method does not seem to work in 100% of cases when using spur data calculated from Analog's ADI SimFrequencyPlanner. At present it is unknown why. Use with caution. """ cmd = self.plo.config_refsrc() cmd += self.plo.config_reference_divider() cmd += self.plo.config_analog_enables() cmd += self.plo.config_autocal() cmd += self.plo.config_frequency(self.freq, full_reg_update=True) write_cmd(ser, cmd)
[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