Co to jest Pluto+
Urządzenie to klon ADALM-Pluto z AliExpressu: metalowa obudowa, cztery gniazda SMA (dwa kanały RX i dwa TX), w środku ten sam tandem co w oryginale: AD9363 jako transceiver RF i Zynq 7010 jako SoC. Firmware jest kompatybilny z pyadi-iio, wykorzystuje ten sam stos libiio i ten sam adres 192.168.2.1 po USB. Z perspektywy hosta nie istnieje różnica między oryginałem od ADI a klonem.
Klony zwykle mają szersze pasmo robocze niż oryginalny "niebieski Pluto": fabrycznie wgrany firmware z hackiem 70 MHz – 6 GHz zamiast oficjalnych 325 MHz – 3.8 GHz. Druga istotna różnica: klony często wymagają karty SD do pracy. Większy rootfs (z dodatkowymi narzędziami, obsługą 4 kanałów, czasem GNU Radio) nie mieści się w 32 MB wewnętrznego flasha, więc bez karty urządzenie albo nie wstaje poprawnie, albo wstaje w okrojonej konfiguracji.
Pluto to pełnoprawny mały komputer z Linuksem i FPGA. Host ma dostęp do strumieni próbek i atrybutów przez libiio.
| Parametr | Wartość |
|---|---|
| Częstotliwość RF | 70 MHz – 6 GHz (firmware z hackiem fabrycznie) |
| Sample rate | 521 kSPS – 61.44 MSPS po USB realnie ~5 MSPS |
| RF bandwidth | do 20 MHz (filtr analogowy AD9363) |
| Tor RF | 2× TX + 2× RX, full-duplex, gniazda SMA |
| Moc TX | ~7 dBm (5 mW) |
| Rozdzielczość ADC/DAC | 12 bitów + AGC |
| Interfejs hosta | USB 2.0 OTG |
| Zasilanie | z USB (~0.5 A) |
| Nazwa | Typ | Rola |
|---|---|---|
| AD9363 | RF transceiver | cały tor analogowy — 2× TX + 2× RX wyprowadzone na SMA |
| Zynq 7010 (XC7Z010) | SoC (ARM + FPGA) | mózg urządzenia — Linux na ARM, DSP w FPGA |
| N25Q256A / podobny | QSPI flash 32 MB | bootloader U-Boot, kernel Linux, device tree, rootfs |
| MT41K256M16 (2×) | DDR3L 512 MB | RAM współdzielony przez ARM i FPGA — bufory próbek, Linux |
| Slot microSD | czytnik kart | opcjonalny boot z karty |
| USB micro-B | USB 2.0 OTG | komunikacja z hostem + zasilanie + tryb host po przełączeniu konfiguracji |
| 4× SMA | gniazda RF | 2× wyjście TX i 2× wejście RX z filtrami balun |
| ADP5072 / LDO | power management | generacja napięć 1.0 V / 1.8 V / 3.3 V dla SoC, RF i pamięci |
| 40 MHz TCXO | oscylator | referencja zegarowa dla AD9363 i Zynq — stabilność ±25 ppm |
AD9363 (datasheet) to RF transceiver Analog Devices i to on jest "radiem" w urządzeniu — wszystko między gniazdem SMA a światem cyfrowym dzieje się wewnątrz tego jednego chipa.

Po stronie analogowej znajdują się mieszacze kwadraturowe, syntezatory PLL, LNA na wejściu, PA na wyjściu i przestrajalne filtry analogowe. Po stronie cyfrowej: 12-bitowe przetworniki ADC (RX) i DAC (TX) w trybie I/Q, podpięte przez LVDS do FPGA. Oficjalny zakres to 325 MHz – 3.8 GHz, ale fizycznie syntezator dochodzi od ~70 MHz do ~6 GHz. Klonowany egzemplarz ma firmware fabrycznie skonfigurowany na zakres rozszerzony. Maksymalna szerokość pasma analogowego to 20 MHz — limit fundamentalny AD9363 (AD9361 i AD9364 mają 56 MHz).
Trzy poziomy programowania Pluto
Urządzenie udostępnia trzy różne warstwy do wykonywania kodu. Każda ma inną latencję, przepustowość i próg wejścia. Wybór warstwy determinuje, gdzie fizycznie odbywa się logika i jak szybko może reagować na próbki.
graph TB
subgraph Host["💻 Host (PC / laptop)"]
H["Python / C++ / GNU Radio<br/>━━━━━━━━━━━━━━━━━━━━━━<br/>pyadi-iio → libiio"]
end
subgraph Pluto["🔲 Pluto (Zynq 7010)"]
A["ARM Cortex-A9<br/>(Linux + iiod)<br/>━━━━━━━━━━━━<br/>własne demony, skrypty,<br/>SSH, web UI"]
F["FPGA (PL)<br/>━━━━━━━━━━━━<br/>filtry, DMA,<br/>własne IP w Verilogu"]
R["AD9363 RF"]
end
H <-->|"USB / Ethernet<br/>(TCP iiod)"| A
A <-->|"AXI<br/>(rejestry, DMA)"| F
F <-->|"LVDS<br/>(próbki IQ)"| R
Trzy poziomy:
- Host: logika aplikacji znajduje się na komputerze PC, urządzenie pełni rolę zdalnego strumienia próbek. Skrypt w Pythonie (albo flowgraph GNU Radio) wysyła polecenia konfiguracyjne i odbiera/wysyła bufory IQ przez USB lub Ethernet. Każda decyzja w pętli przechodzi przez kabel i stos sieciowy, latencja wynosi milisekundy. To wystarczy do analizy off-line, prototypów DSP i większości projektów dydaktycznych. Tryb domyślny i najczęstszy.
- ARM na Pluto: logika przenosi się do układu scalonego na urządzeniu, konkretnie na rdzeń ARM w Zynqu. Próbki nie pokonują USB w obie strony. Kod sterujący i dane znajdują się w tej samej pamięci, a host odbiera tylko gotowe wyniki. Latencja pętli spada z milisekund do setek mikrosekund.
- FPGA na Pluto: logika trafia do programowalnej części Zynqa, czyli równoległego sprzętu przetwarzającego każdą próbkę w momencie pojawienia się na interfejsie LVDS z AD9363. Decyzje zapadają w nanosekundach, osiągalny jest pełny sample rate 61.44 MSPS.
| Aspekt | Host (zdalnie) | ARM na Pluto | FPGA na Pluto |
|---|---|---|---|
| Język | Python / C++ / GNU Radio | C / Python | Verilog / VHDL / HLS |
| Toolchain | pip install pyadi-iio | gcc cross-compile + scp | Vivado |
| Latencja pętli | milisekundy | setki μs | nanosekundy |
| Przepustowość | ~5 MSPS (USB) | < 1 MSPS | 61.44 MSPS |
| Próg wejścia | niski | średni | wysoki |
| Ryzyko cegły | zero | małe | duże |
| Typowe zastosowanie | analiza, prototypy DSP | standalone, demony, web UI | real-time DSP, low-latency |
| Czy potrzebny host | tak | nie | nie |
Podział na trzy poziomy jest podstawowy, ale istnieją mniej oczywiste warianty w obrębie każdego z nich.
Na warstwie ARM nie wymagane jest pisanie własnego kodu do przeniesienia logiki na płytkę. Ten sam GNU Radio flowgraph, który uruchamia się z ip:pluto.local, można uruchomić lokalnie na Pluto z URI local:. libiio robi transport przezroczystym, więc identyczny flowgraph działa zdalnie i lokalnie, różnicą jest tylko zmiana URI. Wymaga to obrazu z wkompilowanym GNU Radio.
Bardziej zaawansowany wariant na tej warstwie to bare-metal na ARM: zamiast bootować Linux, wgrywa się na Cortex-A9 binarkę napisaną w czystym C lub działającą pod FreeRTOS. Po zaimportowaniu system_top.hdf z plutosdr-fw, kod pisze się bezpośrednio na rejestrach AXI. Daje to deterministyczne czasy reakcji i ~kilkadziesiąt μs latencji zamiast setek mikrosekund, ale tracony jest cały stos USB/sieci.
Na warstwie FPGA Verilog/VHDL to nie jedyna opcja. Vitis HLS pozwala napisać blok DSP w C++ z pragmami i wygenerować syntezowalny IP, co obniża próg wejścia i jest realne dla bloków czysto numerycznych (FIR, FFT), słabiej dla logiki sterującej. Migen / Amaranth to frameworki Pythonowe do opisu sprzętu: logika pisze się w Pythonie, generuje Verilog, który wpina się do projektu Vivado. Pozwalają pozostać w jednym ekosystemie języka.
Wariant łączący dwie warstwy to OpenAMP. Zynq 7010 ma 2 rdzenie ARM. Można skonfigurować Linux tylko na CPU0, a na CPU1 uruchomić bare-metal. Pluto otrzymuje Linux do obsługi USB/IP i równolegle deterministyczną pętlę DSP na drugim rdzeniu.
Firmware i karta SD
Na poziomie podstawowym karta SD jest niepotrzebna. Pluto bootuje z wewnętrznego 32 MB QSPI flash, co wystarczy do całej zwykłej pracy z pyadi-iio po USB lub Ethernet. Urządzenie działa od razu po wyjęciu z pudełka.
Firmware jest open-source. Oprócz oficjalnego ADI istnieje kilka community-buildów, m.in. Maia SDR (web UI z waterfallami i nagrywaniem IQ, Pluto jako standalone analizator widma) oraz zmodyfikowane firmware community z wyższym realnym sample rate po USB. Popularny jest również „hack" 70 MHz–6 GHz i odblokowanie drugiej pary kanałów (2×RX/TX), które wymagają tylko zmianę zmiennych U-Boota przez SSH, bez wymiany firmware.
Karta SD jest niezbędna, gdy stockowe 32 MB przestaje wystarczać: (1) przy konfiguracji własnego rootfs z GNU Radio lub cięższymi aplikacjami, (2) przy eksperymentach z FPGA/własnym bitstreamem, (3) przy odzyskiwaniu urządzenia po nieudanej aktualizacji firmware. Wyjątkiem są klony Pluto+, gdzie karta SD jest wymagana, gdyż większy rootfs nie mieści się we flashu.
Wbudowane FPGA
FPGA to część Pluto przetwarzająca próbki strumieniowo, w czasie rzeczywistym, z latencją liczoną w nanosekundach. ARM Cortex-A9 osiąga pół-stabilność przy kilku MSPS, FPGA obsługuje pełne 61.44 MSPS z zapasem mocy na własną logikę.
FPGA jest już w urządzeniu i funkcjonuje od razu. Standardowy bitstream ADI zawiera interfejs LVDS do AD9363, filtry decymacyjne CIC i half-band FIR, DMA do DDR3. Korzysta się z FPGA przez cały czas, nawet pisząc prosty skrypt w pyadi-iio, z tą różnicą, że działa niewidocznie pod warstwą iiod.
Można dodać własny IP (demodulator, korelator, FFT online, custom DDC) i uzyskać przetwarzanie próbka-po-próbce, którego host przez USB nie osiągnie. Rozwiązanie to znacznie bardziej skomplikowane niż pisanie skryptu.
Wszystko, co musi reagować szybciej niż milisekunda albo przetwarzać próbki strumieniowo, powinno siedzieć w FPGA. Wszystko, co może poczekać i mieści się w przepustowości USB — może zostać na hoście w Pythonie.
Połączenie przez USB
Domyślny i najszybszy sposób pracy. Po podłączeniu kabla USB Pluto emuluje trzy urządzenia jednocześnie:
- Wirtualną kartę sieciową z adresem
192.168.2.1. Host dostaje192.168.2.10. - Dysk masowy (FAT16, ~256 kB) z plikiem
config.txtiinfo.html— najprostszy sposób konfiguracji bez SSH. - Wirtualny port szeregowy — konsola U-Boot/Linux do debugowania.
Cała komunikacja z libiio przechodzi tunelem TCP/IP po wirtualnej karcie sieciowej. Z tego powodu ten sam kod działa po USB i po LAN, zmienia się wyłącznie adres IP.
ping 192.168.2.1
# Lista kanałów i atrybutów
iio_info -u ip:192.168.2.1 | head -30
Pluto nie jest przekazywane do hosta jako urządzenie USB w klasycznym sensie. Z punktu widzenia OS to nowa karta sieciowa, dlatego działa zarówno z WSL2 jak i z natywnego Windows, nie wymaga usbipd.
Połączenie przez Ethernet
Pluto zawiera pełny embedded Linux z demonem iiod i SSH.
Po podłączeniu USB Pluto montuje się jako dysk masowy z plikiem config.txt. To podstawowy sposób konfiguracji.
# /Volumes/PlutoSDR/config.txt (lub D:\config.txt na Windows)
# --- USB-Ethernet (domyślne) ---
ipaddr = 192.168.2.1
ipaddr_host = 192.168.2.10
netmask = 255.255.255.0
# --- Hostname ---
hostname = pluto
# --- Tryb USB host (potrzebne dla adapterów Ethernet/WiFi) ---
udc_handle_suspend = 0
# --- WiFi (po podpięciu dongla USB w trybie host) ---
ssid_wlan = MojaSiec
pwd_wlan = haslo123
# --- Statyczny adres na interfejsie wlan0 / eth0 ---
ipaddr_eth = 192.168.1.50
netmask_eth = 255.255.255.0
gateway_eth = 192.168.1.1
Po edycji dysk wysuwia się za pomocą eject. Pluto wykrywa zmianę, wykonuje fw_setenv w U-Boocie i automatycznie się resetuje. Po ~30 sekundach uruchomi się z nową konfiguracją.
Domyślne hasło SSH (ssh root@192.168.2.1) to analog.
PS C:\Users\RobertPC> ssh root@192.168.2.1
root@192.168.2.1's password:
# Welcome to:
# ______ _ _ _________________
# | ___ \ | | | / ___| _ \ ___ \
# | |_/ / |_ _| |_ ___ \ `--.| | | | |_/ /
# | __/| | | | | __/ _ \ `--. \ | | | /
# | | | | |_| | || (_) /\__/ / |/ /| |\ \
# \_| |_|\__,_|\__\___/\____/|___/ \_| \_|
#
# 80403-dirty
# https://wiki.analog.com/university/tools/pluto
#
Setup na Windows
Instalacja rozpoczyna się od sterowników USB: PlutoSDR-M2k-USB-Drivers, następnie libiio.
ping 192.168.2.1
iio_info -u ip:192.168.2.1
iio_info bez argumentów na Windows zawsze zwraca Unable to create Local IIO context : Function not implemented (40). To nie jest błąd instalacji. libiio próbuje wtedy backendu Local, który czyta /sys/bus/iio/ i działa wyłącznie na Linuksie. Na Windows wymagane jest wskazanie URI: iio_info -u ip:192.168.2.1 lub iio_info -s.
Setup na Ubuntu
sudo apt update
sudo apt install -y libiio-dev libiio-utils
lsusb | grep -i -E "analog|0456"
# Bus 001 Device 049: ID 0456:b673 Analog Devices, Inc. LibIIO based AD9363 Software Defined Radio [ADALM-PLUTO]
sudo dmesg | tail -40 | grep rndis_host
# [871500.522047] rndis_host 1-12:1.0 eth0: register 'rndis_host' at usb-0000:00:14.0-12, RNDIS device, 00:e0:22:ad:c8:3b
# [871500.525100] usbcore: registered new interface driver rndis_host
# [871500.540823] rndis_host 1-12:1.0 enx00e022adc83b: renamed from eth0
sudo ip link set enx00e022adc83b up
sudo ip addr add 192.168.2.10/24 dev enx00e022adc83b
ip addr show enx00e022adc83b
ping -c 3 192.168.2.1
iio_info -u ip:192.168.2.1 | head -20
# iio_info version: 0.25 (git tag:v0.25)
# Libiio version: 0.25 (git tag: v0.25) backends: local xml ip usb
# IIO context created with network backend.
# Backend version: 0.25 (git tag: v0.25)
# Backend description string: 192.168.2.1 Linux (none) 5.15.0 #7 SMP PREEMPT Sun Nov 3 04:49:37 PST 2024 armv7l
# IIO context has 9 attributes:
# hw_model: FISH Ball PlutoSDR Rev.A (Z7020-AD9361)
# hw_model_variant: 1
# hw_serial:
# fw_version: 80403-dirty
# ad9361-phy,xo_correction: 40000000
# ad9361-phy,model: ad9361
# local,kernel: 5.15.0
# uri: ip:192.168.2.1
# ip,ip-addr: 192.168.2.1
# IIO context has 4 devices:
# iio:device0: ad9361-phy
# 11 channels found:
# altvoltage1: TX_LO (output)
# 9 channel-specific attributes found:
Narzędzia z libiio-utils
libiio-utils umożliwia weryfikację sprzętu, przegląd atrybutów i zrzut próbek IQ.
Wybór kontekstu: na Windows backend Local zawsze zwraca Function not implemented, URI musi być wskazany jawnie.
iio_info -s # Skan dostępnych backendów (USB, sieć, DNS-SD)
#Available contexts:
# 0: 192.168.2.1 (FISH Ball PlutoSDR Rev.A (Z7020-AD9361)), serial= [ip:pluto.local]
# 1: 0456:b673 (Analog Devices Inc. PlutoSDR (ADALM-PLUTO)), serial= [usb:1.11.5]
iio_info -u ip:192.168.2.1 # URI po USB-Ethernet
iio_info -u ip:pluto.local # URI po LAN (Pluto+)
iio_info -V
# iio_info version: 0.26 (git tag:a0eca0d)
# Libiio version: 0.26 (git tag: a0eca0d) backends: xml ip usb serial
iio_attr -u ip:192.168.2.1 -d # Wszystkie atrybuty wszystkich urządzeń
# IIO context has 4 devices:
# iio:device0, ad9361-phy: found 18 device attributes
# iio:device1, xadc: found 1 device attributes
# iio:device2, cf-ad9361-dds-core-lpc: found 2 device attributes
# iio:device3, cf-ad9361-lpc: found 2 device attributes
iio_attr -u ip:192.168.2.1 -c ad9361-phy altvoltage0 frequency # Odczyt pojedynczego atrybutu (aktualna częstotliwość LO RX)
# 2400000000
iio_attr -u ip:192.168.2.1 -c ad9361-phy temp0 input # Odczyt temperatury w milistopniach Celsjusza
# 55263
iio_attr -u ip:192.168.2.1 -c ad9361-phy voltage0 hardwaregain 40 # Zapis atrybutu. Ustawienie wzmocnienia RX na 40 dB
iio_reg -u ip:192.168.2.1 ad9361-phy 0x000 # Surowy dostęp do rejestrów AD9363
iio_readdev -u ip:192.168.2.1 -b 4096 cf-ad9361-lpc > raw.iq # Zrzut próbek IQ do pliku binarnego bez użycia Pythona
Pierwszy chirp — zig-zag na waterfalli
Liniowa modulacja częstotliwości, w której częstotliwość narasta od −500 kHz do +500 kHz, a następnie opada. Trójkątny chirp wokół nośnej 600 MHz wytwarzający na waterfalli odbiornika charakterystyczny zig-zag.
Nadawanie sygnałów radiowych podlega regulacjom prawnym. Do testów używaj kabla SMA z tłumikiem 20–30 dB między portem TX Pluto a portem RX odbiornika — nie podłączaj anteny do TX bez licencji radioamatorskiej. Moc Pluto jest niewielka (~7 dBm), ale nadawanie na częstotliwościach licencjonowanych bez zezwolenia jest nielegalne.
pip install pyadi-iio numpy
import numpy as np
import adi
class Device:
def __init__(self, uri="ip:192.168.2.1"):
self.sdr = adi.Pluto(uri)
def configure_tx(self, lo, sample_rate, bandwidth, gain_db):
self.sdr.sample_rate = sample_rate
self.sdr.tx_lo = lo
self.sdr.tx_rf_bandwidth = bandwidth
self.sdr.tx_hardwaregain_chan0 = gain_db
@property
def fs(self):
return self.sdr.sample_rate
def transmit(self, iq, cyclic=True):
self.sdr.tx_cyclic_buffer = cyclic
self.sdr.tx(iq.astype(np.complex64))
def stop(self):
self.sdr.tx_destroy_buffer()
class TriangleChirpGenerator:
def __init__(self, fs, f_dev, t_sweep):
self.fs = fs
self.f_dev = f_dev
self.t_sweep = t_sweep
def _sweep(self, start, stop):
n = int(self.fs * self.t_sweep)
t = np.arange(n) / self.fs
return start + (stop - start) * (t / self.t_sweep)
def iq(self, scale=2**14):
f_up = self._sweep(-self.f_dev, +self.f_dev)
f_dn = self._sweep(+self.f_dev, -self.f_dev)
phase_up = 2 * np.pi * np.cumsum(f_up) / self.fs
phase_dn = phase_up[-1] + 2 * np.pi * np.cumsum(f_dn) / self.fs
iq = np.concatenate([np.exp(1j * phase_up), np.exp(1j * phase_dn)])
return iq * scale
pluto = Device("ip:192.168.2.1")
pluto.configure_tx(lo=600_000_000, sample_rate=2_000_000, bandwidth=2_000_000, gain_db=-20)
chirp = TriangleChirpGenerator(fs=pluto.fs, f_dev=500_000, t_sweep=1.0)
pluto.transmit(chirp.iq())
input("Nadaję chirp ±500 kHz wokół 600 MHz. Enter = stop.")
pluto.stop()
Kluczowa pułapka w kodzie to sposób liczenia fazy. Dla sygnału o zmiennej częstotliwości faza chwilowa to całka z f(t): np.cumsum(f)/fs. Błędem byłoby napisanie exp(2j*pi*f(t)*t), ponieważ taki wzór działa wyłącznie dla stałej f. Przy zmiennej f(t) faza przeskakuje nieciągle, chirp wydaje się zniekształcony z trzaskami w spektrum.
Druga kwestia to styk między zboczem narastającym i opadającym. Gdyby drugi segment zaczął całkowanie od zera, na styku pojawił by się skok fazy. Dlatego phase_dn startuje od ostatniej wartości phase_up. Bez tego waterfall pokazywałby pęknięcie na każdym wierzchołku zig-zaga.
Pozostałe szczegóły są mniej krytyczne, ale warto je znać. tx_cyclic_buffer = True zapętla cały trójkątny przebieg w DMA. Wystarczy załadować jeden okres, Pluto odtwarza go w nieskończoność, bez obciążenia CPU i bez przerw między cyklami. Skalowanie przez 2**14 wynika z tego, że pyadi-iio przyjmuje complex64 w pełnej skali 16-bitowej, mimo że DAC ma realnie 12 bitów. Bez tego sygnał byłby praktycznie niesłyszalny. T_sweep razem z f_dev determinują stromość zig-zaga na waterfalli: krótszy sweep daje bardziej pionowe kreski, większa dewiacja rozszerza je na boki.