diff --git a/charon_vna/gui.py b/charon_vna/gui.py index 6ac734a..996d7b4 100644 --- a/charon_vna/gui.py +++ b/charon_vna/gui.py @@ -105,7 +105,7 @@ class MainWindow(QMainWindow): prog_sweep.setMaximum(100) prog_sweep.setFormat("%v / %m") # prog_sweep.setTextVisible(False) - prog_sweep.setValue(50) + prog_sweep.setValue(0) window_layout.addWidget(prog_sweep) self.prog_sweep = prog_sweep diff --git a/charon_vna/vna.py b/charon_vna/vna.py index f1c43b5..b3bc2f6 100644 --- a/charon_vna/vna.py +++ b/charon_vna/vna.py @@ -1,11 +1,11 @@ # %% imports import copy +from enum import IntEnum, unique from pathlib import Path -from typing import Any, Callable, Dict, Tuple +from typing import Any, Callable, Dict, Literal, Tuple import adi - -# import iio +import iio import numpy as np import skrf as rf import xarray as xr @@ -34,6 +34,38 @@ def generate_tone(f: float, fs: float, N: int = 1024, scale: int = 2**14): return iq +@unique +class AD9361Register(IntEnum): + AUXDAC1_WORD = 0x018 + AUXDAC2_WORD = 0x019 + AUXDAC1_CONFIG = 0x01A + AUXDAC2_CONFIG = 0x01B + AUXADC_CLOCK_DIVIDER = 0x01C + AUXADC_CONFIG = 0x01D + AUXADC_WORD_MSB = 0x01E + AUXADC_WORD_LSB = 0x01F + AUTO_GPIO = 0x020 + AGC_GAIN_LOCK_DELAY = 0x021 + AGC_ATTACK_DELAY = 0x022 + AUXDAC_ENABLE_CONTROL = 0x023 + RX_LOAD_SYNTH_DELAY = 0x024 + TX_LOAD_SYNTH_DELAY = 0x025 + EXTERNAL_LNA_CONTROL = 0x026 + GPO_FORCE_AND_INIT = 0x027 + GPO0_RX_DELAY = 0x028 + GPO1_RX_DELAY = 0x029 + GPO2_RX_DELAY = 0x02A + GPO3_RX_DELAY = 0x02B + GPO0_TX_DELAY = 0x02C + GPO1_TX_DELAY = 0x02D + GPO2_TX_DELAY = 0x02E + GPO3_TX_DELAY = 0x02F + AUXDAC1_RX_DELAY = 0x030 + AUXDAC1_TX_DELAY = 0x031 + AUXDAC2_RX_DELAY = 0x032 + AUXDAC2_TX_DELAY = 0x033 + + class Charon: FREQUENCY_OFFSET = 1e6 @@ -49,7 +81,8 @@ class Charon: self.frequency = frequency # everything RF - self.sdr = adi.ad9361(uri=f"ip:{ip}") + uri = f"ip:{ip}" + self.sdr = adi.ad9361(uri=uri) for attr, expected in [ ("adi,2rx-2tx-mode-enable", True), # ("adi,gpo-manual-mode-enable", True), @@ -81,14 +114,16 @@ class Charon: self.sdr.tx_hardwaregain_chan0 = -10 # # switch control - # ctx = iio.Context(uri) - # self.ctrl = ctx.find_device("ad9361-phy") - # # raw ad9361 register accesss: - # # https://ez.analog.com/linux-software-drivers/f/q-a/120853/control-fmcomms3-s-gpo-with-python - # # https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf # noqa: E501 - # self.ctrl.reg_write(0x26, 0x90) # bit 7: AuxDAC Manual, bit 4: GPO Manual - # self._set_gpo(self.ports[0] - 1) - # # TODO: init AuxDAC + ctx = iio.Context(uri) + self.ctrl = ctx.find_device("ad9361-phy") + # raw ad9361 register accesss: + # https://ez.analog.com/linux-software-drivers/f/q-a/120853/control-fmcomms3-s-gpo-with-python + # https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf # noqa: E501 + self.ctrl.reg_write(AD9361Register.EXTERNAL_LNA_CONTROL, 0x90) # bit 7: AuxDAC Manual, bit 4: GPO Manual + self.ctrl.reg_write(AD9361Register.AUXDAC_ENABLE_CONTROL, 0x3F) + self._set_gpo(0b0000) + self._set_dac(value=0, channel=1) + self._set_dac(value=0, channel=2) def get_config(self) -> Dict[str, Any]: config = dict() @@ -113,10 +148,45 @@ class Charon: return config def _get_gpo(self) -> int: - return (self.ctrl.reg_read(0x27) >> 4) & 0x0F + return (self.ctrl.reg_read(AD9361Register.GPO_FORCE_AND_INIT) >> 4) & 0x0F def _set_gpo(self, value: int) -> None: - self.ctrl.reg_write(0x27, (value & 0x0F) << 4) # bits 7-4: GPO3-0 + self.ctrl.reg_write(AD9361Register.GPO_FORCE_AND_INIT, (value & 0x0F) << 4) # bits 7-4: GPO3-0 + + def _set_dac(self, value: int, channel: Literal[1, 2]): + if channel not in [1, 2]: + raise ValueError(f"Invalid channel {channel}. Must be 1 or 2") + + if value > 0x3FF or value < 0: + raise ValueError("Invalid value for 10 bit DAC. Must be between 0 and 0x3FF (inclusive)") + + @unique + class Vref(IntEnum): + VREF_1V0 = 0b00 + VREF_1V5 = 0b01 + VREF_2V0 = 0b10 + VREF_2V5 = 0b11 + + @unique + class StepFactor(IntEnum): + FACTOR_2 = 0b0 + FACTOR_1 = 0b1 + + # https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf + # page 13 + # vout = 0.97 * vref + (0.000738 + 9e-6 * (vref * 1.6 - 2)) * auxdac_word[9:0] * step_factor - 0.3572 * step_factor + 0.05 + # vout ~= (vref - 0.3572 * step_factor) + 0.000738 * auxdac_word[9:0] * step_factor + # which gives a 1.5V swing with step_factor == 2 and 0.75V swing with step_factor == 1 + # vref basically just changes the minimum voltage with negligible impact on output scaling + + self.ctrl.reg_write( + AD9361Register.__getitem__(f"AUXDAC{channel}_WORD"), + (value >> 2) & 0xFF, + ) + self.ctrl.reg_write( + AD9361Register.__getitem__(f"AUXDAC{channel}_CONFIG"), + (value & 0x3) | (Vref.VREF_1V0.value << 2) | (StepFactor.FACTOR_2 << 4), + ) def set_output_power(self, power: float): # FIXME: this is a hack because I don't want to go through re-calibration