双ADALM-PLUTO与Matlab实现OFDM收发链路实战
简介这份资源面向通信工程、电子信息等专业的学生与科研人员提供一套基于MATLAB与两台ADALM-PLUTO软件定义无线电设备的OFDM调制解调系统完整实现方案可用于毕业设计、课程实践或科研项目的前期原型验证。压缩包共56个文件约29KB以44个m脚本为核心覆盖发送端与接收端全流程另含zbak备份文件、txt说明文档及一个备份压缩包便于对照与恢复。资源围绕OFDM原理、帧结构设计、导频插入、信道估计与均衡、同步校准等关键环节展开代码结构清晰、注释完整硬件配置部分说明了设备连接与参数设置方法。目前已有63人学习下载读者可直接部署运行也可在此基础上进行功能扩展与优化适合作为通信系统算法学习与工程实现的参考范例。1. 双 ADALM-PLUTO 跑 OFDM为什么这套组合值得你花一个周末手里只有一块 Pluto你永远只能做半双工的自说自话手里有两块你才第一次拥有一个能跑通完整链路的微型 SDR 实验台。OFDM 调制解调是通信里最值得亲手做一遍的东西——它把串并转换、IFFT、循环前缀、信道估计、同步这些散落的知识点拧成一根绳而 ADALM-PLUTO 恰好是能让你用几百块钱就摸到真实射频的硬件。Matlab 负责算法验证和离线仿真两块 Pluto 一块发一块收中间隔着空气和线缆星座图、误码率、频谱全都能实时看到。这套方案适合通信专业的学生、刚转射频的嵌入式工程师以及想从纯仿真跨到真实空口的人。它不要求你懂 FPGA但要求你愿意折腾采样率、中心频率和帧同步这些玄学参数。下面我按自己踩过的顺序把从环境搭建到双机联调的完整路径讲清楚。2. 先把 OFDM 收发链路在 Matlab 里跑通不接硬件的纯仿真2.1 为什么先做纯仿真而不是直接上 Pluto很多人拿到 Pluto 第一件事就是插上 USB 开始发数据结果收到的全是噪声然后开始怀疑硬件坏了。血泪经验是先把 OFDM 链路在 Matlab 里用纯浮点跑通确认星座图收敛、误码率为零再引入射频前端。纯仿真阶段你能控制每一个比特能直接看 IFFT 前后的频域时域波形能单独调循环前缀长度看它对符号间干扰的抑制效果。这一步不涉及任何硬件驱动只依赖 Matlab 的通信工具箱和信号处理工具箱。如果你用的是较新的 Matlab 版本comm.OFDMModulator和comm.OFDMDemodulator这两个 System object 能省掉大量手写索引的功夫但建议第一遍还是手写 IFFT/FFT否则你永远不知道保护间隔是怎么插进去的。2.2 手写一版最小 OFDM 调制器下面这段代码是我常用的最小可运行版本参数按 20 MHz 采样率、1024 点 FFT、600 个有效子载波来设循环前缀占 128 点。你可以直接复制到 Matlab 脚本里跑。% 最小 OFDM 调制器手写 IFFT 循环前缀 N_fft 1024; % FFT 点数 N_cp 128; % 循环前缀长度 N_data 600; % 有效数据子载波数 M 4; % QPSK 调制 num_sym 100; % OFDM 符号数 % 1. 生成随机比特并做 QPSK 映射 bits randi([0 1], N_data*log2(M)*num_sym, 1); data_sym qammod(bits, M, InputType,bit, UnitAveragePower,true); data_sym reshape(data_sym, N_data, num_sym); % 2. 子载波映射直流置零两端留保护带 ofdm_freq zeros(N_fft, num_sym); idx [2:N_data/21, N_fft-N_data/21:N_fft]; % 避开直流 ofdm_freq(idx, :) data_sym; % 3. IFFT 变换到时域 ofdm_time ifft(ofdm_freq, N_fft); % 4. 加循环前缀把尾部 N_cp 个样点复制到前面 ofdm_cp [ofdm_time(end-N_cp1:end, :); ofdm_time]; % 5. 串行化形成基带波形 tx_waveform ofdm_cp(:);逻辑说明第 2 步的子载波映射决定了你的频谱形状直流子载波必须置零否则接收端做 FFT 后直流偏置会污染整个星座图。第 4 步的循环前缀不是随便加的它的长度必须大于信道的最大时延扩展否则符号间干扰会直接让误码率爆掉。参数说明N_fft决定子载波间隔20 MHz 采样率下 1024 点对应 19.53 kHz 间隔N_cp占符号总长度的 12.5%这个比例在室内短距场景够用如果你要传几百米得加到 256 甚至 512。N_data留出保护带是为了避开 Pluto 前端滤波器的滚降区实际用的时候两端各留 200 多个子载波不传数据。2.3 接收端解调与星座图验证接收端就是反过来做一遍但顺序不能错先去循环前缀再 FFT再取有效子载波最后解调。% 最小 OFDM 解调器 rx_cp reshape(tx_waveform, N_fftN_cp, num_sym); % 恢复符号结构 rx_time rx_cp(N_cp1:end, :); % 去循环前缀 rx_freq fft(rx_time, N_fft); % FFT rx_data rx_freq(idx, :); % 取有效子载波 rx_bits qamdemod(rx_data(:), M, OutputType,bit, UnitAveragePower,true); % 星座图 scatterplot(rx_data(:)); title(接收端星座图纯仿真);跑完这段你应该看到四个清晰的 QPSK 点误码率为零。如果星座点糊成一团先检查idx索引有没有对齐再检查qammod和qamdemod的UnitAveragePower是否一致。这一步不涉及任何硬件但它是后面所有工作的基准。纯仿真跑通之后你才有资格去碰 Pluto 的采样率、增益和天线。3. 把 Matlab 和两块 ADALM-PLUTO 接起来驱动、采样率与增益3.1 安装通信工具箱硬件支持包Matlab 对 Pluto 的支持是通过Communications Toolbox Support Package for ADALM-PLUTO Radio实现的。打开 Matlab 的 Add-On Explorer搜索这个支持包并安装。安装完成后在命令行敲plutoradioList如果能看到设备信息说明驱动层通了。注意这个支持包对 Matlab 版本有要求太老的版本可能不兼容建议用 R2022b 之后的版本。安装过程中会提示你安装 Pluto 的固件更新工具跟着走就行不要跳过固件更新否则可能出现采样率设不上去的情况。3.2 单机回环先确认收发通道正常在双机联调之前用一块 Pluto 做线缆回环是最稳妥的验证。把 TX1 用 SMA 线接到 RX1加一个 30 dB 衰减器然后跑下面的代码。% 单块 Pluto 线缆回环测试 tx sdrtx(Pluto, RadioID,usb:0, ... CenterFrequency,2.4e9, ... BasebandSampleRate,1e6, ... Gain,0); rx sdrrx(Pluto, RadioID,usb:0, ... CenterFrequency,2.4e9, ... BasebandSampleRate,1e6, ... GainSource,Manual, Gain,30, ... SamplesPerFrame,1e5); % 发送一段单音 fs 1e6; t (0:1e5-1)/fs; tx_sig 0.5*exp(1j*2*pi*100e3*t); tx(tx_sig); % 接收并看频谱 rx_sig rx(); pwelch(rx_sig, [], [], [], fs);逻辑说明sdrtx和sdrrx分别创建发送和接收对象RadioID用usb:0指定第一块 Pluto。参数说明BasebandSampleRate设 1 MHz 是为了先低速跑通OFDM 实际需要 20 MHz 以上CenterFrequency用 2.4 GHz 是 Pluto 的常用频段但注意这个频段干扰多实验室里最好用屏蔽箱或线缆直连Gain发送端设 0 是为了避免线缆直连时饱和接收端 30 dB 是补偿衰减器的损耗。如果你看到 100 kHz 处有一根清晰的谱线说明收发通道正常。如果底噪抬得很高或者全是杂散先检查衰减器是不是接反了再检查采样率是否匹配。3.3 双机配置两块 Pluto 的同步问题两块 Pluto 各自插在电脑的 USB 口上Matlab 通过RadioID区分它们。先用plutoradioList确认两块设备的序列号然后在代码里分别指定。% 双 Pluto 配置一块发一块收 tx sdrtx(Pluto, RadioID,usb:0, ... CenterFrequency,2.4e9, ... BasebandSampleRate,20e6, ... Gain,-10); rx sdrrx(Pluto, RadioID,usb:1, ... CenterFrequency,2.4e9, ... BasebandSampleRate,20e6, ... GainSource,Manual, Gain,40, ... SamplesPerFrame,2e6);这里最大的坑是两块 Pluto 之间没有共享时钟采样率虽然都设成 20 MHz但实际晶振有几十 ppm 的偏差跑几毫秒之后符号就会漂移。常见做法是在接收端做基于循环前缀的定时同步或者用前导码做帧检测。我一般会在每帧数据前面加一段 ZC 序列作为同步头接收端用互相关找峰值。如果你只是做演示可以先把帧长缩短到 1 ms 以内漂移还不明显要做长时间传输同步算法必须加上。4. 双机 OFDM 空口联调帧结构、同步与误码率测量4.1 设计一个带前导码的 OFDM 帧纯仿真里的 OFDM 符号是连续发的真实空口必须加帧头和同步序列。我常用的帧结构是ZC 序列做同步头后面跟一个 OFDM 符号做信道估计再跟若干个数据符号。% 构造带 ZC 前导码的 OFDM 帧 zc_len 128; zc zadoffChuSeq(25, zc_len); % 根序列 25长度 128 sync_head [zc; zc]; % 重复两次便于互相关检测 % 信道估计符号所有有效子载波发已知 QPSK est_freq zeros(N_fft, 1); est_freq(idx) qammod(randi([0 3], N_data, 1), 4, UnitAveragePower,true); est_time ifft(est_freq, N_fft); est_cp [est_time(end-N_cp1:end); est_time]; % 数据符号沿用第 2 章的调制结果 frame [sync_head; est_cp; ofdm_cp(:)];逻辑说明ZC 序列的自相关特性很好接收端做互相关会出现尖锐峰值用来定位帧起始。重复两次是为了抗频偏即使有一次相关峰被噪声淹没另一次还能用。参数说明zc_len设 128 是因为它要能被 20 MHz 采样率下的样点数整除根序列 25 是常用值换成其他互质数也行。信道估计符号的作用是让接收端知道每个子载波上的信道响应后面做均衡用。4.2 接收端同步与信道均衡接收端先做互相关找同步头然后去循环前缀、FFT、取有效子载波再用信道估计符号算均衡系数。% 接收端互相关同步 [rx_sig, ~] rx(); corr abs(conv(rx_sig, conj(flipud(zc)), valid)); [~, peak_idx] max(corr); frame_start peak_idx - zc_len 1; % 提取帧并做信道估计 rx_frame rx_sig(frame_start : frame_start length(frame)-1); rx_est_cp reshape(rx_frame(zc_len*21 : zc_len*2N_fftN_cp), N_fftN_cp, 1); rx_est_time rx_est_cp(N_cp1:end); rx_est_freq fft(rx_est_time, N_fft); H_est rx_est_freq(idx) ./ est_freq(idx); % 信道响应 % 数据符号均衡 rx_data_cp reshape(rx_frame(zc_len*2N_fftN_cp1:end), N_fftN_cp, num_sym); rx_data_time rx_data_cp(N_cp1:end, :); rx_data_freq fft(rx_data_time, N_fft); rx_data_eq rx_data_freq(idx, :) ./ H_est; % 迫零均衡 rx_bits qamdemod(rx_data_eq(:), 4, OutputType,bit, UnitAveragePower,true); % 误码率 ber sum(rx_bits ~ bits) / length(bits); fprintf(误码率: %.4f\n, ber);逻辑说明互相关用conv配合翻转共轭实现峰值位置减去序列长度就是帧起始。信道估计用迫零均衡简单但会放大噪声如果信噪比低换成 MMSE 均衡效果更好。参数说明frame_start的计算要小心边界如果峰值太靠前会索引越界实际代码里要加保护。误码率在 20 MHz 带宽、线缆直连加衰减器的条件下QPSK 应该能跑到 1e-3 以下。如果误码率在 0.5 附近说明同步没对上检查 ZC 序列的根序列号是否和发送端一致。4.3 用 EVM 和星座图判断链路质量误码率是最终指标但调试的时候 EVM 和星座图更直观。在均衡之后加一行comm.EVM的计算能看到每个子载波的误差向量幅度。evm comm.EVM(ReferenceSignalSource,Estimated from reference constellation, ... ReferenceConstellation, qammod((0:3), 4, UnitAveragePower,true)); evm_pct evm(rx_data_eq(:)); fprintf(EVM: %.2f%%\n, evm_pct); scatterplot(rx_data_eq(:));EVM 在 10% 以下说明链路健康超过 30% 星座图就开始糊了。常见原因是发送增益太高导致功放非线性或者接收增益太低导致量化噪声占主导。调增益的时候每次改 5 dB观察 EVM 的变化趋势找到拐点。5. 避坑与排查双 Pluto 联调最容易翻车的五个地方5.1 采样率设不上去或者报错现象调用sdrtx时提示采样率超出范围或者设置成功但实际波形畸变。原因Pluto 的基带采样率有上限不同固件版本支持的范围不一样常见是 521 kHz 到 61.44 MHz但 20 MHz 以上对 USB 带宽要求很高。解决先用 1 MHz 跑通再逐步往上加如果 20 MHz 不稳定降到 10 MHz 或 15.36 MHz。另外确认 USB 线是数据线而不是充电线劣质线缆会导致采样数据丢包。5.2 接收端全是噪声找不到同步峰现象互相关结果没有明显峰值星座图是一团云。原因发送和接收的中心频率没对齐或者天线没接好或者接收增益太低。解决先用频谱仪模式看接收信号的功率谱确认中心频率处有信号检查CenterFrequency两个对象是否完全一致接收增益从 40 dB 开始往上加直到频谱上能看到明显的 OFDM 矩形谱。5.3 星座图旋转现象星座点清晰但整体在旋转误码率时好时坏。原因两块 Pluto 的晶振偏差导致载波频偏OFDM 对频偏非常敏感子载波间隔 19.53 kHz频偏超过几百 Hz 就会导致子载波间干扰。解决在接收端加频偏估计和补偿常用方法是利用循环前缀做最大似然估计或者用前导码的两个重复序列做相关。如果只是演示把帧长缩短到 0.5 ms 以内旋转还不明显。5.4 发送功率过高导致频谱再生现象频谱上除了 OFDM 信号本身两边还出现明显的杂散。原因Pluto 的发送增益设得太高功放进入非线性区产生互调产物。解决发送增益降到 -10 dB 甚至 -20 dB用外部衰减器配合接收增益来补偿。线缆直连时尤其要注意不加衰减器很容易烧接收端。5.5 Matlab 版本与支持包不匹配现象sdrtx报错说找不到 Pluto 设备但plutoradioList能看到。原因通信工具箱支持包的版本和 Matlab 主版本不匹配或者 Pluto 的固件版本太老。解决在 Add-On Manager 里检查支持包更新同时用 Pluto 的固件更新工具把固件升到最新。如果还不行换一个 USB 口避免用 USB Hub。6. 进阶技巧用 EVM 扫描找最佳增益组合与频偏补偿的快速验证调增益这件事手动一次改 5 dB 太慢。我一般写一个扫描脚本发送端固定接收端增益从 20 dB 到 60 dB 步进 5 dB每个增益点采 10 帧算平均 EVM然后画曲线找最小值。% 接收增益扫描找 EVM 最低点 gains 20:5:60; evm_results zeros(size(gains)); for k 1:length(gains) rx.release(); rx.Gain gains(k); evm_vals zeros(10,1); for f 1:10 [rx_sig, ~] rx(); % 这里插入同步、均衡、EVM 计算 % evm_vals(f) evm(rx_data_eq(:)); end evm_results(k) mean(evm_vals); end plot(gains, evm_results, -o); xlabel(接收增益 (dB)); ylabel(EVM (%));这段代码的框架你可以直接套中间同步和均衡的部分复用第 4 章的代码。跑完之后你会看到一条 U 形曲线最低点就是最佳接收增益。发送增益也用同样的方法扫一遍但注意发送增益太高会触发非线性曲线会在高增益端翘起来。频偏补偿的快速验证方法是故意把接收端的中心频率偏移 1 kHz观察星座图旋转速度然后加上频偏估计代码看旋转是否被纠正。我常用的频偏估计是用循环前缀做相关取相位角再除以 2π 和符号长度。这个技巧在 20 MHz 采样率下能纠正几百 Hz 的频偏再大就得用前导码了。最后说一个我自己的习惯每次改完参数先存一份 IQ 数据到文件用离线脚本反复调算法不要在实时链路上反复试。实时链路只用来验证最终参数调试阶段全部离线做。这样能省下大量插拔 USB 和重启设备的时间。希望帮到你。本文还有配套的精品资源点击获取