19. Phased Arrays con Phaser

En este cap铆tulo usaremos el Analog Devices Phaser, (tambi茅n conocido como CN0566 o ADALM-PHASER), que es un Phased Array SDR de bajo costo de 8 canales que combina PlutoSDR, Raspberry Pi y beamformers ADAR1000, dise帽ado para operar alrededor de 10,25 GHz. Cubriremos los pasos de configuraci贸n y calibraci贸n y luego veremos algunos ejemplos de formaci贸n de haces en Python. Para aquellos que no tienen una Phaser, hemos incluido capturas de pantalla y animaciones de lo que ver铆a el usuario.

The Phaser (CN0566) by Analog Devices

Introducci贸n al Phased Arrays

Proximamente!

Revisi贸n del Hardware

The front and back of the Phaser unit

El Phaser es una placa 煤nica que contiene un Phased Array y muchos otros componentes, con una Raspberry Pi conectada en un lado y un Pluto montado en el otro lado. El diagrama de bloques de alto nivel se muestra a continuaci贸n. Algunos elementos a tener en cuenta:

  1. Aunque parece una matriz 2D de 32 elementos, en realidad es una matriz 1D de 8 elementos.

  2. Se utilizan ambos canales de recepci贸n en el Pluto (el segundo canal usa un conector u.FL)

  3. El LO a bordo se utiliza para convertir la se帽al recibida de aproximadamente 10,25 GHz a aproximadamente 2 GHz, para que el Pluto pueda recibirla.

  4. Cada ADAR1000 tiene cuatro desfasadores con ganancia ajustable y los cuatro canales se suman antes de enviarse al Pluto.

  5. El Phaser contiene esencialmente dos 鈥渟ubarreglos鈥, cada uno de los cuales contiene cuatro canales.

  6. No se muestran a continuaci贸n las se帽ales GPIO y seriales de Raspberry Pi utilizadas para controlar varios componentes en Phaser.

The components of the Phaser (CN0566) including ADF4159, LTC5548, ADAR1000

Por ahora ignoremos el lado de transmisi贸n del Phaser, ya que en este cap铆tulo solo usaremos el dispositivo HB100 como transmisor de prueba. El ADF4159 es un sintetizador de frecuencia que produce un tono de hasta 13 GHz de frecuencia, lo que llamamos oscilador local o LO. Este LO se alimenta a un mezclador, el LTC5548, que es capaz de realizar una conversi贸n ascendente o descendente, aunque lo usaremos para una conversi贸n descendente. Para la conversi贸n descendente, toma el LO y una se帽al entre 2 y 14 GHz y los multiplica, lo que realiza un cambio de frecuencia. La se帽al convertida resultante puede oscilar entre DC y 6 GHz, aunque nuestro objetivo ser谩 alrededor de 2 GHz. El ADAR1000 es un beamforming anal贸gico de 4 canales, por lo que el Phaser utiliza dos de ellos. Un beamforming anal贸gico tiene desfasadores y ganancia ajustables independientemente para cada canal, lo que permite que cada canal se retrase y se aten煤e antes de sumarse en el dominio anal贸gico (lo que da como resultado un solo canal). En el Phaser, cada ADAR1000 emite una se帽al que se convierte y luego se recibe en el Pluto. Usando Raspberry Pi podemos controlar la fase y la ganancia de los ocho canales en tiempo real para realizar la formaci贸n de haces. Tambi茅n tenemos la opci贸n de realizar procesamiento de matriz/beamforming digital de dos canales, que se analiza en el siguiente cap铆tulo.

Para aquellos interesados, a continuaci贸n se proporciona un diagrama de bloques un poco m谩s detallado.

Detailed block diagram of the Phaser (CN0566)

Preparaci贸n de la tarjeta SD

Asumiremos que est谩 utilizando la Raspberry Pi integrada en el Phaser (directamente, con un monitor/teclado/rat贸n). Esto simplifica la configuraci贸n, ya que Analog Devices publica una imagen de tarjeta SD predise帽ada con todos los controladores y software necesarios. Puede descargar la imagen de la tarjeta SD y encontrar instrucciones de im谩genes SD aqui. La imagen est谩 basada en el sistema operativo Raspberry Pi e incluye todo el software que necesitar谩 ya instalado.

Preparaci贸n del hardware

  1. Conecte el puerto micro-USB CENTRAL del Pluto a la Raspberry Pi

  2. Opcionalmente, enrosque con cuidado el tr铆pode en el soporte del tr铆pode.

  3. Asumiremos que est谩s usando una pantalla HDMI, un teclado USB y un mouse USB conectados a la Raspberry pi.

  4. Encienda la placa Pi y Phaser a trav茅s del puerto tipo C del Phaser (CN0566), es decir, NO conecte una fuente al USB C de Raspberry Pi.

Instalaci贸n de software

Una vez que haya booteado la Raspberry Pi usando la imagen preconstruida, usando el usuario/contrase帽a analog/analog predeterminado, se recomienda ejecutar los siguientes pasos:

wget https://github.com/mthoren-adi/rpi_setup_stuff/raw/main/phaser/phaser_sdcard_setup.sh
sudo chmod +x phaser_sdcard_setup.sh
./phaser_sdcard_setup.sh
sudo reboot

sudo raspi-config

Para obtener m谩s ayuda con la configuraci贸n de Phaser, consulte la Phaser wiki quickstart page.

Configuraci贸n del HB100

HB100 that comes with Phaser

El HB100 que viene con el Phaser es un m贸dulo de radar Doppler de bajo costo que usaremos como transmisor de prueba, ya que transmite un tono continuo alrededor de los 10 GHz. Funciona con 2 bater铆as AA o una fuente de mesa de 3V, y cuando est茅 encendido, tendr谩 un LED rojo fijo.

Debido a que el HB100 es de bajo costo y utiliza componentes de RF baratos, su frecuencia de transmisi贸n var铆a de una unidad a otra, en cientos de MHz, que es un rango mayor que el ancho de banda m谩s alto que podemos recibir usando el Pluto (56 MHz). Entonces, para asegurarnos de que estamos sintonizando nuestro Pluto y nuestro convertidor descendente de manera que siempre reciban la se帽al HB100, debemos determinar la frecuencia de transmisi贸n del HB100. Esto se hace usando una aplicaci贸n de ejemplo de Analog Devices, que realiza un barrido de frecuencia y calcula FFT mientras busca un pico. Aseg煤rese de que su HB100 est茅 encendido y cerca del Phaser y luego ejecute la utilidad con:

cd ~/pyadi-iio/examples/phaser
python phaser_find_hb100.py

Deber铆a crear un archivo llamado hb100_freq_val.pkl en el mismo directorio. Este archivo contiene la frecuencia de transmisi贸n del HB100 en Hz (decapada, por lo que no se puede ver en texto sin formato) que usaremos en el siguiente paso.

Calibraci贸n

Por 煤ltimo, necesitamos calibrar el phased array. Esto requiere sostener el HB100 apuntando al arreglo (0 grados). Del lado del HB100 con el c贸digo de barras es el lado que transmite la se帽al, por lo que esa cara debe mantenerse a unos metros de distancia del Phaser, justo enfrente y centrada, y luego apuntar directamente al Phaser. En el siguiente paso puedes experimentar con diferentes 谩ngulos y orientaciones, pero por ahora ejecutemos la utilidad de calibraci贸n:

python phaser_examples.py cal

Esto crear谩 dos archivos pickle: fase_cal_val.pkl y ganancia_cal_val.pkl, en el mismo directorio. Cada uno contiene una serie de 8 n煤meros correspondientes a la fase y los ajustes de ganancia necesarios para calibrar cada canal. Estos valores son 煤nicos para cada Phaser, como pueden variar durante la fabricaci贸n. Las ejecuciones posteriores de esta utilidad generar谩n valores ligeramente diferentes, lo cual es normal.

Aplicaci贸n de ejemplo

Ahora que hemos calibrado nuestro Phaser y encontramos la frecuencia HB100, podemos ejecutar la aplicaci贸n de ejemplo que proporciona Analog Devices.

python phaser_gui.py

Si marca la casilla de verificaci贸n 鈥淎ctualizar datos autom谩ticamente鈥 en la parte inferior izquierda, deber铆a comenzar a ejecutarse. Deber铆a ver algo similar a lo siguiente cuando sostenga el HB100 apuntando al Phaser.

Phaser example GUI tool by Analog Devices

Phaser en Python

Ahora nos sumergiremos en la parte pr谩ctica en Python. Para aquellos que no tienen una Phaser, se proporcionan capturas de pantalla y animaciones.

Inicializaci贸n del Phaser y Pluto

El siguiente c贸digo Python configura nuestro Phaser y Pluto. En este punto, ya deber铆a haber ejecutado los pasos de calibraci贸n, que producen tres archivos pickle. Aseg煤rese de ejecutar el siguiente script de Python desde el mismo directorio que estos archivos pickle.

Hay muchas configuraciones con las que lidiar, por lo que est谩 bien si no absorbes todo el fragmento de c贸digo a continuaci贸n, solo ten en cuenta que estamos usando una frecuencia de muestreo de 30 MHz, ganancia manual que configuramos muy baja, configuramos todos los elementos de ganancia al mismo valor y apuntar el phase array de frente (0 grados).

import time
import sys
import matplotlib.pyplot as plt
import numpy as np
import pickle
from adi import ad9361
from adi.cn0566 import CN0566

phase_cal = pickle.load(open("phase_cal_val.pkl", "rb"))
gain_cal = pickle.load(open("gain_cal_val.pkl", "rb"))
signal_freq = pickle.load(open("hb100_freq_val.pkl", "rb"))
d = 0.014  # element to element spacing of the antenna

phaser = CN0566(uri="ip:localhost")
sdr = ad9361(uri="ip:192.168.2.1")
phaser.sdr = sdr
print("PlutoSDR and CN0566 connected!")

time.sleep(0.5) # recommended by Analog Devices

phaser.configure(device_mode="rx")

# Set all antenna elements to half scale - a typical HB100 will have plenty of signal power.
gain = 64 # 64 is about half scale
for i in range(8):
    phaser.set_chan_gain(i, gain, apply_cal=False)

# Aim the beam at boresight (zero degrees)
phaser.set_beam_phase_diff(0.0)

# Misc SDR settings, not super critical to understand
sdr._ctrl.debug_attrs["adi,frequency-division-duplex-mode-enable"].value = "1"
sdr._ctrl.debug_attrs["adi,ensm-enable-txnrx-control-enable"].value = "0" # Disable pin control so spi can move the states
sdr._ctrl.debug_attrs["initialize"].value = "1"
sdr.rx_enabled_channels = [0, 1] # enable Rx1 and Rx2
sdr._rxadc.set_kernel_buffers_count(1) # No stale buffers to flush
sdr.tx_hardwaregain_chan0 = int(-80) # Make sure the Tx channels are attenuated (or off)
sdr.tx_hardwaregain_chan1 = int(-80)

# These settings are basic PlutoSDR settings we have seen before
sample_rate = 30e6
sdr.sample_rate = int(sample_rate)
sdr.rx_buffer_size = int(1024)  # samples per buffer
sdr.rx_rf_bandwidth = int(10e6)  # analog filter bandwidth

# Manually gain (no automatic gain control) so that we can sweep angle and see peaks/nulls
sdr.gain_control_mode_chan0 = "manual"
sdr.gain_control_mode_chan1 = "manual"
sdr.rx_hardwaregain_chan0 = 10 # dB, 0 is the lowest gain.  the HB100 is pretty loud
sdr.rx_hardwaregain_chan1 = 10 # dB

sdr.rx_lo = int(2.2e9) # The Pluto will tune to this freq

# Set the Phaser's PLL (the ADF4159 onboard) to downconvert the HB100 to 2.2 GHz plus a small offset
offset = 1000000 # add a small arbitrary offset just so we're not right at 0 Hz where there's a DC spike
phaser.lo = int(signal_freq + sdr.rx_lo - offset)

Reciviendo muestras del Pluto

En este punto, el Phaser y el Pluto est谩n configurados y listos para funcionar. Ahora podemos empezar a recibir datos del Pluto. Tomemos un solo lote de 1024 muestras y luego tomemos la FFT de cada uno de los dos canales.

# Grab some samples (whatever we set rx_buffer_size to), remember we are receiving on 2 channels at the same time
data = sdr.rx()

# Take FFT
PSD0 = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(data[0])))**2)
PSD1 = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(data[1])))**2)
f = np.linspace(-sample_rate/2, sample_rate/2, len(data[0]))

# Time plot helps us check that we see the HB100 and that we're not saturated (ie gain isnt too high)
plt.subplot(2, 1, 1)
plt.plot(data[0].real) # Only plot real part
plt.plot(data[1].real)
plt.xlabel("Data Point")
plt.ylabel("ADC output")

# PSDs show where the HB100 is and verify both channels are working
plt.subplot(2, 1, 2)
plt.plot(f/1e6, PSD0)
plt.plot(f/1e6, PSD1)
plt.xlabel("Frequency [MHz]")
plt.ylabel("Signal Strength [dB]")
plt.tight_layout()
plt.show()

Lo que veas en este punto depender谩 de si tu HB100 est谩 encendido y hacia d贸nde apunta. Si lo sostienes a unos metros del Phaser y lo apuntas hacia el centro, deber铆as ver algo como esto:

Phaser initial example

Tenga en cuenta el fuerte pico cerca de 0 Hz, el segundo pico m谩s corto es simplemente un artefacto que puede ignorarse, ya que tiene alrededor de 40 dB menos. El gr谩fico superior, que muestra el dominio del tiempo, muestra la parte real de los dos canales, por lo que la amplitud relativa entre los dos variar谩 ligeramente dependiendo de d贸nde sostenga el HB100.

Desempe帽o Beamforming

A continuaci贸n, 隆hacemos un barrido en la fase! En el siguiente c贸digo barremos la fase de 180 negativos a 180 grados positivos, en un paso de 2 grados. Tenga en cuenta que este no es el 谩ngulo que apunta el formador de haz; es la diferencia de fase entre canales adyacentes. Debemos calcular el 谩ngulo de llegada correspondiente a cada paso de fase, utilizando el conocimiento de la velocidad de la luz, la frecuencia de RF de la se帽al recibida y el espaciamiento de los elementos del Phaser. La diferencia de fase entre elementos adyacentes viene dada por:

\phi = \frac{2 \pi d}{\lambda} \sin(\theta_{AOA})

donde \theta_{AOA} es el 谩ngulo de llegada de la se帽al con respecto a la orientaci贸n frontal, d es el espacio entre antenas en metros y \lambda es la longitud de onda de la se帽al. Usando la f贸rmula para la longitud de onda y resolviendo \theta_{AOA} obtenemos:

\theta_{AOA} = \sin^{-1}\left(\frac{c \phi}{2 \pi f d}\right)

Veremos esto cuando calculemos steer_angle abajo:

powers = [] # main DOA result
angle_of_arrivals = []
for phase in np.arange(-180, 180, 2): # sweep over angle
    print(phase)
    # set phase difference between the adjacent channels of devices
    for i in range(8):
        channel_phase = (phase * i + phase_cal[i]) % 360.0 # Analog Devices had this forced to be a multiple of phase_step_size (2.8125 or 360/2**6bits) but it doesn't seem nessesary
        phaser.elements.get(i + 1).rx_phase = channel_phase
    phaser.latch_rx_settings() # apply settings

    steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1))) # arcsin argument must be between 1 and -1, or numpy will throw a warning
    # If you're looking at the array side of Phaser (32 squares) then add a *-1 to steer_angle
    angle_of_arrivals.append(steer_angle)
    data = phaser.sdr.rx() # receive a batch of samples
    data_sum = data[0] + data[1] # sum the two subarrays (within each subarray the 4 channels have already been summed)
    power_dB = 10*np.log10(np.sum(np.abs(data_sum)**2))
    powers.append(power_dB)
    # in addition to just taking the power in the signal, we could also do the FFT then grab the value of the max bin, effectively filtering out noise, results came out almost exactly the same in my tests
    #PSD = 10*np.log10(np.abs(np.fft.fft(data_sum * np.blackman(len(data_sum))))**2) # in dB

powers -= np.max(powers) # normalize so max is at 0 dB

plt.plot(angle_of_arrivals, powers, '.-')
plt.xlabel("Angle of Arrival")
plt.ylabel("Magnitude [dB]")
plt.show()

Para cada valor de phase (recuerde, esta es la fase entre elementos adyacentes) configuramos los desfasadores, despu茅s de agregar los valores de calibraci贸n de fase y forzar que los grados est茅n entre 0 y 360. Luego tomamos un lote de muestras con rx(), sume los dos canales y luego calcule la potencia en la se帽al. Luego graficamos la potencia sobre el 谩ngulo de llegada. El resultado deber铆a verse as铆:

Phaser single sweep

En este ejemplo, el HB100 se mantuvo ligeramente hacia el lado de la mira.

Si desea un gr谩fico polar, puede utilizar lo siguiente:

# Polar plot
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
ax.plot(np.deg2rad(angle_of_arrivals), powers) # x axis in radians
ax.set_rticks([-40, -30, -20, -10, 0])  # Less radial ticks
ax.set_thetamin(np.min(angle_of_arrivals)) # in degrees
ax.set_thetamax(np.max(angle_of_arrivals))
ax.set_theta_direction(-1) # increase clockwise
ax.set_theta_zero_location('N') # make 0 degrees point up
ax.grid(True)
plt.show()
Phaser single sweep using a polar plot

隆Tomando el m谩ximo podemos estimar la direcci贸n de llegada de la se帽al!

En tiempo real y con reducci贸n espacial

Ahora tomemos un momento para hablar sobre la reducci贸n espacial. Hasta ahora hemos dejado los ajustes de ganancia de cada canal en valores iguales, de modo que los ocho canales se sumen equitativamente. As铆 como aplicamos una ventana antes de tomar una FFT, podemos aplicar una ventana en el dominio espacial aplicando pesos a estos ocho canales. Usaremos exactamente las mismas funciones de ventanas como Hanning, Hamming, etc. Tambi茅n modifiquemos el c贸digo para que se ejecute en tiempo real para que sea un poco m谩s divertido:

plt.ion() # needed for real-time view
print("Starting, use control-c to stop")
try:
    while True:
        powers = [] # main DOA result
        angle_of_arrivals = []
        for phase in np.arange(-180, 180, 6): # sweep over angle
            # set phase difference between the adjacent channels of devices
            for i in range(8):
                channel_phase = (phase * i + phase_cal[i]) % 360.0 # Analog Devices had this forced to be a multiple of phase_step_size (2.8125 or 360/2**6bits) but it doesn't seem nessesary
                phaser.elements.get(i + 1).rx_phase = channel_phase

            # set gains, incl the gain_cal, which can be used to apply a taper.  try out each one!
            gain_list = [127] * 8 # rectangular window          [127, 127, 127, 127, 127, 127, 127, 127]
            #gain_list = np.rint(np.hamming(8) * 127)         # [ 10,  32,  82, 121, 121,  82,  32,  10]
            #gain_list = np.rint(np.hanning(10)[1:-1] * 127)  # [ 15,  52,  95, 123, 123,  95,  52,  15]
            #gain_list = np.rint(np.blackman(10)[1:-1] * 127) # [  6,  33,  80, 121, 121,  80,  33,   6]
            #gain_list = np.rint(np.bartlett(10)[1:-1] * 127) # [ 28,  56,  85, 113, 113,  85,  56,  28]
            for i in range(8):
                channel_gain = int(gain_list[i] * gain_cal[i])
                phaser.elements.get(i + 1).rx_gain = channel_gain

            phaser.latch_rx_settings() # apply settings

            steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1))) # arcsin argument must be between 1 and -1, or numpy will throw a warning
            angle_of_arrivals.append(steer_angle)
            data = phaser.sdr.rx() # receive a batch of samples
            data_sum = data[0] + data[1] # sum the two subarrays (within each subarray the 4 channels have already been summed)
            power_dB = 10*np.log10(np.sum(np.abs(data_sum)**2))
            powers.append(power_dB)

        powers -= np.max(powers) # normalize so max is at 0 dB

        # Real-time view
        plt.plot(angle_of_arrivals, powers, '.-')
        plt.xlabel("Angle of Arrival")
        plt.ylabel("Magnitude [dB]")
        plt.draw()
        plt.pause(0.001)
        plt.clf()

except KeyboardInterrupt:
    sys.exit() # quit python

Deber铆as ver una versi贸n en tiempo real del ejercicio anterior. Intente cambiar qu茅 gain_list se utiliza para jugar con las diferentes ventanas. A continuaci贸n se muestra un ejemplo de ventana rectangular (es decir, sin funci贸n de ventana):

Beamforming animation using the Phaser and a rectangular window

y aqu铆 hay un ejemplo de la ventana Hamming:

Beamforming animation using the Phaser and a Hamming window

Obs茅rvese la falta de l贸bulos laterales para Hamming. De hecho, todas las ventanas, excepto la rectangular, reducir谩n en gran medida los l贸bulos laterales, pero a cambio el l贸bulo principal ser谩 un poco m谩s ancho.

Seguimiento monopulso

Hasta este punto hemos estado realizando barridos individuales para encontrar el 谩ngulo de llegada de un transmisor de prueba (el HB100). Pero digamos que deseamos recibir continuamente una se帽al de radar o de comunicaciones, que puede estar en movimiento y provocar que el 谩ngulo de llegada cambie con el tiempo. Nos referimos a este proceso como seguimiento y supone que ya tenemos una estimaci贸n aproximada del 谩ngulo de llegada (es decir, el barrido inicial ha identificado una se帽al de inter茅s). Usaremos el seguimiento monopulso para actualizar de forma adaptativa los pesos a fin de mantener el l贸bulo principal apuntando a la se帽al a lo largo del tiempo, aunque tenga en cuenta que existen otros m茅todos de seguimiento adem谩s del monopulso.

Inventado en 1943 por Robert Page en el Laboratorio de Investigaci贸n Naval (NRL), el concepto b谩sico del seguimiento monopulso es utilizar dos haces, ambos ligeramente desviados del 谩ngulo de llegada actual (o al menos nuestra estimaci贸n del mismo), pero en lados diferentes como se muestra en el siguiente diagrama.

Monopulse beam diagram showing two beams and the sum beam

Luego tomamos la suma y la diferencia (tambi茅n conocida como delta) de estos dos haces digitales, lo que significa que debemos usar dos canales digitales del Phaser, lo que hace que este sea un enfoque de matriz h铆brida (aunque ciertamente se podr铆a hacer la suma y la diferencia en anal贸gico con dispositivos de hardware personalizados). El haz suma equivaldr谩 a un haz centrado en el 谩ngulo actual de llegada estimado, como se muestra arriba, lo que significa que este haz se puede utilizar para demodular/decodificar la se帽al de inter茅s. El haz delta, como lo llamaremos, es m谩s dif铆cil de visualizar, pero tendr谩 una estimaci贸n nula en el 谩ngulo de llegada. Podemos usar la relaci贸n entre el haz suma y el haz delta (denominado error) para realizar nuestro seguimiento. Este proceso se explica mejor con un breve fragmento de Python; Recuerde que la funci贸n rx() devuelve un lote de muestras de ambos canales, por lo que en el siguiente c贸digo data[0] es el primer canal del Pluto (primer conjunto de cuatro elementos del Phaser) y data[1] es el segundo canal (segundo conjunto de cuatro elementos). Para crear dos direcciones, para cada uno de los dos conjuntos por separado. Podemos calcular la suma, delta y error de la siguiente manera:

data = phaser.sdr.rx()
sum_beam = data[0] + data[1]
delta_beam = data[0] - data[1]
error = np.mean(np.real(delta_beam / sum_beam))

El signo del error nos dice de qu茅 direcci贸n proviene realmente la se帽al y la magnitud nos dice a qu茅 distancia estamos de la se帽al. Luego podemos usar esta informaci贸n para actualizar la estimaci贸n del 谩ngulo de llegada y los pesos. Repitiendo este proceso en tiempo real podemos rastrear la se帽al.

Ahora, saltando al ejemplo completo de Python, comenzaremos copiando el c贸digo que usamos anteriormente para realizar un barrido de 180 grados. El 煤nico c贸digo que agregaremos es sacar la fase en la que la potencia recibida era m谩xima:

# Sweep phase once to get initial estimate for AOA, using code above
# ...
current_phase = phase_angles[np.argmax(powers)]
print("max_phase:", current_phase)

A continuaci贸n crearemos dos haces, comenzaremos probando 5 grados m谩s bajos y 5 grados m谩s altos que la estimaci贸n actual, aunque tenga en cuenta que esto est谩 en unidades de fase, no los hemos convertido a 谩ngulo de direcci贸n, aunque son similares. El siguiente c贸digo es esencialmente dos copias del c贸digo que usamos anteriormente para configurar los desfasadores de cada canal, excepto que usamos los primeros 4 elementos para el haz inferior y los 煤ltimos 4 elementos para el haz superior:

# Now we create the two beams on either side of our current estimate
phase_offset = np.radians(5) # TRY TWEAKING THIS - specify offset from center in degrees
phase_lower = current_phase - phase_offset
phase_upper = current_phase + phase_offset
# first 4 elements will be used for lower beam
for i in range(0, 4):
   channel_phase = (phase_lower * i + phase_cal[i]) % 360.0
   phaser.elements.get(i + 1).rx_phase = channel_phase
# last 4 elements will be used for upper beam
for i in range(4, 8):
   channel_phase = (phase_upper * i + phase_cal[i]) % 360.0
   phaser.elements.get(i + 1).rx_phase = channel_phase
phaser.latch_rx_settings() # apply settings

Antes de realizar el seguimiento real, probemos lo anterior manteniendo constantes los pesos del haz y moviendo el HB100 hacia la izquierda y hacia la derecha (despu茅s de que termine de inicializarse para encontrar el 谩ngulo inicial):

print("START MOVING THE HB100 A LITTLE LEFT AND RIGHT")
error_log = []
for i in range(1000):
   data = phaser.sdr.rx() # receive a batch of samples
   sum_beam = data[0] + data[1]
   delta_beam = data[0] - data[1]
   error = np.mean(np.real(delta_beam / sum_beam))
   error_log.append(error)
   print(error)
   time.sleep(0.01)

plt.plot(error_log)
plt.plot([0,len(error_log)], [0,0], 'r--')
plt.xlabel("Time")
plt.ylabel("Error")
plt.show()
Showing error function for monopulse tracking without actually updating the weights

Lo que sucede en este ejemplo es que estoy moviendo el HB100. Empiezo manteni茅ndolo en una posici贸n estable mientras se realiza el barrido de 180 grados, luego, una vez hecho, lo muevo un poco hacia la derecha y lo muevo, luego lo muevo hacia la izquierda de donde comenc茅 y lo muevo. Luego, alrededor del tiempo = 400 en la trama, lo muevo hacia el otro lado y lo mantengo all铆 por un momento, antes de agitarlo una vez m谩s. La conclusi贸n es que cuanto m谩s se aleja el HB100 del 谩ngulo inicial, mayor es el error, y el signo del error nos indica de qu茅 lado est谩 el HB100 en relaci贸n con el 谩ngulo inicial.

Ahora usemos el valor de error para actualizar los pesos. Nos desharemos del bucle for anterior y crearemos un nuevo bucle for durante todo el proceso. Para mayor claridad, tenemos el ejemplo de c贸digo completo a continuaci贸n, excepto la parte inicial donde hicimos el barrido de 180 grados:

# Sweep phase once to get initial estimate for AOA
# ...
current_phase = phase_angles[np.argmax(powers)]
print("max_phase:", current_phase)

# Now we'll actually update the current_phase based on the error
print("START MOVING THE HB100 A LITTLE LEFT AND RIGHT")
phase_log = []
error_log = []
for ii in range(500):
   # Now we create the two beams on either side of our current estimate, using the specified offset
   phase_offset = np.radians(5)
   phase_lower = current_phase - phase_offset
   phase_upper = current_phase + phase_offset
   # first 4 elements will be used for lower beam
   for i in range(0, 4):
         channel_phase = (phase_lower * i + phase_cal[i]) % 360.0
         phaser.elements.get(i + 1).rx_phase = channel_phase
   # last 4 elements will be used for upper beam
   for i in range(4, 8):
         channel_phase = (phase_upper * i + phase_cal[i]) % 360.0
         phaser.elements.get(i + 1).rx_phase = channel_phase
   phaser.latch_rx_settings() # apply settings

   data = phaser.sdr.rx() # receive a batch of samples
   sum_beam = data[0] + data[1]
   delta_beam = data[0] - data[1]
   error = np.mean(np.real(delta_beam / sum_beam))
   error_log.append(error)
   print(error)

   # Update our estimated angle of arrival based on error
   current_phase += -10 * error # was manually tweaked until it seemed to track at a nice speed
   steer_angle = np.degrees(np.arcsin(max(min(1, (3e8 * np.radians(current_phase)) / (2 * np.pi * signal_freq * phaser.element_spacing)), -1)))
   phase_log.append(steer_angle) # looks nicer to plot steer angle instead of straight phase

   time.sleep(0.01)

fig, [ax0, ax1] = plt.subplots(2, 1, figsize=(8, 10))

ax0.plot(phase_log)
ax0.plot([0,len(phase_log)], [0,0], 'r--')
ax0.set_xlabel("Time")
ax0.set_ylabel("Phase Estimate [degrees]")

ax1.plot(error_log)
ax1.plot([0,len(error_log)], [0,0], 'r--')
ax1.set_xlabel("Time")
ax1.set_ylabel("Error")

plt.show()
Monopulse tracking demo using a Phaser and HB100 being waved around infront of it

Puede ver que el error es esencialmente la derivada de la estimaci贸n de fase; debido a que estamos realizando un seguimiento exitoso, la estimaci贸n de fase es m谩s o menos el 谩ngulo de llegada real. No est谩 claro mirando s贸lo estos gr谩ficos, pero cuando hay un movimiento repentino, al sistema le toma una peque帽a fracci贸n de segundo ajustarse y ponerse al d铆a. El objetivo es que el cambio de 谩ngulo de llegada nunca sea tan r谩pido como para que la se帽al llegue m谩s all谩 de los l贸bulos principales de los dos haces.

Es mucho m谩s f谩cil visualizar el proceso cuando la matriz es solo 1D, pero los casos de uso pr谩cticos de seguimiento monopulso casi siempre son 2D (usando una matriz 2D/planar en lugar de una matriz lineal como el Phaser). Para el caso 2D, se crean cuatro direcciones en lugar de dos, y despu茅s del proceso hay una direcci贸n de suma 煤nica y cuatro direcciones delta que se utilizan para dirigir en ambas dimensiones.

Radar con Phaser

Proximamente!

Conclusiones

El c贸digo completo utilizado para generar las figuras de este cap铆tulo est谩 disponible en la p谩gina GitHub del libro de texto.