5. Python 玩转 PlutoSDR¶
在本章中,我们将学习如何使用 PlutoSDR 的 Python API,它是 Analog Devices 推出的一款低成本 SDR。 我们会先介绍 PlutoSDR 的安装步骤,确保驱动和软件正常运行,然后讨论如何在 Python 中使用 PlutoSDR 进行发射与接收。 最后,我们还会介绍如何借助 Maia SDR 和 IQEngine 把 PlutoSDR 变成一台强大的频谱分析仪!
PlutoSDR 概述¶
PlutoSDR(也叫 ADALM-PLUTO)是一款低成本 SDR(售价略高于 200 美元),能够收发 70 MHz 到 6 GHz 之间的信号。 如果你已经不满足于 20 美元级别的 RTL-SDR,那么它会是一个很不错的升级选择。 Pluto 使用 USB 2.0 接口,因此如果你想长期以 100% 占空比接收全部样本,采样率大约会被限制在 5 MHz 左右。 不过,它本身最高可以采样到 61 MHz,并且一次能够抓取长度约为 1000 万个样本的连续数据块,这使得 Pluto 能够一次覆盖非常宽的频谱范围。 严格来说,它是一台 2x2 设备,但第二路发射和第二路接收通道只能通过机壳内部的 U.FL 接头访问,而且它们共用同一组本振,因此你无法同时在两个不同频率上接收。 下图展示了 Pluto 的方框图,以及 Pluto 内部使用的 AD936x 射频集成电路(RFIC)的方框图。
PlutoSDR 的软件与驱动安装¶
为 PlutoSDR 搭建 Ubuntu 22 VM¶
虽然本书提供的 Python 代码应当可以在 Windows、Mac 和 Linux 下运行,但下面的安装说明是针对 Ubuntu 22 编写的。 如果你按照 Analog Devices 提供的说明 在自己的操作系统上安装软件时遇到困难,我建议安装一个 Ubuntu 22 的虚拟机(VM),然后按照下面的步骤操作。 另一种选择是,如果你使用的是 Windows 11,那么 Windows Subsystem for Linux(WSL)里的 Ubuntu 22 通常运行良好,并且开箱即用地支持图形界面。
- 安装并打开 VirtualBox 。
- 创建一个新的 VM。内存大小建议设置为你电脑总内存的 50%。
- 创建虚拟硬盘时,选择 VDI,并使用动态分配。15 GB 通常就够了;如果你想更稳妥一些,也可以分配更大的空间。
- 下载 Ubuntu 22 Desktop 的 .iso 文件 - https://ubuntu.com/download/desktop
- 启动 VM。它会要求你选择安装介质,请选择 Ubuntu 22 Desktop 的 .iso 文件。选择 “install Ubuntu”,使用默认选项,期间会弹出一个提醒你即将进行更改的窗口,点击继续即可。然后设置用户名和密码,等待 VM 完成初始化。安装结束后 VM 会重启,但请在重启之后先让 VM 关机。
- 进入 VM 设置(齿轮图标)。
- 在 system > processor 中至少选择 3 个 CPU。如果你的电脑有独立显卡,那么在 display > video memory 中可以把显存设得更高一些。
- 启动你的 VM。
- 建议你安装 VM Guest Additions。在 VM 内选择 Devices > Insert Guest Additions CD,弹窗出现时点击运行。按照提示完成安装后重启 VM。共享剪贴板可以通过 Devices > Shared Clipboard > Bidirectional 开启。
连接 PlutoSDR¶
- 如果你使用的是 OSX,请在宿主机的 OSX 中而不是 VM 内,在系统偏好设置中启用 “kernel extensions”,然后安装 HoRNDIS(可能需要重启)。
- 如果你使用的是 Windows,请安装这个驱动: https://github.com/analogdevicesinc/plutosdr-m2k-drivers-win/releases/download/v0.7/PlutoSDR-M2k-USB-Drivers.exe
- 如果你使用的是 Linux,通常不需要做任何额外处理。
- 通过 USB 把 Pluto 连接到宿主机。注意一定要使用 Pluto 中间的那个 USB 接口,因为另一个接口只负责供电。连接之后,Pluto 会表现为一个虚拟网卡,也就是说,它会像一个 USB 以太网适配器那样出现在系统里。
- 在宿主机(不是 VM)上打开终端或你喜欢的 ping 工具,ping
192.168.2.1。如果不通,就先停下来排查网络接口问题。 - 在 VM 内打开一个新的终端。
- Ping
192.168.2.1。如果不通,也先停下来排查。当你在 ping 的时候,拔掉 Pluto,确认 ping 会立刻中断;如果它依然持续响应,那就说明网络里还有其他设备使用了这个 IP,继续之前你得先修改 Pluto(或那个其他设备)的 IP 地址。 - 记下 Pluto 的 IP 地址,因为后面我们在 Python 中使用它时会用到。
安装 PlutoSDR 驱动及 Python API¶
下面这些终端命令会构建并安装以下库的最新版本:
- libiio,Analog Devices 的 “跨平台” 硬件接口库
- libad9361-iio,AD9361 是 PlutoSDR 内部使用的具体射频芯片
- pyadi-iio,也就是 Pluto 的 Python API,这才是我们的最终目标,但它依赖前面两个库
sudo apt-get update
sudo apt-get install build-essential git libxml2-dev bison flex libcdk5-dev cmake python3-pip libusb-1.0-0-dev libavahi-client-dev libavahi-common-dev libaio-dev
cd ~
git clone --branch v0.23 https://github.com/analogdevicesinc/libiio.git
cd libiio
mkdir build
cd build
cmake -DPYTHON_BINDINGS=ON ..
make -j$(nproc)
sudo make install
sudo ldconfig
cd ~
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cd libad9361-iio
mkdir build
cd build
cmake ..
make -j$(nproc)
sudo make install
cd ~
git clone --branch v0.0.14 https://github.com/analogdevicesinc/pyadi-iio.git
cd pyadi-iio
pip3 install --upgrade pip
pip3 install -r requirements.txt
sudo python3 setup.py install
测试 PlutoSDR 驱动以及 Python API¶
在你的 VM 中新开一个终端,然后输入以下命令:
python3
import adi
sdr = adi.Pluto('ip:192.168.2.1') # 或者改成你的 Pluto 实际 IP
sdr.sample_rate = int(2.5e6)
sdr.rx()
如果运行到这里都没有报错,那就可以继续后面的内容了。
修改 Pluto 的 IP 地址¶
如果默认的 192.168.2.1 因为某些原因不适用,例如你的网络里已经存在 192.168.2.0 这个子网,或者你想同时连接多台 Pluto,那么可以按照下面的步骤修改 IP:
- 打开 PlutoSDR 的
config.txt文件进行编辑,它位于 Pluto 的大容量存储设备中(也就是你插上 Pluto 后出现的那个看起来像 U 盘的设备)。把你想使用的新 IP 写进去。 - 弹出这个大容量存储设备(注意,不要拔掉 Pluto!)。在 Ubuntu 22 中,你会在文件管理器里 PlutoSDR 设备旁边看到一个弹出图标。
- 等待几秒钟,然后通过拔下再重新插上 Pluto 的方式重新上电。之后再次打开
config.txt,确认你的修改是否保存成功。
需要注意的是,这个流程也同样用于给 Pluto 刷入不同的固件镜像。更多细节请参见 https://wiki.analog.com/university/tools/pluto/users/firmware 。
“Hack” PlutoSDR 以扩展射频范围¶
PlutoSDR 出厂时的中心频率范围和采样率是受限制的,但其底层芯片实际上支持更高的频率。 按照下面的步骤可以解锁芯片的完整频率范围。 请注意,这个过程本身就是 Analog Devices 官方提供的,因此风险已经尽可能低。 PlutoSDR 的频率限制与 Analog Devices 对 AD936x 芯片按高频性能要求进行 “分档(binning)” 有关。 而对于 SDR 爱好者和实验者来说,我们通常并不会过分在意这些更高频率下的严格性能指标。
开始 hack 吧!打开一个终端(宿主机或 VM 都可以):
默认密码是 analog
你应该会看到 PlutoSDR 的欢迎界面。现在你已经通过 SSH 登录到了 Pluto 本体上的 ARM CPU! 如果你的 Pluto 固件版本是 0.31 或更低,请输入下面的命令:
fw_setenv attr_name compatible
fw_setenv attr_val ad9364
reboot
如果是 0.32 及以上版本,则使用:
fw_setenv compatible ad9364
reboot
现在你应该就可以把中心频率调到最低 70 MHz、最高 6 GHz,并把采样率提高到最高 56 MHz 了!
PlutoSDR 接收¶
使用 PlutoSDR 的 Python API 进行采样是非常直接的。
对于任何一款 SDR,我们都知道至少要告诉它中心频率、采样率以及增益(或者是否启用自动增益控制)。
可能还有其他细节参数,但这三项是最基本的,没有它们 SDR 就不知道该如何接收样本。
有些 SDR 需要你显式发送一条命令才能开始采样,而 Pluto 这样的设备会在初始化完成后立即开始采样。
当 SDR 内部缓冲区被填满后,最旧的样本就会被丢弃。
所有 SDR API 基本都会提供某种 “接收样本” 函数,而 Pluto 中对应的就是 rx(),它会返回一批样本。
每一批返回多少样本,则由你事先设置好的缓冲区大小决定。
下面的代码假设你已经安装好了 Pluto 的 Python API。
这段代码会初始化 Pluto,将采样率设置为 1 MHz,中心频率设置为 100 MHz,并将接收增益设置为 70 dB,同时关闭自动增益控制。
注意,设置中心频率、增益和采样率的顺序通常并不重要。
在下面的代码片段中,我们告诉 Pluto 每次调用 rx() 时返回 10,000 个样本,并打印前 10 个样本。
import numpy as np
import adi
sample_rate = 1e6 # Hz
center_freq = 100e6 # Hz
num_samps = 10000 # 每次调用 rx() 返回的样本数
sdr = adi.Pluto('ip:192.168.2.1')
sdr.gain_control_mode_chan0 = 'manual'
sdr.rx_hardwaregain_chan0 = 70.0 # dB
sdr.rx_lo = int(center_freq)
sdr.sample_rate = int(sample_rate)
sdr.rx_rf_bandwidth = int(sample_rate) # 滤波器带宽,这里先直接设成和采样率一样
sdr.rx_buffer_size = num_samps
samples = sdr.rx() # 从 Pluto 接收样本
print(samples[0:10])
目前我们还不会对这些样本做什么特别有趣的处理,但本书剩下的大部分内容都围绕这类 IQ 样本展开,并使用 Python 对它们进行处理。
PlutoSDR 接收增益(Receive Gain)¶
Pluto 可以配置成固定接收增益,也可以配置成自动接收增益。 自动增益控制(Automatic Gain Control,AGC)会自动调整接收增益,以维持一个较强的信号电平(如果你好奇的话,目标大约是 -12 dBFS)。 AGC 不要和负责把信号数字化的模数转换器(ADC)混淆。 从严格意义上说,AGC 是一个闭环反馈电路,它会根据接收到的信号情况来调节放大器的增益。 它的目标是在输入功率变化时维持相对恒定的输出功率,同时既避免接收机饱和(也就是碰到 ADC 动态范围的上限),又尽可能让信号 “填满” 更多 ADC 位数。
PlutoSDR 内部的射频集成电路(RFIC)包含一个支持多种设置的 AGC 模块。 (RFIC 指的是一种能够同时发射和接收无线电信号的芯片。) 首先需要注意,Pluto 的接收增益范围是 0 到 74.5 dB。 当设置为 “manual” 模式时,AGC 会被关闭,此时你必须显式告诉 Pluto 使用多大的接收增益,例如:
sdr.gain_control_mode_chan0 = "manual" # 关闭 AGC
gain = 50.0 # 允许范围是 0 到 74.5 dB
sdr.rx_hardwaregain_chan0 = gain # 设置接收增益
如果你想启用 AGC,则必须从以下两种模式中选择一种:
sdr.gain_control_mode_chan0 = "slow_attack"sdr.gain_control_mode_chan0 = "fast_attack"
启用 AGC 后,你就不需要再给 rx_hardwaregain_chan0 赋值了。
因为 Pluto 会自行调节增益,所以这个值会被忽略。
Pluto 的 AGC 提供 fast attack 和 slow attack 两种模式,正如上面的代码所示。
它们之间的差别其实很好理解:fast attack 模式对信号变化反应更快。
换句话说,当接收信号的强度变化时,增益值也会更快地跟着变化。
这种对信号功率变化的快速适应在时分双工(TDD)系统中尤其重要,因为 TDD 系统会在同一个频率上轮流发射和接收。
在这种场景下,把增益控制设为 fast attack 模式,可以减小信号被过度压低的程度。
无论使用哪种模式,如果当前没有信号、只有噪声,那么 AGC 都会把增益顶到最大;一旦信号突然出现,接收机会短暂饱和,直到 AGC 反应过来并把增益拉低。
你也可以通过下面的代码实时查看当前的增益值:
sdr._get_iio_attr('voltage0','hardwaregain', False)
关于 Pluto 的 AGC 的更多细节,例如如何修改更高级的 AGC 设置,请参考 这个页面中的 “RX Gain Control” 部分 。
PlutoSDR 发射¶
在用 Pluto 发射任何信号之前,请务必先用一根 SMA 线缆把 Pluto 的 TX 端口和充当接收机的设备连接起来。 尤其是在你还在学习 如何 发射的时候,一定要先通过线缆发射,这样才能确认 SDR 的行为完全符合预期。 发射功率务必要从极低开始,因为线缆不像无线信道那样会对信号产生衰减,接收机的射频前端有可能会过载。 如果你手头有一个衰减器(例如 30 dB),现在正是派上用场的时候。 如果你没有另一台 SDR 或频谱分析仪来充当接收机,理论上可以用同一台 Pluto 的 RX 端口来接收,但那会变得有些复杂。 我更建议你买一个 10 美元级别的 RTL-SDR 作为接收端 SDR。
发射和接收非常相似,只不过这一次不再是告诉 SDR 要接收多少样本,而是把一批要发射的样本交给它。
我们不再设置 rx_lo,而是设置 tx_lo,用于指定载波发射频率。
采样率是 RX 和 TX 共用的,因此仍然像平常一样设置即可。
下面给出了一个完整的发射示例:我们生成一个 +100 kHz 的正弦,然后在 915 MHz 的载波频率上发射这组复信号,因此接收端会在 915.1 MHz 看到一个载波。
这在工程上其实没有什么实际意义,因为我们完全可以直接把 center_freq 设为 915.1e6,再发射一个全 1 的数组。
这里只是为了演示如何生成复数样本。
import numpy as np
import adi
sample_rate = 1e6 # Hz
center_freq = 915e6 # Hz
sdr = adi.Pluto("ip:192.168.2.1")
sdr.sample_rate = int(sample_rate)
sdr.tx_rf_bandwidth = int(sample_rate) # 滤波器截止频率,这里先直接设成和采样率一样
sdr.tx_lo = int(center_freq)
sdr.tx_hardwaregain_chan0 = -50 # 增大该值可以增大发射功率,有效范围是 -90 到 0 dB
N = 10000 # 一次要发射的样本数
t = np.arange(N)/sample_rate
samples = 0.5*np.exp(2.0j*np.pi*100e3*t) # 模拟一个 100 kHz 正弦,因此接收端应在 915.1 MHz 看到它
samples *= 2**14 # PlutoSDR 要求样本范围在 -2^14 到 +2^14 之间,而不是某些 SDR 使用的 -1 到 +1
# 将这一批样本发射 100 次,如果 USB 跟得上,总共就相当于 1 秒的信号
for i in range(100):
sdr.tx(samples) # 发射这一批样本一次
关于这段代码,这里有几点需要说明。
首先,在仿真 IQ 样本时,你应当让它们的数值位于 -1 到 1 之间;但在真正发射之前,由于 Analog Devices 对 tx() 函数的实现方式,我们必须把它们乘以 \(2^{14}\)。
如果你不确定样本的最小值和最大值是多少,最简单的方法就是用 print(np.min(samples), np.max(samples)) 打印出来,或者写一个 if 语句确保它们在乘以 \(2^{14}\) 之前从不超过 1 或低于 -1。
至于发射增益,它的范围是 -90 到 0 dB,其中 0 dB 对应最大的发射功率。
我们总是希望从较低的发射功率开始,必要时再逐步往上调,因此这里默认设置为 -50 dB,属于偏低的一侧。
不要因为信号没出现就直接把它拉到 0 dB,问题很可能出在别的地方,而你并不想把接收机烧坏。
重复发射样本¶
如果你想连续重复发射同一组样本,而不是像上面那样在 Python 里用 for/while 循环不断调用,可以只用一行代码告诉 Pluto 这么做:
sdr.tx_cyclic_buffer = True # 启用循环缓冲区
然后像平常一样调用一次 sdr.tx(samples) 即可,Pluto 会无限循环发射这组信号,直到 sdr 对象被析构。
如果你想更换正在循环发射的样本,不能直接再次调用 sdr.tx(samples) 传入一组新样本,而必须先调用 sdr.tx_destroy_buffer(),然后再调用 sdr.tx(samples)。
如何合法地进行空口发射¶
我已经无数次被学生问到,在美国,使用天线配合 Pluto 发射时,哪些频率是允许发射的。 就我所知,简短答案是:没有。 通常,当人们引用某些提到发射功率限制的法规时,他们指的是 FCC 的 “Title 47, Part 15” (47 CFR 15) 法规 。 但这些规定实际上是面向在 ISM 频段设计和销售设备的制造商,讨论的是这些设备应如何被测试。 所谓 Part 15 设备,是指个人在其所使用的频谱上操作该设备时不需要执照的设备,但设备本身在被营销和销售之前,必须先获得授权/认证,以证明其符合 FCC 法规。 Part 15 规定确实为不同频段规定了最大发射功率和接收功率级别,但这些规定实际上并不适用于个人使用 SDR 或自制无线电设备发射信号的情形。 我能找到的、与并非商品化产品的无线电设备相关的法规,主要只涉及在 AM/FM 广播频段运行低功率的 AM 或 FM 电台。 法规中还有一节提到 “自制设备(home-built devices)”,但它明确表示不适用于通过套件组装出来的设备,而把一个基于 SDR 的发射系统称作自制设备也有些牵强。 总之,FCC 法规并不是简单的 “你只能在这些频率、这些功率以下发射”,而是一整套围绕测试与合规构建出来的复杂规则。
换个角度看,也可以说:“这些不是 Part 15 设备,那我们就姑且按照 Part 15 的规则来约束自己。” 以 915 MHz ISM 频段为例,规则要求该频段内辐射发射的场强在 30 米处不得超过 500 微伏/米,并且测量应基于采用平均检波器的仪器。 所以你可以看到,这并不是一个简单的 “最大发射功率是多少瓦” 的问题。
如果你拥有业余无线电(ham radio)执照,那么 FCC 允许你使用专门划分给业余无线电业务的频段。 这些频段仍然有需要遵守的规则以及最大发射功率限制,但至少这些数值是以有效辐射功率多少瓦来规定的。 这张信息图 展示了不同执照等级(Technician、General 和 Extra)可以使用哪些频段。 我建议所有想用 SDR 做发射的人都去考一个业余无线电执照,更多信息可参见 ARRL 的 Getting Licensed 页面 。
如果有人对哪些行为被允许、哪些不被允许有更详细的信息,请给我发邮件。
同时进行发射与接收¶
借助 tx_cyclic_buffer 这个技巧,你可以很容易地同时进行接收和发射,只需要先把发射机启动起来,再去接收即可。
下面的代码展示了一个可运行的例子:在 915 MHz 频段发射一个 QPSK 信号,同时接收它,并绘制功率谱密度(PSD)。
import numpy as np
import adi
import matplotlib.pyplot as plt
sample_rate = 1e6 # Hz
center_freq = 915e6 # Hz
num_samps = 100000 # 每次调用 rx() 返回的样本数
sdr = adi.Pluto("ip:192.168.2.1")
sdr.sample_rate = int(sample_rate)
# 配置发射
sdr.tx_rf_bandwidth = int(sample_rate) # 滤波器截止频率,这里先直接设成和采样率一样
sdr.tx_lo = int(center_freq)
sdr.tx_hardwaregain_chan0 = -50 # 增大该值可以增大发射功率,有效范围是 -90 到 0 dB
# 配置接收
sdr.rx_lo = int(center_freq)
sdr.rx_rf_bandwidth = int(sample_rate)
sdr.rx_buffer_size = num_samps
sdr.gain_control_mode_chan0 = 'manual'
sdr.rx_hardwaregain_chan0 = 0.0 # dB,增大该值可以提高接收增益,但要小心不要让 ADC 饱和
# 创建发射波形(QPSK,每个符号 16 个样本)
num_symbols = 1000
x_int = np.random.randint(0, 4, num_symbols) # 0 到 3
x_degrees = x_int*360/4.0 + 45 # 45、135、225、315 度
x_radians = x_degrees*np.pi/180.0 # sin() 和 cos() 使用的是弧度
x_symbols = np.cos(x_radians) + 1j*np.sin(x_radians) # 生成我们的 QPSK 复符号
samples = np.repeat(x_symbols, 16) # 每个符号 16 个样本(矩形脉冲)
samples *= 2**14 # PlutoSDR 要求样本范围在 -2^14 到 +2^14 之间,而不是某些 SDR 使用的 -1 到 +1
# 启动发射机
sdr.tx_cyclic_buffer = True # 启用循环缓冲区
sdr.tx(samples) # 开始发射
# 为了稳妥起见先清空一下缓冲区
for i in range (0, 10):
raw_data = sdr.rx()
# 接收样本
rx_samples = sdr.rx()
print(rx_samples)
# 停止发射
sdr.tx_destroy_buffer()
# 计算功率谱密度(即信号的频域表示)
psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples)))**2
psd_dB = 10*np.log10(psd)
f = np.linspace(sample_rate/-2, sample_rate/2, len(psd))
# 绘制时域图
plt.figure(0)
plt.plot(np.real(rx_samples[::100]))
plt.plot(np.imag(rx_samples[::100]))
plt.xlabel("Time")
# 绘制频域图
plt.figure(1)
plt.plot(f/1e6, psd_dB)
plt.xlabel("Frequency [MHz]")
plt.ylabel("PSD")
plt.show()
如果你的天线或线缆连接正确,你应该会看到类似下图的结果:
一个很好的练习是缓慢调整 sdr.tx_hardwaregain_chan0 和 sdr.rx_hardwaregain_chan0,确认接收到的信号会按预期变强或变弱。
Maia SDR 与 IQEngine¶
想把你的 Pluto 变成电脑或手机上的实时频谱分析仪吗? 开源的 Maia SDR 项目为 Pluto 提供了一个修改过的固件镜像,它会在 Pluto 的 FPGA 上运行 FFT,并在 Pluto 的 ARM CPU 上运行一个 Web 服务器! 这个 Web 界面可以用来设置频率和其他 SDR 参数,并以瀑布图形式查看时频谱。 你还可以录制最大 400 MB 的原始 IQ 样本,并将它们下载到电脑或手机上,或者直接在 IQEngine 中查看。
安装最新版本的 Maia Pluto 固件时,先下载 latest release,具体要选文件名为 plutosdr-fw-maia-sdr-vX.Y.Z.zip 的那个。
解压后,将其中的 pluto.frm 文件复制到 Pluto 的大容量存储设备中(它看起来就像一个 U 盘),然后弹出 Pluto(不要拔线)。
这和升级 Pluto 固件的过程是一样的;设备会闪烁几分钟,然后自动重启。
最后,像我们在 “hack Pluto” 一节中那样,通过终端执行 ssh root@192.168.2.1 来 SSH 登录 Pluto,默认密码为 analog。
登录之后,你需要按顺序逐条执行下面这三条命令:
fw_setenv ramboot_verbose 'adi_hwref;echo Copying Linux from DFU to RAM... && run dfu_ram;if run adi_loadvals; then echo Loaded AD936x refclk frequency and model into devicetree; fi; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw earlyprintk clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config}'
fw_setenv qspiboot_verbose 'adi_hwref;echo Copying Linux from QSPI flash to RAM... && run read_sf && if run adi_loadvals; then echo Loaded AD936x refclk frequency and model into devicetree; fi; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw earlyprintk clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config} || echo BOOT failed entering DFU mode ... && run dfu_sf'
fw_setenv qspiboot 'set stdout nulldev;adi_hwref;test -n $PlutoRevA || gpio input 14 && set stdout serial@e0001000 && sf probe && sf protect lock 0 100000 && run dfu_sf; set stdout serial@e0001000;itest *f8000258 == 480003 && run clear_reset_cause && run dfu_sf; itest *f8000258 == 480007 && run clear_reset_cause && run ramboot_verbose; itest *f8000258 == 480006 && run clear_reset_cause && run qspiboot_verbose; itest *f8000258 == 480002 && run clear_reset_cause && exit; echo Booting silently && set stdout nulldev; run read_sf && run adi_loadvals; envversion;setenv bootargs console=ttyPS0,115200 maxcpus=${maxcpus} rootfstype=ramfs root=/dev/ram0 rw quiet loglevel=4 clk_ignore_unused uio_pdrv_genirq.of_id=uio_pdrv_genirq uboot="${uboot-version}" && bootm ${fit_load_address}#${fit_config} || set stdout serial@e0001000;echo BOOT failed entering DFU mode ... && sf protect lock 0 100000 && run dfu_sf'
(关于为什么需要这样设置,更多信息请参见 Maia 的安装页面 。)
再重启一次 Pluto。 此时,Pluto 就应该已经运行 Maia 了! 在浏览器中打开 http://192.168.2.1:8000 ,你应该会看到 Maia 的实时频谱分析界面和 SDR 控制面板,如下图所示:
想测试 Maia 能跑多快,可以尝试把 Spectrum Rate 调到 100 Hz 或更高。
除了频率、采样率和增益等常见 SDR 旋钮之外,你还可以点击底部的 Record 按钮,把原始 IQ 样本录制到 Pluto 的板载内存中。
随后你可以点击 Recording 按钮,再点击 View in IQEngine 链接,在 IQEngine 中查看录制内容,如下图所示,或者把文件保存到你的设备上。
参考 API¶
如果你想查看完整的 SDR 属性和可调用函数列表,请参考 pyadi-iio 中的 Pluto Python 代码(AD936X) 。
PlutoSDR Python 练习¶
与其直接给你一段现成代码去运行,不如通过几个练习来巩固。 这些练习中大约 95% 的代码已经给出,剩下的部分只是一些相对直接的 Python 内容,需要你自己补上。 这些练习并不是为了难住你,而是只留下足够少的空白,让你能主动思考一下。
练习 1:测出你的 USB 吞吐量¶
让我们尝试从 PlutoSDR 接收样本,同时看看通过 USB 2.0 连接,每秒到底能推多少样本到主机上。
你的任务是编写一个 Python 脚本,测量 Python 端每秒实际接收到的样本数。也就是说,要统计接收到的总样本数并记录所花费的时间,然后用它们算出速率。接着,尝试不同的 :code:`sample_rate` 和 :code:`buffer_size`,看看它们会如何影响可达到的最高速率。
请记住,如果你每秒接收到的样本数小于设定的 sample_rate,那就说明有一部分样本丢失了,而这在高采样率下很可能发生。
Pluto 只使用 USB 2.0。
下面这段代码可以作为起点,并且其中已经包含了完成任务所需的基础设置。
import numpy as np
import adi
import matplotlib.pyplot as plt
import time
sample_rate = 10e6 # Hz
center_freq = 100e6 # Hz
sdr = adi.Pluto("ip:192.168.2.1")
sdr.sample_rate = int(sample_rate)
sdr.rx_rf_bandwidth = int(sample_rate) # 滤波器截止频率,这里先直接设成和采样率一样
sdr.rx_lo = int(center_freq)
sdr.rx_buffer_size = 1024 # 这是 Pluto 用来缓存样本的缓冲区大小
samples = sdr.rx() # 从 Pluto 接收样本
此外,为了测量某段代码执行了多久,你可以使用下面的方式:
start_time = time.time()
# 做一些事情
end_time = time.time()
print('seconds elapsed:', end_time - start_time)
下面是一些提示,帮助你开始:
提示 1:你需要把 samples = sdr.rx() 放进一个循环里反复执行很多次(例如 100 次)。每次调用 sdr.rx() 时,都要统计返回了多少样本,同时记录经过了多少时间。
提示 2:虽然你要计算的是每秒样本数,但这并不意味着你必须刚好采 1 秒。你只需要把接收到的样本总数除以所经过的时间即可。
提示 3:就像示例里那样,先从 sample_rate = 10e6 开始,因为这个速率已经远远超出了 USB 2.0 的承载能力。这样你就能看到究竟有多少数据真的传了过来。然后你可以调整 rx_buffer_size,把它调得大很多,看看会发生什么。等你写好了可运行脚本并尝试过不同的 rx_buffer_size 后,再去调整 sample_rate。最终找出你需要把采样率降到多低,才能在 Python 中 100% 接收所有样本(也就是达到 100% 占空比采样)。
提示 4:在那个循环里调用 sdr.rx() 时,尽量少做别的事情,避免额外增加执行延迟。不要在循环内部做昂贵操作,比如反复打印。
通过这个练习,你会对 USB 2.0 的最大吞吐量建立一个直观认识。 你也可以上网查找资料,验证自己的结果是否合理。
额外挑战:试着修改 center_freq 和 rx_rf_bandwidth,看看它们是否会影响你从 Pluto 接收样本的速率。
练习 2:创建时频谱/瀑布图¶
在这个练习中,你将创建一个时频谱,也就是我们在 频率域 章节末尾学过的瀑布图。 时频谱本质上就是把一堆 FFT 结果一层层堆叠显示出来。 换句话说,它是一张图像,其中一个轴表示频率,另一个轴表示时间。
在 频率域 章节中,我们已经学过如何用 Python 执行 FFT。 这个练习里,你可以复用上一题里的代码片段,再加上一点基础 Python 代码就够了。
提示:
- 可以尝试把
sdr.rx_buffer_size设成 FFT 大小,这样每次调用sdr.rx()时就刚好执行 1 次 FFT。 - 构建一个二维数组来保存所有 FFT 结果,其中每一行表示 1 次 FFT。一个填满 0 的 2D 数组可以通过下面的方式创建:
np.zeros((num_rows, fft_size))。访问数组的第i行可以使用:waterfall_2darray[i,:]。 plt.imshow()是显示二维数组的一个非常方便的方法,它会自动帮你缩放颜色。
额外挑战:让这个时频谱实时更新。
Pluto+¶
Pluto+(也叫 Pluto Plus)是原版 PlutoSDR 的非官方升级版本,主要可以在 AliExpress 上买到。 它带有千兆以太网接口、通过 SMA 引出的两路 RX 和两路 TX、MicroSD 插槽、0.5PPM 的 VCTCXO,以及 PCB 上通过 U.FL 提供的外部时钟输入。
以太网口是一个非常大的升级,因为它极大提高了你在 100% 占空比接收或发射时能够达到的采样率。 Pluto 和 Pluto+ 默认都用 16 位来表示 I 和 Q,尽管它实际上只有一个 12 位 ADC,因此每个 IQ 样本总共占 4 个字节。 按 90% 传输效率估算,千兆以太网约等于 900 Mb/s,也就是 112.5 MB/s,因此若每个 IQ 样本占 4 字节,那么如果你想在较长时间内(例如超过 1 秒)把所有样本都接收下来,对应的最大采样率大约是 28 MHz。 作为对比,USB 3.0 大约可以做到 56 MHz,而 USB 2.0 大约是 5 MHz。 除此之外,你的电脑性能、你打算在这些样本上运行的具体 DSP 应用(或者如果你只是录制到文件中,那么磁盘写入速度)也都会形成额外瓶颈。 对于基于 Python 的 SDR 应用来说,通过以太网使用 Pluto+ 时,更现实的采样率通常接近 10 MHz。
要给以太网口设置 IP 地址,请先通过 USB 连接 Pluto+,打开它的大容量存储设备,编辑 config.txt 里的 [USB_ETHERNET] 部分。
然后给 Pluto+ 重新上电。
此时你应该就能通过刚刚填入的 IP 地址,经由以太网 SSH 到 Pluto+ 了。
如果这样可以工作,你就可以把 micro USB 线改插到 5V 供电口,让它只负责给 Pluto+ 供电,而所有通信都通过以太网完成。
请记住,即使是普通的 PlutoSDR(以及 Pluto+),也能够以最高 61 MHz 的带宽进行采样,并一次抓取大约 1000 万个连续样本,只要你在两次抓取之间稍作等待即可,因此它依然非常适合做强大的频谱感知应用。
Pluto+ 的 Python 代码与普通 PlutoSDR 完全一样,只需要把 192.168.2.1 替换成你为以太网设置的 IP 地址即可。
你可以试着在循环中不断接收样本,并统计每秒接收到了多少,从而看看在 Python 端仍然能够每秒接收到接近设定采样率样本数的情况下,采样率究竟能推多高。
提示:把 rx_buffer_size 调得非常大,会有助于提高吞吐量。
AntSDR E200¶
AntSDR E200(下文简称 AntSDR)是一款基于 936X 的低成本 SDR,由中国上海的一家公司 MicroPhase 制造,在设计上和 Pluto、Pluto+ 非常相似。 与 Pluto+ 类似,它使用 1 Gb 以太网连接,不过 AntSDR 不提供 USB 数据连接选项。 AntSDR 的独特之处在于,它既可以像 Pluto 一样使用 IIO 库工作,也可以像 USRP 一样使用 UHD 库工作。 默认情况下,它出厂时表现得像一台 Pluto,但切换到 USRP/UHD 模式只需要一次简单的固件更新。 这两套固件本质上都是在 Analog Devices/Ettus 原始固件基础上做了极少量修改,以适配 AntSDR 的硬件。 另一个特别之处是,你可以购买安装了 9363 或 9361 芯片的版本;虽然它们在功能上是同一类芯片,但 9361 在工厂分档时会被归为具有更高高频性能的版本。 注意,Pluto 和 Pluto+ 都只使用 9363。 AntSDR 的规格说明声称,基于 9363 的版本最高只能到 3.8 GHz、采样率最高 20 MHz,但事实并非如此;它实际上可以达到完整的 6 GHz,以及大约 60 MHz 的采样率(当然,1 Gb 以太网上无法长期传回 100% 的样本)。 和其他 Pluto 一样,AntSDR 也是一台 2x2 设备,第二路发射和接收通道可以通过板上的 U.FL 接头访问。 其余射频性能和技术指标则与 Pluto/Pluto+ 相近甚至完全相同。 它可以从 Crowd Supply 和 AliExpress 购买。
AntSDR 上那个小小的 DIP 开关用于在 SD 卡启动和板载 Quad SPI(QSPI)闪存启动之间切换。 在写作本文时,E200 的 QSPI 中预装的是 Pluto 固件,而 SD 卡中预装的是 USRP/UHD 固件,因此只需拨动这个开关,就可以在两种模式之间切换,无需额外操作。
下面展示的是 E200 的方框图。
以 Pluto 模式配置和使用 AntSDR 的方法与 Pluto+ 类似,只需要注意它的默认 IP 是 192.168.1.10,并且它没有 USB 数据连接,因此也就没有可以用来更新固件或修改设置的大容量存储设备。
相应地,你可以通过 SD 卡更新固件,通过 SSH 修改设置。
另外,如果你能够 SSH 登录到设备,也可以使用下面这条命令修改设备的 IP 地址: fw_setenv ipaddr_eth 192.168.2.1 ,只需把这里的 IP 替换成你想要的地址即可。
Pluto/IIO 固件可以在这里找到: AntSDR Pluto 固件仓库 ,USRP/UHD 固件在这里: AntSDR UHD 固件仓库 。
如果你的 SD 卡里没有附带 USRP/UHD 驱动,或者你想安装最新版,那么可以参考 MicroPhase 的 Quick Start Guide ,在 AntSDR 上安装 USRP/UHD 固件,同时在你的主机上安装一份经过轻微修改的 UHD 主机端驱动。
之后你就可以像平常一样使用 uhd_find_devices 和 uhd_usrp_probe (更多信息以及适用于 AntSDR 的 USRP 模式示例代码,请参见 USRP 章节)。
下面这些命令是在 Ubuntu 22 上安装主机端代码时使用的:
sudo apt-get update
sudo apt-get install autoconf automake build-essential ccache cmake cpufrequtils doxygen ethtool \
g++ git inetutils-tools libboost-all-dev libncurses5 libncurses5-dev libusb-1.0-0 libusb-1.0-0-dev \
libusb-dev python3-dev python3-mako python3-numpy python3-requests python3-scipy python3-setuptools \
python3-ruamel.yaml
cd ~
git clone [email protected]:MicroPhase/antsdr_uhd.git
cd host
mkdir build
cd build
cmake -DENABLE_X400=OFF -DENABLE_N320=OFF -DENABLE_X300=OFF -DENABLE_USRP2=OFF -DENABLE_USRP1=OFF -DENABLE_N300=OFF -DENABLE_E320=OFF -DENABLE_E300=OFF ../
# 注意:此时请确认在 “enabled components” 中能看到 ANT 和 LibUHD - Python API
make -j8
sudo make install
sudo ldconfig
export PYTHONPATH="${PYTHONPATH}:/usr/local/lib/python3/dist-packages"
sudo sysctl -w net.core.rmem_max=1000000
sudo sysctl -w net.core.wmem_max=1000000
设备端则直接使用随 AntSDR 附带的 SD 卡中已有的 USRP 固件,只需把以太网口下方的 DIP 开关拨到 “SD” 即可。
可以使用下面的命令识别并探测 AntSDR:
uhd_find_devices --args addr=192.168.1.10
uhd_usrp_probe --args addr=192.168.1.10
下面是工作正常时的一段示例输出:
$ uhd_find_devices --args addr=192.168.1.10
[INFO] [UHD] linux; GNU C++ version 11.3.0; Boost_107400; UHD_4.1.0.0-0-d2f0b1b1
--------------------------------------------------
-- UHD Device 0
--------------------------------------------------
Device Address:
serial: 0223D80FF0D767EBC6D3AAAA6793E64D
addr: 192.168.1.10
name: ANTSDR-E200
product: E200 v1
type: ant
$ uhd_usrp_probe --args addr=192.168.1.10
[INFO] [UHD] linux; GNU C++ version 11.3.0; Boost_107400; UHD_4.1.0.0-0-d2f0b1b1
[INFO] [ANT] Detected Device: ANTSDR
[INFO] [ANT] Initialize CODEC control...
[INFO] [ANT] Initialize Radio control...
[INFO] [ANT] Performing register loopback test...
[INFO] [ANT] Register loopback test passed
[INFO] [ANT] Performing register loopback test...
[INFO] [ANT] Register loopback test passed
[INFO] [ANT] Setting master clock rate selection to 'automatic'.
[INFO] [ANT] Asking for clock rate 16.000000 MHz...
[INFO] [ANT] Actually got clock rate 16.000000 MHz.
_____________________________________________________
/
| Device: B-Series Device
| _____________________________________________________
| /
| | Mboard: B210
| | magic: 45568
| | eeprom_revision: v0.1
| | eeprom_compat: 1
| | product: MICROPHASE
| | name: ANT
| | serial: 0223D80FF0D767EBC6D3AAAA6793E64D
| | FPGA Version: 16.0
| |
| | Time sources: none, internal, external
| | Clock sources: internal, external
| | Sensors: ref_locked
| | _____________________________________________________
| | /
| | | RX DSP: 0
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | RX DSP: 1
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | RX Dboard: A
| | | _____________________________________________________
| | | /
| | | | RX Frontend: A
| | | | Name: FE-RX1
| | | | Antennas: TX/RX, RX2
| | | | Sensors: temp, rssi, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 76.0 step 1.0 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | RX Frontend: B
| | | | Name: FE-RX2
| | | | Antennas: TX/RX, RX2
| | | | Sensors: temp, rssi, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 76.0 step 1.0 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | RX Codec: A
| | | | Name: B210 RX dual ADC
| | | | Gain Elements: None
| | _____________________________________________________
| | /
| | | TX DSP: 0
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | TX DSP: 1
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | TX Dboard: A
| | | _____________________________________________________
| | | /
| | | | TX Frontend: A
| | | | Name: FE-TX1
| | | | Antennas: TX/RX
| | | | Sensors: temp, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 89.8 step 0.2 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | TX Frontend: B
| | | | Name: FE-TX2
| | | | Antennas: TX/RX
| | | | Sensors: temp, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 89.8 step 0.2 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | TX Codec: A
| | | | Name: B210 TX dual DAC
| | | | Gain Elements: None
最后,你可以用下面这段 Python 代码在 Python 终端或脚本里测试 Python API:
import uhd
usrp = uhd.usrp.MultiUSRP("addr=192.168.1.10")
samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50)
print(samples[0:10])
这段代码会在中心频率 100 MHz、采样率 1 MHz、增益 50 dB 的设置下接收 10,000 个样本。 它会打印前 10 个样本的 IQ 值,以确认一切正常。 后续步骤以及更多示例,请参见 Python 玩转 USRP 章节。
如果 import uhd 报出 ModuleNotFoundError,你可能需要把下面这一行加入你的 .bashrc 文件:
export PYTHONPATH="${PYTHONPATH}:/usr/local/lib/python3/dist-packages"
AntSDR E310¶
除了 E200 之外,MicroPhase 还推出了一款名为 AntSDR E310 的型号。 AntSDR E310 与 E200 非常相似,只不过它把第二路接收和第二路发射通道也通过前面板 SMA 接口引出,并且目前只支持 Pluto/IIO 模式(不支持 USRP 模式)。 它使用与 E200 相同的 FPGA。 另一个区别是,它多了一个 USB-C 接口,可以作为 USB OTG 接口使用(例如连接一个 U 盘)。 AntSDR E310 只能在 AliExpress 上购买(不像 E200 那样也会在 Crowd Supply 销售)。 在写作本文时,E310 的价格和 E200 差不多,因此如果你不打算使用 “USRP 模式”,并且更看重通过 SMA 暴露出来的额外通道,即便这意味着体积稍微更大一些,那么 E310 会是一个不错的选择。