ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA+Verilog实现AM信号解调:从原理到上板调试全解析

FPGA+Verilog实现AM信号解调:从原理到上板调试全解析 简介本资源面向FPGA开发初学者与通信工程实践者提供一套完整的AM调幅信号数字解调Verilog实现方案覆盖从MATLAB建模、Quartus II综合到ModelSim仿真验证的全流程。压缩包共620个文件约243.27MB包含149个Quartus编译数据库cdb、146个硬件描述数据库hdb、134个Verilog源文件v及20个VHDL封装文件vhd支撑AM解调核心模块如同步采样、包络检波、低通滤波的RTL级设计与功能验证另有大量日志、报告、备份与脚本文件保障工程可复现性。资源已获616人学习下载配套中文注释详尽、操作步骤清晰并附带Windows Media Player可播放的仿真操作录屏视频帮助读者快速理解AM信号在FPGA上的数字化解调原理与实现细节特别适合课程设计、毕业设计及通信系统FPGA实践项目参考。 做FPGA无线通信方向的人迟早会遇到一个经典题目AM信号解调。我当年第一次把这套逻辑在板子上用示波器拉出完整波形时最大感受是——它看似是通信书上的老知识实际落地到Verilog里处处是位宽、时钟、滤波这些硬功夫。这篇文章就把我基于FPGAVerilog实现AM信号解调的完整过程整理出来包含可直接参考的程序、逐行注释、仿真方法和上板操作步骤适合正在入门FPGA数字信号处理或者想在无线通信方向上做综合练习的同学。全文覆盖原理推导、参数计算、代码实现、调试排查四个方面按顺序跟一遍基本能复现。1. 项目整体设计思路先搞清楚信号长什么样再写代码1.1 AM信号的本质与两种解调路线AM调制的数学表达式是s(t) A_c * (1 m * x(t)) * cos(2 * pi * f_c * t)其中A_c是载波幅度m是调制指数0到1之间x(t)是音频或基带信号f_c是载波频率。整条信号的特点就是载波的幅度包络跟随基带信号变化。所以解调的本质很直接——把“包络”提取出来再把直流分量去掉就得到原始信号。回到FPGA实现上常见路线有两条相干解调本地产生一个同频同相载波用乘法器把载波搬回基带再低通滤波。优点是理论上信噪比更好但难点在于必须做载波同步需要锁相环或者额外同步算法工程复杂度明显上升。非相干解调直接提取包络常用方法包括绝对值低通、平方低通开方或者直接过峰值检测。实现简单不需要精确同步适合广播接收这类对成本敏感的场合。我做这个项目用的是绝对值低通滤波的非相干方案。选它的原因很现实AM本身在很多场景下对解调信噪比要求不高而FPGA里做绝对值只要取符号位异或再翻转资源开销几乎为零后续低通用滑动平均或者CIC滤波器就能解决整个数据通道不需要乘法器逻辑资源占用极少非常适合作为入门练习。1.2 系统参数规划采样率、载波频率和滤波器截止频率怎么定参数选择直接决定代码写起来顺不顺手。我用了一个比较经典的组合载波频率f_c 100kHz采样率f_s 1MHz调制信号频率f_audio 1kHz调制指数m 0.5解调输出速率约62.5kHz满足1kHz音频的奈奎斯特条件采样率选1MHz的原因是载波每周期刚好10个采样点波形还原度足够同时1MHz时钟在FPGA里非常容易生成无论是PLL分频还是直接外部晶振都能满足。更关键的是1MHz对应100kHz载波带外镜像落在900kHz以上后期滤波压力小。低通滤波器截止频率的选择可以这样估算AM信号经绝对值处理后频谱包含基带分量0到f_audio、二倍载波分量2*f_c附近以及它们的组合分量。我的目标是保留1kHz调制信号同时尽量抑制200kHz以上的高频分量。滑动平均滤波器本质是梳状滤波器的零点位置由窗口长度决定窗口长度N 16时第一零点在1MHz/16 62.5kHz虽然不能完全滤掉200kHz分量但配合后端CIC抽取衰减量已经完全够用。这里有一个工程经验不要试图在第一级就把所有高频分量滤干净滑动平均的带外衰减只有约20*log10(N)大概是24dB如果觉得不够再加一级CIC或者直接提高抽取倍数。我实测下来16点滑动平均再加16倍抽取输出波形人眼看不出失真示波器上看谐波分量也在可接受范围内。1.3 模块划分与信号链走向整个解调链路在FPGA内部按照数据流方向划分成四个模块ADC输入(1MHz采样) - 绝对值检波 - 16点滑动平均 - 16倍抽取 - 解调输出这样的划分有几个好处每个模块功能单一便于仿真定位问题。滑动平均和抽取可以合并成CIC结构节省一个独立FIFO。数据位宽逐级明确方便做时序收敛。我在顶层用了两组时钟系统时钟1MHz直接驱动所有逻辑输出端通过时钟使能信号控制62.5kHz的采样节奏而不是真的切时钟域。这样避免多时钟域带来的亚稳态问题代码写起来也简单。2. 核心Verilog模块实现代码、注释与设计细节2.1 绝对值检波模块三行代码解决的事别整复杂绝对值检波的核心思想是AM信号是双极性的取绝对值后载波的负半周翻到正半周这样输出信号的直流分量就包含了载波幅度信息而包络变化就叠加在这个直流分量上。数学上|cos(w_c*t)|的平均值是一个常数2/pi所以输出中始终包含一个与载波幅度成正比的直流项后面用隔直或者减去均值就能把基带信号恢复出来。代码如下module abs_detector #( parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, output reg signed [DATA_WIDTH-1:0] data_out ); // 取绝对值判断符号位为1则取反加1 always (posedge clk or negedge rst_n) begin if (!rst_n) data_out {DATA_WIDTH{1b0}}; else if (data_in[DATA_WIDTH-1]) data_out ~data_in 1b1; else data_out data_in; end endmodule这段代码注意两点data_in必须声明为signed否则data_in[DATA_WIDTH-1]判断符号位时虽然能工作但取反加一的数值语义会出错。输出位宽保持和输入一致即可不需要扩展位宽因为绝对值不会超过输入范围。2.2 滑动平均低通滤波累加器位宽是第一道坎滑动平均是FIR滤波器的一种特例所有系数都是1输出等于最近N个输入的平均值。它的传递函数是H(z) (1/N) * (1 - z^(-N)) / (1 - z^(-1))在FPGA里实现时最常用的方式是“累加器减法”而不是真的存N个点求和。核心思想是维护一个累加器每个时钟周期把新进来样点加进去把N个周期前的样点减掉这样累加器始终保存着最近N个点的和。这种结构对资源非常友好复杂度跟N无关。module moving_average #( parameter DATA_WIDTH 16, parameter AVG_LEN 16 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, input wire data_valid_in, output reg signed [DATA_WIDTH-1:0] data_out, output reg data_valid_out ); localparam ACC_WIDTH DATA_WIDTH $clog2(AVG_LEN); reg signed [ACC_WIDTH-1:0] acc; reg signed [DATA_WIDTH-1:0] shift_reg [0:AVG_LEN-1]; reg [$clog2(AVG_LEN)-1:0] wr_ptr; always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc {ACC_WIDTH{1b0}}; data_out {DATA_WIDTH{1b0}}; data_valid_out 1b0; wr_ptr 0; end else if (data_valid_in) begin // 加上新值 acc acc data_in; // 减去最早的值 acc acc - shift_reg[wr_ptr]; acc acc data_in - shift_reg[wr_ptr]; shift_reg[wr_ptr] data_in; wr_ptr wr_ptr 1b1; data_out acc[ACC_WIDTH-1:DATA_WIDTH-1] acc[DATA_WIDTH-2]; // 输出均值带舍入 data_valid_out 1b1; end else begin data_valid_out 1b0; end end endmodule这里有几个容易踩的坑累加器位宽ACC_WIDTH DATA_WIDTH ceil(log2(AVG_LEN))因为N个数求和最大值是N * (2^(DATA_WIDTH-1)-1)需要额外位存放进位。我习惯用参数化方式写防止改N时位宽忘了调。移位寄存器指针用环形指针写入每来一个有效数据写一个位置覆盖掉N周期前的数据。不要用for循环在每个周期把整个寄存器搬移一遍那样会消耗大量寄存器。输出舍入我写的代码里做了最简单的四舍五入acc[DATA_WIDTH-2]是舍弃部分的最高位加进去实现舍入。如果要求不高可以直接截断但波形在过零点会有轻微台阶感。2.3 CIC抽取模块用积分器梳状器实现高速率转换滑动平均的本质就是一阶CIC滤波器。如果需要进一步降低数据率、减轻后端处理负担可以直接把CIC抽取单独做成一个模块。经典CIC结构是积分器Integrator级联、抽取器、梳状器Comb级联传递函数为H(z) ((1 - z^(-D*M)) / (1 - z^(-1)))^N其中D是微分延迟通常取1或2M是抽取倍数N是级数。一阶CIC的频响就是滑动平均多级可以提高阻带衰减。我工程里用的是一阶CIC抽取抽取倍数M16。输入1MHz输出62.5kHz。实现上就是上面滑动平均模块加一个采样使能计数器module cic_decimate #( parameter DATA_WIDTH 16, parameter DECIMATION 16 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, input wire data_valid_in, output reg signed [DATA_WIDTH-1:0] data_out, output reg data_valid_out ); localparam ACC_WIDTH DATA_WIDTH $clog2(DECIMATION); reg signed [ACC_WIDTH-1:0] integrator; reg signed [ACC_WIDTH-1:0] comb_reg; reg [$clog2(DECIMATION)-1:0] cnt; reg valid_comb; // 积分器 always (posedge clk or negedge rst_n) begin if (!rst_n) integrator {ACC_WIDTH{1b0}}; else if (data_valid_in) integrator integrator data_in; end // 梳状器与抽取 always (posedge clk or negedge rst_n) begin if (!rst_n) begin comb_reg {ACC_WIDTH{1b0}}; cnt 0; data_out {DATA_WIDTH{1b0}}; data_valid_out 1b0; end else if (data_valid_in) begin if (cnt DECIMATION - 1) begin cnt 0; data_out integrator - comb_reg; comb_reg integrator; data_valid_out 1b1; end else begin cnt cnt 1b1; data_valid_out 1b0; end end end endmodule这个模块的典型问题是位增长积分器里数据不断累加位宽不够会溢出。解决方法是直接用全位宽累加不做截断最后输出时再截取高位。对于一阶CIC位增长上限是DATA_WIDTH ceil(log2(M))这对M16来说完全够用。2.4 顶层模块整合把数据流串起来顶层模块把上述子模块串成完整信号链。我用一个使能信号en_1mhz表示ADC采样有效数据进来后依次经过绝对值、滑动平均、CIC抽取最后输出demod_out。为了调试方便额外输出一个demod_valid方便示波器或逻辑分析仪对齐。module am_demod_top #( parameter DATA_WIDTH 16 )( input wire clk_1mhz, input wire rst_n, input wire signed [DATA_WIDTH-1:0] adc_data, input wire adc_valid, output reg signed [DATA_WIDTH-1:0] demod_out, output reg demod_valid ); wire signed [DATA_WIDTH-1:0] abs_out; wire signed [DATA_WIDTH-1:0] avg_out; wire signed [DATA_WIDTH-1:0] dec_out; wire avg_valid, dec_valid; abs_detector #(.DATA_WIDTH(DATA_WIDTH)) u_abs ( .clk(clk_1mhz), .rst_n(rst_n), .data_in(adc_data), .data_out(abs_out) ); moving_average #( .DATA_WIDTH(DATA_WIDTH), .AVG_LEN(16) ) u_avg ( .clk(clk_1mhz), .rst_n(rst_n), .data_in(abs_out), .data_valid_in(adc_valid), .data_out(avg_out), .data_valid_out(avg_valid) ); cic_decimate #( .DATA_WIDTH(DATA_WIDTH), .DECIMATION(16) ) u_dec ( .clk(clk_1mhz), .rst_n(rst_n), .data_in(avg_out), .data_valid_in(avg_valid), .data_out(dec_out), .data_valid_out(dec_valid) ); always (posedge clk_1mhz or negedge rst_n) begin if (!rst_n) begin demod_out {DATA_WIDTH{1b0}}; demod_valid 1b0; end else begin demod_out dec_out; demod_valid dec_valid; end end endmodule这样顶层代码非常干净每个子模块都可以独立仿真。adc_valid如果在你的系统里是持续为高的直接拉高就行只要采样率确实是1MHz。3. 仿真验证与板上调试完整流程3.1 写一个能生成AM信号的Testbench没有仿真直接上板等于闭着眼睛调电路。Testbench的核心任务是模拟出符合公式的AM信号然后喂给被测模块同时把中间节点导出来看波形。我用一个简单的频率累加器生成载波然后用乘法器调制具体代码timescale 1ns / 1ps module tb_am_demod; parameter DATA_WIDTH 16; parameter CLK_PERIOD 1000; // 1MHz - 1000ns reg clk; reg rst_n; reg signed [DATA_WIDTH-1:0] adc_data; reg adc_valid; wire signed [DATA_WIDTH-1:0] demod_out; wire demod_valid; // DUT am_demod_top #(.DATA_WIDTH(DATA_WIDTH)) dut ( .clk_1mhz(clk), .rst_n(rst_n), .adc_data(adc_data), .adc_valid(adc_valid), .demod_out(demod_out), .demod_valid(demod_valid) ); // 载波生成参数 localparam FC_WORD 100000 * 65536 / 1000000; // 载波100kHz用16位累加器 localparam FM_WORD 1000 * 65536 / 1000000; // 调制信号1kHz reg [15:0] carrier_phase; reg [15:0] audio_phase; wire signed [15:0] carrier $signed({1b0, carrier_phase[15:1]}) - 16384; wire signed [15:0] audio $signed({1b0, audio_phase[15:1]}) - 16384; always #(CLK_PERIOD/2) clk ~clk; initial begin clk 0; rst_n 0; adc_data 0; adc_valid 0; carrier_phase 0; audio_phase 0; #(CLK_PERIOD*10); rst_n 1; #(CLK_PERIOD*2); adc_valid 1; #(CLK_PERIOD*50000); $finish; end always (posedge clk) begin carrier_phase carrier_phase FC_WORD; audio_phase audio_phase FM_WORD; // AM调制载波幅度 A*(1 m*audio) adc_data $signed(carrier) * (16384 $signed({{8{audio[15]}}, audio[15:8]}) * 0) 15; end endmodule注意实际AM乘法可以用更简单的方式比如16位载波乘以常量小比例音频。上面代码里我保留了一个0系数换成你需要的调制指数即可。重点是把audio按1:20的幅值叠加到载波幅度上调制指数0.5相当于音频最大时载波幅度在原基础上下浮动50%。3.2 ModelSim/Vivado仿真操作步骤我用的是Vivado自带的仿真器操作步骤如下新建工程添加所有源文件abs_detector.v、moving_average.v、cic_decimate.v、am_demod_top.v。添加Testbench文件tb_am_demod.v设为仿真顶层。点击Run Simulation - Run Behavioral Simulation。在波形窗口添加信号dut.abs_out、dut.avg_out、dut.dec_out以及demod_out。运行时间至少设置10ms以上否则可能看不到完整几个周期的调制信号1kHz周期是1ms。仿真结果应该看到abs_out是一个频率为200kHz的整流波形avg_out波形变平滑dec_out是最终包络形状与1kHz音频一致。如果发现avg_out平滑度不够调大AVG_LEN或增加CIC级数。如果用的是ModelSim/Questa流程类似只是需要手动编译库和设置vsim -L unisims_ver之类Vivado直接一键运行更方便。我自己平时更喜欢Vivado的仿真器对AXI总线支持也好调试效率明显高。3.3 上板调试信号源、示波器还有ILA的实际操作仿真通过后上板调试是真正考验耐心的时候。我用的是Xilinx Artix-7系列型号XC7A35T开发板自带ADC的采样率2MS/s我把它固定配置成1MS/s和代码参数对齐。具体步骤如下用信号发生器产生AM信号载波100kHz调制信号1kHz调制深度30%。注意信号发生器的输出阻抗设为50欧姆幅度控制在ADC满量程的70%左右避免削顶失真。把AM信号连接到ADC输入引脚。如果开发板上有SMA接口用SMA线直连最稳杜邦线在100kHz下虽然能工作但容易引入噪声。综合布局布线生成bitstream并下载。打开Vivado硬件管理器添加ILA核。ILA的探针设置为顶层demod_out16位和demod_valid1位采样深度4096。触发方式设为demod_valid上升沿跑一次采集。用ILA看到的波形应该是一个缓慢变化的包络。如果波形很平检查ADC数据是否有有效位如果波形噪声大检查电源纹波和ADC参考电压。我之前踩过的最大坑是ADC数据位序反了。第一次上板时ILA里看到的输出是一个毛刺信号后来发现ADC输出是MSB在前而我的顶层代码里按LSB在前解析导致数据完全错乱。Debug方法很简单在ILA里看原始ADC波形如果不加输入信号时低噪声看起来很“陡”说明位序很可能反了调一下即可。4. 常见问题排查与实操经验4.1 解调常见问题速查表下面这几类问题是我在项目里真实遇到过的整理成表格方便按图索骥。现象可能原因排查方法输出全是直流没有交流包络绝对值模块输入输出位宽不匹配ADC没有真正采集到数据先用ILA抓ADC原始波形确认输入信号存在再用仿真验证abs模块逻辑输出波形有高频毛刺低通截止频率太高或者抽取前没滤波增大滑动平均窗口长度到32或64检查CIC抽取时序解调波形幅度很小调制指数设置过低或输入信号幅度没有达到ADC满量程增大信号发生器输出幅度到满量程的70%检查ADC配置寄存器输出波形有明显台阶滑动平均输出截断过狠或抽取计数器相位不对保留舍入位增加数据位宽检查CIC计数器是否从0开始仿真正常但上板无输出引脚约束错误或时钟未锁定检查xdc文件确认PLL锁定信号用ILA确认时钟存在信号失真包络有过冲输入信号幅度过大导致ADC削顶或绝对值溢出调低信号幅度检查ADC满量程判断排查思路的核心是“逐级定位”先用ILA看ADC原始数据再逐级看abs、avg、dec的输出哪一级异常就从哪一级开始查。不要一开始就怀疑算法先把数据通路打通。4.2 资源优化与代码编写经验这个项目的资源占用非常小在XC7A35T上综合后逻辑单元不到200个几乎不占资源。不过做工程项目时依然有几点值得留意不用在滑动平均里例化大量寄存器。我见过有人用移位寄存器数组存N个值每个周期整体搬移一遍N64时逻辑瞬间爆炸。采用环形指针或CIC结构资源消耗与N无关。尽量避免用浮点。FPGA做浮点除非有专门DSP单元否则代价极高。像这里所有系数都化成整数移位或者常量硬件实现简单时序也容易收敛。位宽宁可多留两位也不要少。特别是累加器位宽不足的表现很诡异平时波形正常一到信号幅度大的时候突然跳变。我吃过这亏后来习惯把中间级位宽按理论值再加2。仿真时用$dumpfile和$dumpvars导出VCD文件可以用GTKWave打开比自带波形工具更灵活适合批量对比多组参数。4.3 从AM解调延伸出去的几个方向做完这个项目后可以往几个方向扩展技术路线是相通的CIC多级级联与补偿滤波器如果对带外衰减要求更高用多级CIC并在抽取后加一级CIC补偿FIR这就是比较典型的数字下变频链路。DDC完整链路把NCO混频、CIC、FIR结合起来就是软件无线电里最常用的数字下变频器能直接接收中频信号。高阶调制解调AM解调做熟了再去碰DSB、SSB、FM会容易很多。FM解调核心是鉴频器本质上也是先做CORDIC相位提取再差分。和高速接口结合如果ADC速率上到几百MS/s输出就需要和DDR3、PCIe、JESD204B这些接口联动到时候你会用到类似高速串行收发器IBERT核调试通道或者用JESD204B完成ADC到FPGA的物理层对接。这些我在后续项目中逐一踩过的坑就是另一篇长篇分享了。5. 一些小技巧和最后的建议如果只让我给新手一个建议那就是先仿真再上板一步一步来。我最初做这个项目时跳过仿真直接上板结果ILA里看不到一个能解释的信号花了整整一个晚上排查最后发现只是ADC位序接反。有了仿真基础上板后只需确认AD数据和时钟链路就基本稳了。再分享一个调试技巧给顶层模块加一个测试模式用一个内部计数器的低几位作为伪随机或正弦数据源直接注入到解调链路里。这样在开发板上不需要信号发生器也能验证逻辑是否工作正常我当时就是靠这个功能在出差没有仪器的环境下把功能验证完的。这个思路对后续所有信号处理项目都适用。AM解调虽然是个几十年前就成熟的技术但作为FPGA数字信号处理的入门项目它完整覆盖了采样、滤波、抽取、时序配合、仿真验证这些核心技能。把这套逻辑吃透你会发现后面做DDC、DUC、甚至更复杂的调制解调系统时很多东西都是相通的。如果你也正在做这个项目希望这篇文章能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表