BladeRF en Python

Le bladeRF 2.0 (Ă©galement appelĂ© bladeRF 2.0 micro) de Nuand Nuand est un SDR (rĂ©cepteur audio numĂ©rique) USB 3.0 dotĂ© de deux canaux de rĂ©ception, deux canaux d’émission, une bande passante ajustable de 47 MHz Ă  6 GHz et une capacitĂ© d’échantillonnage jusqu’à 61 MHz, voire 122 MHz aprĂšs modification. Il utilise le circuit intĂ©grĂ© RF AD9361, tout comme l’USRP B210 et le PlutoSDR, offrant ainsi des performances RF similaires. Sorti en 2021, le bladeRF 2.0 conserve un format compact de 2,5 » x 4,5 » et est disponible en deux tailles de FPGA (xA4 et xA9). Bien que ce chapitre soit consacrĂ© au bladeRF 2.0, une grande partie du code est Ă©galement applicable au bladeRF original, sorti en 2013.

Photo glamour du BladeRF 2.0

Architecture du bladeRF

De maniĂšre gĂ©nĂ©rale, le bladeRF 2.0 repose sur le circuit intĂ©grĂ© RF AD9361, associĂ© Ă  un FPGA Cyclone V (49 kLE 5CEA4 ou 301 kLE 5CEA9), et un contrĂŽleur USB 3.0 Cypress FX3 dotĂ© d’un cƓur ARM9 cadencĂ© Ă  200 MHz et d’un firmware personnalisĂ©. Le schĂ©ma fonctionnel du bladeRF 2.0 est prĂ©sentĂ© ci-dessous :

Schéma fonctionnel de bladeRF 2.0

Le FPGA contrĂŽle le circuit intĂ©grĂ© RF, effectue le filtrage numĂ©rique et formate les paquets pour leur transfert via USB (entre autres). Le code source de l’image FPGA, disponible Ă  l’adresse https://github.com/Nuand/bladeRF/tree/master/hdl, est Ă©crit en VHDL et nĂ©cessite le logiciel de conception gratuit Quartus Prime Lite pour compiler des images personnalisĂ©es. Des images prĂ©compilĂ©es sont disponibles ici :.

Le code source du firmware Cypress FX3 (disponible à l’adresse https://github.com/Nuand/bladeRF/tree/master/fx3_firmware) est open source et inclut le code permettant de :

  1. Charger l’image FPGA

  2. TransfĂ©rer les Ă©chantillons IQ entre le FPGA et l’hĂŽte via USB 3.0

  3. ContrĂŽler les E/S du FPGA via UART

Du point de vue du flux de signal, il existe deux canaux de rĂ©ception et deux canaux d’émission. Chaque canal possĂšde une entrĂ©e/sortie basse et haute frĂ©quence vers le circuit intĂ©grĂ© RF (RFIC), selon la bande utilisĂ©e. C’est pourquoi un commutateur Ă©lectronique RF unipolaire bidirectionnel (SPDT) est nĂ©cessaire entre le RFIC et les connecteurs SMA. Le circuit de polarisation intĂ©grĂ© fournit environ 4,5 V CC sur le connecteur SMA et permet d’alimenter un amplificateur externe ou d’autres composants RF. Ce dĂ©calage CC supplĂ©mentaire se situe cĂŽtĂ© RF du SDR et n’interfĂšre donc pas avec le fonctionnement de base en rĂ©ception/Ă©mission.

JTAG est une interface de débogage permettant de tester et de vérifier les conceptions pendant leur développement.

À la fin de ce chapitre, nous aborderons l’oscillateur VCTCXO, la PLL et le port d’extension.

Configuration matĂ©rielle et logicielle

Ubuntu (ou Ubuntu dans WSL)

Sur Ubuntu et autres systÚmes basés sur Debian, vous pouvez installer le logiciel bladeRF avec les commandes suivantes :

sudo apt update
sudo apt install cmake python3-pip libusb-1.0-0
cd ~
git clone --depth 1 https://github.com/Nuand/bladeRF.git
cd bladeRF/host
mkdir build && cd build
cmake ..
make -j8
sudo make install
sudo ldconfig
cd ../libraries/libbladeRF_bindings/python
sudo python3 setup.py install

Cela installera la bibliothÚque libbladerf, les liaisons Python, les outils en ligne de commande bladeRF, le programme de téléchargement du firmware et celui du flux de bits FPGA. Pour vérifier la version de la bibliothÚque installée, utilisez la commande bladerf-tool version (ce guide a été rédigé avec la version 2.5.0 de libbladerf).

Si vous utilisez Ubuntu via WSL, vous devrez, cĂŽtĂ© Windows, rediriger le pĂ©riphĂ©rique USB bladeRF vers WSL. Pour cela, installez d’abord la derniĂšre version de l’utilitaire usbipd (fichier MSI :` <https://github.com/dorssel/usbipd-win/releases>`_) (ce guide suppose que vous disposez de usbipd-win 4.0.0 ou version ultĂ©rieure), puis ouvrez PowerShell en mode administrateur et exĂ©cutez la commande suivante :

usbipd list
# (Trouvez le BUSID étiqueté bladeRF 2.0 et remplacez-le dans la commande ci-dessous.)
usbipd bind --busid 1-23
usbipd attach --wsl --busid 1-23

Sous WSL, vous devriez pouvoir exĂ©cuter la commande lsusb et voir un nouvel Ă©lĂ©ment nommĂ© Nuand LLC bladeRF 2.0 micro. Notez que vous pouvez ajouter l’option --auto-attach Ă  la commande usbipd attach pour activer la reconnexion automatique.

(Cette Ă©tape peut ĂȘtre inutile.) Sous Linux natif et sous WSL, il est nĂ©cessaire d’installer les rĂšgles udev afin d’éviter les erreurs de permissions.

sudo nano /etc/udev/rules.d/88-nuand.rules

et collez la ligne suivante : .. code-block:

ATTRS{idVendor}=="2cf0", ATTRS{idProduct}=="5250", MODE="0666"

Pour enregistrer et quitter nano, utilisez : Ctrl+O, puis Entrée, puis Ctrl+X. Pour actualiser udev, exécutez :

sudo udevadm control --reload-rules && sudo udevadm trigger

Si vous utilisez WSL et que le message d’erreur suivant s’affiche Échec de l'envoi de la requĂȘte de rechargement : Aucun fichier ou rĂ©pertoire de ce type, cela signifie que le service udev n’est pas en cours d’exĂ©cution et que vous devrez exĂ©cuter la commande sudo nano /etc/wsl.conf et ajouter les lignes suivantes :

[boot]
command="service udev start"

RedĂ©marrez ensuite WSL Ă  l’aide de la commande suivante dans PowerShell en tant qu’administrateur : wsl.exe --shutdown.

Débranchez puis rebranchez votre bladeRF (les utilisateurs de WSL devront la reconnecter), et testez les autorisations avec :

bladerf-tool probe
bladerf-tool info

and you’ll know it worked if you see your bladeRF 2.0 listed, and you don’t see Found a bladeRF via VID/PID, but could not open it due to insufficient permissions. If it worked, note reported FPGA Version and Firmware Version.

(Optionally) Install the latest firmware and FPGA images (v2.4.0 and v0.15.0 respectively when this guide was written) using:

cd ~/Downloads
wget https://www.nuand.com/fx3/bladeRF_fw_latest.img
bladerf-tool flash_fw bladeRF_fw_latest.img

# for xA4 use:
wget https://www.nuand.com/fpga/hostedxA4-latest.rbf
bladerf-tool flash_fpga hostedxA4-latest.rbf

# for xA9 use:
wget https://www.nuand.com/fpga/hostedxA9-latest.rbf
bladerf-tool flash_fpga hostedxA9-latest.rbf

Unplug and plug in your bladeRF to cycle power.

Now we will test its functionality by receiving 1M samples in the FM radio band, at 10 MHz sample rate, to a file /tmp/samples.sc16:

bladerf-tool rx --num-samples 1000000 /tmp/samples.sc16 100e6 10e6

a couple Hit stall for buffer is expected, but you’ll know if it worked if you see a 4MB /tmp/samples.sc16 file.

Lastly, we will test the Python API with:

python3
import bladerf
bladerf.BladeRF()
exit()

You’ll know it worked if you see something like <BladeRF(<DevInfo(...)>)> and no warnings/errors.

Windows and macOS

For Windows users (who do not prefer to use WSL), see https://github.com/Nuand/bladeRF/wiki/Getting-Started%3A-Windows, and for macOS users, see https://github.com/Nuand/bladeRF/wiki/Getting-started:-Mac-OSX.

bladeRF Python API Basics

To start with, let’s poll the bladeRF for some useful information, using the following script. Do not name your script bladerf.py or it will conflict with the bladeRF Python module itself!

from bladerf import _bladerf
import numpy as np
import matplotlib.pyplot as plt

sdr = _bladerf.BladeRF()

print("Device info:", _bladerf.get_device_list()[0])
print("libbladeRF version:", _bladerf.version()) # v2.5.0
print("Firmware version:", sdr.get_fw_version()) # v2.4.0
print("FPGA version:", sdr.get_fpga_version())   # v0.15.0

rx_ch = sdr.Channel(_bladerf.CHANNEL_RX(0)) # give it a 0 or 1
print("sample_rate_range:", rx_ch.sample_rate_range)
print("bandwidth_range:", rx_ch.bandwidth_range)
print("frequency_range:", rx_ch.frequency_range)
print("gain_modes:", rx_ch.gain_modes)
print("manual gain range:", sdr.get_gain_range(_bladerf.CHANNEL_RX(0))) # ch 0 or 1

For the bladeRF 2.0 xA9, the output should look something like:

Device info: Device Information
    backend  libusb
    serial   f80a27b1010448dfb7a003ef7fa98a59
    usb_bus  2
    usb_addr 5
    instance 0
libbladeRF version: v2.5.0 ("2.5.0-git-624994d")
Firmware version: v2.4.0 ("2.4.0-git-a3d5c55f")
FPGA version: v0.15.0 ("0.15.0")
sample_rate_range: Range
    min   520834
    max   61440000
    step  2
    scale 1.0

bandwidth_range: Range
    min   200000
    max   56000000
    step  1
    scale 1.0

frequency_range: Range
    min   70000000
    max   6000000000
    step  2
    scale 1.0

gain_modes: [<GainMode.Default: 0>, <GainMode.Manual: 1>, <GainMode.FastAttack_AGC: 2>, <GainMode.SlowAttack_AGC: 3>, <GainMode.Hybrid_AGC: 4>]

manual gain range: Range
    min   -15
    max   60
    step  1
    scale 1.0

The bandwidth parameter sets the filter used by the SDR when performing the receive operation, so we typically set it to be equal or slightly less than the sample_rate/2. The gain modes are important to understand: the SDR uses either a manual gain mode, where you provide the gain in dB, or automatic gain control (AGC), which has three different settings (fast, slow, hybrid). For applications such as spectrum monitoring, manual gain is advised so you can see when signals come and go. For applications such as receiving a specific signal you expect to exist, AGC is more useful because it automatically adjusts the gain to allow the signal to fill the analog-to-digital converter (ADC).

To set the main parameters of the SDR, we can add the following code:

sample_rate = 10e6
center_freq = 100e6
gain = 50 # -15 to 60 dB
num_samples = int(1e6)

rx_ch.frequency = center_freq
rx_ch.sample_rate = sample_rate
rx_ch.bandwidth = sample_rate/2
rx_ch.gain_mode = _bladerf.GainMode.Manual
rx_ch.gain = gain

Receiving Samples in Python

Next, we will work off the previous code block to receive 1M samples in the FM radio band, at 10 MHz sample rate, just like we did before. Any antenna on the RX1 port should be able to receive FM, since it is so strong. The code below shows how the bladeRF synchronous stream API works; it must be configured and a receive buffer must be created, before the receiving begins. The while True: loop will continue to receive samples until the number of samples requested is reached. The received samples are stored in a separate numpy array, so that we can process them after the loop finishes.

# Setup synchronous stream
sdr.sync_config(layout = _bladerf.ChannelLayout.RX_X1, # or RX_X2
                fmt = _bladerf.Format.SC16_Q11, # int16s
                num_buffers    = 16,
                buffer_size    = 8192,
                num_transfers  = 8,
                stream_timeout = 3500)

# Create receive buffer
bytes_per_sample = 4 # don't change this, it will always use int16s
buf = bytearray(1024 * bytes_per_sample)

# Enable module
print("Starting receive")
rx_ch.enable = True

# Receive loop
x = np.zeros(num_samples, dtype=np.complex64) # storage for IQ samples
num_samples_read = 0
while True:
    if num_samples > 0 and num_samples_read == num_samples:
        break
    elif num_samples > 0:
        num = min(len(buf) // bytes_per_sample, num_samples - num_samples_read)
    else:
        num = len(buf) // bytes_per_sample
    sdr.sync_rx(buf, num) # Read into buffer
    samples = np.frombuffer(buf, dtype=np.int16)
    samples = samples[0::2] + 1j * samples[1::2] # Convert to complex type
    samples /= 2048.0 # Scale to -1 to 1 (it is using a 12-bit ADC)
    x[num_samples_read:num_samples_read+num] = samples[0:num] # Store buf in samples array
    num_samples_read += num

print("Stopping")
rx_ch.enable = False
print(x[0:10]) # look at first 10 IQ samples
print(np.max(x)) # if this is close to 1, you are overloading the ADC, and should reduce the gain

A few Hit stall for buffer is expected at the end. The last number printed shows the maximum sample received; you will want to adjust your gain to try to get that value around 0.5 to 0.8. If it is 0.999 that means your receiver is overloaded/saturated and the signal is going to be distorted (it will look smeared throughout the frequency domain).

In order to visualize the received signal, let’s display the IQ samples using a spectrogram (see spectrogram-section for more details on how spectrograms work). Add the following to the end of the previous code block:

# Create spectrogram
fft_size = 2048
num_rows = len(x) // fft_size # // is an integer division which rounds down
spectrogram = np.zeros((num_rows, fft_size))
for i in range(num_rows):
    spectrogram[i,:] = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(x[i*fft_size:(i+1)*fft_size])))**2)
extent = [(center_freq + sample_rate/-2)/1e6, (center_freq + sample_rate/2)/1e6, len(x)/sample_rate, 0]
plt.imshow(spectrogram, aspect='auto', extent=extent)
plt.xlabel("Frequency [MHz]")
plt.ylabel("Time [s]")
plt.show()
bladeRF spectrogram example

Chaque ligne verticale ondulĂ©e reprĂ©sente un signal radio FM. L’origine des pulsations Ă  droite reste inconnue ; mĂȘme en baissant le gain, elles persistent.

Transmission d’échantillons en Python

Le processus de transmission d’échantillons avec la bladeRF est trĂšs similaire Ă  la rĂ©ception. La principale diffĂ©rence rĂ©side dans la gĂ©nĂ©ration des Ă©chantillons Ă  transmettre, suivie de leur Ă©criture sur la bladeRF Ă  l’aide de la mĂ©thode sync_tx, capable de traiter l’ensemble du lot d’échantillons en une seule fois (jusqu’à environ 4 milliards d’échantillons). Le code ci-dessous illustre la transmission d’une tonalitĂ© simple, rĂ©pĂ©tĂ©e 30 fois. La tonalitĂ© est gĂ©nĂ©rĂ©e avec NumPy, puis mise Ă  l’échelle entre -2048 et 2048 pour s’adapter au convertisseur numĂ©rique-analogique (CNA) 12 bits. Elle est ensuite convertie en octets (reprĂ©sentant des entiers 16 bits) et utilisĂ©e comme tampon de transmission. L’API de flux synchrone est utilisĂ©e pour la transmission des Ă©chantillons, et la boucle while True: assure la transmission jusqu’à ce que le nombre de rĂ©pĂ©titions souhaitĂ© soit atteint. Si vous souhaitez transmettre des Ă©chantillons Ă  partir d’un fichier, utilisez simplement samples = np.fromfile('yourfile.iq', dtype=np.int16) (ou tout autre type de donnĂ©es) pour lire les Ă©chantillons, puis convertissez-les en octets Ă  l’aide de samples.tobytes(), en tenant compte de la plage de -2048 Ă  2048 du CNA.

from bladerf import _bladerf
import numpy as np

sdr = _bladerf.BladeRF()
tx_ch = sdr.Channel(_bladerf.CHANNEL_TX(0)) # Donnez-lui un 0 ou un 1

sample_rate = 10e6
center_freq = 100e6
gain = 0 # de -15dB à 60 dB pour transmettre, commencez par une faible valeur et augmentez-la progressivement, en veillant à ce que l'antenne soit bien connectée.
num_samples = int(1e6)
repeat = 30 # nombre de fois pour répéter notre signal
print('duration of transmission:', num_samples/sample_rate*repeat, 'seconds')

# Générer des échantillons IQ à transmettre (dans ce cas, une simple tonalité)
t = np.arange(num_samples) / sample_rate
f_tone = 1e6
samples = np.exp(1j * 2 * np.pi * f_tone * t) # will be -1 to +1
samples = samples.astype(np.complex64)
samples *= 2048.0 # Scale to -1 to 1 (it is using a 12-bit DAC)
samples = samples.view(np.int16)
buf = samples.tobytes() # Convertir nos échantillons en octets et les utiliser comme tampon de transmission

tx_ch.frequency = center_freq
tx_ch.sample_rate = sample_rate
tx_ch.bandwidth = sample_rate/2
tx_ch.gain = gain
# Configurer le flux synchrone
sdr.sync_config(layout=_bladerf.ChannelLayout.TX_X1, # ou TX_X2

fmt=_bladerf.Format.SC16_Q11, # int16s num_buffers=16, buffer_size=8192, num_transfers=8, stream_timeout=3500)

print(« Démarrage de la transmission! ») repeats_remaining = repeat - 1 tx_ch.enable = True while True:

sdr.sync_tx(buf, num_samples) # write to bladeRF print(repeats_remaining) if repeats_remaining > 0:

repeats_remaining -= 1

else:

break

print(« ArrĂȘt de la transmission ») tx_ch.enable = False

Quelques erreurs de type Hit stall for buffer Ă  la fin sont normales.

Pour transmettre et recevoir simultanĂ©ment, il faut utiliser des threads. Vous pouvez par exemple utiliser l’exemple de Nuand : txrx.py, qui fait exactement cela.

Oscillateurs, PLLs, and Ă©talonnage

Tous les SDR Ă  conversion directe (y compris ceux basĂ©s sur l’AD9361, comme l’USRP B2X0, l’Analog Devices Pluto et le bladeRF) utilisent un oscillateur unique pour fournir une horloge stable Ă  l’émetteur-rĂ©cepteur RF. Tout dĂ©calage ou gigue de frĂ©quence produit par cet oscillateur se traduit par un dĂ©calage et une gigue de frĂ©quence dans le signal reçu ou Ă©mis. Cet oscillateur est intĂ©grĂ©, mais peut ĂȘtre stabilisĂ© par un signal carrĂ© ou sinusoĂŻdal externe injectĂ© dans le bladeRF via un connecteur U.FL sur la carte.

La carte bladeRF embarque un oscillateur Abracon VCTCXO (oscillateur Ă  compensation de tempĂ©rature commandĂ© en tension) cadencĂ© Ă  38,4 MHz. La compensation de tempĂ©rature lui confĂšre une grande stabilitĂ© sur une large plage de tempĂ©ratures. La commande en tension permet d’ajuster finement la frĂ©quence de l’oscillateur grĂące Ă  un niveau de tension spĂ©cifique. Sur la bladeRF, cette tension est fournie par un convertisseur numĂ©rique-analogique (CNA) 10 bits externe, reprĂ©sentĂ© en vert dans le schĂ©ma fonctionnel ci-dessous. Ce systĂšme permet un rĂ©glage prĂ©cis de la frĂ©quence de l’oscillateur par logiciel, et c’est ainsi que l’on calibre (ou ajuste) le VCTCXO de la bladeRF. Heureusement, les lames RF sont calibrĂ©es en usine, comme nous le verrons plus loin dans cette section, mais si vous disposez de l’équipement de test nĂ©cessaire, vous pouvez toujours affiner cette valeur, surtout au fil des annĂ©es et de la dĂ©rive de la frĂ©quence de l’oscillateur.

Schéma fonctionnel du bladeRF 2.0

Lorsqu’une rĂ©fĂ©rence de frĂ©quence externe est utilisĂ©e (pouvant atteindre pratiquement n’importe quelle frĂ©quence jusqu’à 300 MHz), le signal de rĂ©fĂ©rence est directement injectĂ© dans la boucle Ă  verrouillage de phase (PLL) Analog Devices ADF4002 intĂ©grĂ©e Ă  la carte bladeRF. Cette PLL se synchronise sur le signal de rĂ©fĂ©rence et envoie un signal Ă  l’oscillateur VCTCXO (reprĂ©sentĂ© en bleu ci-dessus) proportionnel Ă  la diffĂ©rence de frĂ©quence et de phase entre l’entrĂ©e de rĂ©fĂ©rence (mise Ă  l’échelle) et la sortie du VCTCXO. Une fois la PLL synchronisĂ©e, ce signal entre la PLL et le VCTCXO constitue une tension continue stable qui maintient la sortie du VCTCXO Ă  38,4 MHz (en supposant que la rĂ©fĂ©rence soit correcte) et synchronisĂ©e en phase avec l’entrĂ©e de rĂ©fĂ©rence. Pour utiliser une rĂ©fĂ©rence externe, vous devez activer clock_ref (via Python ou l’interface de ligne de commande) et dĂ©finir la frĂ©quence de rĂ©fĂ©rence d’entrĂ©e (refin_freq), qui est de 10 MHz par dĂ©faut. L’utilisation d’une rĂ©fĂ©rence externe permet notamment une meilleure prĂ©cision de frĂ©quence et la possibilitĂ© de synchroniser plusieurs SDR sur la mĂȘme rĂ©fĂ©rence.

Chaque valeur de rĂ©glage du convertisseur numĂ©rique-analogique (CNA) VCTCXO de bladeRF est calibrĂ©e en usine Ă  1 Hz prĂšs Ă  38,4 MHz Ă  tempĂ©rature ambiante. Vous pouvez consulter la valeur de calibration d’usine en saisissant votre numĂ©ro de sĂ©rie sur la page <https://www.nuand.com/calibration/>`_ (trouvez votre numĂ©ro de sĂ©rie sur la carte ou Ă  l’aide de l’outil bladerf-tool probe). Selon Nuand, une carte neuve devrait prĂ©senter une prĂ©cision largement infĂ©rieure Ă  0,5 ppm, et probablement plus proche de 0,1 ppm. Si vous disposez d’un Ă©quipement de test pour mesurer la prĂ©cision de frĂ©quence ou si vous souhaitez la rĂ©tablir Ă  la valeur d’usine, vous pouvez utiliser les commandes suivantes :

$ bladeRF-cli -i
bladeRF> flash_init_cal 301 0x2049

Remplacez 301 par la taille de votre bladeRF et 0x2049 par la valeur de réglage DAC VCTCXO au format hexadécimal. Un redémarrage est nécessaire pour que la modification soit prise en compte.

Échantillonnage Ă  122 MHz

À venir!

Ports d’extension

Le bladeRF 2.0 est dotĂ©e d’un port d’extension utilisant un connecteur BSH-030. Plus d’informations sur l’utilisation de ce port prochainement !

Pour en savoir plus

  1. bladeRF Wiki

  2. Nuand’s txrx.py example