news 2026/9/16 2:45:46

基于FPGA的BPSK变频器电路设计:从NCO到ADC/DAC的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于FPGA的BPSK变频器电路设计:从NCO到ADC/DAC的完整实现

简介:面向基于FPGA的BPSK变频器电路设计需求,该压缩包提供了一套从发射端数字上变频到接收端变频解调的完整工程参考。设计将1兆赫兹的BPSK基带信号变频到4兆赫兹中频载波,覆盖直接数字频率合成、正交混频、载波生成等关键模块,并包含多个DDS和IQ调制器相关IP核文件,便于对照学习变频链路的硬件实现。资源包采用ZIP压缩格式,共包含796个文件,压缩后整体大小约9.48兆字节,属于轻量级FPGA工程设计包。文件类型涵盖Verilog/VHDL硬件描述语言源文件、Xilinx ISE工程配置、IP核与约束文件、Tcl脚本及仿真日志,并附有PDF和HTML说明文档,方便梳理设计流程与参数设置。已有486人学习下载,适合通信电子或数字信号处理方向的FPGA入门者、课程设计学生以及需要在此基础上进行二次开发和参数调试的工程师。

1. 为什么“基于FPGA的BPSK变频器电路设计”值得自己做一遍

很多人把BPSK当成QPSK去掉一路来理解,但真要把一个基带码流变成可发射的中频信号,或者把一个中频信号搬回基带,往往会卡在变频级:NCO相位步进算错一个位宽,输出全是乱码;滤波器系数定晚了,眼图闭合一大半;DAC时钟抖动偏大,EVM面板上的数字直接让人不敢看。这个标题绕不开三件事:BPSK映射与解调、数字变频结构、ADC/DAC周边电路。把这三件事在FPGA里串起来,是一个典型的中频收发链路微缩工程,既能作为fpga入门到进阶的分水岭,也是通信板卡调测的常客。以下按“原理→代码→仿真→电路设计与调试”的顺序展开,所有参数都按能复现的目标给出。

2. BPSK变频器原理与“数字化之后先想清楚”的三个问题

2.1 BPSK调制为什么是“乘±1”而不是“换相位表”

BPSK的本质是用一个二进制符号控制载波相位取0°或180°,反映到数字域就是一个实信号乘上“+A”或“−A”的基带电平,再与载波混频。很多fpga入门的同学会下意识地建一张相位查找表,在符号到来时切换NCO输出的相位偏移,这能做但非常浪费,而且相位跳变容易产生毛刺。

更常见的做法是把编码后的比特流映射成有符号电平:比特1对应+1,比特0对应−1(或按格雷码反相)​。映射后的基带信号幅度只有两个值,与正交载波相乘后就变成了载波同相信号的BPSK。解调侧用同一个载波做相干下变频,再通过符号判决恢复原始比特。

如果直接在FPGA内部做上变频,不需要真正把载波搬到模拟域。常见做法是先用NCO产生数字载波,再在数字域完成混频,最后送入DAC。这一步的优势是载波频率由相位累加器精确控制,不存在模拟本振的温漂和相位噪声问题。下述设计都基于这种全数字变频思路。

2.2 数字变频的结构:NCO、混频器和滤波器在数字域干什么

数字上变频(DUC)的典型链路是“码流→BPSK电平映射→数字混频→DAC→模拟滤波器”。数字下变频(DDC)则反过来:“ADC→数字混频→低通滤波→定时恢复→判决”。两套结构在FPGA中共享同一个NCO和乘法器,只是数据流向相反。

正交变频也不是必需的。BPSK的频谱只在载波一侧,用实数混频就能实现。但工程上仍常保留I/Q两路结构,原因有两个:一是便于后续扩展QPSK或多电平调制;二是解调侧用一个90°移相网络把镜像信号分离,否则实数混频后正负频率会发生折叠。

数字域混频公式非常直接:

x_IF[n] = d[n] × A[n],其中A[n] = cos(2π·f_c·n/f_s)。

d[n]是±A的基带电平,A[n]由NCO查找表给出。FPGA里乘法器一个时钟周期就能完成,瓶颈反而在NCO的相位步进和累加位宽上。载波频率分辨率由相位累加器位宽决定:

Δf = f_s / 2^N。

若f_s=50MHz,N=32,分辨率约为0.0116Hz,完全够用。位宽过小则无法精确表示10.7MHz这类非整数分频频率,最终产生频率偏差。

2.3 采样率、载波频率和位宽的选型:以 50MSPS 和 10.7MHz 为例

这里给出一组可以直接落地的参数表。该参数适配常见Xilinx ARTIX-7或Altera Cyclone V开发板,也适合用黑金或正点原子等fpga开发板验证。

参数取值选择理由
系统时钟 f_clk50MHz与ADC/DAC采样率一致,省去多时钟域
采样率 f_s50MSPS满足10.7MHz中频信号,f_c<f_s/2
中频载波 f_c10.7MHz常见中频,带外抑制容易做
NCO相位累加器位宽32bit频率分辨率0.0116Hz
NCO输出位宽16bit兼顾查找表存储量与EVM
基带符号速率 R_b1Msps中频带宽约2MHz,便于滤波
基带电平位宽16bit与载波直接相乘后仍是16bit
混频输出位宽16bit乘法后截位,防溢出

位宽的选择要留裕量:混频乘法后位宽会翻倍,一般先扩展到32bit,再截回16bit。截位不能直接砍低几位,否则直流偏置可能导致频谱出现载波泄漏。用“舍入+饱和”策略比“直接截断”好很多。后面章节的代码默认使用这种策略。

3. 用Verilog在FPGA上实现BPSK变频器的核心代码

3.1 顶层模块与端口规划

顶层模块把NCO、BPSK映射、混频和DAC接口拆开,方便后续替换成更高阶调制。端口建议直接对应FPGA开发板上的DAC芯片引脚,比如AD9708这类并行DAC,也可以把数据写入FIFO由外部接口读取。

module bpsk_duc_top #( parameter PHASE_WIDTH = 32, parameter DATA_WIDTH = 16, parameter LUT_WIDTH = 16 )( input wire clk, input wire rst_n, input wire bit_in, // 1比特串行码流 output reg [DATA_WIDTH-1:0] dac_data, // 并行DAC数据 output wire dac_clk // DAC时钟,可接DCM输出 ); wire [DATA_WIDTH-1:0] nco_sin; wire [DATA_WIDTH-1:0] nco_cos; wire signed [DATA_WIDTH-1:0] bpsk_lv; // BPSK电平映射:1 -> +12000,0 -> -12000 assign bpsk_lv = bit_in ? 16'sd12000 : -16'sd12000; // NCO产生载波 nco_dds #( .PHASE_WIDTH(PHASE_WIDTH), .DATA_WIDTH (LUT_WIDTH) ) u_nco_dds ( .clk (clk), .rst_n (rst_n), .sin_o (nco_sin), .cos_o (nco_cos) ); // 混频器,常用 DSP48E 实现乘法 reg signed [DATA_WIDTH-1:0] mixed; always @(posedge clk or negedge rst_n) begin if (!rst_n) mixed <= 'd0; else mixed <= $signed(bpsk_lv) * $signed(nco_sin); end // 截位到16bit,同时饱和处理 always @(posedge clk or negedge rst_n) begin if (!rst_n) dac_data <= 'd0; else begin if (mixed[31:16] == 16'h7fff) dac_data <= 16'h7fff; else if (mixed[31:16] == 16'h8000) dac_data <= 16'h8000; else dac_data <= mixed[23:8]; // 舍去低8位 end end assign dac_clk = clk; endmodule

代码要点说明:

  • bpsk_lv是BPSK映射后的有符号电平,不使用查找表,只做一个条件判断,节省block RAM。
  • NCO例化输出的nco_sin作为载波;nco_cos保留下来,便于后续扩展正交调制或DDC解调。
  • 混频用Verilog内建乘法器。Xilinx综合时会自动推断为DSP48E1,若在Altera上则会被映射为硬件乘法器,不需要手动干预。
  • 截位选择高位mixed[23:8],既防溢出,又保留足够动态范围。若直接取mixed[31:16]会丢失大量有效位,输出频谱会有明显量化噪声。

3.2 相位累加器与正弦余弦查找表(NCO)

NCO的代码是变频器核心。相位累加器每个时钟周期加一次频率控制字( frequency tuning word,FTW),高16位作为查找表地址。查找表可以用ROM初始化文件,也可以直接用块内存IP。

module nco_dds #( parameter PHASE_WIDTH = 32, parameter DATA_WIDTH = 16, parameter LUT_DEPTH = 4096 )( input wire clk, input wire rst_n, output reg [DATA_WIDTH-1:0] sin_o, output reg [DATA_WIDTH-1:0] cos_o ); localparam unsigned [PHASE_WIDTH-1:0] FTW = $rtoi(10.7e6 / 50e6 * 2.0**PHASE_WIDTH); reg [PHASE_WIDTH-1:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 'd0; else phase_acc <= phase_acc + FTW; end wire [LUT_DEPTH-1:0] idx = phase_acc[PHASE_WIDTH-1 -: LUT_DEPTH]; // 以下为查找表逻辑,省略真实系数表,综合时替换为ROM IP reg [DATA_WIDTH-1:0] sin_table [0:LUT_DEPTH-1]; reg [DATA_WIDTH-1:0] cos_table [0:LUT_DEPTH-1]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sin_o <= 'd0; cos_o <= 'd0; end else begin sin_o <= sin_table[idx]; cos_o <= cos_table[idx]; end end endmodule

参数说明:

  • FTW计算:FTW = f_c/f_s × 2^PHASE_WIDTH,在Verilog里用$rtoi完成浮点转整数,综合时是常数。
  • 查找表深度选4096,输出位宽16bit。若想降低资源占用可以减到1024,但杂散会升高。
  • 实际项目中不要手写几千行正弦表,应调用Xilinx DDS Compiler IP或Altera NCO IP。IP核内部可以选择CORDIC算法或ROM查找表,CORDIC比纯ROM省存储,但在FPGA上DSP资源便宜,优先选“ROM+泰勒校正”模式。

3.3 BPSK映射、上变频混频与DAC输出

上变频的混频器在一拍内完成乘法,但要注意符号位扩展。$signed()必须同时作用于两个操作数,否则Verilog会按无符号数处理,负电平被当成很大的正数。

DAC接口通常是并行数据加时钟。许多高速DAC要求数据在时钟上升沿之前建立,因此需要把混频结果打一拍再输出。这也是上面顶层代码里dac_data是寄存器输出的原因。直接使用组合逻辑输出会带来严重的时序问题。

如果开发板上DAC数据位宽只有12bit,则截位规则要改:

assign dac_data = mixed[19:8];

12bit位宽下满量程为±2048,BPSK输出电平最好设在满量程的70%~80%,给模拟后端留出余量。实际工程中可用“峰值回退”方式调节DAC幅度,避免后级放大器饱和。这也是“变频器电路设计”里跟模拟电路对接时经常踩的坑:数字域滿码不代表模拟域干净。

3.4 数字下变频的复用:镜像抑制与低通滤波

同一套硬件可以复用做DDC,核心是:ADC采样得到中频信号,乘以NCO的cossin,分别得到I/Q两路,再经过低通滤波器滤除2f_c分量。这时NCO频率要改成与发送端一致,并且需要做载波同步。

低通滤波器的实现选择常用的三种结构,对比如下:

滤波器类型资源占用处理速度适用场景
直接型FIR高,乘法器多高,无反馈系数少、阶数低
半带滤波器中,乘法器减半抽取/内插2倍
CIC积分梳状极低,无需乘法器最高大抽取比、宽带信号

BPSK只需要2MHz带宽,若从10.7MHz中频变到基带,建议先数字混频,再用半带滤波器做2倍抽取,最后用CIC做4倍或8倍抽取。CIC的优点是结构里没有乘法器,功耗低;代价是通带滚降明显,需要在后续用FIR均衡。这一组合是fpga开发中DUC/DDC链路的经典搭配,可以直接当作模板复用。

4. 仿真验证:用Testbench确认BPSK变频波形不再“玄学”

4.1 写一个能让BPSK变频器自检的测试平台

Testbench不复杂,但有一点很关键:把比特流结果和混频输出分别写入两个文件,方便后处理。下面给出一个自检激励,模拟发送端产生BPSK基带序列,并同时观察中间信号。

`timescale 1ns / 1ps module tb_bpsk_duc; reg clk = 0; reg rst_n = 0; reg bit_in = 0; wire signed [15:0] dac_data; wire dac_clk; always #10 clk = ~clk; // 50MHz initial begin #100 rst_n = 1; // 发送一段已知伪随机序列 repeat(20) begin bit_in = $random; #100; // 1Msps end #1000 $finish; end initial begin integer f; f = $fopen("dac_data.txt", "w"); // 在混频输出有效时写入文件,采集点和系统时钟对齐 end always @(posedge clk) begin if (rst_n) begin $fdisplay(f, "%d", $signed(dac_data)); end end bpsk_duc_top dut ( .clk(clk), .rst_n(rst_n), .bit_in(bit_in), .dac_data(dac_data), .dac_clk(dac_clk) ); endmodule

这段激励的逻辑说明:

  • 时钟周期20ns,50MHz;bit_in每100ns跳一次,模拟1Msps码率。
  • $random产生随机比特,覆盖连续同相码元的情况。
  • $fdisplay把DAC输出按有符号数写入文本,供Python或MATLAB读取做频谱分析。
  • 如果想仿真解调链路,可以把dac_data再接入一个DDC模型,验证解调后的码元与发送码元是否一致。

4.2 用Vivado/ModelSim看波形:眼图、星座图和频谱怎么检查

跑完仿真后,第一步看时域波形:dac_data应表现为高频载波包络随基带电平翻转。BPSK时载波在符号边界处相位反转180°,这正是“相位连续”和“相位突变”的分界点。如果载波包络没有随码流翻转,说明映射或混频接错了。

第二步看频谱。先在Vivado中运行行为仿真,导出dac_data.txt,再用Python离线画图:

import numpy as np import matplotlib.pyplot as plt data = np.loadtxt("dac_data.txt") / 32768.0 fs = 50e6 n = len(data) freq = np.fft.fftfreq(n, 1/fs) spec = 20*np.log10(np.abs(np.fft.fft(data)) + 1e-12) plt.figure(figsize=(10, 4)) plt.plot(freq/1e6, spec) plt.xlim([0, fs/2/1e6]) plt.xlabel("Frequency (MHz)") plt.ylabel("Amplitude (dB)") plt.title("BPSK DUC Output Spectrum") plt.grid(True) plt.show()

这个脚本的作用是快速验证混合后信号的主瓣是否在10.7MHz附近,而不是在0Hz或21.4MHz。如果主瓣出现在21.4MHz,说明混频后没有滤除镜像;如果主瓣在0Hz附近,多半是NCO的sin/cos接错,把cos接到I路,却用sin做了实数混频。

眼图可以用Python把每个符号周期内的波形叠在一起画,也可以直接看仿真波形。BPSK眼图在符号中点张开最大,若眼图上眼皮和下眼皮都变厚,说明载波有幅度噪声或DAC量化位宽不够。

4.3 仿真中常见的三个坑:无符号数、回环初值、异步复位

仿真不过的时候,优先查三件事,几乎能覆盖八成问题:

第一个坑是Verilog乘法器操作数无符号化。bpsk_lv是负数时,若乘法表达式里漏写$signed(),仿真器按无符号数理解,结果完全失真。排查方法是给复位后第一个码元打波形,检查混频输出是否出现符号翻转。

第二个坑是NCO初值。相位累加器复位置0没问题,但查找表输出复用部队时机没对齐,混频结果会延迟一拍,导致仿真和实际板子对不上。在所有寄存器输出上统一打一拍,能规避初始相位不确定的问题。

第三个坑是异步复位信号在仿真里使用#5这类随机时间释放,导致复位撤销时正好撞上时钟上升沿,引起FF亚稳态。标准做法是用同步复位:

always @(posedge clk) begin if (!rst_n) phase_acc <= 'd0; else phase_acc <= phase_acc + FTW; end

如果设计里既有异步复位又有同步复位,仿真环境里所有约束都要跟真实时序对齐,否则综合前后仿真行为不一致。

5. 硬件调试与进阶:给BPSK变频器“收尾”的技术细节

5.1 时钟抖动与电源噪声在ADC/DAC电路板上的三个布局要点

上板后最容易出问题的不是逻辑,而是“模拟电路设计”。时钟抖动会让混频后的载波边带变宽,电源噪声则直接抬高DAC输出的噪声底。常见的做法有三个方面:一是给ADC/DAC提供独立的高质量时钟,不要把FPGA内部逻辑时钟直接接到DAC,建议用专用时钟Buffer并靠近DAC引脚放置;二是DAC模拟电源和数字电源做单点连接或磁珠隔离,GND层在器件下方完整铺铜,而且不要在DAC正下方开槽走数字线;三是混频器输出到DAC输入线保持等长、尽量短,并加RC低通滤波到地。

5.2 用ILA和VIO调试变频器:从“仿真过了”到“板子能跑”

板级调试时,用Vivado的ILA抓DAC前一级数据,设触发条件为bit_in变化沿。观察数据是否呈“正弦包络+相位翻转”特征。VIO可以在线调整FTW,验证频率调节是否准确,进而确认NCO环路和DAC时钟频率。若ILA中数据正常但频谱仪上无信号,检查DAC参考时钟和模拟输出匹配网络。这一步把fpga开发从代码阶段带入信号完整性验证阶段,是回归现实电路的必由之路。

5.3 进阶:Simulink与FPGA联合仿真的思路、CIC优化后能省多少资源

如果要对变频器做更完整的验证,可以在Simulink中搭建变频链路模型,生成激励数据导入FPGA仿真,再把FPGA输出导回MATLAB对比误码率。Simulink的频谱分析工具可以直接观测BPSK频谱,辅助确认滤波器阶数。对于CIC优化,用4级CIC做8倍抽取替换等效FIR,约能节省70%以上的DSP乘法器资源,代价是带内容差需要在后续FIR中补偿。这个优化在资源紧张的项目中几乎是必做步骤。若进一步把CIC补偿和半带滤波器合并成一个多相FIR,单路BPSK链路的DSP48E用量可以控制在个位数。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 2:45:48

3个坑教你wordpress富文本表单怎么选不踩雷

3个坑教你wordpress富文本表单怎么选不踩雷 网站做好了没人访问,这比网站打不开还让人头疼。很多甲方朋友找我们做站,最担心的就是上线后流量惨淡,SEO做了一堆没反应。其实问题往往出在细节上,比如表单交互太卡,用户填一半就跑了。wordpress富文本表单怎么选,直接决定了你的留资效率和搜索权重…

作者头像 李华
网站建设 2026/9/16 2:44:54

NFS与autofs自动挂载实战:从原理到配置详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 2:44:16

eCognition面向对象分类:批量样本选择与shp高效导出全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 2:43:19

一本通提高篇标程数据包的解压整理与批量评测指南

简介&#xff1a;一本通提高篇的配套标程与数据包&#xff0c;面向正在学习或复习该书的编程读者。包内集中收录各章标准程序实例和相关数据&#xff0c;覆盖冒泡、快速、归并等经典排序算法&#xff0c;数组、链表、栈、队列、树等数据结构&#xff0c;并延伸至图论、网络编程…

作者头像 李华
网站建设 2026/9/16 2:43:18

PHP域名出售系统源码解析:从数据库设计到安全与优化

简介&#xff1a;这是一套以PHP编写的主机域名出售程序源码&#xff08;Domain Shop Script v1.0&#xff09;&#xff0c;面向需要搭建域名销售或主机代购平台的开发者、站长&#xff0c;以及希望学习电商交易流程的PHP初学者。程序涵盖域名实时查询、用户注册登录、订单处理、…

作者头像 李华
网站建设 2026/9/16 2:42:47

OpenClaw 2.0 实战适配指南:Kali/容器/Android跨平台部署与风控绕过

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华