6、USRP【入门软件无线电(SDR)】PySDR:使用 Python 的 SDR 和 DSP 指南

因为设备不同,本教程未实测,仅作为USRP参考

在这里插入图片描述
在本章中,我们将学习如何使用UHD Python API通过USRP控制和接收/传输信号,USRP是由Ettus Research(现为NI的一部分)制造的一系列SDR。我们将讨论 Python 中 USRP 上的发送和接收,并深入探讨 USRP 特定的主题,包括流参数、子设备、通道、10 MHz 和 PPS 同步。

软件/驱动程序安装

虽然本教科书中提供的 Python 代码应该可以在 Windows、Mac 和 Linux 下工作,但我们只会提供特定于 Ubuntu 22 的驱动程序/API 安装说明(尽管下面的说明应该适用于大多数基于 Debian 的发行版)。我们将首先创建一个 Ubuntu 22 VirtualBox VM;如果操作系统已准备就绪,请随意跳过 VM 部分。或者,如果您使用的是Windows 11,则使用Ubuntu 22的Linux(WSL)的Windows子系统往往运行良好,并且支持开箱即用的图形。

设置 Ubuntu 22 VM

1、下载 Ubuntu 22.04 桌面版 .iso- https://ubuntu.com/download/desktop
2、安装并打开VirtualBox.
3、创建新的 VM。对于内存大小,我建议使用计算机 RAM 的 50%。
4、创建虚拟硬盘,选择 VDI,然后动态分配大小。15 GB 应该足够了。如果你想真正安全,你可以使用更多。
5、启动 VM。它会要求您提供安装介质。选择 Ubuntu 22 桌面.iso文件。选择“安装 ubuntu”,使用默认选项,弹出窗口将警告您即将进行的更改。点击继续。选择名称/密码,然后等待 VM 完成初始化。完成后,VM 将重新启动,但应在重新启动后关闭 VM 电源。
6、进入 VM 设置(齿轮图标)。
7、在“系统>处理器”下,>至少选择 3 个 CPU。如果您有实际的视频卡,那么在显示器>视频内存中>选择更高的视频卡。
8、启动 VM。
9、对于 USB 类型的 USRP,您需要安装 VM 来宾添加功能。在 VM 中,转到“设备>插入来宾添加 CD”>在弹出框时点击运行。按照说明操作。重新启动 VM,然后尝试将 USRP 转发到该 VM,假设它显示在“> USB 的设备”下的列表中。可以通过“双向共享剪贴板”>“共享剪贴板”>设备启用共享剪贴板。

安装 UHD 和 Python API

下面的终端命令应该构建并安装最新版本的UHD,包括Python API:

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

有关更多帮助,请参阅Ettus的官方构建和从源代码页面安装UHD(https://files.ettus.com/manual/page_build_guide.html)。请注意,还有一些安装驱动程序的方法不需要从源代码构建。

测试 UHD 驱动程序和 Python API

打开一个新终端并键入以下命令:

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

如果没有发生错误,您就可以开始了!

在 Python 中对 USRP 速度进行基准测试

如果您使用的是标准的源安装,以下命令应使用 Python API 对 USRP 的接收速率进行基准测试。如果使用 56e6 导致许多样本丢失或溢出,请尝试降低该数字。丢弃的样本不一定会毁掉任何东西,但这是测试使用 VM 或旧计算机可能带来的低效率的好方法。如果使用 B 2X0,一台具有 USB 3.0 端口正常运行的相当现代的计算机应该能够做到 56 MHz 而不会丢失样本,尤其是在num_recv_frames设置如此之高的情况下。

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

接收

使用内置的便利函数“recv_num_samps()”从USRP接收样本非常容易,下面是使用1 MHz采样率将USRP调谐到100 MHz的Python代码,并使用50 dB的接收增益从USRP中抓取10000个样本:

import uhd
usrp = uhd.usrp.MultiUSRP()
samples = usrp.recv_num_samps(10000, 100e6, 1e6, [0], 50) # units: N, Hz, Hz, list of channel IDs, dB
print(samples[0:10])

[0] 告诉 USRP 使用其第一个输入端口,并且只接收一个通道的样本(例如,对于同时在两个通道上接收的 B210,您可以使用 [0, 1])
如果您尝试以高速率接收但溢出(O 显示在您的控制台中),这里有一个提示。代替 usrp = uhd.usrp.MultiUSRP() ,请使用:

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

这使得接收缓冲区更大(默认值为 32),有助于减少溢出。缓冲区的实际大小(以字节为单位)取决于 USRP 和连接类型,但只需将 num_recv_frames 设置为远高于 32 的值往往会有所帮助。
对于更严肃的应用程序,我建议不要使用便利函数 recv_num_samps(),因为它隐藏了一些有趣的行为,并且每次调用都会发生一些设置,我们可能只想在开始时执行一次,例如,如果我们想无限期地接收样本。以下代码具有与 recv_num_samps() 相同的功能,实际上它几乎正是使用 便利函数时调用的功能,但现在我们可以选择修改行为:

import uhd
import numpy as np

usrp = uhd.usrp.MultiUSRP()

num_samps = 10000 # number of samples received
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)

# Set up the stream and receive buffer
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)

# Start Stream
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont)
stream_cmd.stream_now = True
streamer.issue_stream_cmd(stream_cmd)

# Receive Samples
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]

# Stop Stream
stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)
streamer.issue_stream_cmd(stream_cmd)

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

将num_samps设置为 10,000 且recv_buffer设置为 1000 时,for 循环将运行 10 次,即对 streamer.recv 的调用将有 10 次。请注意,我们将recv_buffer硬编码为 1000,但您可以使用 streamer.get_max_num_samps() 找到允许的最大值,通常约为 3000 左右。另请注意,recv_buffer必须是 2d,因为一次接收多个通道时使用相同的 API,但在我们的例子中,我们只接收了一个通道,所以 recv_buffer[0] 给了我们想要的 1D 样本数组。您现在不需要太了解流的开始/停止方式,但要知道除了“连续”模式之外还有其他选项,例如接收特定数量的样本并让流自动停止。 尽管我们在此示例代码中不处理元数据,但它包含发生的任何错误,如果需要,您可以通过查看循环每次迭代的metadata.error_code来检查这些错误(由于 UHD,错误往往也会显示在控制台本身中,因此不必觉得必须在 Python 代码中检查它们)。

接收增益(Receive Gain)

以下列表显示了不同USRP的增益范围,它们都从0 dB到下面指定的数字。请注意,这不是 dBm,它本质上是 dBm 与一些未知偏移相结合,因为这些不是校准设备。

  • 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

您也可以在终端中使用命令 uhd_usrp_probe ,在RX前端部分中,它将提及增益范围。
指定增益时,您可以使用普通的 set_rx_gain() 函数,该函数以 dB 为单位获取增益值,但您也可以使用 set_normalized_rx_gain() 函数,该函数接收 0 到 1 之间的值,并自动将其转换为您正在使用的 USRP 范围。这在制作支持不同型号 USRP 的应用程序时很方便。使用归一化增益的缺点是不再以dB为单位的单位,因此,例如,如果要将增益增加10 dB,则现在必须计算该量。

自动增益控制

包括B200和E310系列在内的一些USRP支持自动增益控制(AGC),它将根据接收信号电平自动调整接收增益,以最好地“填充”ADC的位。可以使用以下命令打开 AGC:

usrp.set_rx_agc(True, 0) # 0 for channel 0, i.e. the first channel of the USRP

如果您的 USRP 未实现 AGC,则在运行上述行时将引发异常。打开 AGC 时,设置增益不会执行任何操作。

流参数

在上面的完整示例中,您将看到行 st_args = uhd.usrp.StreamArgs(“fc32”, “sc16”) 。第一个参数是 CPU 数据格式,这是样本在主机上的数据类型。使用 Python API 时,UHD 支持以下 CPU 数据类型:在这里插入图片描述
您可能会在 UHD C++ API 的文档中看到其他选项,但至少在撰写本文时,这些选项从未在 Python API 中实现。
第二个参数是“在线”数据格式,即样本通过USB/以太网/SFP发送到主机时的数据类型。对于 Python API,选项包括:“sc16”、“sc12”和“sc8”,其中 12 位选项仅受某些 USRP 支持。这种选择很重要,因为 USRP 和主机之间的连接通常是瓶颈,因此通过从 16 位切换到 8 位,您可能会获得更高的速率。还要记住,许多USRP的ADC限制为12位或14位,使用“sc16”并不意味着ADC是16位。
有关 st_args 的通道部分,请参阅下面的子设备和通道小节。

传输

与 recv_num_samps() 便利功能类似,UHD 提供 send_waveform() 功能来传输一批样本,示例如下所示。如果您指定的持续时间(以秒为单位)比提供的信号长,它将简单地重复它。它有助于将样本的值保持在 -1.0 和 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) # create random signal
duration = 10 # seconds
center_freq = 915e6
sample_rate = 1e6
gain = 20 # [dB] start low then work your way up
usrp.send_waveform(samples, duration, center_freq, sample_rate, [0], gain)

有关此便利功能如何在后台工作的详细信息,请参阅源代码(https://github.com/EttusResearch/uhd/blob/master/host/python/uhd/usrp/multi_usrp.py) .

发送增益(Transmit Gain)

与接收端类似,发射增益范围因USRP型号而异,从0 dB到以下指定值:

  • 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

如果您想使用范围 0 到 1 指定发射增益,还有一个 set_normalized_tx_gain() 函数。

同时发送和接收

如果要同时使用相同的 USRP 进行发送和接收,关键是在同一进程中使用多个线程进行传输和接收;USRP 不能跨越多个进程。例如,在txrx_loopback_to_file C++示例中,创建了一个单独的线程来运行发送器,接收在主线程中完成。你也可以只生成两个线程,一个用于传输,一个用于接收,就像在benchmark_rate Python 示例中所做的那样。这里没有显示一个完整的例子,仅仅是因为它将是一个相当长的例子,而Ettus的benchmark_rate.py总是可以作为某人的起点。

子设备、信道和天线

使用 USRP 时,一个常见的混淆来源是如何选择正确的子设备和通道 ID。您可能已经注意到,在上面的每个示例中,我们使用了通道 0,并且没有指定与 subdev 相关的任何内容。如果您使用的是 B210,并且只想使用 RF:B 而不是 RF:A,您只需选择通道 1 而不是 0。但是在像 X310 这样有两个子板插槽的 USRP 上,您必须告诉 UHD 是要使用插槽 A 还是 B,以及该子板上的哪个通道,例如:

usrp.set_rx_subdev_spec("B:0")

如果要使用 TX/RX 端口而不是 RX2(默认),只需:

usrp.set_rx_antenna('TX/RX', 0) # set channel 0 to 'TX/RX'

它基本上只控制 USRP 上的射频开关,以从另一个 SMA 连接器路由。
要同时在两个通道上接收或发送,请提供一个列表,而不是使用 st_args.channels = [0] ,例如 [0,1] 。在这种情况下,接收样本缓冲区的大小必须为 (2, N),而不是 (1,N)。请记住,对于大多数USRP,两个通道共享一个LO,因此您无法同时调谐到不同的频率。

同步到 10 MHz 和 PPS

与其他 SDR 相比,使用 USRP 的巨大优势之一是它们能够同步到外部源或板载 GPSDO 。如果您已将外部 10 MHz 和 PPS 源连接到 USRP,则需要确保在初始化 USRP 后呼叫以下两条线路:

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

如果您使用的是板载 GPSDO,您将改为使用:

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

在频率同步方面,没有太多其他事情要做;USRP混频器中使用的LO现在将绑定到外部源或GPSDO。但在计时方面,例如,您可能希望命令USRP在PPS上准确开始采样。这可以通过以下代码完成:

# copy the receive example above, everything up until # Start Stream

# Wait for 1 PPS to happen, then set the time at next PPS to 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) # keep waiting till it happens- if this while loop never finishes then the PPS signal isn't there
usrp.set_time_next_pps(uhd.libpyuhd.types.time_spec(0.0))

# Schedule Rx of num_samps samples exactly 3 seconds from last 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) # set start time (try tweaking this)
streamer.issue_stream_cmd(stream_cmd)

# Receive Samples.  recv() will return zeros, then our samples, then more zeros, letting us know it's done
waiting_to_start = True # keep track of where we are in the cycle (see above comment)
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

如果看起来它不起作用,但没有抛出任何错误,请尝试将该 3.0 数字从 1.0 到 5.0 之间的任何值更改。您也可以在调用 recv() 后检查元数据,只需检查 if metadata.error_code != uhd.types.RXMetadataErrorCode.none: 即可。
为了进行调试,您可以通过检查 usrp.get_mboard_sensor(“ref_locked”, 0) 的返回来验证 10 MHz 信号是否显示到 USRP。如果 PPS 信号没有显示,您就会知道它,因为上面代码中的第一个 while 循环永远不会完成。

猜你喜欢

转载自blog.csdn.net/yuuuuuuuk/article/details/129486924