在我们之前的所有 Python 示例中,我们将信号存储为“复杂浮点数”类型的 1D NumPy 数组。在本章中,我们将学习如何将信号存储到文件中,然后读回Python,并介绍SigMF标准。将信号数据存储在文件中非常有用;您可能希望将信号记录到文件中,以便手动离线分析,或与同事共享,或构建整个数据集。
二进制文件
回想一下,基带上的数字信号是复数序列。
示例:[0.123 + j0.512, 0.0312 + j0.4123, 0.1423 + j0.06512, …]
这些数字对应于 [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, …]
当我们想将复数保存到文件中时,我们以 IQIQIQIQIQIQIQIQIQI 格式保存它们。也就是说,我们连续存储一堆浮点数,当我们读回它们时,我们必须将它们分离回 [I+jQ, I+jQ, …]。
虽然可以将复数存储在文本文件或csv文件中,但我们更喜欢将它们保存在所谓的“二进制文件”中以节省空间。在高采样率下,您的信号记录很容易达到数GB,我们希望尽可能提高内存效率。如果您曾经在文本编辑器中打开过一个文件,并且它看起来像下面的屏幕截图一样难以理解,那么它可能是二进制的。二进制文件包含一系列字节,您必须自己跟踪格式。二进制文件是存储数据的最有效方式,假设已执行所有可能的压缩。因为我们的信号通常看起来像一个随机的浮点序列,所以我们通常不会尝试压缩数据。二进制文件用于许多其他事情,例如,编译的程序(称为“二进制文件”)。当用于保存信号时,我们称它们为二进制“IQ文件”,使用文件扩展名.iq。
在 Python 中,默认的复杂类型是 np.complex128,它为每个样本使用两个 64 位浮点数。但在DSP/SDR中,我们倾向于使用32位浮点数,因为SDR上的ADC没有那么高的精度来保证64位浮点数。在 Python 中,我们将使用 np.complex64,它使用两个 32 位浮点数。当你只是在Python中处理信号时,这并不重要,但是当你去将1d数组保存到文件中时,你要首先确保它是一个np.complex64的数组。
python实例
在 Python 中,特别是 numpy 中,我们使用 tofile() 函数将 numpy 数组存储到文件中。下面是一个简短的示例,用于创建简单的 BPSK 信号加噪声并将其保存到我们运行脚本的同一目录中的文件中:
import numpy as np
import matplotlib.pyplot as plt
num_symbols = 10000
x_symbols = np.random.randint(0, 2, num_symbols)*2-1 # -1 and 1's
n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN with unity power
r = x_symbols + n * np.sqrt(0.01) # noise power of 0.01
print(r)
plt.plot(np.real(r), np.imag(r), '.')
plt.grid(True)
plt.show()
# Now save to an IQ file
print(type(r[0])) # Check data type. Oops it's 128 not 64!
r = r.astype(np.complex64) # Convert to 64
print(type(r[0])) # Verify it's 64
r.tofile('bpsk_in_noise.iq') # Save to file
现在检查生成的文件的详细信息并检查它有多少字节。它应该是 num_symbols * 8,因为我们使用了 np.complex64,它是每个样本 8 个字节,每个浮点数 4 个字节(每个样本2个浮点数)。
使用新的 Python 脚本,我们可以使用 np.fromfile() 读取此文件,如下所示:
import numpy as np
import matplotlib.pyplot as plt
samples = np.fromfile('bpsk_in_noise.iq', np.complex64) # Read in file. We have to tell it what format it is
print(samples)
# Plot constellation to make sure it looks right
plt.plot(np.real(samples), np.imag(samples), '.')
plt.grid(True)
plt.show()
一个很大的错误是忘记告诉np.fromfile()文件格式。二进制文件不包含有关其格式的任何信息。默认情况下,np.fromfile() 假定它正在读取 float64 数组。
大多数其他语言都有读取二进制文件的方法,例如,在 MATLAB 中您可以使用 fread()。要直观地分析 RF 文件,请参阅以下部分。
如果你发现自己在处理 int16(又名短整数)或任何其他 numpy 没有复杂等价物的数据类型,你将被迫将样本视为真实样本,即使它们实际上是复杂的。诀窍是将它们读成真实,然后将它们交错回 IQIQIQ…格式化自己,下面显示了几种不同的方法:
samples = np.fromfile('iq_samples_as_int16.iq', np.int16).astype(np.float32).view(np.complex64)
or
samples = np.fromfile('iq_samples_as_int16.iq', np.int16)
samples /= 32768 # convert to -1 to +1 (optional)
samples = samples[::2] + 1j*samples[1::2] # convert to IQIQIQ...
直观地分析 RF 文件
尽管我们在频域章节中学会了如何创建自己的频谱图,但没有什么比使用已经创建的软件更好的了,并且在分析长RF记录时,我建议使用inspectrum。Inspectrum是一个相当简单但功能强大的图形工具,用于直观地扫描RF文件,并可以精细控制颜色图范围和FFT大小(缩放量)。您可以按住 alt 并使用滚轮在时间之间切换。它具有可选的光标来测量两次能量突发之间的增量时间,并能够将RF文件的一部分导出到新文件中。要在基于 Debian 的平台(如 Ubuntu)上安装,请使用以下命令:
sudo apt-get install qt5-default libfftw3-dev cmake pkg-config libliquid-dev
git clone https://github.com/miek/inspectrum.git
cd inspectrum
mkdir build
cd build
cmake ..
make
sudo make install
inspectrum
最大值和饱和度 (Max Values and Saturation)
从 SDR 接收样本时,了解最大样本值非常重要。许多 SDR 将使用最大值 1.0 和最小值 -1.0 将样本输出为浮点数。其他 SDR 会以整数形式为您提供样本,通常为 16 位,在这种情况下,最大值和最小值将为 +32767 和 -32768(除非另有说明),您可以选择除以 32768 以将它们转换为从 -1.0 到 1.0 的浮点数。了解SDR最大值的原因是由于饱和:当接收到非常响亮的信号时(或者如果增益设置得太高),接收器将“饱和”,并将高值截断为最大采样值。SDR 上的 ADC 位数有限。在制作 SDR 应用程序时,明智的做法是始终检查饱和度,当它发生时,您应该以某种方式指示它。
饱和的信号在时域中看起来不稳定,如下所示:
由于时域的突然变化,由于截断,频域可能看起来模糊不清。换句话说,频域将包括错误特征;由饱和引起的特征,实际上不是信号的一部分,这可能会在分析信号时让人们失望。
SigMF 和注释 IQ 文件
由于 IQ 文件本身没有任何与之关联的元数据,因此通常有一个包含信号信息的第二个文件,具有相同的文件名,但.txt或其他文件扩展名。这至少应包括用于收集信号的采样率,以及调谐SDR的频率。分析信号后,元数据文件可能包含有关有趣特征的样本范围的信息,例如能量爆发。样本索引只是一个整数,从 0 开始,并递增每个复杂样本。如果您知道从样本492342到528492有能量,那么您可以读取文件并拉出数组的那部分: samples[492342:528493] 。
幸运的是,现在有一个开放标准指定了用于描述信号记录的元数据格式,称为 SigMF .通过使用像SigMF这样的开放标准,多方可以更轻松地共享RF记录,并使用不同的工具对相同的数据集进行操作。它还可以防止RF数据集的“bitrot”,由于记录的细节未与记录本身并置,捕获的详细信息会随着时间的推移而丢失。
用 SigMF 标准描述您创建的二进制 IQ 文件的最简单(也是最少)的方法是将 .iq 文件重命名为 .sigmf-data,并创建一个名称相同但扩展名为 .sigmf-meta 的新文件,并确保元文件中的数据类型字段与数据文件的二进制格式匹配。这个元文件是一个充满 json 的纯文本文件,所以你可以简单地用文本编辑器打开它并手动填写(稍后我们将讨论以编程方式执行此操作)。下面是一个可以用作模板的示例 .sigmf-meta 文件:
{
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": 1000000,
"core:hw": "PlutoSDR with 915 MHz whip antenna",
"core:author": "Art Vandelay",
"core:version": "1.0.0"
},
"captures": [
{
"core:sample_start": 0,
"core:frequency": 915000000
}
],
"annotations": []
}
请注意, core:cf32_le 表示您的 .sigmf 数据类型为 IQIQIQIQ…使用32位浮点数,即我们之前使用的NP.complex64。参考其他可用数据类型的规范,例如,如果您有实数数据而不是复数数据,或者使用 16 位整数而不是浮点数以节省空间。
除了数据类型,要填写的最重要的行是 core:sample_rate 和 core:frequency 。最好在 core:description 中输入有关用于捕获录音的硬件( core:hw )的信息,例如SDR类型和天线,以及有关录音中信号的已知描述。 core:version 只是创建元数据文件时使用的 SigMF 标准版本。
如果您从 Python 中捕获射频记录,例如,将 Python API 用于 SDR,则可以避免使用 SigMF Python 包手动创建这些元数据文件。这可以安装在基于 Ubuntu/Debian 的操作系统上,如下所示:
cd ~
git clone https://github.com/gnuradio/SigMF.git
cd SigMF
sudo pip install .
为本章开头的示例编写 .sigmf-meta 文件的 Python 代码如下所示,我们在其中保存了 bpsk_in_noise.iq:
import numpy as np
import datetime as dt
from sigmf import SigMFFile
# <code from example>
# r.tofile('bpsk_in_noise.iq')
r.tofile('bpsk_in_noise.sigmf-data') # replace line above with this one
# create the metadata
meta = SigMFFile(
data_file='example.sigmf-data', # extension is optional
global_info = {
SigMFFile.DATATYPE_KEY: 'cf32_le',
SigMFFile.SAMPLE_RATE_KEY: 8000000,
SigMFFile.AUTHOR_KEY: 'Your name and/or email',
SigMFFile.DESCRIPTION_KEY: 'Simulation of BPSK with noise',
SigMFFile.VERSION_KEY: sigmf.__version__,
}
)
# create a capture key at time index 0
meta.add_capture(0, metadata={
SigMFFile.FREQUENCY_KEY: 915000000,
SigMFFile.DATETIME_KEY: dt.datetime.utcnow().isoformat()+'Z',
})
# check for mistakes and write to disk
assert meta.validate()
meta.tofile('bpsk_in_noise.sigmf-meta') # extension is optional
只需将 8000000 和 915000000 分别替换为用于存储采样率和中心频率的变量即可。
若要将 SigMF 记录读入 Python,请使用以下代码。在此示例中,两个 SigMF 文件应命名为 bpsk_in_noise.sigmf-meta 和 bpsk_in_noise.sigmf-data 。
from sigmf import SigMFFile, sigmffile
# Load a dataset
filename = 'bpsk_in_noise'
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
print(samples[0:10]) # lets look at the first 10 samples
# Get some metadata and all annotations
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
sample_count = signal.sample_count
signal_duration = sample_count / sample_rate
有关更多详细信息,请参阅 SigMF 文档 。
后面写有关于SigMF python版本详细的
对于那些读到这里的人来说,这是一个小小的奖励;SigMF徽标实际上存储为SigMF记录本身,当信号随时间绘制为星座(IQ图)时,它会产生以下动画:
用于读取徽标文件(位于此处)并生成上面的动画 gif 的 Python 代码如下所示,供好奇的人使用:
import numpy as np
import matplotlib.pyplot as plt
import imageio
from sigmf import SigMFFile, sigmffile
# Load a dataset
filename = 'sigmf_logo' # assume its in the same directory as this script
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
# Add zeros to the end so its clear when the animation repeats
samples = np.concatenate((samples, np.zeros(50000)))
sample_count = len(samples)
samples_per_frame = 5000
num_frames = int(sample_count/samples_per_frame)
filenames = []
for i in range(num_frames):
print("frame", i, "out of", num_frames)
# Plot the frame
fig, ax = plt.subplots(figsize=(5, 5))
samples_frame = samples[i*samples_per_frame:(i+1)*samples_per_frame]
ax.plot(np.real(samples_frame), np.imag(samples_frame), color="cyan", marker=".", linestyle="None", markersize=1)
ax.axis([-0.35,0.35,-0.35,0.35]) # keep axis constant
ax.set_facecolor('black') # background color
# Save the plot to a file
filename = '/tmp/sigmf_logo_' + str(i) + '.png'
fig.savefig(filename, bbox_inches='tight')
filenames.append(filename)
# Create animated gif
images = []
for filename in filenames:
images.append(imageio.imread(filename))
imageio.mimsave('/tmp/sigmf_logo.gif', images, fps=20)