DSP - 如何解析 IQ 文件
前言
本文主要介绍 SDR (软件定义无线电) 中非 [SigMF][https://github.com/sigmf/SigMF] 标准下的解析 IQ 文件的思路与方法。虽然 SigMF 是一个开放标准,可以方便大家使用不同的工具来操作相同的数据集,但是在企业应用场景中,由于各个厂家或合作方都可能有自己的一套标准格式,需要按照对方提供的数据格式协议进行解析,而你可能会见到各种各样的数据格式以及元数据标注方式,阅读本文可以学习到以下内容:
- 什么是 IQ 文件,它是如何存储的?
- 如何解析 IQ 文件以及大文件解析时的优化措施。
- IQ 文件元数据标注常见的几种方式以及解析方式。
- 解析得到了 IQ 数据后要做什么?
什么是 IQ 文件
在 SDR 领域,我们通常使用 IQ 采样进行信号的采集工作,并且在采样结束后会将采集的 IQ 数据保存成 IQ 文件。为了节省存储空间,我们会将 IQ 数据保存在二进制文件 (Binary File) 中,通常文件后缀名为 .iq ,由于二进制文件是按照约定好的一系列字节组织保存的,所以我们在解析二进制文件时,也需要按照约定好的格式进行解析。
如何解析 IQ 文件
我们采样后得到的 IQ 数据其实就是一串复数。
例如:[0.324 + j0.627, 0.0324 + j0.0627, 0.3324 + j0.1627, 0.1224 + j0.3627, ...]
以上数据就对应了 [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, ...]
在理想的情况下,我们会使用 IQIQIQIQIQIQ 这样的格式进行保存,也就是存成一系列浮点数,我们只需要按顺序和字节数读取并重新分离成 [I+jQ, I+jQ, ...]
假设我们是使用 np.complex64 类型进行的文件存储,则每个采样点占 8 字节(由 2 个 float32 组成,每个占 4 字节),在解析时我们可以直接使用 np.fromfile() 来读取:
import numpy as np
# 读取整个iq文件, 得到所有采样点
samples = np.fromfile('xxxx.iq', np.complex64)
在使用 np.fromfile() 进行解析时,不要忘记传入文件的数据类型,否则它会按照 float64 进行解析。如果 IQ 文件是使用 np.complex128 (由 2 个 float64 组成, 每个占 8 字节) 存储,我们则需要传入数据类型 np.complex128:
import numpy as np
# 读取整个iq文件, 得到所有采样点
samples = np.fromfile('xxxx.iq', np.complex128)
事实上由于每个合作方或厂家的接收机在保存 IQ 文件时,可能都会有自己的一套标准,接下来会介绍另一种情况:IQ 文件存储时不是按复数进行存储的,而是按实数对进行存储,那么即使它们真实意义是复数,也需要先以实数类型读取,再将它们交错重组回 IQIQIQ... 的形式。
举个例子,IQ 文件以 int16 短整型进行存储,有以下两种方式可以解析:
import numpy as np
# 读取整个iq文件, 得到所有采样点
samples = np.fromfile('xxxx.iq', np.int16).astype(np.float32).view(np.complex64)
或者
import numpy as np
# 读取整个iq文件, 得到所有采样点
samples = np.fromfile('xxxx.iq', np.int16)
samples = samples[::2] + 1j * samples[1::2] # 转换为 IQIQIQ 格式的复数
大文件分段解析
前面介绍的方法都是直接读取整个 IQ 文件的数据,但在高采样率下,你拿到的 IQ 文件可能轻松超过多个 GB,在处理解析这样的大文件时,我个人更推荐分段将数据取出,而不是一次性都拿出来放到内存中,而且通常情况下,我们对 IQ 文件进行解析后做的业务处理更多是针对频域上的,例如 FFT (快速傅里叶变换)、信号分选/检测和信号识别等等,这些都不需要全部的 IQ 数据。 下面介绍两种分段读取 IQ 文件数据的方法,以实数对类型存储的文件为例:
import numpy as np
def read_iq_file_in_chunks(filename, dtype=np.int16, chunk_size=2048 * 200):
sample_size = np.dtype(dtype).itemsize
read_len = chunk_size * sample_size * 2 # 两个实数为一个采样点 I, Q
with open(filename, 'rb') as f:
while True:
data = f.read(read_len)
if not data:
break
iq_data = np.frombuffer(data, dtype=np.int16)
yield iq_data
iq_file = 'xxxx.iq'
for iq_data in read_iq_file_in_chunks(iq_file):
print(iq_data) # 每段取出的 IQ 数据
或者
import numpy as np
def read_iq_in_fixed_batches(file_path, points_per_batch, start_idx, end_idx, dtype=np.int16):
sample_size = np.dtype(dtype).itemsize
start_byte = start_idx * sample_size
end_byte = end_idx * sample_size
with open(file_path, 'rb') as f:
f.seek(start_byte)
while f.tell() + points_per_batch * sample_size <= end_byte:
batch_data = np.fromfile(f, dtype=dtype, count=points_per_batch)
yield batch_data
iq_file = 'xxxx.iq'
start_idx = 100
stop_idx = 1e6
for iq_data in read_iq_in_fixed_batches(iq_file, 2048 * 40, start_idx, stop_idx):
print(iq_data) # 每段取出的 IQ 数据
第二种读取数据方式的区别在于,你可以控制你读取 IQ 数据的起始点和结束点,这样就可以实现 IQ 信号文件在时域上的截取!例如你想要从第 100 个采样点开始读取,一直读取到第 100 万个采样点,就只需要传入相应的 start_idx 与 stop_idx 即可。
常见的 IQ 文件元数据标注方式
通常情况下,我们光拿到 IQ 数据是不足以继续做后续业务处理的,至少还需要采样的相关信息,例如采样率、采样的中心频率、采样带宽等数据,而记录这些数据通常有以下三种方式:
- 单独用一个文件记录这些信息
以 SigMF 标准举例,它会将你的
.iq文件重命名为.sigmf-data,再创建一个拥有相同文件名但后缀名为.sigmf-meta的文件。该文件内存储着一个 JSON 格式的纯文本记录了我们需要的那些信息。
{
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": 1000000,
"core:hw": "PlutoSDR with 915 MHz whip antenna",
"core:author": "Art Vandelay",
"core:version": "1.2.0"
},
"captures": [
{
"core:sample_start": 0,
"core:frequency": 915000000
}
],
"annotations": []
}
我们只需要读取这个文件并按照 JSON 格式解析其中的文本,就可以得到相关信息。虽然并非所有文件都按照这个格式存储,但我们可以根据对应的数据类型进行读取。
2. 使用文件名称记录这些信息
也有一些 IQ 文件在保存时,会将这些信息记录在文件名称上,例如一个名为 1000000000_800000_1280000.iq 的文件,它就以下划线分隔记录了 中心频率_带宽_采样率这三个信息,我们可以通过 split() 方法解析文件名:
import os
file_path = '1000000000_800000_1280000.iq'
names = os.path.splitext(file_path)[0].split('_')
center_freq = float(names[0])
bandwidth = float(names[1])
sample_rate = float(names[2])
- 在 IQ 文件内部记录这些信息
有些 IQ 文件在前
n个字节中,存储了我们需要的这些信息,从n+1个字节开始才是真正的 IQ 数据,所以我们只需要按照存储时的数据格式一一将这些元数据解析出来,再将剩余的 IQ 数据读取解析出来即可。
得到 IQ 数据后可以做什么
在你成功解析 IQ 文件后,就可以进行后续的业务处理了,你可以把 IQ 数据通过 FFT 转换为频谱数据绘制出来,或者做一些信号分析相关的工作,例如信号分选/检测、信号截取等等。
总结
至此,我们对 IQ 文件的存储方式及解析流程进行了介绍。我们也详细介绍了在解析大文件时的优化措施,以及如何有效标注和解析 IQ 文件的元数据。这些知识为后续的数据分析和应用奠定了基础。
在实际应用中,处理 IQ 数据时常常需要根据具体需求进行调整和优化,因此灵活变通至关重要。下一篇文章中,我们将进一步探讨 IQ 数据的 FFT 转换以及频域上的截取操作。
尽管我在本文中尽可能地详细介绍了每一个步骤和细节,但是难免会存在一些错误和不足之处。如果您在使用本文中介绍的方法时发现了任何错误或者有更好的方法,非常欢迎您指正并提出建议,以便我能够不断改进和提升文章的质量。
我是Ricky,一个兴趣使然的开发者。非常感谢您阅读本文,希望本文对您有所帮助!