简介:这份资源面向FPGA数字信号处理初学者与通信原理课程设计者,提供基于VIVADO的完整调制解调工程实现。内容围绕AM调制展开,载波频率1M-10MHz、调制信号1kHz-10kHz,调制深度0-1.0可调,并通过VIO在线配置参数、ILA观测波形,解调误差要求优于1%,同时配套MATLAB数据验证流程,适合作为课设、竞赛或自学练手项目。压缩包共1338个文件,约95.58MB,以Verilog与VHDL源码、VIVADO工程脚本、仿真与综合日志、约束文件及MATLAB脚本为主,另含少量说明文档与配置文件,工程结构完整。目前已有3282人学习下载。读者可从中获得可直接运行的VIVADO工程、调制解调参考代码、VIO与ILA调试配置、MATLAB验证脚本及仿真测试环境,便于快速复现实验并理解AM调制解调链路的关键设计细节。
1. 从一块空板子到能收能发的调制解调链路:这份 VIVADO FPGA 工程到底给了什么
如果你手上正躺着一块 Zynq 或 Artix 开发板,想跑通一条完整的「基带调制 → 上变频 → 信道 → 下变频 → 解调」链路,却卡在 VIVADO 里那一堆 IP 核、时钟约束和 Testbench 上,那这份工程大概率能帮你省掉两周的试错时间。它不是一个只点亮 LED 的入门 Demo,而是一套能在 VIVADO 里直接综合、实现、上板验证的 FPGA 信号调制与解调参考设计,覆盖了 DDS 载波生成、ASK/FSK/PSK 调制、相干解调、低通滤波和判决输出这几个关键环节。适合谁?适合已经会写 Verilog、能看懂时序图,但没完整搭过通信链路的人;也适合做课程设计或项目预研、需要一份能跑通的基线工程来改参数的熟手。下面我按「先讲清结构、再动手复现、最后说坑」的顺序拆开讲。
2. 工程结构拆解:DDS、调制器、解调器在 VIVADO 里怎么摆
拿到一个 FPGA 工程,第一件事不是急着点 Generate Bitstream,而是先看清楚它的层次和时钟域。这份设计用的是典型的「DDS 产生载波 + 乘法器完成调制 + 相干解调 + FIR 滤波」结构,整体挂在同一个系统时钟下,通过分频产生符号速率。下面把关键模块和它们在 VIVADO 里的组织方式讲透。
2.1 顶层模块与时钟树:先搞清 50MHz 怎么分频出符号率
顶层模块一般叫top_modem之类,输入是板载晶振(常见 50MHz 或 100MHz),输出是 DAC 或 GPIO 上的调制波形。核心思路是:用一个 MMCM/PLL 把晶振倍频到一个较高的采样时钟(比如 100MHz),再用计数器分频得到符号时钟clk_sym。这里有个容易翻车的点——符号时钟不能直接用分频后的信号去驱动 always 块,否则会引入大量时钟域和毛刺。常见做法是用时钟使能en_sym代替分频时钟。
// 符号率使能生成:假设采样时钟 100MHz,符号率 1MHz // 每 100 个采样点产生一个符号使能脉冲 reg [6:0] cnt_sym; reg en_sym; always @(posedge clk_100m or negedge rst_n) begin if (!rst_n) begin cnt_sym <= 7'd0; en_sym <= 1'b0; end else if (cnt_sym == 7'd99) begin cnt_sym <= 7'd0; en_sym <= 1'b1; // 单周期脉冲,作为后续模块的使能 end else begin cnt_sym <= cnt_sym + 1'b1; en_sym <= 1'b0; end end这段代码的逻辑很直白:cnt_sym从 0 数到 99,数满就拉高一个周期的en_sym。参数说明——clk_100m是采样时钟,en_sym的周期就是符号周期。改符号率只需要改99这个比较值,比如要 500kHz 符号率就改成199。注意en_sym是使能不是时钟,后面所有符号级处理都用if (en_sym)来限定,这样整个设计只有一个时钟域,时序约束也好写。
2.2 DDS 载波生成:相位累加器位宽和频率控制字怎么算
DDS 是这份工程里最值得细看的模块。它的原理是相位累加器每个采样时钟累加一次频率控制字(FTW),累加结果的高位作为 ROM 地址查正弦表。输出频率公式是:
[ f_{out} = \frac{FTW \times f_{clk}}{2^N} ]
其中 N 是相位累加器位宽,f_clk是采样时钟。假设 N=32,f_clk=100MHz,想要 1MHz 载波,FTW 就是(1e6 × 2^32) / 100e6 ≈ 42949673。这个数在 Verilog 里写成 32 位十六进制是32'h028F5C29。
// 32 位相位累加器 DDS,输出 1MHz 正弦波 reg [31:0] phase_acc; always @(posedge clk_100m or negedge rst_n) begin if (!rst_n) phase_acc <= 32'd0; else phase_acc <= phase_acc + 32'h028F5C29; // FTW,改这里改频率 end // 取高 10 位作为 ROM 地址,对应 1024 点正弦表 wire [9:0] rom_addr = phase_acc[31:22]; wire [11:0] sin_out; sin_rom u_sin_rom ( .clka (clk_100m), .addra (rom_addr), .douta (sin_out) // 12 位有符号正弦样点 );逻辑说明:phase_acc每个时钟加一次 FTW,溢出自然回绕,这就是 DDS 的「相位连续」特性。参数说明——rom_addr取高 10 位意味着正弦表 1024 点,频率分辨率由 FTW 决定,位宽越大分辨率越高。常见坑是 ROM 初始化文件(.coe)的位宽和深度与 IP 核配置不一致,导致输出全是 0 或乱码,这个后面避坑章节会细说。
2.3 调制器实现:ASK/FSK/PSK 在 RTL 里怎么区分
三种基本调制在这份工程里是并列的,通过一个模式选择信号切换。ASK 最简单,用基带数据直接控制载波幅度;FSK 用数据选择两个不同的 FTW;PSK 用数据控制相位偏移。下面以 BPSK 为例,因为它最能体现「相位」这个维度。
// BPSK 调制:数据为 1 时相位加 180 度,即相位累加器加半周期偏移 // 半周期对应 2^31(32 位累加器) wire [31:0] phase_offset = data_bit ? 32'h80000000 : 32'h00000000; wire [31:0] phase_final = phase_acc + phase_offset; wire [9:0] rom_addr_bpsk = phase_final[31:22];逻辑说明:data_bit是基带比特,为 1 时给相位累加器叠加2^31,相当于把正弦波平移半个周期,也就是 180 度相移。参数说明——32'h80000000是 32 位累加器的一半,对应 180 度;如果要做 QPSK,就用两位数据映射到 0、90、180、270 四个偏移量。注意这里phase_final是组合逻辑,直接接 ROM 地址,综合时不会出问题,但仿真时要注意data_bit的建立时间。
2.4 解调器与滤波:相干解调为什么需要本地载波同步
解调部分用的是相干解调,核心是把接收信号和本地恢复的载波相乘,再经过低通滤波取出基带分量。这里最容易被忽略的是本地载波的相位对齐——如果本地载波和发送载波差 90 度,解调输出直接为零。工程里用了一个简单的 Costas 环或者平方环来做载波恢复,具体实现取决于版本。
// 相干解调:接收信号与本地载波相乘 wire signed [11:0] rx_signal; // 来自 ADC 或仿真输入 wire signed [11:0] local_carrier; wire signed [23:0] product = rx_signal * local_carrier; // 乘法器输出 // 低通滤波:用 FIR 编译器生成的 IP 核 wire signed [23:0] baseband; fir_lpf u_fir_lpf ( .aclk (clk_100m), .s_axis_data_tvalid (1'b1), .s_axis_data_tdata (product[23:8]), // 截取高位,防止位宽爆炸 .m_axis_data_tvalid (), .m_axis_data_tdata (baseband) );逻辑说明:乘法器输出位宽是 24 位,直接送 FIR 会位宽不匹配,所以截取[23:8]保留高 16 位。参数说明——FIR 低通滤波器的截止频率要设在符号率附近,比如符号率 1MHz,截止频率就设 1.5MHz 左右,过渡带留够。常见做法是用 VIVADO 的 FIR Compiler IP,系数用 MATLAB 的fdatool生成,导出成 .coe 文件加载。
3. 从零复现:VIVADO 建工程、加 IP、跑仿真的完整步骤
上一章讲的是「里面有什么」,这一章讲「你怎么把它跑起来」。我按自己复现时的顺序走一遍,每一步都给出关键操作和参数,你照着做基本不会卡。
3.1 新建工程与添加源文件:器件选型和目录结构
打开 VIVADO,Create Project,器件选你板子上实际的那颗。比如 Artix-7 的xc7a35tfgg484-2或者 Zynq 的xc7z020clg400-2。工程目录建议这样分:rtl/放 Verilog 源文件,ip/放 IP 核,sim/放 Testbench,constr/放 XDC 约束文件。把工程里的.v文件全部 Add Sources,注意顶层模块要设为 Top。
这里有个细节:如果工程里用了 ROM IP 和 FIR IP,这些 IP 的.xci文件也要一起加进来,否则综合时会报「module not found」。常见做法是先把 IP 核的.xci拖进工程,等 IP 生成完毕再综合。
3.2 IP 核配置:DDS Compiler 和 FIR Compiler 的关键参数
VIVADO 自带 DDS Compiler,但这份工程用的是自己写的相位累加器 + ROM,所以只需要配置一个 Block Memory Generator 作为正弦表。关键参数:
| 参数项 | 设置值 | 说明 |
|---|---|---|
| Memory Type | Single Port ROM | 只读,一个地址端口 |
| Width | 12 | 正弦样点位宽,有符号 |
| Depth | 1024 | 正弦表点数 |
| Enable Port Type | Always Enabled | 简化控制 |
| Init File | sin_1024.coe | 正弦表系数文件 |
FIR Compiler 的配置更关键,直接决定解调能不能出正确波形:
| 参数项 | 设置值 | 说明 |
|---|---|---|
| Filter Type | Single Rate | 单速率,不抽取 |
| Coefficient Width | 16 | 系数位宽 |
| Data Width | 16 | 输入数据位宽 |
| Coefficient File | lpf_1p5m.coe | 低通系数 |
| Output Rounding | Truncation | 截断,省资源 |
配置完点 OK,等 IP 生成。注意 DDS 的 ROM 初始化文件格式是每行一个十六进制数,位宽要和配置一致,否则会报错。
3.3 Testbench 编写:怎么在仿真里看到调制波形
上板之前一定要先仿真。写一个 Testbench,产生时钟和复位,给基带数据一个简单的 0/1 序列,然后观察调制输出和解调输出。
// Testbench 核心部分:产生 100MHz 时钟和基带数据 reg clk = 0; reg rst_n = 0; always #5 clk = ~clk; // 100MHz,周期 10ns reg data_bit; initial begin rst_n = 0; data_bit = 0; #100 rst_n = 1; // 发送 1010 序列,每个比特持续 1us(对应 1MHz 符号率) #1000 data_bit = 1; #1000 data_bit = 0; #1000 data_bit = 1; #1000 data_bit = 0; #5000 $finish; end // 例化顶层 top_modem u_top ( .clk_100m (clk), .rst_n (rst_n), .data_in (data_bit), .mod_out (), .demod_out() );逻辑说明:#5 clk = ~clk产生 10ns 周期时钟,#1000对应 1us,正好是一个符号周期。参数说明——data_bit的变化时刻要和en_sym对齐,否则解调采样点会偏。仿真时把mod_out和demod_out加到波形窗口,看解调输出是否在几个符号周期后恢复出 1010。
3.4 综合、实现与上板:XDC 约束和 ILA 调试
仿真通过后,加 XDC 约束。至少要有:主时钟约束create_clock -period 10.000 [get_ports clk],以及输出引脚约束。如果板子上有 DAC,把调制输出接到 DAC 数据口;如果没有,可以用 GPIO 输出,用示波器看波形。
上板后如果波形不对,用 ILA(Integrated Logic Analyzer)抓内部信号。在综合后的网表里找到phase_acc、baseband这些信号,Mark Debug,重新实现后就能在 Hardware Manager 里看到实时波形。常见做法是抓en_sym和demod_out,看解调输出是否在符号使能时稳定。
4. 避坑与排查:仿真对、上板错,问题出在哪
这一章是我自己踩过的坑,每条都按「现象 → 原因 → 解决」写。如果你卡住了,先对照这里排查。
4.1 现象:仿真波形完美,上板后解调输出全是噪声
原因:仿真时用的是理想时钟和理想数据,上板后晶振有抖动,而且 ADC/DAC 的采样时刻和 FPGA 内部时钟不同步,导致采样点偏移。另一个常见原因是 XDC 里没有对输入数据做时序约束,建立/保持时间不满足。
解决:在 XDC 里加set_input_delay和set_output_delay,根据 ADC/DAC 的手册填数值。如果抖动严重,在解调前加一个简单的滑动平均滤波。我一般会先用 ILA 抓rx_signal,看采样点是不是落在符号中心,偏了就调en_sym的相位。
4.2 现象:DDS 输出频率和计算值对不上,差一个固定比例
原因:相位累加器位宽和 FTW 计算时用的 N 不一致。比如代码里累加器是 32 位,但计算 FTW 时用了 16 位,结果频率差 65536 倍。另一个可能是 ROM 地址截取位数不对,导致波形周期数变化。
解决:统一位宽。在代码里用parameter PHASE_WIDTH = 32;定义,FTW 计算和地址截取都用这个参数。改频率时只改 FTW,不改位宽。仿真时用$display打印实际周期,和理论值对比。
4.3 现象:FIR 滤波器输出一直是 0 或者饱和
原因:输入数据位宽和 FIR IP 配置的 Data Width 不匹配,或者系数文件格式不对。常见的是系数用了浮点数,但 IP 要求定点整数。另一个原因是乘法器输出截取位置不对,把有效位截掉了。
解决:检查 FIR IP 的 Data Width 和系数位宽,确保和代码里送入的数据一致。系数用 MATLAB 生成时,用fdatool的Fixed-point导出,或者手动乘以2^15取整。截取时保留符号位和高位,比如 24 位乘积截[23:8]保留 16 位。
4.4 现象:综合报时序违例,但功能仿真正常
原因:组合逻辑路径太长,比如phase_acc + phase_offset直接接 ROM 地址,中间没有寄存器。或者乘法器没有用 DSP 硬核,综合成 LUT 后延迟大。
解决:在关键路径插寄存器。比如把phase_final打一拍再接 ROM,代价是输出延迟一个时钟,但时序能过。乘法器用(* use_dsp = "yes" *)属性强制映射到 DSP48。如果还不过,降低采样时钟频率,或者把 FIR 拆成多级。
4.5 现象:上板后 ILA 抓不到信号,或者抓到的全是 0
原因:ILA 的采样时钟和被测信号时钟不一致,或者 Mark Debug 的信号被综合优化掉了。另一个原因是 ILA 的采样深度不够,抓不到慢速信号。
解决:确保 ILA 的clk接的是被测信号所在的时钟域。Mark Debug 时选DEBUG属性,不要选DONT_TOUCH。采样深度设大一点,比如 4096,触发条件设成en_sym == 1。如果信号被优化,在代码里加(* keep = "true" *)。
5. 进阶技巧:把固定调制改成可配置,以及一个验证习惯
工程跑通之后,下一步通常是让它更通用。我一般会做两件事:一是把调制模式和符号率做成寄存器可配置,二是加一个简单的误码率统计模块。下面说具体怎么做。
5.1 用 AXI-Lite 寄存器配置调制参数
如果板子是 Zynq,可以加一个 AXI-Lite 从机,把 FTW、符号率分频值、调制模式这些参数映射到寄存器。PS 端通过Xil_Out32写寄存器,PL 端实时生效。关键是把参数打两拍同步到 PL 时钟域,避免亚稳态。
// AXI-Lite 写寄存器后,同步到 PL 时钟域 reg [31:0] ftw_reg_axi; reg [31:0] ftw_reg_pl; always @(posedge clk_100m) begin ftw_reg_pl <= ftw_reg_axi; // 打一拍同步 end // DDS 用 ftw_reg_pl 作为频率控制字这样改频率不用重新综合,PS 端写个数就行。参数说明——ftw_reg_axi是 AXI 时钟域,ftw_reg_pl是 PL 时钟域,中间加两级寄存器更稳。
5.2 误码率统计:怎么在 FPGA 里对比发送和接收比特
误码率统计需要把发送的基带比特延迟到和解调输出对齐,然后逐比特比较。延迟量等于调制、信道、解调的总流水线延迟,可以在仿真里量出来。
// 误码统计:延迟后的发送比特与解调比特比较 reg [7:0] delay_cnt; reg tx_delayed; always @(posedge clk_100m) begin if (en_sym) begin delay_cnt <= delay_cnt + 1'b1; if (delay_cnt == 8'd10) // 延迟 10 个符号对齐 tx_delayed <= data_bit; end end reg [15:0] err_cnt; always @(posedge clk_100m) begin if (en_sym && (tx_delayed != demod_bit)) err_cnt <= err_cnt + 1'b1; end逻辑说明:delay_cnt数到 10 后把data_bit锁存到tx_delayed,和解调输出demod_bit比较。参数说明——延迟值 10 是仿真量出来的,实际工程里可能不同,用 ILA 抓一下对齐点。err_cnt可以接到 AXI 寄存器让 PS 读。
5.3 一个验证习惯:先跑通环路,再改参数
最后说个习惯。我刚开始做这个工程时,总想一步到位把符号率、载波频率、滤波器系数全改成自己想要的,结果一上板全是错,排查都不知道从哪下手。后来学乖了:先用工程默认参数跑通整个环路,用 ILA 确认每个节点的波形都对,再一个一个改参数,每改一个就仿真一次、上板一次。这样出问题能立刻定位到是哪个参数引起的。
从那以后我每次拿到新的 FPGA 通信工程,都强制走一遍「默认参数 → 逐点验证 → 单参数修改」的流程,再急也不跳步。希望帮到你。
本文还有配套的精品资源,点击获取