IQ 信号 FFT 频谱分析 实现原理详细步骤
IQ 数据说明
一、单路实信号表示(完全不用正交两路)
核心特点:仅使用一路实数采样,不区分 I/Q 两路信号
- 时域实采样(Real Sampling)
直接对中频 / 射频信号进行实数采样:s(t)=A(t)cos(ω0t+φ(t))
相位信息隐含在信号过零点中,直观可见的只有幅度包络。
典型应用:AM 检波、基础频谱分析、示波器观测。
- 幅度 / 包络(Envelope)
表达式:A(t)=I2+Q2
缺陷:完全丢失相位信息,无法用于 QAM、PSK 等调制方式的解调。
- 相位(Phase)
表达式:φ(t)=arctan2(Q,I)
典型应用:FM/PM 解调、相位同步。
- 功率 / 平方律检测
P(t)=I2+Q2与幅度仅相差一个平方关系,同样不携带独立相位信息。
信号链路与 IQ 正交基本概念
天线接收的原始射频(RF)为单路高频实信号,本身不存在正交结构。
混频降频后得到中频(IF),仍为单路实信号;经正交处理后成为中频正交信号(IF IQ)。
继续混频至 0Hz,得到零中频(Zero-IF),即常用的 IQ 正交基带数据。
本振区分:
余弦本振 → I 路
正弦本振 → Q 路
步骤 1:原始 IQ 数据读取(文件解析)
原理说明
IQ 信号在 txt 文件中以实数交替存储(I0, Q0, I1, Q1, …, In, Qn),需先读取文件并校验数据有效性,确保能正确拆分 I/Q 分量。
代码对应逻辑
import numpy as np
# 读取txt中所有实数数据
x = np.loadtxt(filename)
# 数据合法性校验
if x.size < 2:
raise ValueError("数据太短,至少需要2个数(I0,Q0)。")
if x.size % 2 != 0:
x = x[:-1] # 奇数长度时截断最后一个数,保证I/Q成对
工程考量
-
校验数据长度:避免因数据残缺导致后续 I/Q 拆分失败;
-
截断奇数长度数据:是「容错设计」,兼容实际采集时可能的末尾数据丢失问题。
步骤 2:I/Q 分量拆分与复数信号构造
原理说明
IQ 信号的本质是复信号(I 为实部,Q 为虚部),需从交替的实数序列中拆分 I、Q 通道,再组合为复数形式,才能正确表征射频信号的相位和幅度信息。
代码对应逻辑
# 步长2取所有I分量(第0、2、4...位)
i = x[0::2].astype(np.float64)
# 步长2取所有Q分量(第1、3、5...位)
q = x[1::2].astype(np.float64)
# 组合为复信号:实部=I,虚部=Q
iq = i + 1j * q
# 转为complex64(平衡精度与内存)
return iq.astype(np.complex64)
工程考量
-
先转float64再计算:避免低精度运算导致的误差;
-
最终转complex64:复数单精度足够满足射频信号分析需求,且内存占用仅为complex128的一半。
步骤 3:IQ 信号预处理(去直流分量)
原理说明
直流分量(信号均值)会导致频谱中零频处出现强峰值,掩盖有用信号,需通过「减去信号均值」消除直流分量。
代码对应逻辑
# 统一数据类型
y = np.asarray(iq, dtype=np.complex64)
if remove_dc:
# 去直流:减去复信号的均值(实部/虚部分别去直流)
y = y - np.mean(y)
原理补充
复信号均值计算:np.mean(y) = mean(Re(y)) + 1j*mean(Im(y)),减去均值后,实部(I)和虚部(Q)的直流分量均被消除。
步骤 4:窗函数选择与生成
原理说明
直接对非周期信号做 FFT 会产生「频谱泄露」(能量扩散到相邻频率),窗函数通过「加权信号两端」(使信号平滑过渡到 0)降低泄露,不同窗函数的泄露抑制 / 频率分辨率 trade-off 不同。
代码对应逻辑
def make_window(window, n):
# 兼容大小写输入(如"Hann"和"hann")
window = window.lower()
if window in ("hann", "hanning"):
return np.hanning(n) # 汉宁窗(默认,泄露抑制好)
if window == "hamming":
return np.hamming(n) # 汉明窗(主瓣更窄)
if window == "blackman":
return np.blackman(n) # 布莱克曼窗(泄露抑制最优,分辨率最低)
if window in ("rect", "rectangle"):
return np.ones(n) # 矩形窗(无加权,分辨率最高,泄露最严重)
工程考量
-
统一转为小写:提升接口易用性,避免因输入大小写导致的错误;
-
窗长度与信号长度一致:保证每个采样点都能被加权。
步骤 5:FFT 点数自适应确定
原理说明
FFT 的计算效率在点数为 2 的幂时最高(基 2-FFT 算法),需自动选择「≥信号长度且≥1024」的最小 2 的幂,兼顾计算效率和频谱分辨率。
代码对应逻辑
if nfft is None:
# 计算≥max(n,1024)的最小2的幂
nfft = 1 << int(np.ceil(np.log2(max(n, 1024))))
原理补充
-
np.log2(max(n,1024)):计算目标点数的对数; -
np.ceil():向上取整(保证点数≥目标值); -
1 << 整数:等价于2^整数,快速得到 2 的幂。
步骤 6:信号加窗处理
原理说明
将 IQ 复信号与窗函数逐点相乘,使信号两端平滑衰减,降低频谱泄露,加窗后信号的能量会被窗函数加权,后续幅值归一化需考虑窗函数的能量。
代码对应逻辑
# 生成窗函数
w = make_window(window, n).astype(np.float64)
# 复信号逐点乘窗函数(I/Q同时加权)
xw = (x * w).astype(np.complex64)
工程考量
-
窗函数转float64:保证加权运算的精度;
-
结果转complex64:维持数据类型一致性。
步骤 7:FFT 计算与频谱移位
原理说明
-
FFT 默认输出的频谱是「0 频→正频→负频」排列,需用fftshift将其移位为「负频→0 频→正频」,符合人对频率轴的直观认知;
-
复信号 FFT 的结果是复数,其模(绝对值)代表频率分量的幅值。
代码对应逻辑
# FFT+移位(中心为0频)
X = np.fft.fftshift(np.fft.fft(xw, n=nfft))
原理补充
-
np.fft.fft(xw, n=nfft):对加窗后的信号做 nfft 点 FFT(信号长度 <nfft 时自动补零,>nfft 时截断); -
fftshift:将 FFT 结果的左右半部分交换,使 0 频位于频谱中心。
步骤 8:幅值归一化与 dB 转换
原理说明
-
幅值归一化:将幅值缩放到 [0,1] 范围,方便不同信号的频谱对比;
-
dB 转换:将线性幅值转为对数刻度(dB),更符合人耳 / 仪器对信号强度的感知,公式:20×log10(A)(A 为幅值)。
代码对应逻辑
# 归一化到[0,1]
mag = np.abs(X) / (np.max(np.abs(X)) + 1e-12)
# 转为dB值
mag_db = 20.0 * np.log10(mag + 1e-12)
工程考量
-
加1e-12:避免除以 0(幅值全为 0 时)或对数计算中出现log10(0)(会得到 - inf);
-
归一化到最大值:消除信号绝对幅值的影响,聚焦频谱形状。
步骤 9:频率轴生成与结果格式化
原理说明
-
频率轴需与 FFT 结果一一对应,fftfreq根据采样率和 FFT 点数生成频率刻度,再用fftshift移位,保证与频谱中心对齐;
-
结果转为 JSON 字符串:方便跨平台 / 跨语言传输(如前端绘图、网络接口返回)。
代码对应逻辑
import json
# 生成频率轴:范围[-fs/2, fs/2),间隔fs/nfft
f = np.fft.fftshift(np.fft.fftfreq(nfft, d=1.0 / fs_hz))
# 格式化为JSON字符串
result = json.dumps({
"frequency_hz": f.tolist(), # numpy数组转列表(JSON不支持数组)
"magnitude_db": mag_db.tolist()
})
原理补充
-
fftfreq(nfft, d=1/fs_hz):生成原始频率轴,范围 [0, fs),间隔fs/nfft; -
fftshift后:频率轴变为 [-fs/2, fs/2),对应负频→0 频→正频的频谱排列。
总结
-
核心流程:文件读取→I/Q 拆分→预处理(去直流)→加窗→FFT 计算→频谱移位→幅值归一化(dB)→频率轴生成→结果格式化;
-
关键优化:自动选择 2 的幂作为 FFT 点数(提升效率)、加窗抑制频谱泄露、容错处理(数据长度校验、防除零 / 对数错误);
-
工程设计:统一数据类型(complex64)、兼容大小写窗函数输入、JSON 格式化结果(适配跨平台使用)。

1万+

被折叠的 条评论
为什么被折叠?



