
前阵子在Matlab里把一个基于AD9361的零中频收发链路完整仿真了一遍从QPSK基带成型、上变频、加噪声、下变频到解调判决全部跑通顺手还把AD9361最典型的IQ不平衡和直流偏置问题做了建模和补偿。过程中发现很多在硬件上难排查的问题其实在Matlab仿真里早就应该暴露出来。这篇文章把整个链路拆开讲清楚同时附上完整可运行的Matlab代码给正在做SDR、软件无线电或者AD9361相关项目的朋友作参考。这套仿真覆盖的是零中频架构的核心发射端把基带I/Q信号直接调制到射频载波接收端用正交本振下变频还原I/Q信号。理解了这条链路再去操作AD9361的寄存器、配置FIR滤波器、调AGC会顺畅很多。无论你是刚接触软件无线电的学生还是在调FMCOMMS板卡的老手只要想弄明白零中频收发信号的全过程这篇都值得看完。1. 零中频收发架构的核心原理与选型考量1.1 零中频架构到底是怎么工作的零中频Zero-IF也叫直接变频Direct Conversion核心思想很粗暴本振频率直接等于射频载波频率一次混频就把信号从射频搬到基带或者从基带搬到射频中间不经过中频。用我们平时收发快递来类比超外差是先送到城市中转站再送到家零中频是直接从机场一站到家门口少了中转环节。发射链路里基带信号经过成型滤波后分成I路和Q路两路分别和本振的cos分量、sin分量相乘后叠加得到射频信号。数学表达就是s_rf(t) I(t)cos(2pifct) - Q(t)sin(2pifct)接收链路正好反过来射频信号分别乘以cos和-sin再经过低通滤波就能分别还原出I路和Q路信号。关键在于I/Q两路必须严格正交本振频率也必须严格等于射频载波频率。一旦这两个条件被破坏镜像干扰和直流偏置就来了。1.2 AD9361为什么敢用零中频AD9361是ADI公司的射频敏捷收发器频段覆盖70MHz到6GHz通道带宽从200kHz到56MHz可调内部集成12位ADC/DAC、可编程滤波器、自动增益控制、正交校准等一大堆功能。这种软件无线电芯片的核心诉求是在不同频段、不同带宽、不同调制方式之间灵活切换。如果用超外差架构每一段频点都需要对应的中频滤波器和镜像抑制滤波器器件数量多、面积大、调试成本高SDR的灵活优势就没了。零中频把混频器、滤波器、增益控制尽可能集成到芯片内部对外只暴露I/Q数字接口。用户只需要配置寄存器就能在很宽的频率范围内收发信号。AD9361内部还集成了自动校准逻辑开机后会对I/Q不平衡、直流偏置、本振泄漏做一次校准这也是零中频架构能规模商用的重要原因。在Matlab仿真里我们可以把AD9361当成一个理想的零中频收发器来建模同时把它的非理想特性IQ不平衡、直流偏置用数学模型注入这样就能在PC上预先验证算法和参数再上板调试。1.3 零中频绕不开的三个坑零中频最大的优点是简单最大的麻烦也来自这种简单。硬件上最常见的三个问题在仿真里必须建模直流偏置DC Offset本振信号会通过混频器泄漏到射频端口再被反射回混频器和自己混频产生直流分量。这个直流分量直接叠加在基带信号上会严重影响解调。I/Q不平衡理想情况下I/Q两路幅度相同、相位严格相差90度。实际电路中两路的增益和相位不可能完全一致会形成镜像信号在频谱上表现为关于本振对称的干扰分量。闪烁噪声1/f噪声工作频率越低噪声越大。这对宽带信号影响有限但对窄带信号非常致命。AD9361内部有一整套校准机制处理这些问题比如利用估计算法计算I/Q失配参数在数字域纠正。Matlab仿真中我们就是用同样的思路把失配建模、补偿算法跑通这样上板后用芯片自带的校准也能理解到底校准了什么。2. 开发环境准备与Matlab仿真链路规划2.1 Matlab环境与工具箱准备这套仿真用到的Matlab功能都在通信和信号处理两个工具箱里分别对应Communications Toolbox和Signal Processing Toolbox。如果你后面要连接真实的AD9361板卡还需要安装官方支持包Communications Toolbox Support Package for AD9361。这个支持包提供了一系列系统对象可以配置AD9361的采样率、本振频率、增益、滤波器参数并在Matlab里直接收发数据。如果只是做这文章里的链路仿真不需要支持包也不需要一个真实的射频板卡。我这里用的是Matlab R2022b版本但其实R2018之后的版本跑这段代码都没问题核心函数就是rcosdesign、filter、upsample、awgn、xcorr这些都属于基础工具箱。安装建议如果你的Matlab是较老版本先确认三个模块——Communications Toolbox、Signal Processing Toolbox、DSP System Toolbox——是否完整。少了哪个运行时会直接报函数未定义那时候再去补装比较麻烦。2.2 仿真链路总体设计整条仿真链路按照AD9361的收发路径来搭分四段发射端随机比特生成 - QPSK符号映射 - 上采样 - RRC平方根升余弦成型滤波 - 零中频上变频得到射频信号。信道加高斯白噪声模拟实际无线信道中最基本的噪声。接收端射频信号乘cos和-sin做正交下变频 - 低通滤波滤除高频分量 - RRC匹配滤波 - 符号同步 - 采样判决。后处理计算误码率绘制星座图对比有无IQ不平衡补偿的差异。这里要特别说明为了在数字域模拟射频信号我在Matlab里把仿真采样率设置得比基带符号速率高很多。通常我们说的AD9361基带接口采样率是复数的I/Q采样率并不需要这么高。仿真中的高采样率本质上是在过采样地表示一路实射频信号避免上下变频后的高频分量与基带分量发生频谱混叠这是纯数字仿真的一种技巧。2.3 先把参数算清楚后面少踩坑整个链路有个参数计算逻辑。符号速率Rs设为1Msps每个符号的采样点数sps设为16那么仿真采样率fsRssps16MHz。这个采样率必须满足奈奎斯特条件同时还要容纳下变频后产生的2倍频分量不落进有用信号频带。我选的射频载波fc是3MHz基带成型信号带宽大概是Rs(1rolloff)1MHz(10.35)1.35MHz射频信号占用约2.325MHz到3.675MHz最高频率3.675MHz小于fs的一半8MHz满足采样条件。接收端下变频后基带信号回到零频附近2fc的分量则在6MHz左右距离基带足够远低通滤波器能干净地滤除。如果fc取得太高比如10MHz而fs只有16MHz虽然理论上数字仿真也能处理但2fc分量会与采样频率发生折叠混乱程度会显著增加。所以我建议仿照我的参数Rs取1MHzsps取16fc取3MHz整套代码跑起来很稳。3. 发射链路建模与Matlab实现3.1 基带信号生成与成型滤波发射端第一步是把随机比特流映射成QPSK符号。我把原始比特流拆成I路和Q路每路取1个比特映射成1或-1再乘1/sqrt(2)做功率归一化。这样生成的QPSK符号平均功率为1方便后续计算信噪比。成型滤波选用平方根升余弦滤波器滚降因子0.35跨度span取8个符号周期。为什么要做成型滤波因为QPSK这种恒包络调制在符号跳变时会产生很宽的频谱旁瓣不滤波会把邻道能量打得过界。RRC滤波器在发射端和接收端各放一个级联后等效为一个完整的升余弦滤波器可以做到无码间干扰。Matlab里用rcosdesign函数生成滤波器系数rrcFilter rcosdesign(0.35, 8, 16, sqrt);这个函数的第二个参数是符号跨度第三个参数是每符号采样数。生成的滤波器长度为8*161129个抽头群延迟正好是64个采样点。这个群延迟在后面符号同步时需要补偿。3.2 零中频上变频与IQ不平衡建模成型滤波后的复基带信号在理想零中频发射机里直接与复数本振相乘再取实部就得到了射频信号。这一步对应的是DAC输出的模拟信号与射频本振混频的过程。在仿真里加入IQ不平衡很简单我的做法是给Q路一个幅度失配系数alpha和相位失配phi。alpha取0.95代表Q路幅度比I路低5%phi取0.08rad约为4.6度相位误差。建模公式是Q_imb alpha * (-sin(phi) * I cos(phi) * Q);这样就构造出了一个不那么理想的发射前端。实际AD9361出厂校准后IQ不平衡通常好于0.1dB和0.1度但温度、增益档位变化时失配会变差所以建模时故意把失配调得明显一点方便观察它对星座图的影响。3.3 发射链路关键代码发射链路部分的核心代码如下% 参数定义 Rs 1e6; % 符号速率 1 Msps sps 16; % 每个符号采样点数 fs Rs * sps; % 仿真采样率 16 MHz fc 3e6; % 射频载波 3 MHz rolloff 0.35; % RRC滚降因子 span 8; % 滤波器符号跨度 Nbits 10000; % 发送比特数 % 生成随机比特和QPSK符号 bits randi([0 1], Nbits, 1); bitsI bits(1:2:end); bitsQ bits(2:2:end); sym_tx (2*bitsI-1)/sqrt(2) 1j*(2*bitsQ-1)/sqrt(2); % 成型滤波 rrcFilter rcosdesign(rolloff, span, sps, sqrt); sig_bb_tx filter(rrcFilter, 1, upsample(sym_tx, sps)); % 零中频上变频 N length(sig_bb_tx); t (0:N-1)/fs; sig_rf real(sig_bb_tx .* exp(1j*2*pi*fc*t)); % 注入发射端IQ不平衡可选 % alpha 0.95; phi 0.08; % sig_i real(sig_bb_tx); % sig_q alpha * (-sin(phi)*sig_i cos(phi)*imag(sig_bb_tx)); % sig_bb_tx_imb sig_i 1j*sig_q; % sig_rf real(sig_bb_tx_imb .* exp(1j*2*pi*fc*t));注意如果启用了IQ不平衡注入后面接收端补偿就需要对比实际发送的复基带信号。所以我在默认代码里没有打开这个开关而是把开关留在接收端做对比实验。4. 接收链路建模、补偿与解调4.1 零中频下变频与正交恢复接收端是整个链路里最容易出问题的地方。射频信号进入接收机后先乘以本振的cos分量和-sin分量分成I/Q两路再各经过一个低通滤波器把2fc附近的高频分量滤掉。数学上这是正交解调的标准过程差异在于零中频架构的本振频率严格等于载波频率所以下变频后信号直接落在基带不需要第二级中频处理。低通滤波器设计是关键。我用的低通是128阶FIR滤波器通带截止频率1.2MHz阻带起始频率2.5MHz。通带选1.2MHz是因为基带有效率带宽只有约0.675MHz留出足够余量又不会把6MHz附近的干扰放进通带。FIR滤波器的群延迟等于阶数的一半也就是64个采样点这个值在后面符号同步时要统一补偿。注意接收端这里没有用AD9361内部自动校准的模型而是把问题暴露出来再通过算法解决。这样才能理解为什么上板之后要等芯片校准完成再开始收数。4.2 IQ不平衡补偿原理与实现IQ不平衡补偿的核心思想是接收到的复信号z(t)包含真实信号s(t)和它的共轭镜像s(t)*的线性组合只要估计出共轭项的权重w就能通过减法消除镜像。具体估计方法基于统计二阶矩w E[z^2] / E[|z|^2]得到w后补偿信号为z_comp z - w * conj(z)。这个算法的效果是把幅度相位失配造成的镜像干扰压制到本底噪声水平。在AD9361的数字域校准中内部有类似机制的估计模块只是实现时用的是迭代收敛方式会实时跟踪温度变化引起的失配漂移。Matlab里用一段统计均值就能快速验证这个思路是否有效。在发射端没有注入IQ失配的情况下接收端即使不补偿星座图也基本正常一旦发射端或者接收端存在失配星座图会从方形变成平行四边形补偿后星座图立刻回归标准QPSK的四个点。4.3 接收链路关键代码接收链路核心代码如下% 信道加噪 snr_dB 15; sig_rx awgn(sig_rf, snr_dB, measured); % 正交下变频 rx_i sig_rx .* cos(2*pi*fc*t); rx_q -sig_rx .* sin(2*pi*fc*t); % 低通滤波器 lpfOrder 128; lpf fir1(lpfOrder, 1.2e6/(fs/2), low); rx_i_lp filter(lpf, 1, rx_i); rx_q_lp filter(lpf, 1, rx_q); rx_bb rx_i_lp 1j*rx_q_lp; % IQ不平衡补偿 % 如果没有失配该步骤对结果影响很小; 存在失配时效果显著 w_est mean(rx_bb .* rx_bb) / mean(rx_bb .* conj(rx_bb)); rx_comp rx_bb - w_est * conj(rx_bb); % 匹配滤波 rx_mf filter(rrcFilter, 1, rx_comp); % 符号同步与采样 delay_total 3 * (span * sps / 2); % 发射RRC 接收LPF 接收RRC start_idx delay_total 1; rx_sym rx_mf(start_idx : sps : start_idx length(sym_tx)*sps - 1); % QPSK判决 rx_bitsI real(rx_sym) 0; rx_bitsQ imag(rx_sym) 0; rx_bits(1:2:Nbits) rx_bitsI; rx_bits(2:2:Nbits) rx_bitsQ; % 误码率 ber sum(rx_bits. ~ bits) / Nbits;这里最容易被忽略的是delay_total的计算。发射RRC滤波器、接收低通滤波器、接收RRC滤波器都是有群延迟的三个延迟加起来之后信号的第一个有效符号位置不在第1个采样点而是在延迟量之后。我这里是线性相位FIR滤波器群延迟是滤波器长度减1再除以2。如果滤波器改成了IIR这一套固定延迟补偿就失效了必须改用互相关同步或者采用更复杂的定时恢复算法。5. 完整收发仿真与误码率分析5.1 完整可运行的仿真代码下面给一份完整可跑的脚本把发射、信道、接收、误码率统计全部串起来。为了观察IQ不平衡补偿效果我在接收端人为地给Q路注入了5%幅度失配和4.6度相位失配然后分别跑出无补偿和有补偿的误码率。% 零中频架构QPSK收发信号完整仿真 % 适用版本: R2018 clear; close all; clc; %% 参数定义 Rs 1e6; sps 16; fs Rs * sps; fc 3e6; rolloff 0.35; span 8; Nbits 20000; %% 发射端 bits randi([0 1], Nbits, 1); bitsI bits(1:2:end); bitsQ bits(2:2:end); sym_tx (2*bitsI-1)/sqrt(2) 1j*(2*bitsQ-1)/sqrt(2); rrcFilter rcosdesign(rolloff, span, sps, sqrt); sig_bb_tx filter(rrcFilter, 1, upsample(sym_tx, sps)); N length(sig_bb_tx); t (0:N-1)/fs; sig_rf real(sig_bb_tx .* exp(1j*2*pi*fc*t)); %% 接收端(含IQ不平衡注入) alpha 0.95; phi 0.08; snr_list 0:2:20; ber_no_comp zeros(size(snr_list)); ber_comp zeros(size(snr_list)); for k 1:length(snr_list) snr_dB snr_list(k); rng(k); % 保证噪声可复现 sig_rx awgn(sig_rf, snr_dB, measured); rx_i sig_rx .* cos(2*pi*fc*t); rx_q -sig_rx .* sin(2*pi*fc*t); lpfOrder 128; lpf fir1(lpfOrder, 1.2e6/(fs/2), low); rx_i_lp filter(lpf, 1, rx_i); rx_q_lp filter(lpf, 1, rx_q); % 注入IQ不平衡 rx_i_imb rx_i_lp; rx_q_imb alpha * (-sin(phi) * rx_i_lp cos(phi) * rx_q_lp); rx_bb_imb rx_i_imb 1j * rx_q_imb; % 不补偿直接匹配滤波 rx_mf0 filter(rrcFilter, 1, rx_bb_imb); delay_total 3 * (span * sps / 2); start_idx delay_total 1; rx_sym0 rx_mf0(start_idx : sps : start_idx length(sym_tx)*sps - 1); rx_bits0 reshape([real(rx_sym0) 0; imag(rx_sym0) 0], [], 1); ber_no_comp(k) mean(rx_bits0 ~ bits); % 补偿 w_est mean(rx_bb_imb .* rx_bb_imb) / mean(rx_bb_imb .* conj(rx_bb_imb)); rx_bb_comp rx_bb_imb - w_est * conj(rx_bb_imb); rx_mf1 filter(rrcFilter, 1, rx_bb_comp); rx_sym1 rx_mf1(start_idx : sps : start_idx length(sym_tx)*sps - 1); rx_bits1 reshape([real(rx_sym1) 0; imag(rx_sym1) 0], [], 1); ber_comp(k) mean(rx_bits1 ~ bits); end %% 结果画图 figure; semilogy(snr_list, ber_no_comp, ro-, LineWidth, 1.5); hold on; semilogy(snr_list, ber_comp, bo-, LineWidth, 1.5); grid on; box on; xlabel(SNR (dB)); ylabel(误码率 BER); legend(IQ不平衡,未补偿, IQ不平衡,已补偿, Location, southwest); title(零中频QPSK链路误码率对比); % 画16dB时的星座图 snr_dB 16; rng(5); sig_rx awgn(sig_rf, snr_dB, measured); rx_i sig_rx .* cos(2*pi*fc*t); rx_q -sig_rx .* sin(2*pi*fc*t); lpf fir1(128, 1.2e6/(fs/2), low); rx_i_lp filter(lpf, 1, rx_i); rx_q_lp filter(lpf, 1, rx_q); rx_bb_imb rx_i_lp 1j * (0.95 * (-sin(0.08) * rx_i_lp cos(0.08) * rx_q_lp)); w_est mean(rx_bb_imb .* rx_bb_imb) / mean(rx_bb_imb .* conj(rx_bb_imb)); rx_bb_comp rx_bb_imb - w_est * conj(rx_bb_imb); rx_sym_no filter(rrcFilter, 1, rx_bb_imb); rx_sym_yes filter(rrcFilter, 1, rx_bb_comp); start_idx 3 * (span * sps / 2) 1; sym_no rx_sym_no(start_idx : sps : start_idx length(sym_tx)*sps - 1); sym_yes rx_sym_yes(start_idx : sps : start_idx length(sym_tx)*sps - 1); figure; subplot(1,2,1); plot(real(sym_no), imag(sym_no), r.); axis equal; grid on; xlim([-1.5 1.5]); ylim([-1.5 1.5]); title(有IQ不平衡 未补偿); xlabel(I); ylabel(Q); subplot(1,2,2); plot(real(sym_yes), imag(sym_yes), b.); axis equal; grid on; xlim([-1.5 1.5]); ylim([-1.5 1.5]); title(有IQ不平衡 已补偿); xlabel(I); ylabel(Q);5.2 结果解读与星座图分析跑完这段仿真最直观的对比就是星座图。在有IQ不平衡没有补偿的情况下QPSK星座点不再是一个标准正方形而是明显变成一个平行四边形四个点的分布沿I方向和Q方向出现不规则拉伸补偿之后星座点回到规则的十字形分布判决余量增大误码率显著下降。从误码率曲线看在高信噪比区域未补偿的误码率会出现明显的底部平台也就是说噪声再小镜像干扰依然存在误码率降不下去。补偿之后这条曲线会继续往下走跟随AWGN理论曲线。这正是零中频架构在真实硬件中必须做IQ不平衡校准的原因。另外你会注意到在低信噪比区间补偿前后的误码率曲线差距不大。原因是噪声主导时镜像干扰被噪声淹没补偿算法对整体性能的改善不明显。这在实际硬件中也有表现当接收信号很弱时IQ不平衡的镜像天天都在但频谱上看不清信号增强后镜像才明显暴露出来。6. 从仿真到AD9361硬件常见问题与排查经验6.1 常见问题速查表在实际调AD9361板卡时很多问题的根源和仿真里的模型完全一致。我整理了一份速查表方便大家对照排查现象可能原因排查与解决解调后星座图中心有大团亮斑直流偏置未校准好检查芯片DC Offset Calibration是否使能重新触发校准星座图呈平行四边形BER有底噪平台I/Q不平衡开启内部IQ校准或在数字域做本文所述统计补偿信号强度变化后BER急剧恶化AGC切换导致增益失配固定增益模式等待重新校准频谱上关于本振对称的镜像峰IQ相位/幅度失配用单音信号配合FFT测量镜像抑制比再查校准值上电后一段时间性能下降温度漂移导致校准失效定期重触发校准看寄存器温度读数信号过强削顶失真接收增益过高检查RX PGA增益和AGC设定6.2 仿真与硬件联调的几条心得仿真代码跑通之后我把同样结构的算法搬到Ada里调试踩了几个值得记录的坑。第一仿真里的理想本振不存在。Matlab里本振就是精确的cos和sin序列相位噪声为零。AD9361本振的相位噪声会让星座点产生旋转模糊尤其在高载频、高调制阶数时非常明显。如果你仿真QPSK没问题上板换64QAM之后BER突然变差先别怪自己的解调算法查一下LO的相位噪声指标。第二符号同步在仿真里可以偷懒但硬件里不能。上面代码里我用固定延迟采样因为滤波器群延迟已知同步问题被绕过了。真实AD9361链路里信号经过模拟滤波、数字滤波、AGC后符号定时会漂移必须用Gardner定时恢复环或者课程式的Muecke同步器在FPGA/DSP里跟踪。这部分在仿真里也应该加进去只是本文为了聚焦零中频收发没有展开。第三Matlab仿真和AD9361实际接口的采样率需要匹配。AD9361基带接口输出的是复数IQ数据采样率可以配置为30.72MHz、23.04MHz等常见值。如果你的符号速率是1Msps基带采样率可能用4Msps或8Msps就够了不需要像我仿真里那样用16MHz的射频域采样率承载信号。仿真里的高采样率只是为了表现射频波形理解这一点就不会在配置芯片时犯糊涂。第四AD9361自带校准功能不是一劳永逸的。温度、通道增益档位、本振频率变化都会改变系统失配重配频率或切换通道后我一般会强制跑一次完整的DC Offset和IQ校准再开始收发。这在Matlab仿真里可以做对应的重校准操作也就相当于重新估计w_est并代入。我个人在实际项目里的习惯是先在Matlab里把整条链路跑通保存关键波形和参数然后用AD9361支持包把同样的信号灌进真实芯片对比仿真星座图和实测星座图差异。这个过程能帮你快速定位问题是算法层面的还是模拟前端引起的比在硬件上一头雾水地猜寄存器高效得多。最后再分享一个细节做星座图观察时尽量把接收端的增益设置到既不过载也不算太弱的位置不然AGC一直在跳星座图会呈现出缩放抖动特别容易误判为IQ失衡。把这些小问题处理干净后Matlab仿真代码就可以作为一个可靠的基线支撑你后续做载波频偏估计、定时恢复、高阶解调等各种扩展。