☰
Vivado上FPGA实现BPSK调制解调:DDS载波生成到相干解调全链路
2026/10/16 13:23:24 网站建设 项目流程

简介:面向FPGA开发者和通信方向学习者,这份基于Vivado的工程包完整实现信号的调制与解调,针对AM波表达式与载波/调制频率范围、调制深度步进等赛题要求展开,覆盖从RTL设计到VIO/ILA在线调试与MATLAB交叉验证的闭环流程。压缩包内共1338个文件,大小95.58MB,以v/vhd/vhdl源码、xci与xdc配置、tcl脚本为主体,辅以sh/bat/do自动化仿真脚本和coe/mif数据文件,可快速重建工程并复现仿真结果。资源已有3278人学习下载,尤其适合需要完成相关课设或准备FPGA竞赛的读者。通过工程源码、约束与仿真脚本,使用者可掌握载波与调制信号频率由VIO实时调节、ILA观察调制/解调波形的实现方法;解调误差优于1%的设计思路以及利用MATLAB验证数据精度的配套流程,也能为参数化调制解调系统的二次开发提供直接参考。

1. 用Vivado做FPGA调制解调:从DDS到BPSK拿下一整条链路

Vivado上手调制解调,第一道坎不是算法本身,而是搞清楚哪些环节直接调IP核、哪些环节老老实实手写RTL。不少开发者一上来就手推三角函数、自己造DDS,结果时序收敛不了,仿真波形和板级实测对不上,来回折腾一个星期。我的做法是:载波生成交给DDS Compiler IP,数字混频自己写乘法器,环路滤波先用滑动平均跑通、再按需换FIR。这样每个环节都可控,时序上不容易翻车。整套流程走完,BPSK调制解调在7系列或UltraScale+上用基础Verilog就能落地,适合刚接触数字中频、想把通信原理搬上板卡的工程师,也适合做软件无线电预研的团队拿来当参考骨架。

2. DDS载波生成:载波从哪来、位宽怎么定

2.1 为什么选DDS Compiler IP而不是手写查表

DDS(直接数字频率合成)的本质是相位累加加查表。相位累加器每时钟加一个步进值,高位作为查表地址,输出对应正弦幅度。结构看着简单,但手写容易踩坑:查表深度不够杂散大,相位截断引入可预测的噪声底,输出位宽和后续混频器的位宽匹配要反复试错。我一般直接用Xilinx的DDS Compiler IP,内部把相位抖动(dithering)和归一化都处理好了,配置界面直接给无杂散动态范围(SFDR)估算值,比手写查表后盲猜直观得多。手写NCO不是不行,适合资源受限或者想精确控制中间位宽的定制场景,但作为第一步,IP核是性价比最高的选择。

DDS Compiler的AXI4-Stream输出在复位释放后会有一段无效周期,后续模块必须等待tvalid信号再开始工作。这个细节在行为仿真里通常暴露不出来,因为仿真模型对复位的处理偏理想化;但上板后如果tvalid没接对,解调端的积分窗口和发送端的符号边界就对不上,整个环路的数据全乱。

2.2 DDS Compiler配置参数与频率计算

新建DDS Compiler IP核时,几个关键参数按下面配:

参数项设置值说明
Phase Width32 bit相位累加器位宽,决定频率分辨率
Output Width16 bit幅度输出位宽,单路余弦
Noise ShapingNone杂散要求高时可选Dither
Frequency per Channel1 MHz目标载波频率
Clock Frequency100 MHz系统时钟频率
Has TREADY不勾选避免握手信号未驱动导致模块卡住

频率分辨率由 f_res = f_clk / 2^N 决定,N是相位宽度。100 MHz时钟、32位相位累加器,分辨率约0.023 Hz,覆盖常见中频场景绰绰有余。

相位步进计算公式:phase_increment = f_out × 2^N / f_clk。代入1 MHz、100 MHz、32位,算出来约等于42949673。这个值可以填在IP配置的Phase Increment输入里,也可以做成可编程端口,运行中用寄存器写入。每次调整载波频率只需要改这个值,不用重新生成IP。

2.3 输出位宽与混频器的接口匹配

DDS输出是16位有符号数,范围-32768到+32767。混频器把两个16位有符号数相乘,结果是32位有符号数。最容易疏忽的是低位怎么截断。我一般只保留高12到16位,丢弃低位后观察星座图和信噪比是否还能接受。截断少了资源浪费,多了性能下降,16位保留中高12位是常见折中。

代码里实例化DDS:

dds_compiler_0 u_dds ( .aclk(clk), // 100 MHz系统时钟 .aresetn(rst_n), // 低有效复位 .m_axis_data_tvalid(tvalid), // 数据有效指示 .m_axis_data_tdata(carrier_i) // 16-bit余弦输出 );

m_axis_data_tvalid在DDS输出有效数据时拉高。如果后续模块直接消费数据不用缓冲,可以忽略握手信号,因为DDS每个时钟都输出有效样本。但一旦中间插入FIFO或AXI-Stream通道,tvalid和tready必须连完整,否则数据流会断。

提示:复位释放后DDS需要约10到20个时钟周期才输出第一个有效样本,顶层符号计数器一定要等tvalid拉高后再启动,否则开头几个符号会被吃掉。

3. BPSK调制模块:相位切换的直观实现

3.1 BPSK信号表达式与映射

BPSK是最基础的数字调制方式:数据1映射为cos(2πf_ct),数据0映射为-cos(2πf_ct),对应相位0和π。数学本质是载波乘以一个双极性序列,形式上就是一个乘法器。映射关系如下:

基带数据载波相位理想星座点
10°(+1, 0)
0180°(-1, 0)

实际工程里,数据不会每个时钟翻转,而是按符号周期保持。比如符号率10 Msps、系统时钟100 MHz,每个符号持续10个时钟周期。符号周期内的载波周期数最好取整数,否则符号边界和载波过零点错位,解调端积分窗口会截到相邻符号的能量,误码率上升。

3.2 Verilog实现

module bpsk_mod ( input wire clk, input wire rst_n, input wire data_in, // 基带数据 0/1 input wire signed [15:0] cos_i, // 载波,来自DDS output reg signed [15:0] mod_out // 调制输出 ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin mod_out <= 16'sd0; end else begin if (data_in == 1'b1) mod_out <= cos_i; // 相位0 else mod_out <= ~cos_i + 1'b1; // 相位π:补码取负 end end endmodule

逻辑说明:数据1直接透传载波;数据0对载波做补码取负,等价于乘以-1,相位翻转π。~cos_i + 1'b1是Verilog里对有符号数取负的标准写法,结果范围对称,边缘值-32768取负后不会溢出回绕。

参数说明:cos_i和mod_out必须显式声明为signed,否则取补码按无符号逻辑运算,结果完全不对。数据符号率由外部时钟分频产生,模块本身不感知符号周期。

3.3 顶层连接与Testbench仿真流程

调制端顶层把DDS和调制器连起来,跑一个简单的testbench看波形:

module bpsk_top ( input wire clk, input wire rst_n, input wire data_in, output wire [15:0] mod_out ); wire [15:0] carrier_i; dds_compiler_0 u_dds ( .aclk(clk), .aresetn(rst_n), .m_axis_data_tvalid(), .m_axis_data_tdata(carrier_i) ); bpsk_mod u_mod ( .clk(clk), .rst_n(rst_n), .data_in(data_in), .cos_i(carrier_i), .mod_out(mod_out) ); endmodule
module tb_bpsk_top; reg clk; reg rst_n; reg data_in; wire [15:0] mod_out; bpsk_top u_top ( .clk(clk), .rst_n(rst_n), .data_in(data_in), .mod_out(mod_out) ); initial clk = 1'b0; always #5 clk = ~clk; initial begin rst_n = 1'b0; data_in = 1'b0; #100; rst_n = 1'b1; #640 data_in = 1'b1; #640 data_in = 1'b0; #640 data_in = 1'b1; #640 data_in = 1'b1; #640 data_in = 1'b0; #640 $finish; end endmodule

用Vivado自带仿真器跑:

xvlog bpsk_top.v bpsk_mod.v dds_compiler_0.v tb_bpsk_top.v xelab tb_bpsk_top -s sim_bpsk xsim sim_bpsk -runall

#640表示延时640个时间单位,按10ns时钟周期算就是64个时钟周期,对应一个完整符号。打开波形窗口,检查数据为1时调制输出是正余弦波形、数据为0时幅度翻转成负余弦,即BPSK相位切换正确。

4. 相干解调实现:混频、滤波与判决

4.1 相干解调原理

相干解调的核心是接收信号乘以本地同频同相载波,低通取出直流分量,判决恢复数据。BPSK场景下,本地载波和发送载波严格同频同相时,混频输出在数据1时为正直流加二倍频分量,数据0时为负直流加二倍频分量。低通滤掉二倍频后,按极性判决即可。

难点全在“同频同相”四个字。环路测试里可以直接把DDS输出同时送给调制器和解调器,相位天然一致;真实信道中接收端必须做载波恢复,常用Costas环或平方环。本节先讨论理想同步下的解调链路,载波失步的影响放到避坑章节展开。

4.2 混频与积分判决实现

module bpsk_demod ( input wire clk, input wire rst_n, input wire signed [15:0] mod_in, // 接收信号 input wire signed [15:0] cos_i, // 本地同频载波 output reg data_out // 恢复数据 ); wire signed [31:0] mix_result; assign mix_result = mod_in * cos_i; reg signed [47:0] acc; reg [5:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin acc <= 48'sd0; cnt <= 6'd0; data_out <= 1'b0; end else begin acc <= acc + mix_result; cnt <= cnt + 6'd1; if (cnt == 6'd63) begin // 积分完成,按符号判决 data_out <= (acc[47] == 1'b1) ? 1'b0 : 1'b1; acc <= 48'sd0; cnt <= 6'd0; end end end endmodule

逻辑说明:mix_result是32位乘法结果;acc用48位是因为64个符号周期的累加理论上限约32767×32767×64≈6.9×10^10,2^36≈6.9×10^10,48位留足余量防止溢出。判决用acc[47]符号位:负数判为0,非负判为1,和调制端数据映射完全匹配。

参数说明:cnt数到63拉高判决并清零,这个计数上限必须和调制端符号周期严格一致,否则积分窗口会吞掉相邻符号能量。如果符号周期不是64,要同步修改cnt位宽和比较值。

4.3 积分长度与低通效果的权衡

积分64个时钟等效于一个矩形窗低通滤波器,-3dB带宽约0.886×f_s/64,f_s是系统时钟。窗口越长,滤二倍频越干净,但符号率越低、对载波频偏越敏感。

积分长度(时钟数)-3dB带宽(100MHz时钟)支持符号率上限频偏敏感度
165.5 MHz6.25 Msps低
641.38 MHz1.56 Msps中
256346 kHz390 ksps高

如果信道存在残留频偏,积分窗口加长会让星座点缓慢旋转,判决性能明显下降。解决方案两个方向:缩短积分窗口换鲁棒性,或者上Costas环做闭环载波恢复。工程上追求稳妥的话,先缩短窗口确认功能跑通,再逐步加长观察误码变化。

5. Vivado实现中的避坑记录:三条血泪经验

5.1 仿真通过但板级无输出

现象:行为仿真波形完全正常,上板后用示波器看DAC输出却什么都没有。

原因:DDS Compiler IP复位释放后需要若干时钟周期才能稳定输出有效数据,testbench里复位后立刻读数据看不出问题。板级上DAC初始化顺序、复位释放时刻和时钟稳定时间没有对齐,顶层符号计数器过早启动,吞掉了开头几十个有效样本。

解决:在顶层增加一个上电延时状态机,复位释放后等tvalid拉高再使能后续模块。同时检查DDS IP配置里是否勾选了Has TREADY,如果勾了而外部没有驱动tready线,模块会一直等待握手不输出数据。

5.2 混频输出低通后直流漂移

现象:解调出来的数据码型错乱,低通输出直流分量持续小幅摆动,判决结果时好时坏。

原因:混频输出的二倍频分量没有完全滤除。滑动平均的矩形窗在阻带只有约-13dB衰减,二倍频泄漏进判决器,叠加在直流分量上,让靠近判决阈值的样本被误判。

解决:把积分窗口从64改到128,或者换成FIR Compiler配Hamming窗的低通滤波器。代价是符号率降一半,但邻道泄漏特性有明显改善,适合对频域指标有要求的应用场景。

5.3 Vivado综合报位宽不匹配警告

现象:综合时报“Signal acc has 48 bits but is assigned a 32-bit value, potential width mismatch”。

原因:Verilog有符号数运算的位宽规则是结果取两操作数最大位宽,乘法是二者之和。乘法结果直接赋给48位acc时,综合器不确定按零扩展还是符号扩展,于是报警告。

解决:显式做符号扩展,写法是acc <= acc + {{16{mix_result[31]}}, mix_result};,把32位乘法结果的高位按符号位扩展到48位。这个写法在不同综合版本之间行为一致,避免歧义。

6. 上板验证:ILA调试与误码率测试

6.1 ILA配置与触发条件

Vivado里添加ILA时,采样深度1024就够分析一个完整符号帧。我把DDS输出、混频结果、累加器和判决输出都拉进ILA,触发条件设为积分计数器cnt==63,这样正对判决点采样,能看到完整的判决过程。采样深度1024在100 MHz时钟下覆盖10.24微秒,如果观察长序列则加深到5120。ILA会占用Block RAM,项目BRAM利用率可能从30%涨到45%,上板前先看资源余量再决定采样深度。

6.2 误码率统计的简单方法

上板后可以在FPGA内部做一个误码计数器。发送端用PRBS7伪随机序列,接收端判决结果打拍对齐后和发送序列逐位对比,统计错码数量。PRBS7生成器代码:

reg [6:0] lfsr; wire prbs_out = lfsr[6]; always @(posedge clk or negedge rst_n) begin if (!rst_n) lfsr <= 7'b0000001; else lfsr <= {lfsr[5:0], lfsr[6] ^ lfsr[5]}; end

PRBS7的本原多项式是x^7 + x^6 + 1,寄存器6和5异或后反馈到最低位。这个序列周期127 bit,足够覆盖多个符号周期做统计。

接收端对比时需要把发送序列延迟一拍对齐到判决时刻。延迟数等于调制器、解调器、积分器的总流水线深度,用发送端打拍逐级延迟来匹配。对齐不准会把所有样本都数成错码,误码率变成50%,这是最典型的统计陷阱。

6.3 调试习惯

从那以后,我每次做FPGA调制解调都强制走一遍完整流程:先行为仿真把环路跑通,再上板用ILA看真实信号,最后跑误码统计确认结果闭合。仿真只能证明逻辑功能对,ILA看到实际波形和误码数据对上了,才算完整闭环。这套顺序帮我避开了好几次“仿真全对、上板全废”的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询