
简介基于赛灵思A7系列FPGA的FM信号调制解调通信系统完整工程面向FPGA开发人员和通信电子类学生既适合学习数字下变频、FM调制解调等算法在硬件中的实现也适合作为中频信号处理项目的工程模板可用于课程设计、毕业设计或工程预研的基础版本。压缩包为RAR格式共七百九十一个文件整体大小约一百零五点九兆字节内容以硬件描述语言源码为主另有约束文件、核配置脚本、系数与存储器初始化文件、仿真脚本和综合实现过程文件覆盖工程建立、逻辑综合、仿真验证的主要流程能够反映从源码编写、IP核配置到仿真实现的全过程。系统设计覆盖从模数/数模转换接口驱动到数字下变频、FM调制解调的完整信号链路并带有综合完成标记说明工程可复现性较强导入赛灵思A7平台后即可进行代码阅读、模块仿真与二次开发尤其适合用来拆解数字下变频与解调输出的数据处理细节。已有三百五十二人学习适合关注无线通信基带处理的FPGA工程师或研究生使用可以帮助深入理解数字解调算法与硬件电路之间的映射关系为设计类似调制解调系统提供参考。1. FM解调为什么要在FPGA里做FM解调这件事教科书只画到“鉴频器加低通滤波”就停了真正动手写Verilog的时候第一行代码就会卡住数字域里的“瞬时频率”到底怎么提取这套基于Xilinx Artix-7A7的FM调制解调系统给出的是一个能完整跑通的答案。AD/DA驱动、数字下变频DDC、NCO混频、CIC抽取、正交鉴频一条信号链从板卡输入口一直通到音频输出。源码是供学习参考的但结构是工程级的采样率、抽取比、位宽分配这些参数都摆在明面上。对做fpga开发的熟手值得看的是定点化取舍和资源评估对刚入门、正在规划fpga学习路线的人它是一个比孤立教程更完整的参考系。2. FM解调的数学原理与数字下变频架构2.1 先把“相位差分”这个核心说透FM信号的本质是信息调制在瞬时频率上。写成公式就是s(t) A_c · cos(2πf_c·t 2πk_f·∫₀ᵗ m(τ)dτ)对相位求一阶导得到瞬时角频率FM解调在数学上就一句话把相位求出来再做差分差分结果正比于原始调制信号。这句话简单但它是后面所有模块的指挥棒。正交下变频把相位信息拆成I、Q两路I[n] A[n]·cos(φ[n])Q[n] A[n]·sin(φ[n])从I/Q恢复相位工业上有两条路。一条是CORDIC核直接算arctan(Q/I)相位结果与幅度A无关最稳另一条是叉积鉴频器y[n] I[n]·Q[n-1] - Q[n]·I[n-1] ≈ A²·sin(φ[n] - φ[n-1])相位差小时sin(Δφ)≈Δφ输出就直接正比于瞬时频偏。叉积法的代价是输出带了一个A²因子输入信号幅度一旦波动就会被当成噪声调制进去所以工程里要么在前面加AGC要么对I/Q做归一化。两种方案在后续实现里都有落点选择依据是资源预算和信号动态范围不是哪个更“先进”。2.2 DDC架构选型低中频采样是A7平台的正解数字下变频要干的事是把中心频率在f_IF的中频信号搬到零频附近同时把采样率降下来。FPGA里常见的射频接收架构有三类对比如下架构模拟前端复杂度ADC采样率FPGA资源压力A7平台可行性直接射频采样低高于两倍射频频率数据率极高LUT/FF撑不住基本不可行超外差加低中频中20至100 MSPS中等推荐本工程落点零中频正交采样高需要双路模拟I/Q链路2至20 MSPS较低可行但模拟端贵本工程走的是“模拟射频前端把信号搬到一个几十MHz的低中频FPGA用并行ADC采样后做全数字处理”。这个选择有两个实际原因。一是Artix-7这类中端FPGA硬核资源主要是DSP48E1和BRAM直接射频采样会在布线阶段把时序逼到极限二是FM广播这类窄带信号用一个65MSPS级别的ADC采样10.7MHz中频混叠裕量很充裕。工程里常见的配套是AD9226这类12bit并行ADC数据口直接接FPGA引脚驱动逻辑只需要一个状态机加一组IO延迟约束。2.3 采样率、中频和抽取因子的匹配这一步是系统设计的锚点必须先定参数再写代码。FM广播的基带需求是只解单声道15kHz带宽就够要解带RDS或立体声副载波基带得留到53kHz左右。假设ADC采样率65MSPSNCO把10.7MHz中频搬到基带后CIC抽取因子R决定输出码率也决定信号通带。抽取因子R输出码率53kHz带内CIC下垂工程判断128507.8 kHz很小可解立体声但FIR压力大256253.9 kHz约0.2 dBFM广播推荐档位512127 kHz约1.9 dB单声道够用需补偿抽取因子不是越大越好。CIC滤波器的幅频响应是sin(x)/x形状R越大通带边缘的下垂越明显。在R512档53kHz处大约掉了1.9dB听感是高频发闷必须在后面的FIR或者音频通路里做反向下垂补偿。所以工程里常见的做法是CIC只做256倍抽取再让一个53kHz的FIR滤波器同时完成低通整形和CIC补偿。抽取比、带内纹波和FIR阶数三者互相牵制改R之后一定要回来看FIR的通带设计这是一个在仿真里很容易暴露、上板却很难定位的问题。3. Verilog核心模块NCO、CIC与正交鉴频器的定点化实现3.1 本振NCODDS Compiler的频率控制字与位宽Vivado里生成正交本振直接用DDS Compiler IP核。推荐配置是Phase Increment Streaming模式相位累加器32位输出位宽16位。ADC采样时钟65MHzNCO要输出10.7MHz频率控制字的计算是FTW f_out × 2³² / f_clk 10.7e6 × 2³² / 65e6 ≈ 0x2A250000例化模板// DDS Compiler IP核例化输出10.7MHz正交本振I路为cosQ路为sin dds_compiler_0 dds_lo ( .aclk (clk_65m), .s_axis_phase_tvalid (1b1), .s_axis_phase_tdata (32h2A250000), // 频率控制字 .m_axis_data_tvalid (lo_valid), .m_axis_data_tdata ({lo_i, lo_q}) // 高16bitcos低16bitsin );32位累加器在65MHz采样钟下的频率分辨率约0.015Hz远好于FM解调需求。如果后续要微调中心频点只需要替换FTWDDS核本身不用重生成。ADC的12bit样本进混频器之前要和NCO的I/Q对齐到同一个时钟域。混频器本质就是两个有符号乘法器// 混频器ADC样本与NCO_I/Q相乘结果截位到20bit wire signed [11:0] adc_d adc_data; // AD9226 12bit输出 wire signed [15:0] lo_i_s lo_i; wire signed [15:0] lo_q_s lo_q; wire signed [27:0] mix_i_full adc_d * lo_i_s; wire signed [27:0] mix_q_full adc_d * lo_q_s; wire signed [19:0] mix_i mix_i_full[27:8]; // 截低8bit保留符号 wire signed [19:0] mix_q mix_q_full[27:8];混频后截位是FPGA定点化里最常见的坑。12bit ADC乘以16bit NCO得到28bit如果直接给CIC积分器位宽会被顶到40bit以上BRAM和LUT消耗翻倍。截位不是随便砍要保留足够的符号扩展裕量。上面取高20bit是因为后面CIC归一化增益会占掉一部分位宽给积分器留出回旋余量。3.2 CIC抽取滤波器手写还是用IP核CIC的本质是先积分后梳状结构简单适合大抽取比。三阶CIC、抽取256倍的增益高达256³积分器位宽必须按增益上限预留。工程上两种做法都常见手写一个参数化的Hogenauer结构或者直接用Xilinx CIC Compiler IP。手写的优势是想改抽取比和级数随时改缺点是时序和位宽要自己盯。给一个参数化简化版参考module cic_decim #( parameter IW 20, parameter OW 20, parameter RATE 256, parameter STAGES 3 )( input wire clk, input wire rst_n, input wire signed [IW-1:0] din, output reg signed [OW-1:0] dout, output reg valid ); localparam AW IW $clog2(RATE) * STAGES 1; integer i; reg signed [AW-1:0] integ [0:STAGES-1]; reg signed [AW-1:0] comb_dly [0:STAGES-1]; wire signed [AW-1:0] comb_out [0:STAGES-1]; reg [$clog2(RATE)-1:0] cnt 0; assign comb_out[0] integ[STAGES-1] - comb_dly[0]; generate for (i 1; i STAGES; i i 1) begin : comb_chain assign comb_out[i] comb_out[i-1] - comb_dly[i]; end endgenerate always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i STAGES; i i 1) begin integ[i] 0; comb_dly[i] 0; end cnt 0; dout 0; valid 0; end else begin integ[0] integ[0] din; for (i 1; i STAGES; i i 1) integ[i] integ[i] integ[i-1]; valid 0; if (cnt RATE - 1) begin for (i 0; i STAGES; i i 1) comb_dly[i] comb_out[i]; dout comb_out[STAGES-1] (STAGES * $clog2(RATE)); valid 1; cnt 0; end else begin cnt cnt 1; end end end endmodule逻辑说明积分器每个时钟累加输入位宽按最大增益加符号位预留防止大信号时积分器溢出抽取计数器计数到RATE-1时把最后一级积分器结果与梳状延迟相减再依次经过多级差分最后算术右移归一化。参数说明AW必须大于等于IW加3乘log2(RATE)再加1否则满幅输入下CIC内部会翻转输出表现为严重破音这个现象在仿真里不加大信号是看不出来的。valid信号打一拍输出给下游FIR做输入节拍控制。如果时序紧张在comb_out到dout之间可以再插一级流水代价是增加一拍延迟对FM解调这种慢速音频链路无感知。如果项目节奏紧我更推荐直接用CIC Compiler IP核参数选择Decimation、三阶、差分延迟1、抽取256输出位宽20bit。提示CIC Compiler IP核会自动计算内部位宽但输出截位仍然由你决定手写CIC时务必用满幅信号做仿真否则很难发现积分器溢出。3.3 鉴频器叉积方案与CORDIC方案的取舍CIC输出I/Q进鉴频器之前先做幅度归一化。这一步在叉积方案里是必须的否则A²因子会把信号幅度起伏直接送进音频。下面是叉积鉴频的完整实现// 叉积鉴频y I[n-1]*Q[n] - Q[n-1]*I[n] // 输出正比于 sin(瞬时频偏)小频偏时约等于瞬时频偏 reg signed [19:0] i_dly, q_dly; wire signed [39:0] p1 i_dly * q_in; // I[n-1]·Q[n] wire signed [39:0] p2 q_dly * i_in; // Q[n-1]·I[n] wire signed [40:0] freq_raw p1 - p2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin i_dly 0; q_dly 0; end else begin i_dly i_in; q_dly q_in; end endfreq_raw的正负号对应频偏方向数值对应调制深度。保留40bit是有意为之如果直接截到16bit直流偏置会被放大。对音质要求高或者不想写AGC的场景改用CORDIC// CORDIC IParctan模式相位输出为16bit定点角 cordic_atan inst_atan ( .aclk (clk_65m), .s_axis_cartesian_tvalid (valid_in), .s_axis_cartesian_tdata ({i_in[19:4], q_in[19:4]}), .m_axis_dout_tvalid (valid_phase), .m_axis_dout_tdata (phase_out) );CORDIC输出的相位再做一阶差分// 相位差分注意跨±π边界时需要解卷绕 reg signed [15:0] phase_d; wire signed [16:0] diff phase_out - phase_d; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_d 0; else phase_d phase_out; end相位差分在±π边界处会产生一个2π的假跳变听感是周期性爆音。简单的处理办法是在差分后判断绝对值超过π就加减2π或者干脆用叉积法规避。工程里看到解调输出周期性咔哒声多半就是这个边界没处理。两种方案资源对比如下方案DSP48E1LUT/FF延迟幅度敏感性叉积鉴频2少1拍敏感需AGCCORDIC加差分0中等约12拍不敏感最稳4. 工程级信号链联调从仿真到AD/DA实测4.1 Vivado工程结构与时钟域这套系统从模块文件看顶层往下基本是top_fm_rx顶层adc_driverAD9226并行12bit采集ddc_chainNCO、混频、CIC、FIR补偿滤波fm_demod叉积或CORDIC鉴频加DC blockerdac_driverAD9708输出音频clk_genMMCM产生65MHz主时钟和音频码率脉冲时钟域是这条链路最容易出问题的地方。我的做法是整条DDC和鉴频链路全部跑65MHz主时钟CIC抽取后的数据率是65MHz除以256约254kHz再用一个十分频计数器产生127kHz的音频更新脉冲驱动DAC。这样从ADC采样到DAC输出只有一个时钟域省掉一组异步FIFO。DAC更新率用脉冲使能而不是独立时钟避免在FPGA里产生第三个时钟域。4.2 AD/DA驱动的写法AD9226这类并行ADC数据在采样时钟上升沿对齐驱动逻辑极简module adc_driver #( parameter DW 12 )( input wire clk, input wire rst_n, input wire [DW-1:0] adc_data, output reg [DW-1:0] sample, output reg sample_valid ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin sample 0; sample_valid 0; end else begin sample adc_data; sample_valid 1b1; end end endmodulesample_valid每周期拉高因为ADC每个时钟都出新数据不需要节流。如果改用串行ADC或LVDS接口驱动状态机要改成移位寄存器加帧同步信号结构不能套这个模板。IO时序约束上对adc_data要加set_input_delay一般取采样时钟周期的40%到60%set_input_delay -clock [get_clocks clk_65m] -max 8.0 [get_ports adc_data[*]] set_input_delay -clock [get_clocks clk_65m] -min 1.0 [get_ports adc_data[*]]DA侧更简单把鉴频输出的16bit音频在127kHz更新率下送到AD9708数据口。如果DAC位数是8bit用算术右移而不是直接取高位否则低幅度负信号会被削成直流偏置assign dac_data audio_out 8; // 16bit到8bit保留符号4.3 仿真激励的搭建与检查点仿真里不要直接用一个理想正弦去喂解调链最好生成完整的FM调制信号作为激励。Xilinx的DDS Compiler可以直接拿来做FM调制把调制信号叠加到频率控制字上输出就是一个调频波// 发送端FM调制仿真模型 assign mod_ftw 32h2A250000 $signed(m_t) * 32d50; dds_compiler_0 dds_fm_tx ( .aclk (clk_65m), .s_axis_phase_tvalid (1b1), .s_axis_phase_tdata (mod_ftw), .m_axis_data_tdata (fm_tx_raw) );m_t叠加到频率字上就是在做FM调制50这个系数决定最大频偏实际取值按目标调制指数标定。仿真里重点看三个位置CIC输出I/Q应该是等幅正交的正余弦幅度波动代表积分器溢出或截位过狠freq_raw应该是缓慢变化的基带信号有明显台阶就去查CORDIC相位跨边界处理DAC输出用数学工具画频谱带外分量要低于负60dBc。4.4 上板调试ILA探针和故障特征上板后第一条主线是确认AD数据方向正确。用ILA抓adc_data正常输入下信号应该在跳动如果全0或全1多半是采样时钟引脚约束错误或ADC前端没工作。然后抓DDC输出正常情况下I/Q有明显正弦包络不对就同时抓NCO频率字和混频输出逐步往上游排查。故障表现最可能原因查看位置输出音频有高频嘶嘶声FIR带宽过大或CIC抽取不足ddc_chain内FIR系数音量随信号强弱飘叉积鉴频缺少AGCfm_demod输入前级大信号破音CIC积分器溢出AW位宽、抽取比周期性咔哒声CORDIC跨±π没解卷绕phase_out差分值完全无声且I/Q为0ADC输入时钟和引脚约束不匹配adc_data的ILA波形5. 把指标再抠一遍位宽、抽取比与去加重5.1 位宽自查清单FM解调这条链路最容易犯的错误不是算法选错而是位宽在某一级悄悄爆掉。我一般会按下面这张表逐级核对节点建议位宽溢出检查方法ADC输入12bit统计最大值是否接近正负2047混频器输出20bit满幅输入仿真看中间节点CIC积分器输入位宽加3乘log2(R)加1输入满幅正弦看是否削波鉴频输出32bit以上最大频偏时看是否饱和DAC数据等于DAC位宽负数要算术右移波形底部是否被削平一个实际经验当输入给满幅FM信号时CIC输出如果偶尔出现大毛刺先提高AW位宽而不是去调滤波器系数这一步往往能省下半天debug时间。5.2 去加重与FFT验证FM广播在发射端做了预加重接收端必须做去加重时间常数75微秒。一阶IIR实现// alpha按采样率换算fs127kHz时取0.98左右 y[n] x[n] - x[n-1] alpha * y[n-1];Verilog里对应一个18bit系数乘法器。顺序上一定先做去加重再做音量归一化否则音量会随着频段起伏。最后验证不要只靠听感把DAC输出抓回来做FFT。解调一段1kHz正弦FM信号取4096点做FFT1kHz处应出现明显谱峰带外杂散小于负50dBc再用扫频信号遍历15kHz到53kHz看幅度是否是一条平坦线就能判断CIC下垂补偿是否到位。这套工程跑到这一步整条DDC链路就算真正验证完了。以后换频段、换带宽只需要把FTW、抽取比和FIR通带三个参数一起替换仿真激励和ILA观测点都不用动。本文还有配套的精品资源点击获取