这篇文章主要记录一下用FPGA做2FSK解调的过程,主要包含了三个步骤:一个是输入信号的处理、带通滤波器、低通滤波器以及抽样判决的设计
输入信号处理部分
利用MATLAB生成2FSK解调的COE文件,0的载波频率为800KHz,1的载波频率为1MHz,并打印出位宽和深度,深度与位宽用于后续做处理,其中位宽设置为8位,保存形式为无符号,其中需要记住的是采样速率为10MHZ,码元速率为50kbps.【可根据自己的设定自行调整】
clear all; close all; clc;
%% 参数设置
fs = 10e6; % 采样频率 10MHz
f1 = 800e3; % 载波1频率 800kHz (表示'1')
f2 = 1e6; % 载波2频率 1MHz (表示'0')
bit_rate = 5e4; % 码元速率 50kbps
bit_duration = 1/bit_rate; % 码元持续时间
samples_per_bit = fs * bit_duration; % 每个码元的采样点数
%% 生成测试数据序列
data_bits = [1 0 1 0 1 0 1 0 0 1 1]; % 16位测试数据
num_bits = length(data_bits);
% 时间轴
t_bit = 0:1/fs:bit_duration-1/fs; % 单个码元时间
t_total = 0:1/fs:(num_bits*bit_duration-1/fs); % 总时间
%% 生成2FSK调制信号
fsk_signal = [];
for i = 1:num_bits
if data_bits(i) == 1
% 发送载波1 (800kHz)
bit_signal = cos(2*pi*f1*t_bit);
else
% 发送载波2 (1MHz)
bit_signal = cos(2*pi*f2*t_bit);
end
fsk_signal = [fsk_signal bit_signal];
end
% 量化为8位无符号整数(范围0~255)
signal_min = min(fsk_signal); % 计算信号最小值
signal_offset = fsk_signal - signal_min; % 平移信号至非负范围
signal_scaled = signal_offset / max(signal_offset) * (2^8 - 1); % 缩放到0~255
signal_8bit_unsigned = round(signal_scaled); % 取整
signal_8bit_unsigned = max(0, min(2^8 - 1, signal_8bit_unsigned)); % 限幅处理
signal_8bit_unsigned = uint8(signal_8bit_unsigned); % 转换为无符号8位整数
%% 生成2FSK信号COE文件 - 8位无符号版
coe_filename_signal = 'fsk_signal_input_8bit_unsigned.coe';
fid = fopen(coe_filename_signal, 'w');
fprintf(fid, '; 2FSK调制信号输入 COE文件 (8位无符号量化)\n');
fprintf(fid, '; 载波1频率: %.0f kHz (bit 1)\n', f1/1000);
fprintf(fid, '; 载波2频率: %.0f kHz (bit 0)\n', f2/1000);
fprintf(fid, '; 采样频率: %.0f MHz\n', fs/1e6);
fprintf(fid, '; 数据序列: %s\n', num2str(data_bits));
fprintf(fid, '; 每个码元采样点数: %d\n', samples_per_bit);
fprintf(fid, 'memory_initialization_radix=10;\n');
fprintf(fid, 'memory_initialization_vector=\n');
% 写入量化后的数据(无符号)
for i = 1:length(signal_8bit_unsigned)
if i == length(signal_8bit_unsigned)
fprintf(fid, '%d;\n', signal_8bit_unsigned(i));
else
fprintf(fid, '%d,\n', signal_8bit_unsigned(i));
end
end
fclose(fid);
%% 打印COE文件的宽度和深度
coe_width = 8; % 8位无符号
coe_depth = length(signal_8bit_unsigned); % 数据深度
fprintf('COE文件信息:\n');
fprintf(' 文件名: %s\n', coe_filename_signal);
fprintf(' 数据宽度: %d 位(无符号)\n', coe_width);
fprintf(' 数据深度: %d\n', coe_depth);
fprintf(' 每个码元采样点数: %.0f\n', samples_per_bit);
fprintf(' 总码元数: %d\n', num_bits);
fprintf(' 总采样点数: %d\n', coe_depth);
%% 验证COE文件内容
coe_file = fopen(coe_filename_signal, 'r');
if coe_file == 0
error('无法打开COE文件进行验证');
end
% 跳过文件头
skip_header = 1;
coe_data = [];
while ~feof(coe_file)
line = fgetl(coe_file);
if isempty(line), continue; end
% 跳过注释行和头信息
if skip_header
if contains(line, 'memory_initialization_vector=')
skip_header = 0;
end
continue;
end
% 提取数据
data = textscan(line, '%d,', 'Delimiter', ',;');
if ~isempty(data{1})
coe_data = [coe_data; data{1}];
end
end
fclose(coe_file);
% 验证宽度和深度
if length(coe_data) == coe_depth
fprintf('验证通过: COE文件深度与计算值一致\n');
else
warning('验证警告: COE文件深度与计算值不一致');
fprintf(' 计算深度: %d\n', coe_depth);
fprintf(' 文件深度: %d\n', length(coe_data));
end
COE导入到IP核
会生成在左边图片上的COE文件,可以利用COE文件导入到vivodo。

打开vivodo软件之后,首先创建信号源的IP核,如下图找到IP,catalog,点击他右边会弹出页面,搜索ROM,选择BLOCK&ROMs&BRAM,双击他。

弹出来的界面,在Basic界面如下图所示进行配置,配置完之后,点击Port A options进行配置。

跟着下面进行操作。


导入到IP核之后,需要进行实例化,才可以调用,相当于一个函数,生成右面的IP核之后找到veo文件,将这个复制到顶层模块可以进行实例化。

800KHz滤波器设计
在matlab命令行中输入fdatool,具体选择如下图所示,其中采样频率为10Mhz,选择40阶的滤波器,其中要对量化的位宽以及输入的信号做出限制,都如下图所示:



导入至IP核,直接搜索fir就可以,选自FIR COmpiler




直接点击OK就可以了,最后也会弹出来一个generate的界面,也需要进行点击最终才可以生成。生成了IP核也需要在顶层模块中进行实例化,相当于函数声明,同样需要找到它的veo文件。
1Mhz的滤波器设计
这一部分就稍微讲简单一点,我们输入的位宽同样是8位,频率为10Mhz,所以对比于800Khz的滤波器只需要更改matlab端,在新生成一个IP核那里更改coe的地址既可以,其他参数按照800Khz保持不变



一点要点击应用,,输入小数长度为0,vivado的ip核部分按照上面的步骤进行配置就可以了。
低通滤波器进行设计
同样只需要在matlab端更改生成COE文件,然后更改路径就可以了,其余的配置都按照800Khz的IP核进行配置。


最终会有4个IP核,两个带通、1个低通,因为低通的设置都为一样的就可以只设置一个,一个fsk的信号源。

顶层模块的编写
顶层模块的编写包含了生成的滤波器的实例化、抽样判决模块的实例化,代码如下:
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/06/29 14:36:17
// Design Name:
// Module Name: fsk_demod
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module fsk_demod(
input clk,
input rst_n,
output data_out
);
//输入信号IP核
parameter FSK_MEM_DEPTH = 2200; // 存储器深度
reg [11:0]addra;
wire [7:0]fsk_out;
wire [15:0]sub;
wire sample_pulse;
//带通滤波信号800K
wire s_axis_data_8tready=1'b1;
wire m_axis_800_tvalid;
wire [31:0]fi800_out;
//带通滤波信号1M
wire s_axis_data_1tready=1'b1;
wire m_axis_1m_tvalid;
wire [31:0]fi1m_out;
//800K低通
wire s_axis_data_low0tready=1'b1;
wire m_axis_low_pass0;
wire [31:0]low0out;
//1M低通
wire s_axis_data_low1tready=1'b1;
wire m_axis_low_pass1;
wire [31:0]low1out;
//低通滤波的输入
wire [7:0]low0in;
wire [7:0]low1in;
assign low0in=fi800_out[31:24];
assign low1in=fi1m_out[31:24];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin // 复位时地址清零
addra <= 12'd0;
end else begin
if (addra >= FSK_MEM_DEPTH - 1) begin // 达到深度后循环
addra <= 12'd0;
end else begin // 地址递增
addra <= addra + 1'b1;
end
end
end
//输入信号IP核
fsk fsk (
.clka(clk), // input wire clka
.addra(addra), // input wire [12 : 0] addra
.douta(fsk_out) // output wire [7 : 0] douta
);
//带通滤波800K实例化
fir_compiler_0 f800 (
.aclk(clk), // input wire aclk
.s_axis_data_tvalid(1'b1), // input wire s_axis_data_tvalid
.s_axis_data_tready(s_axis_data_8tready), // output wire s_axis_data_tready
.s_axis_data_tdata(fsk_out), // input wire [7 : 0] s_axis_data_tdata
.m_axis_data_tvalid(m_axis_800_tvalid), // output wire m_axis_data_tvalid
.m_axis_data_tdata(fi800_out) // output wire [31 : 0] m_axis_data_tdata
);
//带通滤波1M实例化
fir_1m your_instance_name (
.aclk(clk), // input wire aclk
.s_axis_data_tvalid(1'b1), // input wire s_axis_data_tvalid
.s_axis_data_tready(s_axis_data_1tready), // output wire s_axis_data_tready
.s_axis_data_tdata(fsk_out), // input wire [7 : 0] s_axis_data_tdata
.m_axis_data_tvalid(m_axis_1m_tvalid), // output wire m_axis_data_tvalid
.m_axis_data_tdata(fi1m_out) // output wire [31 : 0] m_axis_data_tdata
);
//实例化低通
fir_compiler_1 low0band (
.aclk(clk), // input wire aclk
.s_axis_data_tvalid(1'b1), // input wire s_axis_data_tvalid
.s_axis_data_tready(s_axis_data_low0tready), // output wire s_axis_data_tready
.s_axis_data_tdata(low0in), // input wire [7 : 0] s_axis_data_tdata
.m_axis_data_tvalid(m_axis_low_pass0), // output wire m_axis_data_tvalid
.m_axis_data_tdata(low0out) // output wire [31 : 0] m_axis_data_tdata
);
fir_compiler_1 low1band (
.aclk(clk), // input wire aclk
.s_axis_data_tvalid(1'b1), // input wire s_axis_data_tvalid
.s_axis_data_tready(s_axis_data_low1tready), // output wire s_axis_data_tready
.s_axis_data_tdata(low1in), // input wire [7 : 0] s_axis_data_tdata
.m_axis_data_tvalid(m_axis_low_pass1), // output wire m_axis_data_tvalid
.m_axis_data_tdata(low1out) // output wire [31 : 0] m_axis_data_tdata
);
judge judgeuut(
. clk(clk), // 系统时钟(5MHz)
. rst_n(rst_n), // 异步复位,低有效
.lowpass0(low0out), // 低通滤波器输出(对应800KHz载波)
.lowpass1(low1out), // 低通滤波器输出(对应1MHz载波)
.data_out(data_out), // 判决输出比特(0:800K,1:1M)
.sub(sub),
.sample_pulse(sample_pulse) // 抽样脉冲(高电平表示判决时刻)
);
endmodule
其中生成的IP核的部分是模块名加上你的名字,比如说 judge judgeuut,judge是模块名 judgeuut是自己在顶层模块用于调用自己取得名字
抽样判决模块编写
module judge(
input clk, // 系统时钟(10MHz)
input rst_n, // 异步复位,低有效
input [31:0] lowpass0, // 低通滤波器输出(对应800KHz载波)
input [31:0] lowpass1, // 低通滤波器输出(对应1MHz载波)
output reg data_out, // 判决输出比特(0:800K,1:1M)
output wire [15:0] sub,
output reg sample_pulse // 抽样脉冲(高电平表示判决时刻)
);
reg [15:0] dout;
// 无符号减法(带饱和处理)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dout <= 16'd0;
end else begin
if (lowpass0[31:24] >= lowpass1[31:24]) begin
dout <= lowpass0[31:16] - lowpass1[31:16];
end else begin
dout <= 16'd0; // 下溢时输出0
end
end
end
assign sub = dout;
// 基于10MHz时钟的参数设置
// 码元速率为50Kbps,则每个码元持续时间为20us
// 在10MHz时钟下,20us = 200个时钟周期
parameter SAMPLE_CYCLE = 200; // 码元周期(10MHz时钟下的周期数)
parameter HALF_CYCLE = SAMPLE_CYCLE/2; // 100(码元中间位置)
parameter DELAY_POINTS = 0; // 延迟点数
// 内部计数器和状态变量
reg [15:0] bit_counter; // 比特周期计数器
reg [15:0] sampled_diff; // 抽样时刻的差值
reg [15:0] sample_point; // 抽样点计数器
// 时钟分频和抽样脉冲生成
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_counter <= 16'd0;
sample_pulse <= 1'b0;
sample_point <= 16'd0;
end else begin
// 比特周期计数器(200个时钟周期为1个码元)
if (bit_counter >= SAMPLE_CYCLE - 1) begin
bit_counter <= 16'd0;
end else begin
bit_counter <= bit_counter + 1'b1;
end
// 生成抽样脉冲(在每个比特周期的中间位置100)
if (bit_counter == HALF_CYCLE) begin
sample_pulse <= 1'b1;
sample_point <= bit_counter; // 记录抽样点位置
end else begin
sample_pulse <= 1'b0;
end
end
end
// 抽样判决逻辑(保持不变)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sampled_diff <= 16'd0;
data_out <= 1'b0;
end else begin
// 在抽样脉冲有效时锁存当前差值
if (sample_pulse) begin
sampled_diff <= dout;
end
// 判决逻辑:根据两路信号的强度比较
if (sample_pulse) begin
if (lowpass0[31:0] > lowpass1[31:0]) begin
data_out <= 1'b0; // 800kHz信号更强,判为0
end else begin
data_out <= 1'b1; // 1MHz信号更强,判为1
end
end
end
end
endmodule
TB仿真激励文件编写
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/06/29 21:51:08
// Design Name:
// Module Name: tb_fsk
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module tb_fsk();
reg clk;
reg rst;
wire [31:0] fir800_out;
wire [31:0] fir1m_out;
wire [7:0] fsk_signal;
wire [31:0] lowpass0out;
wire [31:0] lowpass1out;
wire data_out;
wire sample_pulse;
parameter CLK_PERIOD = 100;
fsk_demod uut (
.clk (clk),
.rst_n (rst),
.data_out (data_out)
);
assign fsk_signal = uut.fsk_out;
assign fir800_out=uut.fi800_out;
assign fir1m_out=uut.fi1m_out;
assign lowpass0out = uut.low0out; // 新增:连接低通输出
assign lowpass1out = uut.low1out; // 新增:连接低通输出
assign sample_pulse = uut.sample_pulse; // 新增:连接抽样脉冲
always #(CLK_PERIOD/2) clk = ~clk;
// 测试序列
initial begin
// 初始化信号
clk = 1'b0;
rst = 1'b0;
// 复位操作
#100; // 等待50ns
rst = 1'b1; // 释放复位
// 运行测试
#50000; // 运行10us (1000个时钟周期)
// 结束仿真
$display("FSK解调测试完成");
$finish;
end
initial begin
$monitor("Time: %0t ns | rst: %b | FSK: %0d | 800k: %0d | 1M: %0d | LP0: %0d | LP1: %0d | Decision: %b | Sample: %b",
$time, rst, fsk_signal, fir800_out, fir1m_out, lowpass0out, lowpass1out, data_out, sample_pulse);
#0; // 立即执行一次监控
end
//波形记录
initial begin
$dumpfile("fsk_demod.vcd");
$dumpvars(0, tb_fsk);
end
endmodule
整个仿真就包含了两个module和一个仿真激励文件,module fsk_demod为顶层,module judge为子模块,ip核也相当于子模块,子模块要在顶层进行调用就需要实例化,代码已经全部给出,可以参照自己的进行修改就可以,整个的时钟频率仿真设置为10Mhz,输入位宽都固定为8位。
出来的效果图如下:

如果有更多的建议请批评指正
2万+





