6. USRP en Python

The family of USRP radios from Ettus Research

En este cap铆tulo aprenderemos c贸mo usar la API UHD Python para controlar y recibir/transmitir se帽ales con un USRP que es una serie de SDR fabricados por Ettus Research (ahora parte de NI). Analizaremos la transmisi贸n y recepci贸n en USRP en Python y profundizaremos en temas espec铆ficos de USRP, incluidos argumentos de transmisi贸n, subdispositivos, canales, sincronizaci贸n de 10 MHz y PPS.

Instalaci贸n de Software/Drivers USRP

Si bien el c贸digo Python proporcionado en este libro de texto deber铆a funcionar en Windows, Mac y Linux, solo proporcionaremos instrucciones de instalaci贸n de controladores/API espec铆ficas para Ubuntu 22 (aunque las instrucciones a continuaci贸n deber铆an funcionar en la mayor铆a de las distribuciones basadas en Debian). Comenzaremos creando una VM VirtualBox Ubuntu 22; no dude en omitir la parte de VM si ya tiene su sistema operativo listo para funcionar. Alternativamente, si est谩 en Windows 11, el Subsistema de Windows para Linux (WSL) que usa Ubuntu 22 tiende a funcionar bastante bien y admite gr谩ficos listos para usar.

Configurar la m谩quina virtual en Ubuntu 22

(Opcional)

  1. Descarge Ubuntu 22.04 Desktop .iso- https://ubuntu.com/download/desktop

  2. Instale y abra VirtualBox.

  3. Cree una nueva m谩quina virtual. Para el tama帽o de la memoria, recomiendo utilizar el 50% de la RAM de su computadora.

  4. Cree el disco duro virtual, elija VDI y asigne tama帽o din谩micamente. 15 GB deber铆an ser suficientes. Si quieres estar realmente seguro, puedes usar m谩s.

  5. Inicie la m谩quina virtual. Le pedir谩 medios de instalaci贸n. Elija el archivo .iso del escritorio de Ubuntu 22. Elija 鈥渋nstalar ubuntu鈥, use las opciones predeterminadas y una ventana emergente le advertir谩 sobre los cambios que est谩 a punto de realizar. Pulsa continuar. Elija nombre/contrase帽a y luego espere a que la VM termine de inicializarse. Despu茅s de finalizar, la VM se reiniciar谩, pero debe apagarla despu茅s del reinicio.

  6. Vaya a la configuraci贸n de VM (el 铆cono de ajustes).

  7. En sistema > procesador > elija al menos 3 CPU. Si tiene una tarjeta de video real, en pantalla > memoria de video > elija algo mucho m谩s alto.

  8. Inicie su m谩quina virtual.

  9. Para USRP de tipo USB, deber谩 instalar adiciones de invitados de VM. Dentro de la m谩quina virtual, vaya a Dispositivos > Insertar CD de Guest Additions > presione ejecutar cuando aparezca un cuadro. Sigue las instrucciones. Reinicie la VM, luego intente reenviar el USRP a la VM, suponiendo que aparezca en la lista en Dispositivos > USB. El portapapeles compartido se puede habilitar a trav茅s de Dispositivos > Portapapeles compartido > Bidireccional.

Instalaci贸n de UHD y API de Python

Los siguientes comandos de terminal deber铆an compilar e instalar la 煤ltima versi贸n de UHD, incluida la API de Python:

sudo apt-get install git cmake libboost-all-dev libusb-1.0-0-dev python3-docutils python3-mako python3-numpy python3-requests python3-ruamel.yaml python3-setuptools build-essential
cd ~
git clone https://github.com/EttusResearch/uhd.git
cd uhd/host
mkdir build
cd build
cmake -DENABLE_TESTS=OFF -DENABLE_C_API=OFF -DENABLE_MANUAL=OFF ..
make -j8
sudo make install
sudo ldconfig

Para obtener m谩s ayuda, consulte el sitio oficial de Ettus. La pagina Construyendo e instalando UHD desde la fuente . Tenga en cuenta que tambi茅n existen m茅todos para instalar los controladores que no requieren compilaci贸n desde la fuente.

Prueba de controladores UHD y API de Python

Abra una nueva terminal y escriba los siguientes comandos:

python3
import uhd
usrp = uhd.usrp.MultiUSRP()
samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50)
print(samples[0:10])

Si no se producen errores, 隆est谩 listo para comenzar!

Evaluaci贸n comparativa de la velocidad USRP en Python

(Opcional)

Si utiliz贸 la instalaci贸n est谩ndar desde el origen, el siguiente comando deber铆a comparar la tasa de recepci贸n de su USRP utilizando la API de Python. Si el uso de 56e6 provoc贸 la ca铆da de muchas muestras o excesos, intente reducir el n煤mero. Las muestras ca铆das no necesariamente arruinar谩n nada, pero es una buena manera de probar las ineficiencias que pueden surgir al usar una m谩quina virtual o una computadora m谩s antigua, por ejemplo. Si usa un B 2X0, una computadora bastante moderna con un puerto USB 3.0 funcionando correctamente deber铆a lograr funcionar a 56 MHz sin p茅rdida de muestras, especialmente con num_recv_frames configurado en un nivel tan alto.

python /usr/lib/uhd/examples/python/benchmark_rate.py --rx_rate 56e6 --args "num_recv_frames=1000"

Recepci贸n USRP

Recibir muestras de un USRP es extremadamente f谩cil usando la funci贸n incorporada 鈥渞ecv_num_samps()鈥, a continuaci贸n se muestra el c贸digo Python que sintoniza el USRP a 100 MHz, usando una frecuencia de muestreo de 1 MHz, y toma 10,000 muestras del USRP, usando una ganancia de recepci贸n de 50 dB:

import uhd
usrp = uhd.usrp.MultiUSRP()
samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) # units: N, Hz, Hz, list of channel IDs, dB
print(samples[0:10])

El [0] le dice al USRP que use su primer puerto de entrada y que solo reciba muestras de un canal (para que un B210 reciba dos canales a la vez, por ejemplo, puede usar [0, 1]).

Aqu铆 tienes un consejo si est谩s intentando recibir a una velocidad alta pero obtienes desbordamientos (aparecen 0鈥檚 en tu consola). En su lugar usa usrp = uhd.usrp.MultiUSRP() :

usrp = uhd.usrp.MultiUSRP("num_recv_frames=1000")

lo que hace que el b煤fer de recepci贸n sea mucho m谩s grande (el valor predeterminado es 32), lo que ayuda a reducir los desbordamientos. El tama帽o real del b煤fer en bytes depende del USRP y del tipo de conexi贸n, pero simplemente configurando num_recv_frames a un valor mucho mayor que 32 tiende a ayudar.

Para aplicaciones m谩s serias, recomiendo no usar la funci贸n de conveniencia recv_num_samps(), porque oculta algunos de los comportamientos interesantes que ocurren bajo el cap贸, y hay algunas configuraciones que ocurren en cada llamada que quiz谩s solo queramos hacer una vez al principio. , por ejemplo, si queremos recibir muestras de forma indefinida. El siguiente c贸digo tiene la misma funcionalidad que recv_num_samps(); de hecho, es casi exactamente lo que se llama cuando usas la funci贸n de conveniencia, pero ahora tenemos la opci贸n de modificar el comportamiento:

import uhd
import numpy as np

usrp = uhd.usrp.MultiUSRP()

num_samps = 10000 # number of samples received
center_freq = 100e6 # Hz
sample_rate = 1e6 # Hz
gain = 50 # dB

usrp.set_rx_rate(sample_rate, 0)
usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_freq), 0)
usrp.set_rx_gain(gain, 0)

# Set up the stream and receive buffer
st_args = uhd.usrp.StreamArgs("fc32", "sc16")
st_args.channels = [0]
metadata = uhd.types.RXMetadata()
streamer = usrp.get_rx_stream(st_args)
recv_buffer = np.zeros((1, 1000), dtype=np.complex64)

# Start Stream
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont)
stream_cmd.stream_now = True
streamer.issue_stream_cmd(stream_cmd)

# Receive Samples
samples = np.zeros(num_samps, dtype=np.complex64)
for i in range(num_samps//1000):
    streamer.recv(recv_buffer, metadata)
    samples[i*1000:(i+1)*1000] = recv_buffer[0]

# Stop Stream
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)
streamer.issue_stream_cmd(stream_cmd)

print(len(samples))
print(samples[0:10])

Con num_samps configurado en 10,000 y recv_buffer configurado en 1000, el bucle for se ejecutar谩 10 veces, es decir, habr谩 10 llamadas a streamer.recv. Tenga en cuenta que codificamos recv_buffer en 1000, pero puede encontrar el valor m谩ximo permitido usando streamer.get_max_num_samps(), que suele rondar los 3000 y tantos. Tambi茅n tenga en cuenta que recv_buffer debe ser 2d porque se usa la misma API cuando se reciben m煤ltiples canales a la vez, pero en nuestro caso solo recibimos un canal, por lo que recv_buffer[0] nos dio la matriz 1D de muestras que quer铆amos. No es necesario que entiendas mucho sobre c贸mo inicia y finaliza la transmisi贸n por ahora, pero debes saber que hay otras opciones adem谩s del modo 鈥渃ontinuo鈥, como recibir una cantidad espec铆fica de muestras y hacer que la transmisi贸n se detenga autom谩ticamente. Aunque no procesamos metadatos en este c贸digo de ejemplo, contiene los errores que ocurren, entre otras cosas, que puede verificar mirando metadata.error_code en cada iteraci贸n del bucle, si lo desea (los errores tienden a aparecer tambi茅n en la propia consola, como resultado de UHD, as铆 que no sienta que tiene que buscarlos en su c贸digo Python).

Ganancia del Receptor

La siguiente lista muestra el rango de ganancia de los diferentes USRP, todos van desde 0 dB hasta el n煤mero especificado a continuaci贸n. Tenga en cuenta que esto no es dBm, es esencialmente dBm combinado con alg煤n desplazamiento desconocido porque estos no son dispositivos calibrados.

  • B200/B210/B200-mini: 76 dB

  • X310/N210 with WBX/SBX/UBX: 31.5 dB

  • X310 with TwinRX: 93 dB

  • E310/E312: 76 dB

  • N320/N321: 60 dB

Tambi茅n puedes usar el comando uhd_usrp_probe en un terminal y en la secci贸n RX Frontend mencionar谩 el rango de ganancia.

Al especificar la ganancia, puede usar la funci贸n normal set_rx_gain() que toma el valor de ganancia en dB, pero tambi茅n puede usar set_normalized_rx_gain() que toma un valor de 0 a 1 y lo convierte autom谩ticamente al rango del USRP. est谩s usando. Esto resulta 煤til a la hora de crear una aplicaci贸n que admita diferentes modelos de USRP. La desventaja de usar ganancia normalizada es que ya no tienes tus unidades en dB, por lo que si quieres aumentar tu ganancia en 10 dB, por ejemplo, ahora tienes que calcular la cantidad.

Control de ganancia autom谩tica

Algunos USRP, incluidas las series B200 y E310, admiten el control autom谩tico de ganancia (AGC), que ajustar谩 autom谩ticamente la ganancia de recepci贸n en respuesta al nivel de la se帽al recibida, en un intento de 鈥渓lenar鈥 mejor los bits del ADC. AGC se puede activar usando:

usrp.set_rx_agc(True, 0) # 0 for channel 0, i.e. the first channel of the USRP

Si tiene un USRP que no implementa un AGC, se generar谩 una excepci贸n al ejecutar la l铆nea anterior. Con AGC activado, configurar la ganancia no har谩 nada.

Argumentos de transmisi贸n

En el ejemplo completo anterior ver谩s la l铆nea st_args = uhd.usrp.StreamArgs("fc32", "sc16"). El primer argumento es el formato de datos de la CPU, que es el tipo de datos de las muestras una vez que est谩n en su computadora. UHD admite los siguientes tipos de datos de CPU cuando se utiliza la API de Python:

Stream Arg

Numpy Data Type

Description

fc64

np.complex128

Complex-valued double-precision data

fc32

np.complex64

Complex-valued single-precision data

Es posible que vea otras opciones en la documentaci贸n de la API UHD C++, pero nunca se implementaron dentro de la API de Python, al menos en el momento de escribir este art铆culo.

El segundo argumento es el formato de datos 鈥減or cable鈥, es decir, el tipo de datos a medida que las muestras se env铆an a trav茅s de USB/Ethernet/SFP al host. Para la API de Python, las opciones son: 鈥渟c16鈥, 鈥渟c12鈥 y 鈥渟c8鈥, y la opci贸n de 12 bits solo es compatible con ciertos USRP. Esta elecci贸n es importante porque la conexi贸n entre el USRP y la computadora host suele ser el cuello de botella, por lo que al cambiar de 16 bits a 8 bits se puede lograr una velocidad m谩s alta. Recuerde tambi茅n que muchos USRP tienen ADC limitados a 12 o 14 bits; usar 鈥渟c16鈥 no significa que el ADC sea de 16 bits.

Para la parte del canal st_args, consulte la subsecci贸n Subdispositivos y canales a continuaci贸n.

Transmisor

De manera similar a la funci贸n de conveniencia recv_num_samps(), UHD proporciona la funci贸n send_waveform() para transmitir un lote de muestras; a continuaci贸n se muestra un ejemplo. Si especifica una duraci贸n (en segundos) mayor que la se帽al proporcionada, simplemente la repetir谩. Ayuda a mantener los valores de las muestras entre -1,0 y 1,0.

import uhd
import numpy as np
usrp = uhd.usrp.MultiUSRP()
samples = 0.1*np.random.randn(10000) + 0.1j*np.random.randn(10000) # create random signal
duration = 10 # seconds
center_freq = 915e6
sample_rate = 1e6
gain = 20 # [dB] start low then work your way up
usrp.send_waveform(samples, duration, center_freq, sample_rate, [0], gain)

Para obtener detalles sobre c贸mo funciona esta pr谩ctica funci贸n interna, consulte el c贸digo fuente. aqui.

Ganancia del transmisor

De manera similar al lado de recepci贸n, el rango de ganancia de transmisi贸n var铆a seg煤n el modelo USRP, desde 0 dB hasta el n煤mero especificado a continuaci贸n:

  • B200/B210/B200-mini: 90 dB

  • N210 with WBX: 25 dB

  • N210 with SBX or UBX: 31.5 dB

  • E310/E312: 90 dB

  • N320/N321: 60 dB

Tambi茅n hay una funci贸n set_normalized_tx_gain() si desea especificar la ganancia de transmisi贸n usando el rango de 0 a 1.

Transmitir y recibir simult谩neamente con USRP

Si deseas transmitir y recibir usando el mismo USRP al mismo tiempo, la clave es hacerlo usando m煤ltiples hilos dentro del mismo proceso; el USRP no puede abarcar m煤ltiples procesos. Por ejemplo, en el txrx_loopback_to_file en el ejemplo de C++ se crea un hilo separado para ejecutar el transmisor y la recepci贸n se realiza en el hilo principal. Tambi茅n puedes generar dos hilos, uno para transmitir y otro para recibir, como se hace en el benchmark_rate del ejemplo en Python. Aqu铆 no se muestra un ejemplo completo, simplemente porque ser铆a un ejemplo bastante largo y benchmark_rate.py de Ettus siempre puede actuar como punto de partida para alguien.

Subdispositivo, canales y antenas

Una fuente com煤n de confusi贸n al utilizar USRP es c贸mo elegir el subdispositivo y la ID de canal correctos. Es posible que hayas notado que en todos los ejemplos anteriores utilizamos el canal 0 y no especificamos nada relacionado con el subdesarrollo. Si est谩 usando un B210 y solo quiere usar RF:B en lugar de RF:A, todo lo que tiene que hacer es elegir el canal 1 en lugar de 0. Pero en USRP como el X310 que tienen dos ranuras para placa secundaria, debe indicarlo. UHD si desea utilizar la ranura A o B y qu茅 canal en esa placa secundaria, por ejemplo:

usrp.set_rx_subdev_spec("B:0")

Si desea utilizar el puerto TX/RX en lugar del RX2 (el predeterminado), es tan simple como:

usrp.set_rx_antenna('TX/RX', 0) # set channel 0 to 'TX/RX'

que b谩sicamente solo controla un interruptor de RF a bordo del USRP, para enrutarlo desde el otro conector SMA.

Para recibir o transmitir en dos canales a la vez, en lugar de utilizar st_args.channels = [0] se proporciona una lista, como [0,1]. El b煤fer de muestras de recepci贸n tendr谩 que ser de tama帽o (2, N) en este caso, en lugar de (1,N). S贸lo recuerde que con la mayor铆a de los USRP, ambos canales comparten un LO, por lo que no puede sintonizar diferentes frecuencias a la vez.

Sincronizaci贸n a 10 MHz y PPS

Una de las grandes ventajas de utilizar un USRP sobre otros SDR es su capacidad de sincronizarse con una fuente externa o integrada. GPSDO, permitiendo aplicaciones multireceptor como TDOA. Si ha conectado una fuente externa de 10 MHz y PPS a su USRP, querr谩 asegurarse de llamar a estas dos l铆neas despu茅s de inicializar su USRP:

usrp.set_clock_source("external")
usrp.set_time_source("external")

Si est谩 utilizando un GPSDO a bordo, utilizar谩 en su lugar:

usrp.set_clock_source("gpsdo")
usrp.set_time_source("gpsdo")

En cuanto a la sincronizaci贸n de frecuencia, no hay mucho m谩s que hacer; El LO utilizado en el mezclador del USRP ahora estar谩 vinculado a la fuente externa o GPSDO. Pero en lo que respecta al tiempo, es posible que desee ordenar al USRP que comience a muestrear exactamente en el PPS, por ejemplo. Esto se puede hacer con el siguiente c贸digo:

# copy the receive example above, everything up until # Start Stream

# Wait for 1 PPS to happen, then set the time at next PPS to 0.0
time_at_last_pps = usrp.get_time_last_pps().get_real_secs()
while time_at_last_pps == usrp.get_time_last_pps().get_real_secs():
    time.sleep(0.1) # keep waiting till it happens- if this while loop never finishes then the PPS signal isn't there
usrp.set_time_next_pps(uhd.libpyuhd.types.time_spec(0.0))

# Schedule Rx of num_samps samples exactly 3 seconds from last PPS
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done)
stream_cmd.num_samps = num_samps
stream_cmd.stream_now = False
stream_cmd.time_spec = uhd.libpyuhd.types.time_spec(3.0) # set start time (try tweaking this)
streamer.issue_stream_cmd(stream_cmd)

# Receive Samples.  recv() will return zeros, then our samples, then more zeros, letting us know it's done
waiting_to_start = True # keep track of where we are in the cycle (see above comment)
nsamps = 0
i = 0
samples = np.zeros(num_samps, dtype=np.complex64)
while nsamps != 0 or waiting_to_start:
    nsamps = streamer.recv(recv_buffer, metadata)
    if nsamps and waiting_to_start:
        waiting_to_start = False
    elif nsamps:
        samples[i:i+nsamps] = recv_buffer[0][0:nsamps]
    i += nsamps

Si parece que no funciona, pero no arroja ning煤n error, intente cambiar ese n煤mero 3.0 entre 1.0 y 5.0. Tambi茅n puede verificar los metadatos despu茅s de la llamada a recv(), simplemente verifique if metadata.error_code != uhd.types.RXMetadataErrorCode.none:.

Por motivos de depuraci贸n, puede verificar que la se帽al de 10 MHz se muestra en el USRP verificando el retorno de usrp.get_mboard_sensor("ref_locked", 0). Si la se帽al PPS no aparece, lo sabr谩s porque el primer bucle while del c贸digo anterior nunca finalizar谩.