资讯动态

从零搭建QPSK调制器:Verilog实现、成形滤波与仿真验证

发布时间:2026/10/17 21:43:04 来源:尧图企业网站定制
1. 从零搭建QPSK调制器的整体思路很多人学Verilog学到一定阶段都会遇到一个共同的瓶颈语法都会了时序也能写但真让你从零实现一个完整的通信模块脑子里却一片空白。我当初学到这里的时候也是这样看了一堆教程每个教程都在讲always块怎么写、阻塞非阻塞怎么区分但没有人告诉你一个真实的调制器到底该怎么拆、怎么搭。QPSK调制就是一个特别好的练手项目它不像FFT那样复杂到让人望而却步也不像简单的计数器那样缺乏实际意义它刚好卡在需要动脑子设计架构和能在几百行代码内跑通之间。QPSK全称是正交相移键控Quadrature Phase Shift Keying说白了就是用四个不同的相位来携带两比特信息。你可以把它想象成四个人站在操场的四个角落你每次喊一个人的名字对方就知道你要传递的是000110还是11。相比BPSK只有两个相位两个人QPSK一次能传两倍的信息量而占用的带宽不变这就是它被广泛用在卫星通信、数字视频广播等领域的原因。这篇文章我会从架构设计开始一步步带你把这个调制器搭出来。包括比特流的串并转换怎么做、星座映射为什么那样选、成形滤波器的系数怎么算、I/Q两路怎么合并输出以及仿真验证时怎么确认波形是对的。代码我会给完整的可综合Verilog仿真平台用Verilog testbench不依赖任何特定的FPGA厂商IP核你拿到任何工具链上都能跑。注意这篇文章假设你已经掌握了Verilog的基本语法always块、模块例化、阻塞与非阻塞赋值如果你连这些还不熟建议先把基础打牢再来看不然会很吃力。2. QPSK调制的数学底子与工程简化2.1 为什么是I/Q两路而不是直接调相位教科书上讲QPSK通常从相位定义出发四个符号分别对应45度、135度、225度、315度。然后写出表达式s(t) A·cos(2πfct φk)其中φk取四个值之一。这个公式没错但如果你直接照着这个公式去写Verilog会发现一个很尴尬的问题——你需要在FPGA里实时计算cos值而cos在硬件里实现起来要么用查找表要么用CORDIC资源开销不小。工程上的做法是把它展开。根据三角恒等式s(t) A·cos(2πfct φk) A·cos(φk)·cos(2πfct) - A·sin(φk)·sin(2πfct)令I cos(φk)Q sin(φk)那么s(t) I·cos(2πfct) - Q·sin(2πfct)。这就是I/Q调制的基本形式。I叫同相分量In-phaseQ叫正交分量Quadrature。对于QPSK来说I和Q各自只取两个值1或-1归一化后是±0.707但工程上通常先取±1最后再统一缩放。这样一来你不需要在FPGA里算任何三角函数只需要根据输入的两个比特决定I和Q分别输出正还是负就行了。这就是为什么所有实际的数字通信系统都用I/Q架构——它把相位调制转化成了两个幅度调制硬件实现难度直线下降。2.2 星座映射的两种约定及选择依据QPSK的星座图有四种点对应四个相位。但具体哪个比特对映射到哪个相位有两种常见的约定比特对(b1 b0)约定AIb1, Qb0约定B格雷码映射00I1, Q1I1, Q101I1, Q-1I-1, Q110I-1, Q1I-1, Q-111I-1, Q-1I1, Q-1约定A最直观b1直接给Ib0直接给Q。约定B用的是格雷码相邻星座点之间只有一个比特不同。为什么要用格雷码因为在实际信道中噪声可能导致接收端判决到相邻的星座点上如果相邻点只有一个比特不同那误码就只错一位如果用约定A相邻点可能两个比特都不同误码率就翻倍了。实操建议做仿真验证的时候用约定A最省事因为映射关系一目了然。但如果你的项目最终要对接真实的通信协议一定要查清楚协议规定的映射方式别想当然。2.3 成形滤波为什么不能省如果直接把I/Q的±1值送到DAC输出你会得到一个频谱无限宽的信号——方波的频谱是sinc函数旁瓣衰减很慢会严重干扰相邻频段。所以必须做成形滤波把信号的频谱收窄到目标带宽内。工程上最常用的是根升余弦滤波器Root Raised Cosine, RRC。为什么是根而不是完整的升余弦因为通信链路两端各放一个RRC接收端和发射端的RRC级联起来正好构成一个完整的升余弦响应满足奈奎斯特第一准则无码间干扰条件。如果你的项目只做发射端那放一个RRC就对了。RRC的冲激响应公式这里不展开推导你只需要知道它的关键参数是滚降系数α也叫beta通常取0.22到0.5之间。α越小频谱效率越高但时域拖尾越长实现时需要更多的抽头。α越大频谱占用越宽但时域波形收敛越快。我在实际项目中一般取α0.35这是一个比较折中的值滤波器阶数用33阶左右就能得到不错的效果。滤波器系数可以用MATLAB的rcosdesign函数生成也可以用Python的scipy.signal.firwin配合根升余弦公式自己算。3. 模块拆解与关键参数计算3.1 顶层架构怎么划分一个完整的QPSK调制器我习惯把它拆成四个子模块串并转换模块S2P把单比特流每两个比特分成一组输出并行的两比特星座映射模块Mapper根据两比特决定I/Q的符号成形滤波模块Pulse Shaping对I/Q两路分别做RRC滤波上变频/输出模块Upconvert把I/Q调制到载波上如果做基带输出则省略这四个模块用流水线方式串联每个模块之间用valid/ready握手或者简单的使能信号同步。对于学习项目来说用使能信号就够了不需要搞太复杂的握手协议。顶层模块的端口大概长这样module qpsk_modulator ( input wire clk, // 系统时钟 input wire rst_n, // 低电平复位 input wire din, // 输入比特流 input wire din_valid, // 输入有效 output wire signed [15:0] i_out, // I路输出 output wire signed [15:0] q_out, // Q路输出 output wire dout_valid // 输出有效 );数据位宽的选择很关键。输入是1比特映射后I/Q是±1如果直接用1比特表示符号0表示11表示-1那滤波器的输入就是1比特。但滤波器系数通常是多位宽的小数乘完之后位宽会膨胀。我一般把映射后的数据扩展成8比特有符号数127表示1-128表示-1滤波器系数用12到16比特输出截取到16比特。这样既有足够的动态范围又不会浪费太多资源。3.2 串并转换的时序陷阱串并转换看起来简单——来两个比特拼成一个两比特的输出。但这里有一个很容易踩的坑比特对齐。假设输入比特流是b0, b1, b2, b3,...你需要把(b0,b1)作为第一组(b2,b3)作为第二组。如果你用一个计数器每来一个valid就加一计数器为0时把当前比特存到高位计数器为1时把当前比特存到低位并输出这样逻辑是对的。但问题是第一个比特到来之前计数器处于什么状态如果复位后计数器是0那第一个比特会被当成高位第二个比特当成低位这没问题。但如果你的复位逻辑没写好或者valid信号有毛刺可能导致比特错位。一旦错位后面所有的符号都错了而且这种错误在波形上不容易发现——你看到I/Q还在变化但映射关系全乱了。我的做法是在S2P模块里加一个状态机只有收到第一个有效比特后才开始计数并且用一个flag标记当前是否处于符号边界。仿真的时候专门测一下复位后第一个比特的情况以及valid信号中间断掉再恢复的情况。// 串并转换核心逻辑 reg bit_cnt; reg [1:0] symbol; always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt 1b0; symbol 2b00; end else if (din_valid) begin if (bit_cnt 1b0) begin symbol[1] din; // 第一个比特放高位 bit_cnt 1b1; end else begin symbol[0] din; // 第二个比特放低位 bit_cnt 1b0; end end end注意这里symbol[0]是在第二个周期才更新的所以输出有效信号应该在bit_cnt从1变0的那个周期拉高。这个细节如果搞错了输出会晚一个周期或者早一个周期导致后续滤波器采样错位。3.3 成形滤波器系数的生成与量化RRC滤波器的系数生成我推荐用Python脚本做方便调参和重复生成。核心公式是h(t) (sin(πt/T·(1-α)) 4αt/T·cos(πt/T·(1α))) / (πt/T·(1-(4αt/T)²))其中T是符号周期α是滚降系数。在实际生成时t取离散值通常每个符号采8个点8倍过采样滤波器跨度取±4个符号这样就是8×4×2165个抽头。如果觉得资源紧张可以降到4倍过采样、±3个符号跨度抽头数降到25个。生成完浮点系数后需要量化成定点数。假设系数用12比特有符号数表示量化步骤是找到系数绝对值的最大值max_coeff计算缩放因子scale (2^11 - 1) / max_coeff每个系数乘以scale后四舍五入取整验证量化后的频率响应是否仍然满足要求量化这一步很容易出问题。如果系数动态范围太大小系数被量化成0滤波器的阻带衰减就不够了。我的经验是如果发现量化后阻带衰减比浮点版本差了超过3dB就增加系数位宽或者减小滚降系数。实操心得生成系数后一定要把量化后的系数导入MATLAB或Python画出实际的频率响应和浮点版本对比。我见过太多人直接拿量化系数去综合结果频谱泄漏严重回头查了半天才发现是量化精度不够。4. Verilog实现中的资源优化与时序收敛4.1 乘法器的复用策略成形滤波器的核心是卷积运算每个输出样点需要N次乘加N是滤波器抽头数。如果直接用N个乘法器并行计算资源消耗很大。以65阶滤波器、8倍过采样为例每个符号周期需要计算8个输出点每个点65次乘加总共520次乘法。如果系统时钟是符号速率的100倍那你可以用1个乘法器分时复用520个时钟周期完成一个符号的处理。但分时复用会带来控制逻辑的复杂度。我的建议是根据你的时钟余量来决定并行度。如果符号速率是1MHz系统时钟100MHz那每个符号有100个时钟周期可用用8个乘法器并行每个负责一个过采样点是比较合理的控制逻辑也不会太复杂。具体实现时可以用一个移位寄存器存储最近的N个输入样点然后每个时钟周期取出一个抽头对应的数据和系数相乘累加到输出。这里要注意移位寄存器的更新时机——只有在新的输入样点到来时才移位否则会重复计算。// 简化的滤波核心单乘法器版本 reg signed [7:0] shift_reg [0:64]; reg [6:0] tap_idx; reg signed [31:0] acc; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tap_idx 7d0; acc 32d0; end else if (calc_en) begin if (tap_idx 7d64) begin tap_idx 7d0; // 输出acc的高16位作为结果 end else begin tap_idx tap_idx 1b1; acc acc shift_reg[tap_idx] * coeff[tap_idx]; end end end4.2 位宽增长的精确计算定点数运算最怕的就是溢出。I/Q输入是8比特有符号范围-128到127系数是12比特有符号范围-2048到2047两者相乘结果是20比特有符号。65个这样的乘积相加最坏情况下需要20ceil(log2(65))20727比特。所以累加器至少要用27比特我一般取32比特留足余量。累加完之后需要截取合适的位宽输出。如果直接取高16位相当于除以2^16可能会损失精度。更好的做法是先做舍入加一个偏移量再截取然后根据实际信号幅度做饱和处理。饱和处理很重要——如果信号超过16比特表示范围直接截断会导致波形严重失真而饱和至少能保持波形形状。// 带舍入和饱和的输出处理 wire signed [31:0] acc_rounded acc 32sd32768; // 加0.5LSB做舍入 wire signed [15:0] result; assign result (acc_rounded[31:16] 16sd32767) ? 16sd32767 : (acc_rounded[31:16] -16sd32768) ? -16sd32768 : acc_rounded[31:16];4.3 时序收敛的常见障碍当你把滤波器综合到FPGA上时最可能出现的时序问题是乘法器到累加器的路径太长。65个抽头的累加如果在一个时钟周期内完成组合逻辑延迟会很大时钟频率上不去。解决办法有两个一是插入流水线寄存器把累加分成多级二是用DSP48内部的累加器让工具自动优化。如果你用的是Xilinx的器件可以在综合属性里加上use_dsp48让工具把乘加映射到DSP单元里。但要注意DSP48的累加器位宽有限通常是48比特如果你的累加器超过这个位宽工具会自动拆分反而可能引入额外的延迟。我的经验是先不加流水线综合一次看时序报告。如果时序余量slack是正的哪怕只有一点点就先不折腾。如果slack是负的再考虑在累加链中间插入寄存器。插入寄存器的位置很关键——最好插在累加树的中间层级而不是随便找个地方插否则会破坏累加的连续性。5. 仿真验证怎么确认波形是对的5.1 Testbench的激励设计仿真QPSK调制器激励不能随便给。我一般用三种测试模式第一种是固定模式输入全0、全1、或者0101交替观察输出是否稳定。全0输入时I/Q应该都是固定的正最大值经过滤波器后有一个稳定的直流分量输出波形应该是一条直线。如果输出在跳变说明滤波器系数或者累加逻辑有问题。第二种是伪随机序列用LFSR生成一个长序列覆盖各种比特组合。这种模式用来验证功能正确性你可以把输入比特和输出I/Q都存到文件里用Python或MATLAB做后处理画出星座图和频谱。第三种是边界模式专门测试复位后第一个符号、valid信号中间断掉、连续输入突然停止等情况。这些边界情况在实际系统中经常出现如果处理不好会导致输出错位或者卡死。// 伪随机激励生成 reg [15:0] lfsr; always (posedge clk or negedge rst_n) begin if (!rst_n) lfsr 16hACE1; else if (din_valid) begin lfsr {lfsr[14:0], lfsr[15]^lfsr[13]^lfsr[12]^lfsr[10]}; end end assign din lfsr[0];5.2 星座图与频谱的验证方法仿真跑完之后把I/Q输出存成文本文件用Python画星座图。正确的QPSK星座图应该是四个清晰的点分布在四个象限。如果点模糊成一团说明噪声太大或者滤波器有问题如果只有两个点说明I或Q有一路没工作如果点偏离了对角线说明I/Q增益不平衡。频谱验证更关键。把I/Q数据做FFT看频谱形状是否符合RRC滤波器的理论响应。重点看三个指标主瓣宽度是否等于(1α)×符号速率、旁瓣衰减是否大于40dB、带外泄漏是否在可接受范围内。注意仿真时如果发现频谱旁瓣很高先检查滤波器系数是否量化得太粗糙再检查是不是忘了做成形滤波。我见过有人直接把映射后的±1输出做FFT然后纳闷为什么旁瓣只有十几dB——那当然方波的频谱就是这样。5.3 常见仿真失败案例排查案例一输出一直是0。先查复位逻辑是不是复位信号一直有效再查时钟是不是testbench里忘了给时钟最后查valid信号是不是din_valid一直没拉高。案例二输出有波形但星座图是乱的。大概率是比特对齐错了。检查S2P模块的计数器逻辑确认第一个比特到来时计数器从0开始。可以在testbench里输入一个已知的序列比如00011011然后手动推算每个符号的I/Q应该是多少和仿真结果对比。案例三频谱有对称的杂散。这通常是I/Q两路不一致导致的。检查两路的滤波器系数是否完全相同累加器的位宽和截取方式是否一致。I/Q不平衡会在频谱上产生镜像分量表现为对称的杂散。案例四时序仿真通过但门级仿真失败。这是典型的综合后问题可能是位宽截断导致的溢出或者综合工具优化掉了你认为重要的逻辑。检查综合报告里的警告信息特别是关于位宽不匹配和未连接端口的警告。6. 从仿真到上板的最后一公里6.1 输出接口的选择与DAC对接仿真跑通之后下一步是把I/Q数据送到DAC。这里有几个实际问题需要处理第一是数据格式。FPGA内部用的是有符号二进制补码而很多DAC芯片要求偏移二进制或者无符号格式。你需要在输出前做格式转换。比如16比特有符号数转无符号只需要把最高位取反即可。第二是采样率匹配。FPGA的处理时钟和DAC的采样时钟可能不同。如果DAC采样率高于FPGA输出速率你需要做插值如果低于需要做抽取。对于学习项目最简单的做法是让FPGA输出速率等于DAC采样率用时钟管理单元生成合适的时钟。第三是接口时序。DAC通常有建立时间和保持时间要求FPGA输出数据需要在时钟边沿前后稳定。如果时序不满足可以在输出端加一级寄存器打拍或者调整时钟相位。6.2 板上调试的实用技巧上板之后如果发现输出不对不要急着改代码。先用示波器看几个关键信号时钟有没有、复位有没有释放、输出有没有在跳变。如果输出是固定的直流先查DAC配置是否正确如果输出在跳变但星座图不对用逻辑分析仪抓一段I/Q数据和仿真结果对比。我习惯在FPGA里加一个简单的嵌入式逻辑分析仪比如Xilinx的ILA抓取S2P输出、映射输出、滤波器输出这几个关键节点。这样一旦出问题可以快速定位是哪个模块的锅。还有一个容易被忽略的点电源噪声。DAC对电源噪声很敏感如果电源纹波大星座图会发散。如果仿真和逻辑分析仪抓的数据都正常但射频输出质量差就要查电源了。6.3 资源占用与功耗的权衡最后说一下资源优化。QPSK调制器的资源消耗主要在滤波器上。65阶滤波器、8倍过采样、16比特数据位宽大概需要资源类型粗略估计优化方向DSP488-16个降低过采样率或抽头数寄存器2000-3000个用移位寄存器替代分布式RAMLUT1500-2500个优化累加器结构块RAM0-2个系数存ROM如果资源紧张可以把过采样率从8降到4抽头数从65降到33资源消耗大概能减半但频谱性能会有所下降。具体怎么取舍取决于你的系统对频谱模板的要求。功耗方面滤波器是主要的动态功耗来源。降低时钟频率、减少同时翻转的寄存器数量、用时钟门控关掉空闲模块的时钟都能有效降低功耗。不过对于学习项目来说功耗通常不是首要考虑因素先把功能跑通再说。最后分享一个小技巧如果你用的是Xilinx的Vivado可以在综合后的报告里看Post-Synthesis Utilization和Post-Implementation Timing两个部分。前者告诉你资源用了多少后者告诉你时序余量还有多少。如果时序余量小于0.5ns建议还是优化一下不然温度变化或者电压波动可能导致时序失败。这个QPSK调制器我从第一次写到现在前前后后改了七八个版本。最开始用行为级描述写综合出来一堆锁存器后来改成RTL又遇到时序收敛问题再后来优化位宽和流水线才慢慢稳定下来。如果你在实现过程中遇到问题大概率不是Verilog语法的问题而是对定点数运算和时序的理解还不够深。多仿真、多看波形、多对比理论值慢慢就有感觉了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑