FPGA实现AM调制解调:从NCO载波到包络检波的完整工程与仿真实践 做FPGA的人十有八九绕不过调制解调这个坎。很多同学拿着《通信原理》教材看AM调制的时域公式觉得挺简单可真要自己在Quartus或者Vivado里把AM调制解调完整做出来就发现一堆莫名其妙的问题ModelSim仿真波形全是红线载波频率不对双边带变单边带上板后示波器看到的包络被削掉半边……这个项目就是来把这个坎填平的基于FPGA完整实现AM调制和解调从NCO载波生成、乘法器振幅调制到接收端包络检波并且在ModelSim里用一套可复用的仿真脚本验证在Quartus 18.1和Vivado两套工具链下都能跑通。不管你是准备课程设计、毕业设计还是刚转FPGA方向的通信方向工程师这套代码和验证流程都能当个底子来用。我写这篇博文不会去复述那些网上一搜一大把的入门教程而是把这次项目里真正踩过的坑、算过的参数、改过的代码全部摊开来讲。尤其是ModelSim仿真时的各种诡异现象还有Quartus和Vivado这种跨工具链移植时的差异点很多教材里根本不提只有自己动手做过一遍才懂。1. 项目整体思路与AM信号链路设计1.1 AM调制解调的基本框图与数学原理AM调制的本质一句话就能说清让高频载波的幅度跟随低频调制信号变化。数学表达式是s(t) [A0 m(t)] * cos(ωc * t)其中A0是直流偏置m(t)是调制信号cos(ωc * t)是载波。这里的A0必须足够大保证A0 m(t)始终大于0否则会出现过调制的相位翻转解调出来的包络就会严重失真。在FPGA里实现时我习惯先把这个公式在脑子里转成数据流图。发射端需要三个模块一个产生载波的正弦信号源一个产生调制信号的低频正弦源以及一个乘法器外加一路直流偏置加法。接收端要恢复原始信号最常见也最好实现的是包络检波也就是全波整流取绝对值之后接一个低通滤波器把高频载波分量滤掉剩下的就是原始调制信号的缓变包络。整个链路的仿真和上板验证都可以围绕这套框图展开。我建议初学者不要一上来就追求高级结构先把最基础的AM调制解调打通再去考虑DSB-SC、SSB或者相干解调。基础链路能跑通整个FPGA的数据通路、时钟管理、位宽设计这些基本功也就都练到了。1.2 FPGA实现方案选型自己写NCO还是调用IP核做载波和调制信号源的时候很多人第一反应是直接调IP核。Quartus里有NCO IPVivado里有DDS Compiler配置界面点几下就能生成一个正弦波输出确实省事。但这里有一个现实问题IP核在不同工具链之间的移植成本很高。Altera的NCO IP生成的RTL代码直接拿到Vivado里根本用不了必须重新配置Xilinx的DDS Compiler参数和接口还不完全一样。我的建议是这个项目用手写NCO也就是相位累加器加查找表的结构。原因有三个。第一手写NCO的代码非常短核心逻辑只有三四行理解起来比黑盒IP容易得多。第二频率控制字可以做成参数仿真时想改频率改一个localparam就行不用每次重开IP配置界面。第三手写模块在Quartus和Vivado之间可以原封不动地直接移植这对很多人来说是最刚的需求因为他们可能不只装了一套FPGA工具。当然如果你做的是一个对相位噪声和杂散要求极高的实际通信产品那还是老老实实用DDS IP核人家做了频谱优化和校准。但作为学习和验证AM调制解调原理手写NCO的精度已经完全够用而且调试时你能看到内部每一个节点的波形这对排错非常有帮助。2. 核心参数计算与关键模块实现2.1 采样率、载波频率与调制信号频率怎么定参数选择直接决定整个设计的成败。我自己做这个项目时系统时钟用的是50 MHz这是FPGA开发板上最常见的时钟频率也让后面的计算特别方便。载波频率我选了1 MHz调制信号选了10 kHz调制度m取0.5。为什么这样选先说载波。1 MHz相对于50 MHz的采样时钟来说每个载波周期有50个采样点足以还原一个光滑的正弦波形不会出现太大的阶梯失真。调制信号10 kHz比载波低两个数量级包络检波之后的低通滤波器可以有很宽的滤波空间不容易把载波泄漏滤不干净。如果你手上的板子时钟频率不同记住一个原则载波频率最好不超过系统时钟的十分之一调制信号频率最好不超过载波频率的十分之一。这样各频点之间拉开距离后级滤波器设计起来会很舒服。还有一点容易被忽略仿真时间。载波1 MHz意味着一个周期就是1 us要看到完整调制包络至少得仿真几百个载波周期。我建议直接在testbench里跑200 us仿真时间设长了也没关系ModelSim跑这种纯数字的RTL仿真很快一般不优化也就几十秒。2.2 NCO频率控制字与正弦查找表的位宽计算手写NCO的核心是相位累加器。我这里的NCO位宽用了32位频率控制字Frequency Control Word简称FCW的计算公式是FCW f_out * 2^N / f_clk其中f_out是想要输出的频率f_clk是系统时钟N是累加器位宽。这个公式就是把期望频率映射到累加器的相位增量上。算出具体数值给大家参考项目数值计算过程载波频率 f_c1 MHz1MHz * 2^32 / 50MHz载波FCW85,899,346约85899345.92取整调制频率 f_m10 kHz10kHz * 2^32 / 50MHz调制FCW859约858.99取整1 MHz载波的FCW是85,899,346这个数字看起来大其实本质意义就是累加器每来一个时钟沿加上这个数字加到32位溢出时正好完成一个正弦周期。调制信号10 kHz的FCW是859相位累加器同样32位溢出周期就是10 kHz。查找表我用了256点也就是用累加器的高8位去查一个256深度的正弦表。正弦表的数值宽度是16位最高点32767最低点-32768这样输出的载波和调制信号都是16位有符号数后续做乘法时精度足够。256点正弦表存1 MHz载波时相当于每个载波周期只取25.6个采样点波形会有一点阶梯感但仿真和实际滤波器之后完全看不出影响。2.3 调制端乘法器、直流偏置与调制度控制发射端的数据通路是这样的调制NCO输出一个16位有符号正弦波幅度范围是-32768到32767我把它右移3位缩到约±4000然后加上8000的直流偏置变成4000到12000之间的正数。载波NCO输出的16位正弦保持不变幅度在±32768之间。两个数送入乘法器输出再取高16位作为最终的AM调制信号。这个直流偏置和调制信号幅值的比例就是调制度m。m的数学定义是调制信号幅度Am除以直流偏置A0。我这里的Am是4000A0是8000所以m0.5。也就是说载波幅度的波动范围是50%上下解调出来的包络线峰峰值刚好是载波峰值的一半。实际操作中千万不要把A0设得比Am小。一旦A0 m(t)在某些时刻小于0乘法器输出的符号就会翻转波形会出现180度相位跳变解调出来的低频信号会被削平甚至直接翻边在ModelSim波形里看起来就像包络被切掉一块非常恶心。我刚做这个项目时就踩过这个坑把直流偏置调小了结果示波器上看到的解调波形完全对不上原始调制信号。还有个位宽问题要单独拿出来说。两个16位有符号数相乘结果应该是32位。我取高16位输出时很多人会直接assign out mult_result[31:16]这样其实没问题因为两个16位整数相乘后的高16位正好对应缩放后的整数部分只要保证中间没有截位误差就行。如果你想让输出范围再贴合理想AM信号可以再右移一位或者做饱和处理这个根据你后级电路的需求来定不是死的。2.4 解调端包络检波与低通滤波器设计解调端最简单的结构就是绝对值器加低通滤波器。AM调制信号经过绝对值运算后负半周全部翻到正半周得到一个频率是载波两倍的整流波形它的包络线就是原来的调制信号。这时候再接一个低通滤波器把高频载波分量滤掉输出就是恢复出来的10 kHz正弦波。低通滤波器我有两个建议方案。如果只是做验证滑动平均滤波器也叫CIC滤波器实现起来只需要几行代码核心思想是把最近的N个采样点求和再平均。采样率50 MHz取平均点数100的话等效的-3dB带宽大概在几百kHz以下能把1 MHz载波压得足够低10 kHz调制信号能无失真通过。这种滤波器在FPGA里实现就是移位加加法不需要乘法器资源消耗极小但缺点是通带不够平坦阻带衰减也不是特别高做演示足够做产品不够。如果想让解调波形更干净可以写一个标准的FIR低通滤波器。阶数取64阶系数用MATLAB的fdatool或者Octave算好量化成16位整数之后存成coe文件或mif文件在Verilog里例化一个带系数的乘累加结构就行。FIR滤波器的好处是线性相位波形不会失真而且阻带衰减可以做到40dB以上滤完的包络线非常平滑。缺点是代码量比滑动平均大一些但对FPGA来说64阶FIR也就是几个乘法器的事资源完全不是问题。我当时为了省事先用滑动平均验证了整个链路波形没问题之后才换成FIR。我建议你也这么干分步调试先跑通结构再优化性能。3. Quartus 18.1 ModelSim 完整仿真流程3.1 Quartus 工程设置与仿真工具关联用Quartus 18.1做RTL仿真很多人卡在最开始这一步ModelSim根本启动不起来。这里我直接给你最稳的操作路径。先把ModelSim装好。注意Quartus 18.1对应的是ModelSim-Altera Edition在Intel官网和Quartus一起下版本好像是10.5b。安装路径千万不要有中文不要带空格我一般习惯装在D:\modeltech64这样的纯英文目录下否则后面的库映射经常出莫名其妙的错。装好之后打开Quartus先做全局设置Tools菜单 - Options - EDA Tool Options在ModelSim-Altera那一栏填上ModelSim的可执行文件路径也就是modelsim.exe所在的目录。这一步相当于告诉Quartus我要用哪个仿真器。然后打开具体工程的Settings找到EDA Tool Settings - Simulation。编译仿真库那一栏选择ModelSim-Altera语言选Verilog HDL。在Test Benches里添加tb_am_top这个testbench注意名字和模块名要一致。完成后点OKQuartus右侧的Tools菜单里就会出现Run Simulation Tool - RTL Simulation点一下就能自动拉起ModelSim并开始编译仿真。如果点下去之后ModelSim启动了但看不到任何波形多半是testbench没有正确添加到工程里或者顶层模块名不匹配。这个东西没有任何技巧就是反复确认你的tb顶层例化了被测模块并且Qii设置里指定的名字跟文件里的module名字一字不差。3.2 ModelSim do文件自动化仿真脚本我强烈建议你别在ModelSim里一个个手动敲命令整个仿真过程用do脚本包起来。手动操作慢不说一旦参数改了重新仿真能把你烦死。项目目录里我会放一个sim文件夹里面三个东西rtl源文件路径、测试文件、run_sim.do脚本。一个最基础的run_sim.do长这样vlib work vmap work work vlog ../rtl/nco_carrier.v vlog ../rtl/nco_mod.v vlog ../rtl/am_modulator.v vlog ../rtl/am_demodulator.v vlog ../rtl/am_top.v vlog tb_am_top.v vsim -t 1ps -voptargsacc work.tb_am_top add wave -radix signed /tb_am_top/* add wave -radix unsigned /tb_am_top/u_mod/carrier_phase run 200us-voptargsacc这个选项建议加上意思是保留所有信号的可访问性不加的话有些内部信号看不见或者看不了值。add wave -radix signed是把波形数值显示成有符号十进制这在看调制信号、基带信号时非常有用不然默认的十六进制看着非常晕。run 200us就是把仿真跑200微秒这个时间足够看到完整的调制包络。在ModelSim命令窗口里执行cd到sim目录敲do run_sim.do就会自动完成建库、编译、加载、加信号、跑仿真的全部流程。如果有编译错误ModelSim会在Transcript窗口里停下来直接把错误位置和行号告诉你。3.3 波形分析与验证要点仿真跑完第一件事看AM调制输出信号。把载波、调制信号、AM输出信号同时加到波形窗口里鼠标滚轮放大盯着一个载波周期看。AM输出应该是在载波正弦波的基础上幅度上下对称地跟随调制信号变化。如果看到载波幅度没有变化说明乘法器输入端有问题多半是直流偏置没有加进去。如果载波会出现负半周完全平底的现象说明发生了过调制回去调A0和Am的比例也就是调制度m。第二件事看解调输出。解调滤波后的波形应该是一条平滑的正弦曲线频率跟发射端的调制信号完全一致幅度略小一些。我实际跑的时候滤波输出幅度大概比原始调制信号低15%左右这是滤波器通带增益和群延迟造成的正常现象不用慌张。还有一个验证的技巧在ModelSim里同时把解调输出和原始调制信号叠加在一个波形窗口里观察两者是否同步。注意输出会有几微秒的固定延迟这是滤波器群延迟造成的只要波形形状一致就说明整个链路是通的。4. Vivado联合ModelSim仿真与跨平台移植4.1 Vivado中配置ModelSim仿真器如果工程要拿到Vivado里跑很多人会想当然用Vivado自带的XSim仿真器。XSim也不是不行但对于习惯用ModelSim调试的人来说检查波形和翻信号的感觉完全不一样。我这次就专门研究了一下Vivado怎么联合外部ModelSim仿真流程其实比Quartus那边稍微复杂一点但只要第一步做好了后面就很顺。打开VivadoTools - Settings - Simulation。Simulator选项里选择ModelSim Simulator然后在下面填两个关键路径Compiled library location是编译好的Xilinx仿真库路径Simulator executable path是modelsim.exe路径。这两个路径一个不对仿真都起不来。在这里要特别提醒的是这个编译库步骤不能跳过。Xilinx器件的仿真需要UNISIM、UNIMacro这些专用库Vivado在启动联合仿真前必须先把这些库用ModelSim编译一遍生成ModelSim能识别的库文件否则编译含有Xilinx原语和IP的RTL代码时ModelSim会报找不到模块典型的就是门级仿真时候那一堆红色错误。4.2 Xilinx仿真库编译与版本匹配问题在Vivado的Tcl Console里执行库编译命令只要一条就能搞定compilelib -simulator modelsim -family all -language all意思是对所有器件族、所有语言Verilog和VHDL编译仿真库。如果你的工程只用7系列或者Ultrascale可以指定-family virtex7之类编译速度会快不少。这个命令执行时间长短取决于你的电脑性能从几十分钟到一个多小时都有烤个咖啡慢慢等就行。这里有一个我踩过的版本坑Vivado和ModelSim的版本必须搭配。我用的是Vivado 2018.3搭配ModelSim SE-64 10.4是比较稳的。如果你装了最新的ModelSim 2020.4编译的时候经常会报一个Java Runtime的错误或者某些库编译不过。这个问题的原因就是ModelSim新版本改动了一些内部API老版本的Vivado库编译脚本跟它不兼容。遇到这种情况要么换个ModelSim老版本要么别费劲了直接用XSim。Vivado版本越新对ModelSim版本的兼容性越好比如Vivado 2021.2官方支持ModelSim 2020.1及以上编译就没什么问题。库编译完成后再回到Settings里的Compiled library location填上编译库的存放路径Vivado就能在仿真时自动映射这些库了。之后点击Run Simulation - Behavioral SimulationVivado会自动生成一个ModelSim脚本并调用modelsim.exe完成仿真波形界面就是ModelSim的界面。4.3 Quartus工程迁迟到Vivado的差异点这个项目我在两套工具里都跑了要说移植遇到的坑主要集中在三个方面我一一列出来你们省得再踩一遍。时钟IP必须重写。Quartus里的altera_pll在Vivado里不存在必须用Vivado的Clocking Wizard重新生成一个MMCM或者PLL模块接口完全不一样。我这个项目没用PLL系统时钟直接接到板载晶振引脚所以省掉了这一步。存储器初始化文件格式不同。如果ROM查找表用了.mif文件Quartus直接能用但Vivado只认.coe文件。查表数据还得导一遍。不过对我来说我更推荐直接用Verilog的initial语句把正弦表数据初始化进去比如reg [15:0] sin_table [0:255]; initial begin sin_table[0]0; ...这样一个文件两边通用不用来回转格式。引脚约束文件完全不同。Quartus的.qsf文件是一行行设置Vivado用.xdc文件语法是天差地别。这个只能手动翻译我在Vivado里新建约束文件把时钟引脚和输出引脚一个个重新绑定。好在AM调制这个项目引脚不多一个时钟一个复位一个输出而已几分钟就写完。5. 常见问题与排查技巧实录5.1 ModelSim仿真波形出现红线X态的排查方法ModelSim里波形变成一条直线这是初学者遇到频率最高的问题我自己做调试时也没少看到这个现象。所谓红色波形在ModelSim里就是未知态也就是信号值既不是0也不是1而是X。出现这个的原因基本围绕三个方向第一信号没有初始化。Verilog里的wire默认是高阻Zreg默认是X如果在initial块里没有给寄存器赋初值没有复位信号或者复位信号没拉起来信号就一直保持X。第二位宽不匹配导致数据传递时出现了高阻态比如把一个32位信号的高16位接到一个16位端口上端口根本没连接。第三驱动冲突多个always块同时往同一个变量赋值仿真器不知道听谁的就给出X。排查方法也很朴素。先把testbench里的复位时序检查一遍确认复位信号在0时刻拉到有效电平然后过一段时间再拉高给寄存器一个明确的初始值。再用ModelSim的Expression窗口看具体的位值是X还是ZX的话找初始化问题Z的话找io连接的悬空问题。我见过很多人一看到红线就直接怀疑代码逻辑其实八成都是testbench里没写复位或者信号初始化漏了先补复位再谈其他。5.2 ModelSim报“Error loading design”的典型原因Error loading design这个报错字面意思是模块加载失败属于仿真根本启动不了的级别。我总结了三个最常见的触发点。第一编译阶段就已经有错误但你没有注意到直接点到了下一步。ModelSim编译文件时如果报错它会提示vlog失败但不会自动终止流程后面vsim的时候找不到正确的模块符号就会报error loading design。解决办法是先回头把vlog的编译错误全部修掉再重新vsim。第二vsim命令指定的模块名写错了。如果你写vsim work.tb_am_top那ModelSim会去work库里找tb_am_top这个模块。名字不对或者库映射不对自然加载不了。尤其要注意文件名、模块名、do脚本里的名字三者必须完全一致。第三IP仿真库没有映射。如果你的设计里有Altera或者Xilinx的IP核编译RTL代码时需要把altera_mf、altera_lnsim、unisim、xilinx_vip这些库映射到work库旁边。没映射的话顶层模块里例化IP的那些子模块解析不出来vsim直接给你一个红红的error。这跟我前面讲的Vivado库编译是一回事Quartus那边用ModelSim-Altera版本时会自动处理好但如果用的是ModelSim SE版本就必须手动map库非常容易踩。5.3 仿真正常但上板效果不对怎么办仿真跑得很漂亮波形完美结果把比特流下载到板子上示波器一测输出全是噪声或者干脆没信号。这个问题我见的太多了基本都能归到三个原因。引脚约束没写对。FPGA代码里声明的输出端口如果在约束文件里没有绑定到具体的物理引脚综合工具会随便分配一个位置你示波器肯定找不到信号。我建议上板前先花一分钟用万用表或者示波器确认一下板载LED和晶振对应哪个引脚再核对约束文件里的管脚名。时钟约束缺失。很多开发板工程不做时序约束也能跑但那是因为时钟频率低运气好碰巧时序收敛。如果你的系统时钟是从PLL倍频出来的不做约束很容易出各种诡异的亚稳态问题上板表现就是偶尔工作偶尔不工作。在Quartus里用TimeQuest加一个create_clock约束在Vivado里用create_clock命令把系统时钟周期声明清楚这一步不能省。复位极性搞反了。很多开发板是高电平复位代码里写的是低电平复位那就永远停在复位状态什么都不会输出。检查一下板子原理图的复位按键是接到低电平还是高电平再对一下代码里的if(rst_n)逻辑确认方向一致。5.4 版本兼容与IP移植避坑清单最后把版本兼容和移植的坑整理成一个清单方便你们对症下药现象可能原因解决建议Quartus无法启动ModelSim路径含中文或空格重装到纯英文路径ModelSim SE编译Altera IP报错缺少IP仿真库映射手动映射altera_mf、altera_lnsim等库Vivado联合ModelSim启动失败版本不匹配换匹配的ModelSim版本或用XSimVivado编译库卡住或报Java错误ModelSim版本太新换老一版ModelSim或换高版本Vivado.mif文件在Vivado里不识别文件格式不兼容改用.coe或用Verilog initial语句初始化ROM上板无输出引脚约束错误核对原理图并修正xdc/qsf文件上板偶尔正常偶尔不行缺少时钟约束补上create_clock约束ModelSim波形中心有跳变复位释放时序不合理延长复位时间确认所有模块复位完成6. 一点实操体会与踩坑记录这个项目整体做完我自己最大的体会是FPGA项目调试不能急尤其是通信信号链这种模块之间因果关系很强的设计每一级都得单独验证。我一开始直接把发射端和解调端一次写完再仿真结果错一个大包络找半天都定位不准是乘法器截位问题还是滤波器系数问题。后来学乖了先把NCO的波形在ModelSim里看干净再接乘法器乘法器输出对着公式算一遍数值确认没错再接解调。每一级都单独做一次波形快照后面联调出问题就知道是模块接口的问题不是逻辑的问题。还要提醒一件事ModelSim里看波形时记得把有符号信号的radix改成signed。我第一次做的时候看默认的十六进制愣是没看出来信号正负交替以为正弦没了变成锯齿。改完radix波形是正负对称的一下就确定NCO没问题了。这些不起眼的小操作恰恰是仿真调试验证效率的关键。另外这个AM调制解调工程如果你有时间可以顺手扩展一下。把发射端的载波NCO和乘法器去掉直接用一个基带信号和一个高频开关组合就能做幅移键控的演示。或者在解调端把包络检波换成乘法器加低通滤波器就是相干解调的DSB-SC接收端。基础链路打通了后面这些变形基本就是复制粘贴改改极性和系数的事。我建议学到这里的朋友别满足于仿真通过上板用示波器亲眼看一下包络和载波那才是真正把通信原理变成肌肉记忆的一步。