
简介面向通信、电子信息及数学等专业学生的课程设计与毕业设计场景这套基于QPSK调制的文本与图像双通道加密传输系统结合ALM-PLUTO硬件平台USRP B210实现从信源编码、调制映射、帧同步、信道估计与均衡到加密解密的完整链路仿真支持MATLAB 2014a/2019b/2024b等多个版本。资源包共49个文件主体为48个MATLAB脚本和1份README说明文档压缩包仅66KB脚本覆盖Turbo/汉明编码、QPSK映射解映射、ZC序列生成、载波频偏估计、混频编解码等功能模块README则梳理了实验流程与运行方式。已有41人学习/下载适合需要快速开展数字通信类课题研究或教学演示的使用者。代码采用参数化编程辅以详细注释并附可直接运行的案例数据在理解QPSK调制原理、双通道加密机制与ALM-PLUTO硬件交互的同时也能方便地调整参数进行个性化扩展与二次开发。1. 为什么拿ALM-PLUTO做QPSK双通道加密传输先回答这东西解决什么问题通信原理课设里最尴尬的瞬间是Simulink里QPSK眼图漂亮得像教科书一搬到物理硬件上就满屏雪花。QPSK正交相移键控本身不复杂难的是让它在真实射频链路上稳定跑起来——载波同步、时钟恢复、功率控制任何一个环节都能让星座图转起来。ALM-PLUTO作为ADI PlutoSDR的国产兼容板卡把AD9363射频收发器和Zynq处理器封装成一块USB供电的SDR硬件恰恰是验证这套东西成本最低的物理平台。加上文本和图像双通道加密传输的需求用户想要的其实是一套从比特到射频的完整链路加密、成帧、QPSK调制、发射、接收、解调、解密。适合的人是卡在“仿真能跑、上板就废”阶段的本科生以及想用真实射频信号把通信原理串起来的SDR入门者。这篇笔记把这条链路拆开讲清楚照着做能收到自己发出去的加密图片。2. QPSK调制在ALM-PLUTO上的映射从星座图到AD9363的I/Q通路2.1 QPSK为什么用I/Q两路相位映射与格雷编码QPSK的本质是用4个载波相位携带2 bit信息I/Q两路各负责一个bit。发送端把二进制流分成奇偶两路分别映射到I路的1/-1和Q路的1/-1合成后就是星座图上四个点(1,1)、( 1,-1)、(-1,1)、(-1,-1)。这四个点对应相位45°、135°、225°、315°。这套映射的好处是I/Q两路正交接收端用相干解调可以分别判决互不干扰。映射时一定要用格雷码排序四个相位按00、01、11、10排列让相邻相位只差1 bit。这样信道噪声把符号推到相邻区域时只错1 bit误比特率比自然二进制编码低一半。很多初学者从课本抄来QPSK调制代码符号映射顺序不对结果误码率曲线始终比理论值高2~3 dB还以为是滤波器参数问题。实际上就是映射表没按格雷码排。ALM-PLUTO上的AD9363是零中频架构基带I/Q数据直接送进DAC不需要额外搬移频。这意味着只要在软件里生成正确的复基带信号射频端的相位关系就由芯片保证。真正需要操心的是采样率、符号速率和脉冲成形滤波器这三个参数的配合。2.2 AD9363发射链路用libiio把载波频率和采样率配好ALM-PLUTO在Linux下通过libiio接口访问最直接的配置方式是用iio_attr命令操作ad9361-phy设备节点。上电后板卡默认IP是192.168.2.1USB网卡枚举后先ping通再操作。我的配置习惯是先设发射本振频率再设采样率最后设射频带宽和衰减。# 配置发射载波频率为2.4GHz ISM频段 iio_attr -u ip:192.168.2.1 -c ad9361-phy TX_LO_FREQ 2400000000 # 配置采样率这里设置为1Msps符号速率200ksps5倍过采样 iio_attr -u ip:192.168.2.1 -c ad9361-phy TX_SAMPLING_FREQ 1000000 # 配置射频带宽典型值为采样率的2~5倍 iio_attr -u ip:192.168.2.1 -c ad9361-phy TX_RF_BANDWIDTH 4000000 # 配置发射衰减单位dBm注意是衰减值越小功率越大 iio_attr -u ip:192.168.2.1 -c ad9361-phy TX_ATTENUATION 20TX_LO_FREQ决定载波频率2.4GHz不需要额外天线增益就能在实验桌上跑通几米距离。TX_SAMPLING_FREQ是AD9363的DAC采样率它决定你基带I/Q样本的速率上限。TX_RF_BANDWIDTH是模拟滤波器的截止带宽设太窄会削掉信号边带设太宽会引入带外噪声采样率的4倍是折中选择。TX_ATTENUATION是反向衰减值20意味着输出功率大约0 dBm附近这个值需要根据收发距离调整后面避坑章节会专门讲。配置完成后用iio_attr -u ip:192.168.2.1 -c ad9361-phy回读所有参数确认每个值都写入成功。AD9363有个特性修改采样率后射频带宽会自动归一化到采样率的整数倍所以回读值可能和你设的不完全一样只要倍数关系对就行。2.3 最小发射脚本Python adi库发一串QPSK符号libiio配好底层后PyADI-IIO库通常import为adi把收发通道封装成了流式接口。这个库的优点是直接操作复数数组I/Q数据不用自己打包。下面这个脚本生成一段随机QPSK符号流做根升余弦脉冲成形后发给ALM-PLUTO发射。import numpy as np import adi # 参数配置 sample_rate 1e6 # 采样率 1Msps symbol_rate 200e3 # 符号率 200ksps sps int(sample_rate / symbol_rate) # 每个符号的采样点数5 num_symbols 2000 # 发送2000个符号 freq 2400000000 # 载波频率 2.4GHz # 生成长度为4的格雷码映射表00-0相位, 01-90度, 11-180度, 10-270度 gray_map { 0: 1 1j, # 00 1: -1 1j, # 01 3: -1 - 1j, # 11 2: 1 - 1j, # 10 } # 随机生成比特流并配对成符号索引 bits np.random.randint(0, 2, num_symbols * 2) symbol_indices bits[0::2] * 2 bits[1::2] # 符号映射到IQ复数平面 symbols np.array([gray_map[idx] for idx in symbol_indices]) # 根升余弦脉冲成形滚降系数0.25滤波器长度41 def rrc_filter(sps, beta0.25, span5): N int(span * sps) t (np.arange(N) - N // 2) / sps h np.zeros_like(t) for i, ti in enumerate(t): if ti 0: h[i] 1 - beta 4 * beta / np.pi elif abs(abs(ti * 4 * sps) - 1) 1e-6: h[i] beta / np.sqrt(2) * ((1 2 / np.pi) * np.sin(np.pi / (4 * beta)) (1 - 2 / np.pi) * np.cos(np.pi / (4 * beta))) else: num np.sin(np.pi * ti * (1 - beta)) 4 * beta * ti * np.cos(np.pi * ti * (1 beta)) den np.pi * ti * (1 - (4 * beta * ti) ** 2) h[i] num / den return h / np.sqrt(sps) rrc rrc_filter(sps, beta0.25, span5) # 将符号序列上采样并卷积成形 baseband np.zeros(num_symbols * sps, dtypecomplex) baseband[::sps] symbols tx_signal np.convolve(baseband, rrc, modesame) # 初始化ALM-PLUTO发射通道 sdr adi.Pluto(ip:192.168.2.1) sdr.tx_rf_bandwidth 4000000 sdr.tx_lo freq sdr.tx_hardwaregain_chan0 -20 # 对应iio_attr里的衰减值20 sdr.tx_buffer_size len(tx_signal) sdr.tx_cyclic_buffer True # 循环发送便于接收端持续观察 print(发射中QPSK符号数{}, 采样点数{}, 每个符号采样点{}.format(num_symbols, len(tx_signal), sps)) sdr.tx(tx_signal)这段代码的关键点有三个。第一个是格雷码映射表用复数直接表示I/Q坐标顺序必须是00、01、11、10这是误码率性能的基础。第二个是根升余弦滤波器的滚降系数0.25它是频谱效率和ISI码间串扰之间的折中超过0.5会明显浪费带宽低于0.1则对定时误差极度敏感。第三个是tx_cyclic_buffer True把同一段QPSK信号循环发射这样接收端能反复观测星座图收敛情况不用每次重跑发射脚本。硬件增益配的是-20对应前面的iio_attr衰减值20两个地方数值相反很容易搞混。发射前最好把天线远离金属桌面和人的手ALM-PLUTO天线接口是SMA板载天线或者外接2.4GHz天线都可以。第一次调试建议用50cm短连接线通过功分器直接连到第二块ALM-PLUTO接收端排除无线环境的干扰因素。3. 文本和图像双通道复用把两个业务塞进一个物理信道3.1 帧结构设计PN帧头加类型标识时分复用一个物理信道双通道的意思是文本和图像两类业务共享同一个QPSK物理信道。最可靠的做法是时分复用TDM也就是在时间轴上把帧分成一个个时隙一帧发文本、下一帧发图像或者把突发长度按比例分配。帧结构是这套系统的地基设计不好后面所有环节都白搭。我的帧结构设计如下帧头用13位m序列PN码用来做滑动相关检测和帧同步类型字段占2字节标记这一帧是文本还是图像长度字段占4字节标记载荷字节数载荷是加密后的业务数据最后跟4字节CRC32校验。全部用二进制拼装拼完后按8 bit一组映射成QPSK符号。字段长度内容帧头PN13 bit固定m序列用于接收端滑动相关同步类型2 byte0x0001文本0x0002图像载荷长度4 byte加密后载荷的字节数小端序加密载荷N byteAES-CTR加密后的业务数据CRC324 byte对类型长度载荷做CRC用于帧完整性校验帧头用13位m序列是通信里的常用选择它的自相关峰尖锐抗噪声能力强13位短序列在200ksps符号率下只占用65微秒帧开销占比很低。类型字段和长度字段不加CRC是因为整体帧有CRC32兜底接收端先检测帧头再读类型和长度最后做CRC校验。如果CRC不对整帧丢弃。双通道的复用逻辑在发送端是这样工作的文本数据量和图像数据量差异巨大文本一帧可能只有几百字节而一张压缩后的JPEG图片可能几十KB。如果按固定时隙轮发发送图像时文本时隙会被浪费。实际做法是发送端维护两个优先级队列文本帧高优先级预占图像帧按块拆分。每个图像块单独成帧配合帧号字段避免重组成图时乱序。帧号放在类型字段之后的2字节里这算我的私有扩展不影响整体结构。3.2 图像通道压缩后二进制流如何切符号图像进QPSK信道前必须压缩JPEG或者PNG都行。我的选择是JPEG质量因子80到90因为QPSK信道的误码率在10^-3量级时JPEG有容错能力PNG无压缩长跑容易因累积误码导致解码失败。压缩后的字节流拼进帧载荷这是图像通道和文本通道在发送端的唯一区别其余调制部分完全一致。以一张1920x1080的JPEG图片为例假设压缩后大小为80KB按每帧载荷2KB切分要拆成40帧。每个帧的载荷长度字段填2048类型填0x0002帧号从0递增到39。接收端按照帧号把载荷塞回一个预先分配好的字节数组等收到帧号39且CRC全部通过再整体交给JPEG解码器。import cv2 import numpy as np # 读取并压缩图像 image cv2.imread(input.png) encode_params [int(cv2.IMWRITE_JPEG_QUALITY), 85] _, compressed cv2.imencode(.jpg, image, encode_params) data_bytes compressed.tobytes() frame_payload_size 2048 # 每帧载荷字节数 num_frames (len(data_bytes) frame_payload_size - 1) // frame_payload_size frames [] for i in range(num_frames): chunk data_bytes[i*frame_payload_size : (i1)*frame_payload_size] # 帧头字段在拼包时统一添加这里只准备载荷 frame_meta b\x00\x02 # 类型图像 frame_meta i.to_bytes(2, byteorderlittle) # 帧号 frame_meta len(chunk).to_bytes(4, byteorderlittle) # 载荷长度 frames.append(frame_meta chunk) print(图像压缩后大小: {} bytes, 拆分为 {} 帧.format(len(data_bytes), num_frames))切帧的逻辑不复杂真正的坑在最后一个分片。最后一帧长度可能不足2048字节长度字段必须填实际字节数接收端要按长度字段截取不能按固定2048去填。很多实现里图像解码出来是花的就是这个细节没处理好。另外JPEG解码器对字节对齐敏感切帧时按字节切没问题但送入调制器时按bit切这里必须保持一致——我在发送端先拼好完整帧字节数组再统一按字节转bit避免在切帧阶段出现bit错位。3.3 文本同步与帧检测滑动相关比锁相环更实用文本帧的特点是短、频繁、对时延敏感。如果用传统的Costas环做载波同步、Gardner算法做定时恢复一套完整接收机写下来几百行代码而且参数调起来像黑匣子。在实验场景下我推荐用滑动相关做帧头检测配合已知的载波频率在符号级做解调。接收端流程是这样的ADC采样数据经过根升余弦匹配滤波后以4倍过采样率滑动计算本地PN序列和接收符号的互相关值。当相关峰超过阈值时认为捕获到帧头从峰值时刻开始按符号周期抽取判决点之后的符号做硬判决得到01序列再按帧格式解析。这个方法不需要锁相环对ALM-PLUTO这种板载时钟精度足够的平台完全可行。import numpy as np # 本地PN序列13位m序列生成多项式 x^3x1 的扩展这里直接用固定数组 pn_local np.array([1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0]) # 从接收IQ序列中恢复符号简化版先粗同步再硬判决 def detect_frame_head(rx_symbols, pn_local, threshold0.7): n len(pn_local) best_corr 0 best_offset 0 for offset in range(len(rx_symbols) - n): # 取一段符号与本地PN做归一化互相关 window (rx_symbols[offset:offsetn].real 0).astype(int) corr np.sum(window pn_local) / n if corr best_corr: best_corr corr best_offset offset if best_corr threshold: return best_offset, best_corr return None, best_corr # 假设rx_symbol是接收端恢复出的复数符号序列 offset, corr detect_frame_head(rx_symbols, pn_local) if offset is not None: payload_start offset len(pn_local) print(检测到帧头偏移位置{}, 相关度{}, 载荷起始{}.format(offset, corr, payload_start))注意这段代码里我把I路实部大于0当作bit1小于0当作bit0这是QPSK硬判决的最简做法前提是接收端已经完成了载波同步。对于文本这种短帧粗同步后直接用I路判决够用但图像长帧就不行因为残留频偏会让星座图慢慢旋转后面避坑章会讲这个问题。滑动相关的阈值0.7是经验值13位PN序列全随机匹配的概率是1/8192所以0.7的阈值基本不会误触发。如果接收信号太弱相关峰被噪声淹没可以降低到0.5但误触发率会上升配合帧内CRC可以丢弃错帧。4. 加密传输怎么和QPSK调制解耦先加密后成帧还是先成帧后加密4.1 选AES-CTR而不是CBC误码不扩散是无线信道的命门加密和调制的关系一句话总结先加密后成帧加密算法选CTR模式别选CBC。QPSK信道经过射频传输后不可避免有误码误码率在10^-3量级是常态这对分组密码模式是生死考验。CBC模式把每个明文块和上一个密文块异或后再加密接收端解密时如果密文块里有1 bit错误会导致当前块完全解密失败还会连带下一个块的对应bit错误。在QPSK信道上一个符号错就是2 bit错CBC的误码扩散效应会让错误帧直接报废。CTR模式本质上是把AES当成流密码用每个密文块独立解密1 bit误码只影响解密后的1 bit不会扩散。对于图像数据传输CTR模式下JPEG还能靠自身容错解码CBC模式基本必炸。from Crypto.Cipher import AES from Crypto.Util import Counter # 16字节密钥和12字节随机数实际应用时密钥应从密钥管理模块获取 key b0123456789abcdef nonce b000000000000 # 固定nonce仅限实验场景真实使用必须每次随机 # 按8字节递增的计数器 ctr Counter.new(64, prefixnonce, initial_value0, little_endianTrue) cipher AES.new(key, AES.MODE_CTR, counterctr) # 加密文本或图像字节统一处理 plaintext bHello QPSK over ALM-PLUTO with double-channel encryption ciphertext cipher.encrypt(plaintext) # 解密端重新构造相同状态的计数器 ctr_dec Counter.new(64, prefixnonce, initial_value0, little_endianTrue) cipher_dec AES.new(key, AES.MODE_CTR, counterctr_dec) decrypted cipher_dec.decrypt(ciphertext) print(明文: , decrypted.decode()) print(密文长度: {} bytes, 与明文一致.format(len(ciphertext)))这段代码里的Counter计数器从0开始每次加密8字节自动递增。注意CTR模式加解密是同一个操作解密就是再次加密密文。这里固定nonce只适用于实验环境验证流程真正用的时候nonce必须每次通信随机生成并且通过帧头字段传给接收端。随机数怎么传是个问题通常做法是在帧结构里增加一个12字节的nonce字段放在载荷头部接收端先读nonce再构造计数器。4.2 加密后的数据为什么还要加扰星座图不能有直流偏好加密后的数据理论上是伪随机的但AES-CTR的输出并不是完美的随机序列特别是当明文有大量重复模式时比如JPEG文件头、全黑图像区域密文可能出现短周期重复。直接调制的后果是星座图上某两个点出现的概率远高于另外两个点产生直流偏置接收端的自动增益控制和载波恢复会被带偏。加扰的标准做法是用LFSR生成伪随机比特流和加密后的数据做异或。这一步和加密是正交的加密保证机密性加扰保证统计特性。QPSK调制器对输入数据有一个隐含要求0和1的概率尽量接近50%四个符号等概率出现。这样才能让星座图对称接收端AGC才能正常工作。我常用的是9级LFSR多项式x^9x^51生成511位循环序列。帧头PN序列和加扰LFSR必须分开否则帧同步会被加扰破坏。加扰在加密之后、调制之前做解扰在解调之后、解密之前做。顺序错了会导致解密彻底失败而且极难排查。# 9级LFSR加扰多项式 x^9 x^5 1 def lfsr_scramble(data_bits, seed0x1FF): state seed 0x1FF output [] for bit in data_bits: # 抽头位置9和5异或反馈 feedback ((state 8) ^ (state 4)) 1 # 输出当前bit异或LFSR输出 output.append(bit ^ feedback) # 移位并注入反馈 state ((state 1) | feedback) 0x1FF return output # 使用方式加密后的字节先转bit流再送入加扰器 encrypted_bits np.unpackbits(np.frombuffer(ciphertext, dtypenp.uint8)) scrambled_bits lfsr_scramble(encrypted_bits)注意LFSR的初始状态不能是全0否则序列永远输出0加扰就失效了。seed取0x1FF就是全1状态这是安全的。接收端解扰需要知道seed值它和加密密钥一起由上层密钥管理系统下发在实验环境里直接硬编码在收发两端。4.3 密钥管理静态密钥能跑通会话密钥才像样实验系统里用硬编码密钥就能验证整套链路但作为传输系统交付密钥管理是绕不开的。常见做法是构建一个会话密钥协商过程通信开始时发送端用预共享密钥加密一个随机生成的会话密钥接收端解密后用会话密钥做AES-CTR加解密。这样每次通信的密钥不同即使某次会话密钥泄露也不会影响其他传输。ALM-PLUTO平台上做密钥协商不能占用数据信道因为接收端还没建立帧同步解不了密。我一般做法是密钥协商阶段用双通道之外的一个固定明文帧承载但这只在实验环境可行。真正可靠的做法是利用ALM-PLUTO的以太网接口在同一台主机上跑一个密钥交换程序通过TCP把会话密钥发过去射频信道只走加密数据。这听起来不够炫酷但保证了密钥协商的可靠性把无线信道的误差拒之门外。密钥更新时机也有讲究。图像帧多、数据量大一个会话密钥不要超过100MB数据文本帧虽然数据量小但长时间挂机也会累积我习惯每发送500帧强制更新一次会话密钥。更新时发送端先暂停发射切换到明文控制帧请求重新协商这在点对点实验环境里够用也方便排查问题。5. QPSK双通道加密传输的5个典型翻车点与排查顺序5.1 星座图旋转接收端解调出的相位持续偏移现象用接收脚本画星座图发现四个点不再是静止的方角而是围绕原点缓慢旋转严重时混成一圈误码率从10^-4恶化到10^-1。原因发送端和接收端的载波频率存在偏差。ALM-PLUTO上AD9363的频率精度可以达到ppm量级但两个板卡即使都设在2.4GHz实际偏差也有几十赫兹到几百赫兹。这个偏差在符号速率200ksps下看起来很小但经过上千个符号累积相位旋转会跨越好几个象限。解决先测量实际频偏方法是在发射端发送未调制的CW波接收端用FFT测峰值频率偏移量。测得偏移后在接收端做数字混频补偿rx_compensated rx_signal * exp(-1j*2*pi*df*t)。如果频偏随时间漂移说明板卡温度变化影响了晶振这时要靠Costas环做闭环跟踪但入门阶段先做固定频偏补偿即可。固定补偿后星座图会稳定住再考虑后续优化。5.2 图像解出来有彩色条纹现象接收端重组JPEG后能打开但图像上有规律的水平条纹或者颜色完全错乱像梵高晚期作品。原因字节对齐错位。帧头检测通过后如果从帧头末尾到载荷起始之间多算了或少算了几个bit整个帧的载荷就错位了。JPEG是逐字节解析的错1个bit后续全错但JPEG的容错机制会让它解码出部分内容而不是直接报错所以看到的就是条纹或花屏。解决排查帧格式的bit计数。常见错误是把类型字段的2字节当成16 bit但拼装时用了str的字符拼接导致字节和bit混乱。我的习惯是在发送端把帧头、类型、长度、载荷都转成numpy数组后打印长度验证接收端解出载荷后先做CRC校验CRC通过才能保证字节对齐正确。还有一个细节图像那个JPEG压缩后以0xFF 0xD9结尾接收端重组时可以检查末尾标记不对就丢弃重传。5.3 加密后误码率雪崩式恶化现象不加密时误码率10^-4加完密直接没法用图像完全解不出来。原因用了CBC模式的锅。CBC模式下密文块互相依赖QPSK信道上一个符号错误会连带后续块集体错误。无线信道突发错误多发CBC的误码扩散把它放大成整帧报废。解决把AES模式换成CTR。CTR模式是流密码特性每个bit独立信道误码不会扩散。另一个容易被忽视的点是解密端必须用和加密端完全相同的nonce和计数器初值否则解密出来是乱的。排查方法是先关掉加密确认数据传输链路无误码再打开加密对比误码率。CTR模式下误码率应该和不加密时基本一致有少量差异是因为密文的随机性让接收端均衡器更难收敛。5.4 发射功率过高导致接收饱和现象两个ALM-PLUTO离得很近小于30cm接收端ADC信号满幅削顶星座图像四个点拉成四个大方块误码率反而比距离远时还差。原因ALM-PLUTO发射功率默认0 dBm附近接收端AGC会自动调整增益到最低但板卡近距离强信号仍然会让ADC输入超过满幅量程削顶后信息丢失。这不是接收灵敏度差是输入过载。解决发送端调大TX_ATTENUATION从20一路往上试我在实验里常用40~60的衰减值对应近距离通信。判断标准是接收端观察IQ幅度峰值调整到满幅的60%~80%为最佳。注意发射衰减改完后要回读确认ALM-PLUTO的增益控制有滞后性有时候同一条命令要执行两次才生效。5.5 双通道帧串扰文本帧被当成图像帧拼进图里现象图像重组后画面里夹杂大量乱码拆帧统计发现帧号重复或跳号而且图像帧之间穿插了文本帧的载荷。原因接收端的解帧逻辑只检测了帧头PN序列没检查类型字段。文本帧和图像帧的帧头相同如果文本帧紧跟图像帧发送接收端把文本帧载荷也塞进图像缓冲区导致数据错乱。解决在接收端解帧时先读类型字段和当前重组状态匹配才处理。更稳妥的方案是在帧头PN序列后加2 bit类型子序列让文本帧和图像帧的帧头相关峰不同。我实际做法是发送端对帧头PN做整体加扰时根据类型选择不同的扰码偏移接收端分别用两种扰码解相关能同时完成帧同步和类型识别一举两得。这个方法调通后双通道的隔离度明显提升文本图像交错发送也不乱了。6. 用qpsk眼图和EVM收尾发射和接收的联合验证技巧验证一套QPSK系统不能只看星座图星座图是“像不像”眼图才是“能不能稳定判决”。ALM-PLUTO上的qpsk眼图测试方法接收端匹配滤波后把每个符号周期内所有采样轨迹叠加显示。眼图张开度直接反映码间串扰和定时误差。眼图中央的“眼睛”睁得越大说明符号间干扰越小最佳采样点越宽。眼图在代码里的实现很简单取接收信号的同相分量按照符号周期切片把所有切片画在同一张图上。QPSK有I/Q两路分别画两个眼图。我一般发一段随机数据取2000个符号的采样点绘制眼图看中心交叉点的厚度和上下电平的收敛程度。import matplotlib.pyplot as plt # rx_signal是接收端匹配滤波后的复数基带信号 # sps是每个符号的采样点数量 def plot_eye(rx_signal, sps, num_symbols400): # 只取同相分量做眼图 real_part rx_signal.real[:num_symbols*sps] t np.arange(sps) / sps for i in range(num_symbols - 1): segment real_part[i*sps : (i1)*sps] plt.plot(t, segment, colorblue, alpha0.1) plt.title(QPSK I-channel Eye Diagram) plt.xlabel(Symbol period) plt.ylabel(Amplitude) plt.grid(True) plt.show() # 调用观察眼图睁开程度 plot_eye(rx_signal_after_match_filter, sps5, num_symbols400)除了眼图EVM误差矢量幅度是衡量调制精度的硬指标。EVM是实际接收符号位置和理想星座点之间的归一化距离5%以下的EVM说明链路质量很好10%以上就需要检查参数。用numpy计算EVM只需几行代码把接收符号按最近邻原则归类到理想星座点计算距离的RMS值除以星座点的平均幅度。def calculate_evm(rx_symbols): # 理想QPSK星座点 ideal np.array([11j, -11j, -1-1j, 1-1j], dtypecomplex) errors [] for sym in rx_symbols: # 找到最近的理想星座点 distances np.abs(ideal - sym) idx np.argmin(distances) errors.append(np.abs(sym - ideal[idx])) evm np.sqrt(np.mean(np.array(errors)**2)) / np.mean(np.abs(ideal)) return evm * 100 # 百分比 evm calculate_evm(rx_symbols) print(EVM {:.2f}%.format(evm))EVM配合眼图能定位问题眼图张开但EVM差说明噪声主导眼图闭合但EVM尚可说明定时恢复有问题。我从这套系统里学到的教训是每次调整参数后不要只看星座图——星座图是玄学眼图和EVM才是定量判据。ALM-PLUTO这套双通道QPSK加密传输系统调通之后建议再做一轮压力测试故意拉开天线距离、增加发射衰减、往信道里注入干扰看误码率和EVM如何退化这样你对系统的边界就有数了。希望帮到你。本文还有配套的精品资源点击获取