Compare commits

...

5 Commits
v0.2.0 ... main

Author SHA1 Message Date
1a76c4e7ab IO control on pluto appears to be working
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 1m27s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-03-09 01:54:05 -07:00
67ddbb0f90 use tabs for plots 2025-03-08 23:11:01 -07:00
97497e640e disable some plots that are annoying me 2025-03-08 23:05:35 -07:00
e78cf5595a fix import issue when running gui entry point 2025-03-08 23:02:02 -07:00
086f348084 add pypi link
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 16s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-17 00:37:30 -07:00
3 changed files with 97 additions and 24 deletions

View File

@ -7,7 +7,8 @@ Named after [Pluto's moon](https://en.wikipedia.org/wiki/Charon_(moon)), Charon
1. Install LibIIO. This is a dependency of [PyADI-IIO](https://wiki.analog.com/resources/tools-software/linux-software/pyadi-iio). 1. Install LibIIO. This is a dependency of [PyADI-IIO](https://wiki.analog.com/resources/tools-software/linux-software/pyadi-iio).
On Ubuntu 22.04 just run `sudo apt-get install -y libiio-dev` On Ubuntu 22.04 just run `sudo apt-get install -y libiio-dev`
2. `pip install charon-vna` 2. Charon releases are published on [PyPi](https://pypi.org/project/charon-vna/). Install using pip:
`pip install charon-vna`
## Hardware Setup ## Hardware Setup

View File

@ -21,13 +21,14 @@ from PySide6.QtWidgets import (
QMainWindow, QMainWindow,
QMenu, QMenu,
QProgressBar, QProgressBar,
QTabWidget,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
from vna import Charon
from charon_vna.plots import PlotWidget from charon_vna.plots import PlotWidget
from charon_vna.util import net2s, s2net from charon_vna.util import net2s, s2net
from charon_vna.vna import Charon
# %% # %%
DEFAULT_CONFIG = Path(__file__).parent / "config_default.json" DEFAULT_CONFIG = Path(__file__).parent / "config_default.json"
@ -104,19 +105,20 @@ class MainWindow(QMainWindow):
prog_sweep.setMaximum(100) prog_sweep.setMaximum(100)
prog_sweep.setFormat("%v / %m") prog_sweep.setFormat("%v / %m")
# prog_sweep.setTextVisible(False) # prog_sweep.setTextVisible(False)
prog_sweep.setValue(50) prog_sweep.setValue(0)
window_layout.addWidget(prog_sweep) window_layout.addWidget(prog_sweep)
self.prog_sweep = prog_sweep self.prog_sweep = prog_sweep
# window_widget.se plot_widget = QTabWidget()
plot_layout = QVBoxLayout()
# TODO: handle plots properly
self.plots = [] self.plots = []
for type_ in ["logmag", "phase", "vswr", "smith"]: for type_ in [
"logmag",
"phase",
"vswr",
"smith",
]:
self.plots.append(PlotWidget(type_=type_)) self.plots.append(PlotWidget(type_=type_))
plot_layout.addWidget(self.plots[-1]) plot_widget.addTab(self.plots[-1], type_)
plot_widget = QWidget()
plot_widget.setLayout(plot_layout)
window_layout.addWidget(plot_widget) window_layout.addWidget(plot_widget)
# Set the central widget of the Window. # Set the central widget of the Window.

View File

@ -1,11 +1,11 @@
# %% imports # %% imports
import copy import copy
from enum import IntEnum, unique
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, Tuple from typing import Any, Callable, Dict, Literal, Tuple
import adi import adi
import iio
# import iio
import numpy as np import numpy as np
import skrf as rf import skrf as rf
import xarray as xr import xarray as xr
@ -34,6 +34,38 @@ def generate_tone(f: float, fs: float, N: int = 1024, scale: int = 2**14):
return iq 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: class Charon:
FREQUENCY_OFFSET = 1e6 FREQUENCY_OFFSET = 1e6
@ -49,7 +81,8 @@ class Charon:
self.frequency = frequency self.frequency = frequency
# everything RF # everything RF
self.sdr = adi.ad9361(uri=f"ip:{ip}") uri = f"ip:{ip}"
self.sdr = adi.ad9361(uri=uri)
for attr, expected in [ for attr, expected in [
("adi,2rx-2tx-mode-enable", True), ("adi,2rx-2tx-mode-enable", True),
# ("adi,gpo-manual-mode-enable", True), # ("adi,gpo-manual-mode-enable", True),
@ -81,14 +114,16 @@ class Charon:
self.sdr.tx_hardwaregain_chan0 = -10 self.sdr.tx_hardwaregain_chan0 = -10
# # switch control # # switch control
# ctx = iio.Context(uri) ctx = iio.Context(uri)
# self.ctrl = ctx.find_device("ad9361-phy") self.ctrl = ctx.find_device("ad9361-phy")
# # raw ad9361 register accesss: # raw ad9361 register accesss:
# # https://ez.analog.com/linux-software-drivers/f/q-a/120853/control-fmcomms3-s-gpo-with-python # 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 # 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.ctrl.reg_write(AD9361Register.EXTERNAL_LNA_CONTROL, 0x90) # bit 7: AuxDAC Manual, bit 4: GPO Manual
# self._set_gpo(self.ports[0] - 1) self.ctrl.reg_write(AD9361Register.AUXDAC_ENABLE_CONTROL, 0x3F)
# # TODO: init AuxDAC 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]: def get_config(self) -> Dict[str, Any]:
config = dict() config = dict()
@ -113,10 +148,45 @@ class Charon:
return config return config
def _get_gpo(self) -> int: 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: 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): def set_output_power(self, power: float):
# FIXME: this is a hack because I don't want to go through re-calibration # FIXME: this is a hack because I don't want to go through re-calibration