简介:这份PDF是一份面向电子信息工程、通信工程专业学生及FPGA初学者的课程设计报告,围绕基于FPGA的FSK调制解调器展开,帮助读者理解数字调制解调原理并完成VHDL硬件实现。报告以Quartus II为开发平台,系统讲解FSK调制与解调的基本原理,给出载频f1=12.5MHz、f2=2MHz、调制信号1MHz的具体参数设计,并附有VHDL调制与解调程序、仿真测试结果及结论分析,涵盖绪论、原理、设计方案、测试与参考文献等完整章节。资源包为单个PDF文件,大小约936KB,内容结构清晰,便于按章节查阅与对照学习。目前已有142人浏览学习,适合需要完成FPGA/CPLD课程设计、准备通信原理实验或学习VHDL硬件描述语言的读者参考,可从中获取完整的FSK系统设计思路、程序框架与仿真验证方法。
1. 从一份 PDF 标题说起:FPGA 上跑 FSK 到底难在哪
一份名为“基于FPGA的FSK调制解调.pdf”的文档,光看标题其实信息量不小:它锁定了三个关键词——FPGA、FSK、调制解调。对做数字通信基带的人来说,这不是新鲜组合,但真正把它落到板子上跑通的人都知道,坑不在“FSK 是什么”,而在“为什么在 FPGA 上做 FSK 会翻车”。FSK(频移键控)原理简单到可以用一句话说清:用不同频率的正弦波表示 0 和 1。可一旦放进 FPGA,你要面对的是采样率、相位连续性、滤波器延迟、判决门限漂移这一连串工程问题。这份文档大概率是在讲一套完整的调制加解调链路,从 DDS 产生载波,到鉴频或非相干解调,再到判决输出比特流。它适合两类人:一类是通信或电子专业的学生,需要把课本上的 FSK 波形变成板子上的真实信号;另一类是做工业数传、遥控遥测的工程师,想用 FPGA 替代专用调制解调芯片,换取灵活性和成本优势。接下来我不复述那份 PDF,而是按我实际做过的路径,把这条链路拆成能复现的步骤。
2. FSK 调制在 FPGA 里的最小实现路径
2.1 为什么选 DDS 而不是查表法
在 FPGA 上产生 FSK 信号,常见做法有两种:查表法和 DDS(直接数字频率合成)。查表法是把一个完整周期的正弦波采样点存进 ROM,按不同步进读地址。DDS 则是用相位累加器加波形 ROM,频率由频率控制字决定。两者本质接近,但 DDS 的优势在于频率切换时相位连续,这对 FSK 至关重要。如果相位不连续,频谱会扩散,解调端误码率直接上去。我一般会选 DDS,因为频率控制字可以实时改,切换两个频率只需要在符号边界更新频率控制字,相位累加器不停。
具体参数上,假设系统时钟 100 MHz,目标载波中心频率 1 MHz,频偏 100 kHz,符号速率 10 kbps。相位累加器位宽选 32 位,频率控制字 = f_out × 2^32 / f_clk。算下来 1 MHz 对应约 42,949,673,1.1 MHz 对应约 47,244,640,0.9 MHz 对应约 38,654,706。这些数值直接写进寄存器,在符号切换时更新。
2.2 用 Verilog 写一个可综合的 FSK 调制器
下面这段代码是一个简化但可综合的 FSK 调制器核心。它包含相位累加器、频率控制字选择、以及一个 10 位正弦波 ROM 的地址生成。ROM 内容可以用 MATLAB 或 Python 生成后初始化。
module fsk_modulator ( input wire clk, // 系统时钟 100 MHz input wire rst_n, // 低电平复位 input wire bit_in, // 输入比特流 input wire bit_valid, // 比特有效指示 output reg [9:0] wave_addr, // 波形 ROM 地址 output reg wave_en // 波形输出使能 ); reg [31:0] phase_acc; // 32 位相位累加器 reg [31:0] freq_word; // 当前频率控制字 reg [31:0] freq_word_0; // 比特 0 对应的频率控制字 reg [31:0] freq_word_1; // 比特 1 对应的频率控制字 initial begin freq_word_0 = 32'd38654706; // 0.9 MHz freq_word_1 = 32'd47244640; // 1.1 MHz end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc <= 32'd0; freq_word <= freq_word_0; wave_en <= 1'b0; end else begin if (bit_valid) begin // 在符号边界更新频率控制字,保证相位连续 freq_word <= bit_in ? freq_word_1 : freq_word_0; wave_en <= 1'b1; end // 相位累加器持续累加,不因频率切换而清零 phase_acc <= phase_acc + freq_word; // 取高 10 位作为 ROM 地址,对应 1024 个采样点 wave_addr <= phase_acc[31:22]; end end endmodule这段代码的关键点有三个。第一,phase_acc在频率切换时不清零,只更新freq_word,这样相位是连续的。第二,wave_addr取的是相位累加器的高 10 位,相当于把 2^32 的相位范围映射到 1024 个 ROM 地址。第三,bit_valid的时序必须和符号速率对齐,通常由外部符号定时模块产生。如果bit_valid抖动,频率切换时刻就会偏移,解调端眼图会闭合。
2.3 符号速率与系统时钟的匹配计算
符号速率 10 kbps 意味着每个比特持续 100 μs。系统时钟 100 MHz,周期 10 ns,所以每个符号持续 10000 个时钟周期。你需要一个计数器,每 10000 个周期产生一次bit_valid。这个计数器就是符号定时器。如果符号速率改成 50 kbps,计数器上限变成 2000。这里有个容易忽略的点:符号定时器的精度直接影响 FSK 频谱的旁瓣。如果计数器有 ±1 个时钟周期的抖动,在 100 MHz 下是 10 ns,相对于 100 μs 的符号长度是 0.01%,影响很小。但如果系统时钟只有 10 MHz,抖动就变成 0.1%,可能需要考虑用时钟使能或更高精度的定时。
提示:符号定时器的复位必须和帧同步对齐,否则第一个符号可能被截断,解调端会丢第一个比特。
3. 解调端:非相干鉴频与判决门限的工程取舍
3.1 为什么非相干解调在 FPGA 上更常见
FSK 解调分相干和非相干。相干解调需要恢复载波相位,在 FPGA 上意味着要跑 Costas 环或平方环,资源消耗大,锁定时间长。非相干解调不需要相位信息,常见方法有过零检测、包络检波加鉴频、以及基于 DFT 的频点能量比较。在工业数传里,非相干解调是主流,因为实现简单、鲁棒性好。我一般会用两个匹配滤波器分别对应两个频率,然后比较输出能量。这种方法本质上是计算两个频点的 DFT 幅值,在符号周期内积分。
具体做法:用两个 N 点滑动 DFT,N 取符号周期内的采样点数。比如符号速率 10 kbps,采样率 100 MHz,每个符号 10000 个点。但 10000 点 DFT 资源太大,实际会降采样。常见做法是先混频到基带,再降采样到 10 倍符号速率,即 100 kHz 采样率,每个符号 10 个点。然后做 10 点 DFT,计算两个频点的能量。这样资源消耗小,精度也够。
3.2 用滑动 DFT 计算两个频点能量的 Verilog 实现
下面是一个简化版的双频点能量计算模块。它用两个复数乘法累加器,分别对应 0.9 MHz 和 1.1 MHz 的基带频点。输入是降采样后的 I/Q 信号。
module fsk_demod ( input wire clk, // 100 MHz input wire rst_n, input wire signed [15:0] i_in, // 降采样后 I 路 input wire signed [15:0] q_in, // 降采样后 Q 路 input wire sample_valid, // 降采样采样有效 output reg bit_out, // 解调输出比特 output reg bit_valid_out // 输出有效 ); // 两个频点的本地振荡器相位增量 // 假设降采样后采样率 100 kHz,频点分别为 -10 kHz 和 +10 kHz // 相位增量 = 2*pi*f/fs,用定点数表示 localparam [15:0] phase_inc_0 = 16'd6554; // -10 kHz 对应 localparam [15:0] phase_inc_1 = 16'd58982; // +10 kHz 对应 reg [15:0] phase_0, phase_1; reg signed [31:0] acc_i_0, acc_q_0, acc_i_1, acc_q_1; reg [3:0] sample_cnt; reg [15:0] cos_0, sin_0, cos_1, sin_1; // 简化的正弦余弦查找表,实际用 ROM 初始化 always @(*) begin // 这里用相位高 4 位查表,仅示意 cos_0 = 16'd32767 - phase_0[15:12] * 16'd4096; sin_0 = phase_0[15:12] * 16'd4096; cos_1 = 16'd32767 - phase_1[15:12] * 16'd4096; sin_1 = phase_1[15:12] * 16'd4096; end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin phase_0 <= 16'd0; phase_1 <= 16'd0; acc_i_0 <= 32'sd0; acc_q_0 <= 32'sd0; acc_i_1 <= 32'sd0; acc_q_1 <= 32'sd0; sample_cnt <= 4'd0; bit_out <= 1'b0; bit_valid_out <= 1'b0; end else if (sample_valid) begin // 更新本地振荡器相位 phase_0 <= phase_0 + phase_inc_0; phase_1 <= phase_1 + phase_inc_1; // 复数乘法累加:I*cos + Q*sin 和 Q*cos - I*sin acc_i_0 <= acc_i_0 + (i_in * cos_0 - q_in * sin_0); acc_q_0 <= acc_q_0 + (q_in * cos_0 + i_in * sin_0); acc_i_1 <= acc_i_1 + (i_in * cos_1 - q_in * sin_1); acc_q_1 <= acc_q_1 + (q_in * cos_1 + i_in * sin_1); sample_cnt <= sample_cnt + 1'b1; if (sample_cnt == 4'd9) begin // 每 10 个采样点做一次判决 // 比较两个频点的能量,能量用 I^2 + Q^2 近似 if ((acc_i_0 * acc_i_0 + acc_q_0 * acc_q_0) > (acc_i_1 * acc_i_1 + acc_q_1 * acc_q_1)) begin bit_out <= 1'b0; end else begin bit_out <= 1'b1; end bit_valid_out <= 1'b1; // 清零累加器,准备下一个符号 acc_i_0 <= 32'sd0; acc_q_0 <= 32'sd0; acc_i_1 <= 32'sd0; acc_q_1 <= 32'sd0; sample_cnt <= 4'd0; end else begin bit_valid_out <= 1'b0; end end end endmodule这段代码的核心是复数乘法累加,它把输入信号分别与两个频点的本地振荡器做相关。相关值越大,说明信号能量越集中在该频点。判决时比较两个能量大小,输出对应比特。参数上,phase_inc_0和phase_inc_1的计算公式是2*pi*f/fs的定点表示。假设降采样后采样率 100 kHz,频点 ±10 kHz,相位增量 = 2pi10000/100000 = 0.628 rad。用 16 位表示 2*pi,则 0.628 对应约 6554。这里用 16 位相位累加器,高 4 位查表,精度有限,实际工程中会用 10 位以上地址和更精细的 ROM。
3.3 判决门限的动态调整策略
固定门限在信噪比变化时会失效。比如发射端功率下降,两个频点的能量都变小,但相对关系不变,固定门限可能把噪声判成信号。我一般会加一个自动增益控制或动态门限。简单做法是取两个能量的平均值作为参考,判决时看哪个能量超过平均值。更稳的做法是用滑动窗口跟踪噪声基底,把门限设为噪声基底的 3 到 4 倍。在 Verilog 里可以用一个一阶 IIR 滤波器估计噪声基底,系数取 1/256 左右,这样门限能跟随信道变化。
注意:动态门限的收敛速度要和符号速率匹配。如果门限更新太慢,突发信号的前几个符号会判错;更新太快,噪声会带动门限抖动。
4. 避坑与排查:FSK 链路里最容易翻车的五个点
4.1 相位不连续导致频谱扩散
现象:解调端误码率居高不下,频谱仪上看发射信号旁瓣明显抬高。原因:频率切换时相位累加器被复位,或者频率控制字在非符号边界更新。解决:确保相位累加器只累加不清零,频率控制字在符号定时器的使能信号下更新,且使能信号与符号边界对齐。
4.2 滤波器延迟导致符号定时偏移
现象:解调端眼图张开度小,判决时刻偏离最佳采样点。原因:调制端或解调端的低通滤波器引入群延迟,符号定时器没有补偿。解决:在符号定时器里加一个延迟补偿计数器,或者用匹配滤波器代替普通低通,匹配滤波器的群延迟是已知的,可以精确补偿。
4.3 降采样后频点混叠
现象:解调端两个频点的能量差不明显,误码率随符号速率变化。原因:降采样前没有做抗混叠滤波,或者降采样率不够,导致两个频点折叠到同一位置。解决:降采样前加一个截止频率为降采样率一半的低通滤波器,降采样率至少是频偏的 4 倍以上。比如频偏 10 kHz,降采样率至少 40 kHz,实际取 100 kHz 更稳。
4.4 判决门限受直流偏置影响
现象:输入全 0 或全 1 时,解调输出有固定偏差。原因:I/Q 路存在直流偏置,导致能量计算时两个频点都偏大,判决门限失效。解决:在降采样后加一个直流消除模块,用高通滤波器或滑动平均减去均值。滑动平均窗口取符号长度的 2 到 3 倍。
4.5 符号定时器抖动累积
现象:长时间运行后误码率逐渐上升,复位后恢复。原因:符号定时器用计数器实现,但计数器没有在帧同步时复位,导致误差累积。解决:每帧开始时用帧同步信号复位符号定时器,或者用锁相环跟踪符号速率,把抖动控制在 ±1 个时钟周期内。
5. 进阶技巧:用 MATLAB 协同验证与在线调试
5.1 用 MATLAB 生成测试向量和 ROM 初始化文件
在 FPGA 上调试 FSK,最怕的是不知道错在哪一级。我一般会先用 MATLAB 生成一组完整的调制信号,包括比特流、调制波形、以及解调后的期望比特。然后把调制波形量化成定点数,写入文本文件,作为 FPGA 仿真的输入。同时用 MATLAB 生成正弦波 ROM 的初始化文件,格式是每行一个十进制数,位宽 10 位或 12 位。
% 生成 FSK 调制信号和 ROM 初始化文件 fs = 100e6; % 采样率 100 MHz fc = 1e6; % 中心频率 1 MHz df = 100e3; % 频偏 100 kHz symbol_rate = 10e3; % 符号速率 10 kbps num_symbols = 100; % 符号数 bits = randi([0 1], 1, num_symbols); % 生成时间轴 t = 0:1/fs:(num_symbols/symbol_rate - 1/fs); % 生成频率序列 freq = fc + df * (2*bits - 1); freq = repelem(freq, fs/symbol_rate); % 积分得到相位 phase = cumsum(2*pi*freq/fs); % 生成调制信号 wave = cos(phase); % 量化成 16 位有符号整数 wave_int = int16(wave * 32767); % 写入文件 fid = fopen('fsk_wave.txt', 'w'); fprintf(fid, '%d\n', wave_int); fclose(fid); % 生成正弦波 ROM 初始化文件,1024 点,10 位无符号 rom_depth = 1024; rom_width = 10; addr = 0:rom_depth-1; sin_rom = round((sin(2*pi*addr/rom_depth) + 1) / 2 * (2^rom_width - 1)); fid = fopen('sin_rom.txt', 'w'); fprintf(fid, '%d\n', sin_rom); fclose(fid);这段 MATLAB 代码做了两件事:生成调制信号用于仿真,生成 ROM 初始化文件用于综合。参数上,fs和symbol_rate的比值决定了每个符号的采样点数,这里是 10000。wave_int是 16 位有符号整数,对应 FPGA 里 DAC 的输入格式。sin_rom是 10 位无符号,对应 ROM 地址 0 到 1023。
5.2 用 ILA 抓取内部信号做在线调试
FPGA 调试离不开 ILA(集成逻辑分析仪)。在 FSK 链路里,我一般会抓四个信号:相位累加器的高 8 位、频率控制字、解调端的两个能量值、以及判决输出。触发条件设在符号边界,这样能看到频率切换时的相位连续性。如果相位累加器在切换时跳变,说明频率控制字更新逻辑有问题。如果两个能量值在符号中间就分出大小,说明积分窗口没对齐。
ILA 的采样深度要足够覆盖至少 10 个符号周期。比如符号速率 10 kbps,10 个符号是 1 ms,系统时钟 100 MHz,需要 100k 采样点。实际 ILA 深度通常 8k 到 16k,所以只能抓局部。我一般会抓 2 到 3 个符号,重点看切换瞬间。
5.3 用误码率测试仪做端到端验证
最后一步是端到端误码率测试。用伪随机比特流作为输入,经过 FPGA 调制后环回或通过 DAC 输出,再经过 ADC 输入到解调端,比较解调输出和原始比特。误码率低于 1e-5 算合格。如果误码率在 1e-3 附近徘徊,通常是判决门限或符号定时的问题。如果误码率随信噪比下降而急剧恶化,可能是解调端的积分窗口太短,能量积累不够。
提示:环回测试时要注意 DAC 和 ADC 的延迟,这个延迟会改变符号定时。我一般会在解调端加一个可调延迟线,手动扫描延迟,找到误码率最低的点。
这套流程走下来,从 DDS 调制到非相干解调,再到 MATLAB 协同和 ILA 调试,基本能把一份“基于FPGA的FSK调制解调.pdf”里的核心链路复现出来。我自己的习惯是先把 MATLAB 仿真跑通,再写 Verilog,最后上板调试。每次翻车都是因为跳过了某一步,比如没做抗混叠滤波,或者符号定时器没复位。希望帮到你。
本文还有配套的精品资源,点击获取