简介:面向无线通信课程与软件无线电入门实验的MATLAB工程包,基于ADALM-PLUTO硬件平台实现了64QAM正交幅度调制的完整收发链路,覆盖调制映射、解调判决、数据包生成与无线传输等关键步骤,适合通信专业学生、研究人员以及刚开始接触软件无线电开发的工程师参考学习。资源包共9个文件,以7个MATLAB脚本为主,分别对应调制、解调、数据生成、传输等核心功能模块,便于按需调用和二次修改;另含数据包示例文本与README说明文档,可快速理解目录结构和运行方式。整个压缩包仅13KB,体量极小,适合直接打开源码逐行阅读。目前已有159人学习,是理解QAM与软件无线电结合实现思路的实用入门材料。通过阅读源码,读者能掌握ADALM-PLUTO与MATLAB的联合使用方法,学会在真实射频硬件上观察信号星座图、分析信道噪声影响并评估误码率;配合文档还能梳理从数据生成到空口传输的完整信号流,为后续扩展更高阶调制方式或自定义通信协议提供了可复用的代码基础。 前阵子我一直在折腾一个念头:能不能完全不碰底层寄存器,直接靠MATLAB把ADALM-PLUTO这块SDR“主动学习模块”驱动起来,稳定跑通64QAM这种高阶QAM调制。这个念头源于一个很现实的场景——很多通信专业课都要求用软件无线电做调制实验,但学生一上来就被驱动、固件、C语言API劝退。ADALM-PLUTO恰恰是面向教学场景设计的,ADI给它的定位就是“Active Learning Module”,翻译过来叫主动学习模块。它和USRP这类偏科研的设备比,最大优势是便宜、便携,官方支持包直接对接MATLAB和Simulink;和HackRF这种纯DIY风格的SDR比,它又有正规的射频收发链路和校准机制。
PLUTO核心板上的AD9363射频收发器覆盖325MHz到3.8GHz频段,瞬时带宽从200kHz到20MHz可调,发射功率能做到几dBm到十几dBm,接收灵敏度也足够应对教室和实验室内的短距离通信。对于大多数调制解调实验来说,这个硬件底子完全够用了。它的原理其实一句话就能讲清楚:射频前端负责把高频率信号搬移成基带I/Q两路复数信号,数字部分由FPGA完成滤波、抽取和内插,最后通过USB把复数基带数据送到MATLAB里处理。我们在MATLAB里看到的不是一个简单的“频率+幅度”的窄带正弦波,而是一长串复数采样点,所有调制、解调、同步、均衡操作都以这些采样点为基础展开。
标题里那个“_64”其实是我的工程目录后缀,意思是“这个版本跑到64QAM”。之所以把64QAM当目标,是因为它比QPSK有分量得多。QPSK每两个比特映射成一个符号,信噪比超过大概14dB时星座点就能分得很开;而64QAM每六个比特才映射成一个符号,星座点被压缩在很小的格子内,接收端想要分辨相邻星座点,对信噪比、频偏、相位噪声、定时同步的要求全部上了一个台阶。换句话说,把64QAM跑通,基本等于验证了整条数字通信链路里所有关键环节都做对了。
站在教学和验证角度,这个项目还有一个很实际的价值:它让人直观看到“高阶调制不是白给的”。64QAM的带宽效率比QPSK高三倍,意味着同样的频谱能传三倍的数据,但代价是对链路预算极其敏感。后面我会展开讲参数怎么算、为什么这个地方要多留裕量,以及实测中那些让人抓狂的星座图“旋转”和“炸开”到底是怎么发生的。
1. 项目概述:从需求拆解到方案可行性
1.1 这个项目要解决的核心问题
关于ADALM-PLUTO,很多人第一反应是“这不就是一块带天线的开发板吗”。听完第一课之后他们会发现,这块板子几乎把无线通信实验需要的东西都装全了。真正让初学者头大的不是硬件本身,而是“我有了这个硬件,然后呢”。项目要解决的核心问题其实只有一个:如何基于MATLAB提供的现成API和官方支持包,快速搭建一条可看的、可测的、可调的QAM无线通信链路,而不是从零开始写FPGA逻辑或者写C驱动。标题里的“基于一些API和现有功能”这句话非常准确,因为整个项目没有自研任何底层射频驱动,所有寄存器配置、采样率切换、增益控制、缓冲管理都靠支持包在后台完成。
这也带出了第二个问题:64QAM到底难在哪。QAM调制阶数越往上走,星座点之间的欧氏距离越小,同样的噪声和频偏下误码率会急剧上升。64QAM是16QAM之外一个很典型的“压力测试点”,它要求接收端不仅有最基本的匹配滤波,还必须有可靠的定时同步、载波频偏估计、相位补偿和合适的AGC策略。把这个链路调通的过程,几乎把本科通信原理课程里所有核心知识点都串了一遍。
1.2 为什么选择ADALM-PLUTO这套SDR硬件
选型方面也值得多说两句。市面上能玩QAM的SDR平台不少,USRP、HackRF、LimeSDR、RTL-SDR等都行,但ADALM-PLUTO在“教学验证”这个场景下有不可替代的优势。第一是价格,千元级价位对学生群体非常友好;第二是软件生态,MATLAB官方就有支持包,装完以后可以直接用sdrtx和sdrrx两个对象完成收发,不用自己去查第三方驱动;第三是体积,一个U盘大小的板子放在桌面上,信号路径短,电磁环境简单,做短距离实验干扰少;第四是收发一体,RTL-SDR只能收不能发,HackRF虽能收发但射频性能要糙一些,PLUTO的AD9363收发器在200kHz到20MHz带宽内提供了比较干净的基带链路,完全够64QAM这种速率不高的实验使用。
顺带说一句,PLUTO的固件是小Linux系统,支持包把它封装成了类似网口设备的形态。实际用起来不用关心这些底层细节,只要保证USB连接稳定、供电足够就行。很多接收端问题其实不是算法问题,而是USB供电不足导致的采样丢失,后面排查章节会再提到。
2. 系统设计:调制方式、链路参数和API选型
2.1 不同调制阶数的选择逻辑
先看一组我自己整理的数据,来自工程实践而不是教科书理想值。在真实SDR链路上,要保证误码率在1e-6量级,QPSK大概需要14dB左右的符号信噪比,16QAM大概需要20到21dB,64QAM则要干到27dB甚至更高。这里的信噪比指的是解调前的Es/N0,不是系统等效信噪比。差距在哪?64QAM的星座点间距比QPSK小得多,稍微有点噪声、相位抖动或者射频非线性失真,相邻点就会串到一起,判决器只能靠猜。
| 调制方式 | 每符号比特 | 理想SNR(1e-6级) | 带宽效率 | 系统复杂度 |
|---|---|---|---|---|
| QPSK | 2 | 约14dB | 2 bit/s/Hz | 低 |
| 16QAM | 4 | 约20dB | 4 bit/s/Hz | 中 |
| 64QAM | 6 | 约27dB | 6 bit/s/Hz | 高 |
这也是为什么很多SDR入门教程都停留在QPSK。用PLUTO跑QPSK,随便设一个中等的发射增益和接收增益,星座图就很清晰,根本不用管载波同步之类细节。但切到64QAM,增益设不对,星座图会整个糊成一团;频偏没补偿,星座点就像风车一样旋转;定时同步没做好,符号判决点就会落在两个符号的交界处,看着画出来是一个点,实际全是乱码。所以64QAM是一个很有代表性的“压力测试方案”,它逼着你把整条链路每个环节都做到位。
2.2 链路参数的计算过程
这个项目里我最终采用的参数如下:
- 中心频率:2.45GHz(ISM频段)
- 基带采样率fs:2MHz
- 符号率Rs:500k符号/秒
- 每个符号采样数sps:4
- 成型滤波器:根升余弦(root raised cosine),滚降系数alpha=0.35
这些参数不是我随手拍的。先说sps=4的原因。如果sps太小,比如等于1,那每个符号只有一个采样点,接收端几乎没有余量做定时恢复;如果sps太大,比如16,数据量会大幅增加,MATLAB实时模式处理不过来,USB传输也容易掉包。在2MHz采样率下取sps=4,符号率就是2M/4=500kSymbols/s,对应二进制数据率500k乘6bit等于3Mbps,对演示和调试来说信息量足够,同时计算压力也小。
再看脉冲成型的带宽。RRC信号占用的双边带宽约等于(1+alpha)乘以符号率,算下来是(1+0.35)乘500k等于675kHz,这个值远小于2MHz的基带采样率,既满足Nyquist采样定理,又给实际硬件留了过渡带。这里多说一句,为什么用根升余弦而不是普通升余弦。因为发射端和接收端各放一个根升余弦滤波器,级联后才等于一个完整的升余弦滤波器,能做到符号间干扰为零。而普通升余弦如果只在发射端用一次,接收端没有任何匹配滤波,抗噪性能会明显下降。这个细节教材里很不起眼,但实测差别很大。
2.3 “基于API和现有功能”具体用到了哪些
这个项目的核心能力来自两块现成的东西。第一是MATLAB Communications Toolbox提供的基带算法函数,比如qammod、qamdemod、rcosdesign、comm.SymbolSynchronizer、comm.CarrierSynchronizer;第二是ADALM-PLUTO Support Package提供的硬件控制对象sdrtx和sdrrx。我没有写任何C语言,没有直接操作libiio,也没有碰AD9363的寄存器,所有寄存器初始化、校准、收发切换都由支持包在后台完成。我们要做的只是像操作普通对象一样设置RadioID、CenterFrequency、BasebandSampleRate、Gain这几个属性,然后调用transmitRepeat或者直接执行对象,底层API就会把复数基带波形通过USB灌到PLUTO的DAC里。
这种做法对教学的帮助很大,学生可以把全部注意力放在通信系统本身,而不是被硬件驱动拉走。当然它也有代价,最明显的是实时性和灵活性不如直接写C,但对我们这种几百千符号率的QAM实验来说完全够用。实际写代码时,整条链路的核心处理函数不超过十几个,剩下大部分时间其实花在调参和看星座图形态上。
3. 用MATLAB实现64QAM收发的完整过程
3.1 环境准备:支持包安装和硬件识别
开始写代码前先把环境弄干净。硬件连接路径是:把PLUTO用USB线插到电脑上,设备管理器里能看到一个串口和一个网络适配器。MATLAB这边需要先安装Communications Toolbox和ADALM-PLUTO Support Package。安装方法是在MATLAB的Add-On Explorer里搜索“ADALM-PLUTO Support Package from Analog Devices”,按提示安装。这里要提醒一句,MATLAB版本不能太老,我用的R2022a以上,早期版本对PLUTO的支持稳定性差一些,驱动版本对不上会一直报找不到设备。
装完以后先用下面这段代码验证设备识别:
rx = sdrrx('Pluto', 'RadioID', 'usb:0');
disp(rx)
如果一切正常,命令行会返回一个sdrrx对象属性表,能看到CenterFrequency、BasebandSampleRate这些参数的默认值。如果报错,基本就是两种情况:一是驱动没装好,去Add-On Explorer重新装驱动;二是RadioID不对,可以在命令行输入sdrinfo查看当前设备列表,确认是usb:0还是usb:1。这种问题看似低级,但确实会卡住不少第一次用SDR的同学。
3.2 发射端:64QAM映射与波形生成
发射端代码我按模块拆开来说。先生成随机数据,这里用整数索引更直白:
M = 64; % 64QAM
k = log2(M); % 每符号6bit
numSymbols = 8192;
data = randi([0 M-1], numSymbols, 1); % 0~63的符号索引
modSig = qammod(data, M, 'gray', 'InputType', 'integer');
qammod的gray参数很关键,它让相邻星座点只有1个比特不同。这样即使判决错误,大概率只错一个bit而不是错多个bit。Gray映射在64QAM这种高阶调制里几乎是必选项,否则一个符号判决错误可能导致三个甚至更多bit同时翻转。接下来做脉冲成型:
sps = 4;
alpha = 0.35;
rrcFilter = rcosdesign(alpha, 8, sps, 'sqrt');
txWaveform = upfirdn(modSig, rrcFilter, sps, 1);
upfirdn的第二个参数是滤波器,第三个参数sps是上采样倍数,也就是每个符号插值的点数。8是滤波器跨符号的长度,这个值越大滤波器越接近理论特性,但首尾损耗也越大。生成出来的txWaveform长度会变成numSymbols乘sps再加滤波器尾巴。这个尾巴是滤波器延迟,不代表有效数据。发射之前还要加前导序列,接收端才能定位数据包的起点:
preamble = randi([0 M-1], 512, 1);
preambleSym = qammod(preamble, M, 'gray', 'InputType', 'integer');
txWaveform = [preambleSym; modSig];
txWaveform = upfirdn(txWaveform, rrcFilter, sps, 1);
前导序列最好用一个固定的、接收端预先知道的符号序列,在实际工程里还会选Zadoff-Chu这类相关性更好的序列,我这里为了演示用了随机数,真正的项目里请替换成固定PN序列。发射波形最后还要归一化,否则幅值过大会把DAC推入非线性区,幅值过小又浪费发射功率:
txWaveform = txWaveform / max(abs(txWaveform)) * 0.8;
这个0.8的缩放比例是我常用的安全值,能留一点余量。真正满格发射虽然更占优,但AD9363这种小射频前端在接近满功率时PA非线性会变明显,64QAM外圈星座点本来就密,非线性一压就彻底分不清了。然后创建发射对象并循环发送:
tx = sdrtx('Pluto', ...
'RadioID', 'usb:0', ...
'CenterFrequency', 2.45e9, ...
'BasebandSampleRate', 2e6, ...
'Gain', -10);
while true
tx.transmitRepeat(txWaveform);
pause(0.2);
end
transmitRepeat的作用是让硬件不断重复发送这段波形,相当于一个无线信号源。调试阶段这个模式特别方便,因为不需要双方约定“什么时候开始发”,接收端随时抓数都能抓到有效数据。等系统调通后,再考虑改成单次发送。
3.3 接收端:捕获、匹配滤波和符号同步
接收端对象的创建方式和发射端几乎一样,但增益模式我建议先关掉自动增益,手动设一个接收增益,比如50dB,这样测星座图时结果更可控:
rx = sdrrx('Pluto', ...
'RadioID', 'usb:0', ...
'CenterFrequency', 2.45e9, ...
'BasebandSampleRate', 2e6, ...
'OutputDataType', 'double', ...
'GainSource', 'Manual', ...
'Gain', 50);
rxData = rx();
每次调用rx()会返回一帧基带复数数据,帧长度取决于缓冲区配置。拿到原始数据后,第一步是匹配滤波,也就是把接收信号和发射端同一个根升余弦滤波器做一次卷积。发射端做过一次成型,接收端再做一次匹配滤波后,两个根升余弦级联成升余弦,理论上符号间干扰降到最低:
rxFiltered = filter(rrcFilter, 1, rxData);
接下来是定时同步。PLUTO的DAC和ADC并不是共用一个时钟,发射端2MHz采样和接收端2MHz采样之间存在微小频率差,这个差值会导致符号最佳判决点逐渐偏移。MATLAB的通信工具箱提供了comm.SymbolSynchronizer,直接设置sps和调制阶数即可:
symbolSync = comm.SymbolSynchronizer(...
'TimingErrorDetector', 'Gardner', ...
'SamplesPerSymbol', sps, ...
'LoopBandwidth', 0.01);
rxSymbols = symbolSync(rxFiltered);
Gardner定时误差检测器不需要训练序列,对QAM系统尤其合适。LoopBandwidth这里不能设太大,太大虽然跟踪快但噪声也大,星座点会发花;太小又跟不上频偏变化。0.01是我反复试出来的折中值。
3.4 载波同步:64QAM最容易翻车的地方
定时同步做完后,接收到的符号序列在星座图上往往还是旋转的,原因是发射本振和接收本振之间有几kHz甚至几十kHz的载波频偏。QPSK对这种低频偏不敏感,但64QAM不行,频偏会让星座点绕着原点旋转,每转完一个完整周期,符号就彻底错乱。处理办法是使用comm.CarrierSynchronizer自动估计频率和相位偏移并补偿:
carrierSync = comm.CarrierSynchronizer(...
'Modulation', 'QAM', ...
'ModulationOrder', 64, ...
'SamplesPerSymbol', 1, ...
'LoopBandwidth', 0.01);
compensated = carrierSync(rxSymbols);
这里SamplesPerSymbol必须设成1,因为定时同步之后已经是符号速率了。经过这一处理,星座图会从旋转状态稳定下来,但可能整体存在一个90度或其他固定相位旋转。原因是QAM星座存在对称性,载波同步器只能消除旋转,不能确定正确初始相位。要解决这个模糊问题,得依靠前导序列做相位解模糊:
% 取前导部分
rxPreamble = compensated(1:length(preambleSym));
% 用最小二乘估计相位差
phaseDiff = angle(mean(rxPreamble ./ preambleSym));
corrected = compensated .* exp(-1i * phaseDiff);
这样星座图就被对正了,最后用qamdemod解调即可。实际项目里我会把前导、定时同步、载波同步、相位校正全部封装成一个函数,输入原始rxData,输出解调比特和星座图。整套函数调试稳定后,换16QAM只是改一个M值的事。
4. 实测效果与常见故障排查
4.1 短距离实测:正常星座图长什么样
我最常跑的测试场景是两台电脑加两个PLUTO,天线距离一到两米,中间没有遮挡。发射端Gain设为-10dB,接收端Gain设为50dB,这个组合下64QAM星座图能清楚看到64个点聚成方块阵列,外圈略微抖动,内圈干净,误码率可以做到1e-4以下甚至更低。如果距离拉到三四米且中间有人走动,星座图外圈会先开始发花,因为多径和衰减让信噪比掉了几dB,64QAM外圈点间距本来就小,最先扛不住。
这个现象其实很有教学意义:QPSK在明显衰减下还能用,64QAM在同样条件下可能完全不可用。所以做自适应调制的人总说“用好64QAM的前提是信道真的够好”,这句话在现场实验里可以直接看到实证,比在讲义上写十遍都有用。
4.2 常见问题速查表
我把整个调试过程中遇到的典型问题整理成了下面这张表,按出现频率排列:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| rx()一直返回NaN或全零 | 接收增益太低、距离太远 | 先把Manual Gain拉到较大值,确认有信号再降下来 |
| 星座图整体旋转 | 载波频偏未补偿 | 检查CarrierSynchronizer是否加入,尝试增大环路带宽 |
| 星座图有64簇但整体偏移 | 相位解模糊不对 | 用前导序列估计残余相位并校正 |
| 星座图中心一团、外圈散开 | 定时同步不准 | 检查sps是否设错,检查发射端上采样倍数是否正确 |
| 接收波形出现间歇空白 | USB实时传输卡顿 | 降低BasebandSampleRate或减少单帧采样点数 |
| 发射端对象无法重复调用 | 上次transmitRepeat未停止 | 调用tx.stop(),或clear tx清除对象句柄 |
4.3 实操中容易被忽略的坑
第一个坑是发射端Gain设得过高。在PLUTO支持包里,发射增益的取值范围一般是0到-88,这里的数值更接近“衰减量”:数值越接近0,输出功率越大。很多同学看到Gain下意识以为越大越好,直接设成0,结果接收端看到一堆削顶失真,64QAM星座图外圈全部塌陷。稳妥的做法是先取-10到-20之间,再根据实际接收效果微调。
第二个坑是2.45GHz频段和周围Wi-Fi互相干扰。实验室Wi-Fi很密,如果接收端看到星座图像随机噪声一样散布,先别怀疑硬件坏了,把载波频率往2.47GHz挪一下,或者把发射端功率稍微抬一点试试。PLUTO这种小功率设备在ISM频段做实验问题不大,但还是要注意控制输出功率,避免干扰周围设备,也避免自己被强干扰源压制。
第三个坑是MATLAB里的数据类型。sdrrx默认输出int16,而qamdemod和scatterplot需要复数double,如果不做转换,画星座图时会出现严重失真。我在代码里特意设置了OutputDataType为double,这一步很不起眼,但能省掉很多无谓的debug时间。
5. 后续扩展空间与个人经验总结
按照这套流程跑通以后,其实只需要换一两个模块就能做很多其他实验。把qammod换成pskmod就是QPSK或8PSK;在qammod前加ofdmmod,再把输出接入PLUTO,就是OFDM加64QAM,可以顺带测多径效应;把单发单收改成循环缓存采集,还能做简单的频谱感知实验。如果想让项目更“工程化”,可以再引入信道编码,比如用LDPC或卷积码把BER直接从1e-3降到1e-6量级,体验一下编码增益带来的实际收益。
我个人最想强调的一点是:这类项目真正的价值不在于“我写了一段代码让数据从天线发出去了”,而在于它把一个完整的通信链路从理论公式变成了可以观察、可以触摸的实物。你在书本上背过“64QAM需要更高信噪比”,远不如看着星座图从一团云雾变成64个清晰星点来得深刻。特别是当传输距离拉远、信噪比掉下来时,你能立刻看到外圈先于内圈崩掉,这个视觉反馈比任何课件都直观。
最后再分享一个我自己调试时的小习惯:先把QPSK调到完全可用的状态,再切16QAM,最后才上64QAM。这样每一层级新引入的问题,无论是频偏、相位还是定时偏差,都能单独暴露出来。要是直接上64QAM,星座图一旦糊成一团,你根本分不清到底是哪一环出了问题。这个逐级递进的思路说穿了就是工程上最简单的分而治之,但确实能帮你少走很多弯路。等这版再稳定一点,我打算把它改成一个Simulink模型,做成拖动参数就能看星座图变化的实时演示板,给通信原理课当教具用。

243

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



