USRP en Python

_images/usrp.png

Dans ce chapitre, nous apprenons Ă  utiliser l’API Python UHD pour contrĂŽler et recevoir/transmettre des signaux avec un USRP qui est une sĂ©rie de SDRs fabriquĂ©s par Ettus Research (qui fait maintenant partie de NI). Nous discuterons de la transmission et de la rĂ©ception sur l’USRP en Python, et nous plongerons dans les sujets spĂ©cifiques Ă  l’USRP, notamment les args de streams ou flux, les sous-devices, les canaux, la synchronisation 10 MHz et PPS.

Installation de logiciels/pilotes

Bien que le code Python fourni dans ce manuel doive fonctionner sous Windows, Mac et Linux, nous ne fournirons que des instructions d’installation des pilotes/API spĂ©cifiques Ă  Ubuntu 20 (bien que les instructions ci-dessous doivent fonctionner sur la plupart des distributions basĂ©es sur Debian). Nous allons commencer par crĂ©er une VM Ubuntu 20 VirtualBox ; n’hĂ©sitez pas Ă  sauter la partie VM si votre systĂšme d’exploitation est dĂ©jĂ  prĂȘt.

Configuration d’une VM Ubuntu 20

(Optionel)

  1. Télécharger Ubuntu 20.04 Desktop .iso- https://ubuntu.com/download/desktop

  2. Installez et ouvrez VirtualBox.

  3. CrĂ©ez une nouvelle VM. Pour la taille de la mĂ©moire, je recommande d’utiliser 50% de la RAM de votre ordinateur.

  4. CrĂ©ez le disque dur virtuel, choisissez VDI, et allouez dynamiquement la taille. 15 Go devraient suffire. Si vous voulez ĂȘtre vraiment sĂ»r, vous pouvez utiliser plus.

  5. DĂ©marrez la VM. Il vous demandera le support d’installation. Choisissez le fichier .iso du bureau Ubuntu 20. Choisissez « install ubuntu », utilisez les options par dĂ©faut, et une fenĂȘtre pop-up vous avertira des changements que vous ĂȘtes sur le point d’effectuer. Cliquez sur continuer. Choisissez le nom/mot de passe et attendez que la VM finisse de s’initialiser. AprĂšs avoir terminĂ©, la VM va redĂ©marrer, mais vous devez Ă©teindre la VM aprĂšs le redĂ©marrage.

  6. Allez dans les paramùtres de la VM (l’icîne de l’engrenage).

  7. Sous systÚme > processeur > choisissez au moins 3 processeurs. Si vous avez une carte vidéo réelle, alors dans affichage > mémoire vidéo > choisissez quelque chose de beaucoup plus élevé.

  8. Démarrez votre VM.

  9. Pour les USRP de type USB, vous devrez installer des ajouts invitĂ©s VM. Dans la VM, allez dans PĂ©riphĂ©riques > InsĂ©rer le CD Guest Additions > cliquez sur run quand une boĂźte apparaĂźt. Suivez les instructions. RedĂ©marrez la VM, puis essayez de transfĂ©rer l’USRP Ă  la VM, en supposant qu’elle apparaisse dans la liste sous PĂ©riphĂ©riques > USB. Le presse-papiers partagĂ© peut ĂȘtre activĂ© via Dispositifs > Presse-papiers partagĂ© > Bidirectionnel.

Installation de l’UHD et de l’API Python

Les commandes de terminal ci-dessous devraient compiler et installer la derniùre version de UHD, y compris l’API 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

Pour plus d’aide, voir la page officielle d’Ettus Building and Installing UHD from source. Notez qu’il existe Ă©galement des mĂ©thodes d’installation des pilotes qui ne nĂ©cessitent pas de construire Ă  partir des sources.

Test des pilotes UHD et de l’API Python

Ouvrez un nouveau terminal et tapez les commandes suivantes :

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

Si aucune erreur ne se produit, vous ĂȘtes prĂȘt Ă  partir !

Analyse comparative de la vitesse de l’USRP en Python

(Optionel)

Si vous avez utilisĂ© l’installation standard, la commande suivante devrait Ă©valuer le taux de rĂ©ception de votre USRP en utilisant l’API Python. Si l’utilisation de 56e6 a causĂ© beaucoup d’échantillons perdus ou de dĂ©passements, essayez de diminuer le nombre. Les Ă©chantillons perdus ne vont pas nĂ©cessairement ruiner quoi que ce soit, mais c’est un bon moyen de tester les inefficacitĂ©s qui peuvent venir de l’utilisation d’une VM ou d’un ordinateur plus ancien, par exemple. Si vous utilisez un B 2X0, un ordinateur assez moderne avec un port USB 3.0 fonctionnant correctement devrait rĂ©ussir Ă  faire 56 MHz sans Ă©chantillons perdus, surtout avec num_recv_frames rĂ©glĂ© aussi haut.

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

RĂ©ception

La rĂ©ception d’échantillons Ă  partir d’une USRP est extrĂȘmement facile grĂące Ă  la fonction de commoditĂ© intĂ©grĂ©e « recv_num_samps() ». Le code Python ci-dessous accorde l’USRP Ă  100MHz, utilise une frĂ©quence d’échantillonnage de 1MHz et prĂ©lĂšve 10 000 Ă©chantillons Ă  partir de l’USRP, en utilisant un gain de rĂ©ception de 50dB :

import uhd
usrp = uhd.usrp.MultiUSRP()
samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) # unités: N, Hz, Hz, liste des canaux IDs, dB
print(samples[0:10])

Le [0] indique Ă  l’USRP d’utiliser son premier port d’entrĂ©e et de ne recevoir qu’un seul canal d’échantillons (pour qu’un B210 reçoive sur deux canaux Ă  la fois, par exemple, vous pourriez utiliser [0, 1]).

Voici une astuce si vous essayez de recevoir Ă  un taux Ă©levĂ© mais que vous obtenez des dĂ©bordements (des O s’affichent dans votre console). Au lieu de usrp = uhd.usrp.MultiUSRP(), utilisez :

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

qui rend le tampon de rĂ©ception beaucoup plus grand (la valeur par dĂ©faut est de 32), ce qui permet de rĂ©duire les dĂ©bordements. La taille rĂ©elle du tampon en octets dĂ©pend de l’USRP et du type de connexion, mais le simple fait de dĂ©finir num_recv_frames Ă  une valeur bien supĂ©rieure Ă  32 permet d’aider.

Pour des applications plus sĂ©rieuses, je recommande de ne pas utiliser la fonction recv_num_samps(), parce qu’elle cache une partie du comportement intĂ©ressant qui se passe sous le capot, et il y a une certaine configuration qui se produit Ă  chaque appel que nous pourrions vouloir faire seulement une fois au dĂ©but, par exemple, si nous voulons recevoir des Ă©chantillons indĂ©finiment. Le code suivant a la mĂȘme fonctionnalitĂ© que recv_num_samps(), en fait c’est presque exactement ce qui est appelĂ© lorsque vous utilisez cette fonction, mais maintenant nous avons la possibilitĂ© de modifier le comportement :

import uhd
import numpy as np

usrp = uhd.usrp.MultiUSRP()

num_samps = 10000 # nombre d'échantillons reçus
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)

# Configurer le flux et le tampon de réception
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)

# Démarrer le flux
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont)
stream_cmd.stream_now = True
streamer.issue_stream_cmd(stream_cmd)

# Recevoir des échantillons
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]

# ArrĂȘter le flux
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)
streamer.issue_stream_cmd(stream_cmd)

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

Avec num_samps fixĂ© Ă  10 000 et le recv_buffer fixĂ© Ă  1000, la boucle for sera exĂ©cutĂ©e 10 fois, c’est-Ă -dire qu’il y aura 10 appels Ă  streamer.recv. Notez que nous avons codĂ© en dur le recv_buffer Ă  1000 mais vous pouvez trouver la valeur maximale autorisĂ©e en utilisant streamer.get_max_num_samps(), qui se situe souvent autour de 3000 et quelques. Notez Ă©galement que recv_buffer doit ĂȘtre 2d car la mĂȘme API est utilisĂ©e lors de la rĂ©ception de plusieurs canaux Ă  la fois, mais dans notre cas, nous n’avons reçu qu’un seul canal, donc recv_buffer[0] nous a donnĂ© le tableau 1D d’échantillons que nous voulions. Pour l’instant, vous n’avez pas besoin d’en savoir trop sur la façon dont le flux dĂ©marre/arrĂȘte, mais sachez qu’il existe d’autres options que le mode « continu », comme recevoir un nombre spĂ©cifique d’échantillons et faire en sorte que le flux s’arrĂȘte automatiquement. Bien que nous ne traitions pas les mĂ©tadonnĂ©es dans cet exemple de code, elles contiennent toutes les erreurs qui se produisent, entre autres choses, que vous pouvez vĂ©rifier en regardant metadata.error_code Ă  chaque itĂ©ration de la boucle, si vous le souhaitez (les erreurs ont tendance Ă  apparaĂźtre Ă©galement dans la console elle-mĂȘme, en raison de l’UHD, donc ne vous sentez pas obligĂ© de les vĂ©rifier dans votre code Python).

Gain de rĂ©ception

La liste suivante montre la gamme de gain des diffĂ©rents USRP, ils vont tous de 0dB au nombre spĂ©cifiĂ© ci-dessous. Notez que ce n’est pas du dBm, c’est essentiellement du dBm combinĂ© Ă  un dĂ©calage inconnu car ce ne sont pas des appareils calibrĂ©s.

  • 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

Vous pouvez également utiliser la commande uhd_usrp_probe dans un terminal et dans la section RX Frontend il mentionnera la gamme de gain.

Pour spĂ©cifier le gain, vous pouvez utiliser la fonction normale set_rx_gain() qui prend la valeur du gain en dB, mais vous pouvez aussi utiliser set_normalized_rx_gain() qui prend une valeur de 0 Ă  1 et la convertit automatiquement dans la gamme de l’USRP que vous utilisez. Ceci est pratique lorsqu’on crĂ©e une application qui supporte diffĂ©rents modĂšles d’USRP. L’inconvĂ©nient de l’utilisation du gain normalisĂ© est que vous n’avez plus vos unitĂ©s en dB, donc si vous voulez augmenter votre gain de 10dB, par exemple, vous devez maintenant calculer la quantitĂ©.

ContrĂŽle automatique du gain

Certains USRP, y compris les sĂ©ries B200 et E310, prennent en charge la commande automatique de gain (AGC pour automatic gain controller en anglais) qui ajuste automatiquement le gain de rĂ©ception en fonction du niveau du signal reçu, afin d’essayer de « remplir » au mieux les bits du CAN. L’AGC peut ĂȘtre activĂ© en utilisant :

usrp.set_rx_agc(True, 0) # 0 pour le canal 0, c'est-Ă -dire le premier canal de l'USRP

Si vous avez une USRP qui n’implĂ©mente pas d’AGC, une exception sera levĂ©e lors de l’exĂ©cution de la ligne ci-dessus. Avec l’AGC activĂ©, le rĂ©glage du gain ne fera rien.

Arguments relatifs aux flux

Dans l’exemple complet ci-dessus, vous verrez la ligne st_args = uhd.usrp.StreamArgs("fc32", "sc16"). Le premier argument est le format de donnĂ©es CPU, qui est le type de donnĂ©es des Ă©chantillons une fois qu’ils sont sur votre ordinateur hĂŽte. UHD supporte les types de donnĂ©es CPU suivants lors de l’utilisation de l’API Python :

Stream Arg

Numpy Data Type

Description

fc64

np.complex128

Complex-valued double-precision data

fc32

np.complex64

Complex-valued single-precision data

Vous pouvez voir d’autres options dans la documentation de l’API UHD C++, mais elles n’ont jamais Ă©tĂ© implĂ©mentĂ©es dans l’API Python, du moins au moment de la rĂ©daction de ce document.

Le deuxiĂšme argument est le format de donnĂ©es « over-the-wire », c’est-Ă -dire le type de donnĂ©es lorsque les Ă©chantillons sont envoyĂ©s Ă  l’hĂŽte via USB/Ethernet/SFP. Pour l’API Python, les options sont : « sc16 », « sc12 » et « sc8 », l’option 12 bits n’étant prise en charge que par certains USRP. Ce choix est important car la connexion entre l’USRP et l’ordinateur hĂŽte est souvent le goulot d’étranglement, donc en passant de 16 bits Ă  8 bits, vous pouvez obtenir un taux plus Ă©levĂ©. Rappelez-vous Ă©galement que de nombreux USRP ont des CAN limitĂ©s Ă  12 ou 14 bits, utiliser « sc16 » ne signifie pas que le CAN est de 16 bits.

Pour la partie canal du st_args, voir la sous-section Sous-dispositif and Channels ci-dessous.

Transmettre

Similaire Ă  la fonction pratique recv_num_samps(), UHD fournit la fonction send_waveform() pour transmettre un lot d’échantillons, un exemple est montrĂ© ci-dessous. Si vous spĂ©cifiez une durĂ©e (en secondes) plus longue que le signal fourni, il sera simplement rĂ©pĂ©tĂ©. Il est utile de garder les valeurs des Ă©chantillons entre -1.0 et 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) # créer un signal aléatoire
duration = 10 # secondes
center_freq = 915e6
sample_rate = 1e6
gain = 20 # [dB] Commencez doucement puis montez en grade
usrp.send_waveform(samples, duration, center_freq, sample_rate, [0], gain)

Pour plus de détails sur la façon dont cette fonction pratique fonctionne sous le capot, voir le code source ici.

Gain d’émission

Comme pour la rĂ©ception, la plage de gain d’émission varie en fonction du modĂšle USRP, allant de 0 dB au nombre spĂ©cifiĂ© ci-dessous :

  • 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

Il existe Ă©galement une fonction set_normalized_tx_gain() si vous souhaitez spĂ©cifier le gain d’émission en utilisant la plage 0 Ă  1.

Transmettre et recevoir simultanĂ©ment

Si vous voulez Ă©mettre et recevoir en utilisant la mĂȘme USRP en mĂȘme temps, la clĂ© est de le faire en utilisant plusieurs threads dans le mĂȘme processus ; l’USRP ne peut pas couvrir plusieurs processus. Par exemple, dans l’exemple C++ txrx_loopback_to_file un thread sĂ©parĂ© est créé pour exĂ©cuter l’émetteur, et la rĂ©ception est faite dans le thread principal. Vous pouvez aussi simplement crĂ©er deux threads, un pour l’émission et un pour la rĂ©ception, comme cela est fait dans l’exemple Python benchmark_rate. Un exemple complet n’est pas montrĂ© ici, simplement parce que ce serait un exemple assez long et que le benchmark_rate.py d’Ettus peut toujours servir de point de dĂ©part Ă  quelqu’un.

Sous-dispositif, canaux et antennes

Une source frĂ©quente de confusion lors de l’utilisation des USRP est de savoir comment choisir le bon ID de sous-dispositif et de canal. Vous avez peut-ĂȘtre remarquĂ© que dans tous les exemples ci-dessus, nous avons utilisĂ© le canal 0, et n’avons rien spĂ©cifiĂ© concernant le subdev. Si vous utilisez un B210 et que vous voulez juste utiliser RF:B au lieu de RF:A, tout ce que vous avez Ă  faire est de choisir le canal 1 au lieu de 0. Mais sur les USRP comme le X310 qui ont deux slots pour carte fille, vous devez dire Ă  UHD si vous voulez utiliser le slot A ou B, et quel canal sur cette carte fille, par exemple :

usrp.set_rx_subdev_spec("B:0")

Si vous voulez utiliser le port TX/RX au lieu de RX2 (par dĂ©faut), c’est aussi simple que cela :

usrp.set_rx_antenna('TX/RX', 0) # Réglez le canal 0 sur 'TX/RX'.

qui ne fait que contrîler un commutateur RF à bord de l’USRP, pour l’acheminer depuis l’autre connecteur SMA.

Pour recevoir ou Ă©mettre sur deux canaux Ă  la fois, au lieu d’utiliser st_args.channels = [0] vous fournissez une liste, telle que [0,1]. Le tampon de rĂ©ception des Ă©chantillons devra ĂȘtre de taille (2, N) dans ce cas, au lieu de (1,N). Rappelez-vous qu’avec la plupart des USRP, les deux canaux partagent un LO, donc vous ne pouvez pas indiquer diffĂ©rentes frĂ©quences en mĂȘme temps.

Synchronisation Ă  10 MHz et PPS

Un des Ă©normes avantages de l’utilisation d’une USRP par rapport Ă  d’autres SDR est leur capacitĂ© Ă  se synchroniser Ă  une source externe ou au GPSDO embarquĂ©. Si vous avez connectĂ© une source externe 10 MHz et PPS Ă  votre USRP, vous voudrez vous assurer d’appeler ces deux lignes aprĂšs avoir initialisĂ© votre USRP :

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

Si vous utilisez un GPSDO embarqué, vous utiliserez plutÎt :

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

Du cĂŽtĂ© de la synchronisation en frĂ©quence, il n’y a pas grand chose d’autre Ă  faire ; la LO utilisĂ©e dans le mĂ©langeur de l’USRP va maintenant ĂȘtre liĂ©e Ă  la source externe ou Ă  GPSDO. Mais du cĂŽtĂ© du timing, vous pouvez souhaiter commander Ă  l’USRP de commencer Ă  Ă©chantillonner exactement sur le PPS, par exemple. Cela peut ĂȘtre fait avec le code suivant :

# copier l'exemple de réception ci-dessus, tout jusqu'à # Start Stream

# Attendez que 1 PPS se produise, puis réglez le temps au prochain PPS à 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) # continuez à attendre jusqu'à ce que ça arrive - si cette boucle while ne se termine jamais alors le signal PPS n'est pas là.
usrp.set_time_next_pps(uhd.libpyuhd.types.time_spec(0.0))

# Planifie la réception des échantillons num_samps exactement 3 secondes aprÚs le dernier 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) # définir l'heure de début (essayez de modifier cela)
streamer.issue_stream_cmd(stream_cmd)

# Recevoir des échantillons. recv() retournera des zéros, puis nos échantillons, puis encore des zéros, pour nous dire que c'est terminé.
waiting_to_start = True # garder la trace de l'endroit oĂč nous sommes dans le cycle (voir le commentaire ci-dessus)
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 vous avez l’impression qu’il ne fonctionne pas, mais qu’il n’y a pas d’erreur, essayez de remplacer le chiffre 3.0 par un chiffre compris entre 1.0 et 5.0. Vous pouvez Ă©galement vĂ©rifier les mĂ©tadonnĂ©es aprĂšs l’appel Ă  recv(), en vĂ©rifiant simplement if metadata.error_code != uhd.types.RXMetadataErrorCode.none:.

Pour des raisons de dĂ©bogage, vous pouvez vĂ©rifier que le signal 10MHz apparaĂźt sur l’USRP en vĂ©rifiant le retour de usrp.get_mboard_sensor("ref_locked", 0). Si le signal PPS n’apparaĂźt pas, vous le saurez car la premiĂšre boucle while du code ci-dessus ne se terminera jamais.