diff --git a/charon_vna/vna.py b/charon_vna/vna.py index 6ecc95e..d1c77b8 100644 --- a/charon_vna/vna.py +++ b/charon_vna/vna.py @@ -1,7 +1,7 @@ # %% imports import copy from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Tuple import adi import iio @@ -94,13 +94,24 @@ class Charon: return config + def _get_gpo(self) -> int: + return (self.ctrl.reg_read(0x27) >> 4) & 0x0F + def _set_gpo(self, value: int) -> None: self.ctrl.reg_write(0x27, (value & 0x0F) << 4) # bits 7-4: GPO3-0 def set_output_power(self, power: float): - if power == -5: - # FIXME: this is a hack because I don't want to go through re-calibration - tx_gain = -8 + # FIXME: this is a hack because I don't want to go through re-calibration + if power == 5: + tx_gain = -1 + elif power == 0: + tx_gain = -7 + elif power == -5: + tx_gain = -12 + elif power == -10: + tx_gain = -17 + elif power == -15: + tx_gain = -22 else: raise NotImplementedError() # # TODO: correct over frequency @@ -108,15 +119,13 @@ class Charon: # tx_gain = pout.coords["tx_gain"][tx_gain_idx] self.sdr.tx_hardwaregain_chan0 = float(tx_gain) - def generate_tone(self, f: float, N: int = 1024, fs: Optional[float] = None): - if fs is None: - fs = self.sdr.sample_rate + def generate_tone(self, f: float, fs: float, N: int = 1024, scale: int = 2**14): fs = int(fs) fc = int(f / (fs / N)) * (fs / N) ts = 1 / float(fs) t = np.arange(0, N * ts, ts) - i = np.cos(2 * np.pi * t * fc) * 2**14 - q = np.sin(2 * np.pi * t * fc) * 2**14 + i = np.cos(2 * np.pi * t * fc) * scale + q = np.sin(2 * np.pi * t * fc) * scale iq = i + 1j * q return iq @@ -128,73 +137,87 @@ class Charon: self.set_output_power(power) self.sdr.tx_lo = int(frequency - self.FREQUENCY_OFFSET) self.sdr.tx_cyclic_buffer = True - self.sdr.tx(self.generate_tone(self.FREQUENCY_OFFSET)) + # self.sdr.tx(self.generate_tone(f=self.FREQUENCY_OFFSET, fs=self.sdr.sample_rate)) + self.sdr.dds_single_tone(self.FREQUENCY_OFFSET, scale=0.9, channel=0) - def _rx(self, count: int = 1) -> npt.ArrayLike: + def _rx(self, count: int = 1, fc: float | None = None) -> npt.ArrayLike: if count < 1: raise ValueError + self.sdr.rx_destroy_buffer() + if fc is not None: + self.sdr.rx_lo = int(fc) + self.sdr.rx_enabled_channels = [0, 1] + self.sdr.gain_control_mode_chan0 = "manual" + self.sdr.gain_control_mode_chan1 = "manual" + self.sdr.rx_hardwaregain_chan0 = 30 + self.sdr.rx_hardwaregain_chan1 = 30 return np.concatenate([np.array(self.sdr.rx()) for _ in range(count)], axis=-1) + def get_b_over_a(self, frequency: float): + self.set_output(frequency=frequency, power=-5) + + data = self._rx(1, fc=frequency - self.FREQUENCY_OFFSET) + ddc_tone = self.generate_tone(f=-self.FREQUENCY_OFFSET, fs=self.sdr.sample_rate, scale=1) + ddc_data = data * ddc_tone + + ddc_rel = ddc_data[1] / ddc_data[0] + + # plt.figure() + # plt.plot( + # np.fft.fftshift(np.fft.fftfreq(ddc_data.shape[-1], 1 / self.sdr.sample_rate)), + # np.abs(np.fft.fftshift(np.fft.fft(ddc_data, axis=-1))).T, + # ) + # plt.show() + + # TODO: calculate sos only once + n, wn = signal.buttord( + wp=0.3 * sdr.FREQUENCY_OFFSET, + ws=0.8 * sdr.FREQUENCY_OFFSET, + gpass=1, + gstop=40, + analog=False, + fs=self.sdr.sample_rate, + ) + sos = signal.butter(n, wn, "lowpass", analog=False, output="sos", fs=self.sdr.sample_rate) + # TODO: figure out why filt sucks. Introduces SO much phase noise (out to several MHz) + filt_data = signal.sosfiltfilt(sos, ddc_data, axis=-1) + + filt_rel = filt_data[1] / filt_data[0] + + return np.mean(data[1] / data[0]) + + def sweep_b_over_a(self): + s = xr.DataArray( + np.zeros( + len(self.frequency), + dtype=np.complex128, + ), + dims=["frequency"], + coords=dict( + frequency=self.frequency, + ), + ) + for frequency in self.frequency: + s.loc[dict(frequency=frequency)] = self.get_b_over_a(frequency=frequency) + return s + # %% -sdr = Charon("ip:192.168.3.1") +sdr = Charon("ip:192.168.3.1", frequency=np.linspace(1e9, 1.1e9, 11)) # %% initialization config = sdr.get_config() +print(sdr.ctrl.debug_attrs["adi,rx-rf-port-input-select"].value) +print(sdr.ctrl.debug_attrs["adi,tx-rf-port-input-select"].value) config # %% generate tone -sdr.set_output(frequency=1e9 + sdr.FREQUENCY_OFFSET, power=-5) +fc = 1e9 +sdr.set_output(frequency=fc + sdr.FREQUENCY_OFFSET, power=-5) # %% capture data -data = sdr._rx(1) - -# %% -fig, axs = plt.subplots(2, 2, sharex=True, tight_layout=True) -# ddc_tone = np.exp( -# -1j * 2 * np.pi * (sdr.FREQUENCY_OFFSET / sdr.sdr.sample_rate) * np.arange(data.shape[-1]), dtype=np.complex128 -# ) -ddc_tone = sdr.generate_tone(-sdr.FREQUENCY_OFFSET) * 2**-14 -ddc_data = data * ddc_tone -axs[0][0].plot(np.real(ddc_data).T, label="DDC") -axs[1][0].plot(np.imag(ddc_data).T, label="DDC") -ddc_rel = ddc_data[1] / ddc_data[0] -axs[0][1].plot(np.real(ddc_rel).T, label="DDC") -axs[1][1].plot(np.imag(ddc_rel).T, label="DDC") -n, wn = signal.buttord( - wp=0.3 * sdr.FREQUENCY_OFFSET, - ws=0.8 * sdr.FREQUENCY_OFFSET, - gpass=1, - gstop=40, - analog=False, - fs=sdr.sdr.sample_rate, -) -sos = signal.butter(n, wn, "lowpass", analog=False, output="sos", fs=sdr.sdr.sample_rate) -filt_data = signal.sosfiltfilt(sos, ddc_data, axis=-1) -axs[0][0].plot(np.real(filt_data).T, label="FILT") -axs[1][0].plot(np.imag(filt_data).T, label="FILT") -filt_rel = filt_data[1] / filt_data[0] -axs[0][1].plot(np.real(filt_rel).T, label="FILT") -axs[1][1].plot(np.imag(filt_rel).T, label="FILT") -s = np.mean(filt_rel) -axs[0][1].axhline(np.real(s), color="k") -axs[1][1].axhline(np.imag(s), color="k") - -axs[0][0].grid(True) -axs[1][0].grid(True) -axs[0][1].grid(True) -axs[1][1].grid(True) - -axs[0][0].legend(loc="lower right") -axs[1][0].legend(loc="lower right") -axs[0][1].legend(loc="lower right") -axs[1][1].legend(loc="lower right") - -axs[0][0].set_ylabel("Real") -axs[1][0].set_ylabel("Imag") -axs[0][0].set_title("By Channel") -axs[0][1].set_title("Relative") +data = sdr._rx(1, fc=fc) # %% Plot in time fig, axs = plt.subplots(2, 1, sharex=True, tight_layout=True) @@ -221,35 +244,16 @@ fig.show() # %% Plot in frequency f = np.fft.fftfreq(data.shape[-1], 1 / sdr.sdr.sample_rate) -RX_BITS = 10 -Pxx_den = np.fft.fft(data, axis=-1) / (len(data) * 2 ** (2 * RX_BITS)) -Pxx_den_ddc = np.fft.fft(ddc_data, axis=-1) / (len(ddc_data) * 2 ** (2 * RX_BITS)) -Pxx_den_filt = np.fft.fft(filt_data, axis=-1) / (len(filt_data) * 2 ** (2 * RX_BITS)) -fft_ddc_tone = np.fft.fft(ddc_tone, axis=-1) / (len(ddc_tone)) +RX_BITS = 12 # for each of i, q (including sign bit) +fft_data = np.fft.fft(data, axis=-1, norm="forward") / (2 ** (RX_BITS - 1)) plt.figure() for cc, chan in enumerate(sdr.sdr.rx_enabled_channels): - # plt.plot( - # np.fft.fftshift(f), - # db20(np.fft.fftshift(Pxx_den[cc])), - # label=f"Channel {chan}", - # ) plt.plot( np.fft.fftshift(f), - db20(np.fft.fftshift(Pxx_den_ddc[cc])), + db20(np.fft.fftshift(fft_data[cc])), label=f"Channel {chan}", ) - plt.plot( - np.fft.fftshift(f), - db20(np.fft.fftshift(Pxx_den_filt[cc])), - label=f"Channel {chan}", - ) -# plt.plot( -# np.fft.fftshift(f), -# db20(np.fft.fftshift(fft_ddc_tone)), -# label="DDC Tone", -# ) plt.legend() -# plt.ylim(1e-7, 1e2) plt.ylim(-100, 0) plt.xlabel("Frequency [Hz]") plt.ylabel("Power [dBfs]")