USRP en Pythonï
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)
Télécharger Ubuntu 20.04 Desktop .iso- https://ubuntu.com/download/desktop
Installez et ouvrez VirtualBox.
CrĂ©ez une nouvelle VM. Pour la taille de la mĂ©moire, je recommande dâutiliser 50% de la RAM de votre ordinateur.
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.
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.
Allez dans les paramĂštres de la VM (lâicĂŽne de lâengrenage).
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é.
Démarrez votre VM.
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.