FPGA学习路程与经验分享
目录
一、学习路程
1.1、第一次接触FPGA
1.2、第一次自主完成外设通信
1.3、考研与工作的抉择
1.4、高速的第一次接触
1.5、ARM的第一次接触
1.6、ZYNQ的第一次接触
1.7、毕业设计
二、点灯
2.1、点亮一个小灯
2.2、花式点灯
2.2.1、流水灯
2.2.2、TestBench
2.2.3、呼吸灯
2.2.4、 闪烁灯
2.2.5 、组合灯
三、外设通信
3.1、UART协议
3.1.1、UART环回
3.1.2、环回测试
3.2、SPI协议
3.2.1、SPI读写
3.2.1.1、SPI主机读写时序编写
四、数字电路设计
4.1、JK触发器
4.2、译码器、编码器
4.3、积分器
4.4、微分器
4.5、IIR无限响应滤波器
4.6、快速傅里叶变换
4.6.1、XFFT ip核的配置
4.6.2、Cordic ip核配置
4.6.3、代码编写
4.6.4、仿真结果
一、学习路程
1.1、第一次接触FPGA
大学期间首次接触FPGA是在大二的EDA课程中。从环境工程跨专业到电子信息工程后,这门课成为继C语言后接触的第二门编程课程。课程使用的VHDL语言介于汇编和C语言之间,作为硬件描述语言需要扎实的数字电路基础。虽然初期存在理解门槛,但随着对语句电路映射关系的掌握,VHDL的编程逻辑逐渐清晰。课程实践环节完成了从LED控制到数字屏显示的基础项目,通过CASE语句构建状态机实现时序逻辑。这些实践揭示了FPGA与单片机的本质差异——其近乎无限的可编程性。课程结束后自学Verilog时发现,已有VHDL基础的情况下,三天便能掌握基础语法,并在一天内复现课程全部内容,包括点灯程序、电子钟和MIDI播放器。
1.2、第一次自主完成外设通信
不满足于课程内容后,开始系统学习通信协议实现。从串并转换起步,逐步完成UART、IIC、SPI等核心协议的自主开发。跳过已掌握的UART电脑通信,直接挑战SD卡的SPI四线模式通信。历时一周的调试后成功实现SD卡读写,并在此基础上构建了真正的WAV无损音乐播放器。这段SD卡开发经历成为重要转折点,不仅验证了FPGA处理复杂外设的能力,更通过完整的音频系统实现,深刻体会到硬件可编程逻辑的工程魅力。这种将抽象协议转化为具体功能的过程,奠定了后续更复杂项目开发的技术基础。在之后,也将该功能移植到了小车上(FPGA开发的小车),让小车在可以红外遥控、自动避障的同时,还能播放音乐(无意义,只是觉得好玩~)。
1.3、考研与工作的抉择
在大二下学期这个关键节点,考研计划被果断放弃,转而将全部精力投入到FPGA技术的研究中。FPGA作为复杂可编程逻辑器件,其学习难度远超传统MCU。为系统性地掌握这项技术,特意申请了Xilinx芯片的FPGA开发板作为实践平台,在完成基础知识梳理后,将研究方向聚焦于图像处理领域。技术探索从LCD驱动开始,通过点亮液晶屏掌握了关键时序控制技术。随后挑战800×480分辨率24位色深的图像显示,初期采用ROM表存储方案虽实现基础功能,但BRAM资源占用率高达98%,暴露了存储瓶颈。这一现实问题催生了图像缩放技术的深入研究。区别于软件开发的便捷性,FPGA实现图像缩放需要从底层架构开始构建。采用4倍降采样方案:行列方向均每隔4个像素保留1个采样点,通过这种压缩存储方式,成功将三帧图像存入有限的BRAM空间。显示环节采用线性插值算法,将压缩图像还原为原始尺寸。基于上述技术积累,完成OV2640摄像头的驱动开发后,构建了完整的实时视频处理流水线。在这之后,也进一步完成了基础的卷积神经网络硬件加速移植实现--边缘检测的实现。
1.4、高速的第一次接触
800×480的分辨率在图像清晰度上存在明显不足,因此转向采用OV5640传感器进行视频采集。通过SCCB协议配置OV5640输出1080P的高清视频信号。这一方案需要解决两大核心问题:高速数据存储与无损显示。数据存储方面采用DDR3内存,通过MIG IP核实现时分复用机制,完成高速缓存读写操作。显示部分基于TMDS协议,将LCD时序经流水线处理后转换为8B/10B编码格式,最终实现15帧/秒的1080P视频实时显示。在基础功能实现后,开发了双目图像拼接算法,并利用帧差法实现了运动目标的实时检测与追踪。为进一步拓展应用场景,后期还探索了两种高速数据传输方案:基于光纤回环的图像传输和采用UDP协议的以太网图像传输。这些实验为FPGA的影像处理学习画上了阶段性句号。
1.5、ARM的第一次接触
在完成基础课程学习后,投入一个月时间系统学习了ARM架构开发。基于NXP i.MX6ULL处理器平台,成功完成了Linux内核的移植工作。该移植成果直接应用于单片机课程设计项目——开发了一套无线音乐播放系统。音乐播放器系统采用以太网通信架构,通过TFTP协议实现与上位机的文件共享功能。用户界面采用底层裸机编程实现,完全独立于QT等高级框架,包含完整的触摸屏交互功能。该系统实现了远程音乐文件传输与播放的核心功能。
1.6、ZYNQ的第一次接触
掌握ARM开发后,转向Xilinx Zynq系列SoC的研究。用一个月时间深入理解Zynq的PL(可编程逻辑)与PS(处理器系统)协同工作机制,完成Linux系统在Zynq平台的移植工作。基于移植系统开发了图像处理与显示应用,验证了硬件加速与软件处理的协同效能。之后在大三暑期,进入专业FPGA开发企业实习。为期十个月的实习期间,参与实际项目开发,积累了企业级FPGA设计经验,包括需求分析、方案设计、实现与测试全流程。这段经历显著提升了我的工程实践能力和行业认知水平。
1.7、毕业设计
在为期10个月的实习期间,我积累了丰富的FPGA开发经验,随后返校完成毕业设计课题。毕业设计延续FPGA技术路线,整合并优化前期多个项目成果,最终构建了一套基于FPGA的实时目标检测系统。该系统采用UART协议实现FPGA与上位机的双向通信,主要完成两项核心功能:动态配置FPGA内部寄存器参数及OV5640摄像头模块的寄存器参数,同时通过HDMI接口将采集的视频流实时传输至上位机显示。
FPGA工程师的职责范围不仅涵盖RTL代码设计与硬件逻辑实现,还需具备全栈开发能力,包括:
自主设计外围硬件电路(如传感器接口、通信模块等)开发上位机驱动程序及通信协议构建用户友好的交互界面(UI) 该领域对综合能力要求较高,需持续学习以提升工程实践水平。成为一名合格的FPGA工程师的路还很漫长。。。
二、点灯
2.1、点亮一个小灯
不管是做51单片机、Stm32,还是FPGA,很多人一开始,都是从点灯开始。点灯,顾名思义,就是点亮一个LED灯,对于FPGA来说,只是点亮一个LED灯是非常容易的,一行代码就能实现。
module LED(
input sys_clk,
input sys_rstn,
output reg led
);
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
led <=1'b0;
end
else begin
led <=1'b1;//灯亮
end
end
endmodule
2.2、花式点灯
单单只是点亮一个小灯,并不能说明什么,也不能说你已经学会了FPGA,甚至可以说,只会点亮一个小灯,你什么都不会。想要学会FPGA不能仅仅只是这么普通的点灯,点灯没这么容易,但也没那么难。
2.2.1、流水灯
FPGA要实现流水灯,类似于像单片机的定时器那样,而FPGA是要做一个计数器,对一串的灯实现不同时间的流水循环点亮,以实现100ms计数为例子。
localparam [23:0] COUNT =24'h4c4b40;
reg [23:0] count_reg;
reg led_valid;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
count_reg <=24'd0;
led_valid <=1'b0;
end
else begin
if(count_reg>=COUNT-1'b1)begin
count_reg <=24'b0;
led_valid <=1'b1;
end
else begin
count_reg <=count_reg+24'd1;
led_valid <=1'b0;
end
end
end
sys_clk为50Mhz,COUNT为计数最大值24‘h4c4b40,即10进制5000000,50Mhz除去这么大周期即为100ms。count_reg进行计数,每100ms led_valid拉高,其余时间低电平,以达成类似定时器的中断功能,但FPGA中并不是中断,而是并行处理。如下面的FPGA LED流水线实现代码,当led_valid高电平时,8位的led_series实现左循环流水,注意,在LED进行流水点灯的时候,上述的计数器,实际上是同时运行的,因此,在速率上,FPGA远远高于单片机。
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
led_series <=8'b10000000;
end
else begin
if(led_valid)begin
led_series <={led_series[6:0],led_series[7]};
end
else begin
led_valid <=led_valid;
end
end
end
2.2.2、TestBench
一般完成一个模块后,都要编写TestBench来验证模块的功能性是否完整,这里编写一个TestBench来对上述的流水灯进行验证的操作,TestBench的编写如果不完成非常复杂的时序,是很容易编写的,下面是我用于验证上述流水灯的TestBench,只需要控制时钟信号和复位信号操作即可。
module SIM(
);
reg sys_clk;
reg sys_rstn;
wire [7:0] led_series;
initial begin
sys_rstn=1'b0;
sys_clk=1'b0;
#2
sys_rstn=1'b1;
end
always begin
#2
sys_clk=~sys_clk;
end
test T(
.sys_clk (sys_clk),//50Mhz
.sys_rstn (sys_rstn),
.led_series (led_series)
);
endmodule
完成TestBench后,即可用Modelsim或者Vivado自带的仿真软件进行验证,这里我习惯使用Vivado自带的仿真软件,比较好操作,图中也可以看到成功实现了流水灯的功能。
2.2.3、呼吸灯
这里并不是要实现呼吸灯,而是类似于呼吸灯,不知道怎么说,就取名为呼吸灯了,要实现类似呼吸灯的功能,其实很简单,就是并口转串口,也就是说,如果掌握了呼吸灯的实现,实际上,使用FPGA实现最基本的串口,也就不那么困难了,起码最基本的UART串口,用FPGA的逻辑搭建,都不是大问题 。最简单的呼吸灯的实现方法,其实一个循环移位器就能实现,如下代码所示,可以实现10101100的状态循环发送,以下的testbench仿真可以看出模块成功实现该功能。
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
trans <=8'b10101100;
led_series <=1'b0;
end
else begin
if(led_valid)begin
led_series <=trans[7];
trans <={trans[6:0],trans[7]};
end
else begin
led_series <=led_series;
trans <=trans;
end
end
end
而如果想要可控制的进行发送呢,也只需要多增加一个触发器和一个计数器就可以了。Led_En为输入的触发信号,led_en_r为触发器,当触发信号为1时,对触发信号进行锁存,当传输8bit完成后,bit_trans_flag信号拉高,标准传输完成,此时所有信号都将进行复位。bit_locate为对一次传输进行计数,当计数8次,代表传输完成,则拉高bit_trans_flag信号,表示传输完成,进行全部信号复位。因此,便能完成可控制的呼吸灯。到此,如果自己一个人都能完成的话,那么,SPI、UART协议通信模块的独立完成也是非常简单,其中的时序跟呼吸灯基本一样。
module test(
input sys_clk,//50Mhz
input sys_rstn,
input led_en,
output reg led_series
);
localparam [23:0] COUNT =24'h4c4b40;
reg [23:0] count_reg;
reg led_en_r;
reg bit_trans_flag;
reg [3:0] bit_locate;
reg [8:0] trans ;
reg led_valid;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
led_en_r <=1'b0;
end
else begin
if(led_en)begin
led_en_r <=1'b1;
end
else begin
if(bit_trans_flag)
led_en_r <=1'b0;
else
led_en_r <=led_en_r;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
trans <=8'b10101100_0;
led_series <=1'b0;
bit_trans_flag<=1'b0;
bit_locate <=4'd8;
end
else begin
if(led_en_r)begin
if(led_valid)begin
led_series <=trans[8];
trans <={trans[7:0],trans[8]};
if(bit_locate==4'd0)begin
bit_locate <=4'd8;
bit_trans_flag <=1'b1;
end
else begin
bit_locate <=bit_locate-4'd1;
bit_trans_flag <=1'b0;
end
end
else begin
led_series <=led_series;
trans <=trans;
bit_trans_flag <=1'b0;
end
end
else begin
trans <=8'b10101100_0;
led_series <=1'b0;
bit_trans_flag<=1'b0;
bit_locate <=4'd8;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
count_reg <=24'd0;
led_valid <=1'b0;
end
else begin
if(~bit_trans_flag&&led_en_r)begin
if(count_reg>=COUNT-1'b1)begin
led_valid <=1'b1;
count_reg <=24'd0;
end
else begin
led_valid <=1'b0;
count_reg <=count_reg+1'b1;
end
end
else begin
led_valid <=1'b0;
count_reg <=24'd0;
end
end
end
endmodule
2.2.4、 闪烁灯
人眼感知到的灯光亮度变化,实际上是 LED 灯闪烁周期中占空比不同造成的。占空比指的是在一个时钟周期内,高电平(点亮状态)持续时间所占的比例。具体而言,在典型的外设电路中,当高电平驱动 LED 点亮时,如果 FPGA 向 LED 提供一个周期信号,则该信号在一个周期内的占空比越大,人眼所感受到的灯光亮度就越高。这种用于控制亮度的周期信号,就是 PWM(脉宽调制)波。FPGA 实现 PWM 波的代码如下。其中,duty_cycle_i为输入占空比,pwm_valid_i为PWM使能,led_pwm为输出给led的信号。根据下面的仿真图可以看到,duty_cycle_i改变后,输出给led的PWM波的占空比也随之改变。
module test(
input sys_clk,//10khz
input sys_rstn,
input [6:0] duty_cycle_i,
input pwm_valid_i,
output reg led_pwm
);
localparam [6:0] DUTY_MAX =7'd100;
reg [6:0] duty_count;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
led_pwm <=1'b0;
end
else begin
if(pwm_valid_i)begin
if(duty_count<=duty_cycle_i-1'b1&&duty_cycle_i!=0)begin
led_pwm <=1'b1;
end
else begin
led_pwm <=1'b0;
end
end
else begin
led_pwm <=1'b0;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
duty_count <=7'd0;
end
else begin
if(pwm_valid_i)begin
if(duty_count>=DUTY_MAX-1'b1)begin
duty_count <= 7'd0;
end
else begin
duty_count <=duty_count+7'd1;
end
end
else begin
duty_count <=7'd0;
end
end
end
endmodule
2.2.5 、组合灯
完成上述功能实现后,即可尝试将呼吸灯功能与 LED 闪烁功能进行叠加。即,使项目能够同时实现并行转串行的 LED 闪烁效果以及对 LED 亮度的调节。这实质上相当于输出一个简单的数字调制信号。为实现该复合功能,采用流水线结构会导致代码结构混乱,不利于维护和理解。因此,将上述功能模块均改为状态机实现,并采用分模块设计。具体划分为:顶层模块、计数器模块、呼吸灯控制模块以及 PWM 模块。通过这种分模块方式完成项目构建。各模块的具体实现代码见下文。
//计数器/时钟分频模块
module Devide_Clk
(
DEVIDE_COUNT,//计数器总数
sys_clk,//系统时钟
sys_rst_n,//低电平复位
de_valid_i,//计数器有效信号
de_valid_o//计数器完成使能输出
);
input [31:0] DEVIDE_COUNT;
input sys_clk;//50Mhz
input sys_rst_n;
input de_valid_i;
output de_valid_o;
reg [31:0] count_r;
reg de_valid_r;
assign de_valid_o=de_valid_r;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
count_r <=DEVIDE_COUNT-1'b1;
de_valid_r <=1'b0;
end
else begin
if(de_valid_i)begin
if(count_r>=DEVIDE_COUNT-1'b1)begin
count_r <=32'd0;
de_valid_r <=1'b1;
end
else begin
count_r <=count_r+1'b1;
de_valid_r <=1'b0;
end
end
else begin
count_r <=DEVIDE_COUNT-1'b1;
de_valid_r <=1'b0;
end
end
end
endmodule
//呼吸灯模块
module Led_Series(
sys_clk,//系统时钟
sys_rst_n,//系统复位
led_series_bps_devide,//串行传输速率
led_series_data_i,//串行一次传输数据
led_series_valid_i,//串行有效
led_series_o//串行输出
);
localparam [1:0] LED_INIT =2'b00;
localparam [1:0] LED_TRANS =2'b01;
localparam [1:0] LED_END =2'b11;
input sys_clk;
input sys_rst_n;
input [31:0] led_series_bps_devide;
input [7:0] led_series_data_i;
input led_series_valid_i;
output led_series_o;
reg [1:0] led_st_r;
reg led_series_r;
reg [7:0] led_series_data_r;
reg [2:0] led_series_count_r;
reg led_series_finish_r;
wire dev_valid_r;
assign led_series_o=led_series_r;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n==1'b0)begin
led_st_r <= LED_INIT;
led_series_r <= 1'b0;
led_series_data_r <= 8'hff;
led_series_count_r <= 3'd7;
led_series_finish_r <= 1'b0;
end
else begin
case (led_st_r)
LED_INIT:begin
if(led_series_valid_i)begin
led_st_r <= LED_TRANS;
led_series_r <= 1'b0;
led_series_data_r <= led_series_data_i;
led_series_finish_r<= 1'b0;
led_series_count_r <= 3'd7;
end
else begin
led_st_r <= LED_INIT;
led_series_r <= 1'b0;
led_series_data_r <= led_series_data_r;
led_series_finish_r <= 1'b0;
led_series_count_r <= 3'd7;
end
end
LED_TRANS:begin
if(dev_valid_r)begin
led_series_r <= led_series_data_r[7];
led_series_data_r <= {led_series_data_r[6:0],led_series_data_r[7]};
if(led_series_count_r==3'd0)begin
led_series_count_r <= 3'd7;
led_st_r <= LED_END;
end
else begin
led_series_count_r <=led_series_count_r-3'd1;
led_st_r <= LED_TRANS;
end
end
else begin
led_st_r <= LED_TRANS;
end
end
LED_END:begin
if(dev_valid_r)begin
led_series_finish_r <= 1'b1;
led_series_r <= 1'b0;
led_st_r <= LED_INIT;
end
else begin
led_st_r <= LED_END;
end
end
endcase
end
end
Devide_Clk devide_clk_series
(
.DEVIDE_COUNT (led_series_bps_devide),
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.de_valid_i (~led_series_finish_r),
.de_valid_o (dev_valid_r)
);
endmodule
//PWM波输出
module PWM(
sys_clk,//系统时钟
sys_rst_n,//系统复位
pwm_duty_i,//占空比输入
pwm_valid_i,//PWM波有效
pwm_o//PWM波输出
);
input sys_clk;
input sys_rst_n;
input [6:0] pwm_duty_i;
input pwm_valid_i;
output pwm_o;
wire dev_valid_r;
reg pwm_r;
reg [6:0] pwm_count;
assign pwm_o=pwm_r;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n==1'b0)begin
pwm_count <=7'd0;
end
else begin
if(pwm_valid_i)begin
if(dev_valid_r)begin
if(pwm_count>=7'd99)
pwm_count <= 7'd0;
else pwm_count <= pwm_count+7'd1;
end
else begin
pwm_count <=pwm_count;
end
end
else begin
pwm_count <=7'd0;
end
end
end
always @(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n==1'b0)begin
pwm_r <=1'b0;
end
else begin
if(pwm_valid_i)begin
if((pwm_count<=pwm_duty_i-1'b1)&&pwm_duty_i!=1'b0)
pwm_r <=1'b1;
else
pwm_r <=1'b0;
end
else begin
pwm_r <=1'b0;
end
end
end
Devide_Clk devide_10Khz
(
.DEVIDE_COUNT (32'd5000),
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.de_valid_i (pwm_valid_i),
.de_valid_o (dev_valid_r)
);
endmodule
//顶层模块
module test(
input sys_clk,
input sys_rst_n,
input [6:0] pwm_duty_i,
input pwm_valid_i,
input [31:0] led_series_bps_devide,
input [7:0] led_series_data_i,
input led_series_valid_i,
output led
);
wire pwm_o;
wire led_series_o;
assign led=pwm_o&led_series_o;//输出给;LED灯的数字调制波
PWM PWM_TOP(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.pwm_duty_i (pwm_duty_i),
.pwm_valid_i (pwm_valid_i),
.pwm_o (pwm_o)
);
Led_Series Led_Series_Top(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.led_series_bps_devide (led_series_bps_devide),
.led_series_data_i (led_series_data_i),
.led_series_valid_i (led_series_valid_i),
.led_series_o (led_series_o)
);
endmodule
到此为止,FPGA的点灯才能真正掌握。
三、外设通信
3.1、UART协议
个人认为,UART协议堪称最简单的通信协议之一:它无需时钟线、复位线或片选线,仅依靠两根单向传输的数据线(RX和TX)即可完成通信。UART的传输速率相对较慢,其常见最高速率为115200 bps(比特每秒)。最常用的帧格式包含1位起始位、8位数据位和1位停止位。因此,理论上的最大数据传输速率约为11.52 kB/s(千字节每秒)。由于缺乏独立的时钟信号,通信双方必须预先约定完全相同的波特率进行数据的发送与接收。下图3.1展示了一个典型的UART传输时序:起始位为逻辑0(低电平),随后是8位数据位,最后以逻辑1(高电平)作为停止位(空闲状态通常也为高电平)。
图3.1 UART时序示例
3.1.1、UART环回
UART协议本身结构简单,基于其搭建环回测试电路则更为便捷。具体步骤如下:
实现计数器模块: 该模块用于时钟分频,使FPGA在发送或接收UART数据时能达到相同的波特率。相关代码如下。
接收端设计要点: 为确保数据采样稳定可靠,应在每个bit周期的中心位置采样数据。
发送端设计要点: 关键在于控制每个bit的发送持续时间,该时间长度即由波特率决定(每个bit的持续时间 = 1 / 波特率)。
module Clock_Count #(
parameter [31:0] COUNT_MAX =32'd434
)(
clk_i,//50Mhz时钟
rst_n_i,//低电平复位
count_start_i,//时钟计数使能
count_middle_valid_o,//计数中间信号
count_end_valid_o//技术完毕信号
);
input clk_i;
input rst_n_i;
input count_start_i;
output count_middle_valid_o;
output count_end_valid_o;
reg [31:0] count_r;
reg count_middle_valid_r;
reg count_end_valid_r;
assign count_middle_valid_o = count_middle_valid_r;
assign count_end_valid_o = count_end_valid_r;
always @(posedge clk_i or negedge rst_n_i)begin
if(rst_n_i==1'b0)begin
count_r <=32'd0;
count_end_valid_r <=1'b0;
count_middle_valid_r<=1'b0;
end
else begin
if(count_start_i)begin
if(count_r>=COUNT_MAX-1'b1)begin
count_r <=32'd0;
count_end_valid_r <=1'b1;
count_middle_valid_r <=1'b0;
end
else begin
count_r <=count_r+32'd1;
count_end_valid_r <=1'b0;
if(count_r==(COUNT_MAX>>1)-1'b1)begin
count_middle_valid_r <=1'b1;
end
else begin
count_middle_valid_r <=1'b0;
end
end
end
else begin
count_r <=32'd0;
count_end_valid_r <=32'd0;
count_middle_valid_r <=1'b0;
end
end
end
endmodule
完成计数器模块后,接着便是接收端的设计,接收端设计代码如下。一般来说,设计时序都是采用状态机来设计的,但由于UART协议较为简单,因此这里采用流水线进行设计UART的接收。
module Uart_Rx(
sys_clk,//50Mhz
sys_rstn,
uart_rx_i,
uart_rx_dat,
uart_rx_valid
);
input sys_clk;//50Mhz时钟
input sys_rstn;//系统复位
input uart_rx_i;//UART RX
output reg [7:0] uart_rx_dat;//UART RX DATA
output reg uart_rx_valid;//UART RX DATA VALID
reg [9:0] recedata_r;
reg [3:0] rece_count_r;
reg rece_finish;
reg receive_valid;
wire count_middle_valid_s;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
uart_rx_dat <=8'd0;
uart_rx_valid <=1'b0;
end
else begin
if(rece_finish)begin
uart_rx_dat <= recedata_r[8:1];
uart_rx_valid <= 1'b1;
end
else begin
uart_rx_dat <=uart_rx_dat;
uart_rx_valid <=1'b0;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
recedata_r <=10'd0;
rece_count_r <=4'd0;
rece_finish <=1'b0;
end
else begin
if(receive_valid)begin
if(count_middle_valid_s)begin
recedata_r <={uart_rx_i,recedata_r[9:1]};
if(rece_count_r >=4'd9)begin
rece_count_r <=4'd0;
rece_finish <=1'b1;
end
else begin
rece_count_r <=rece_count_r+4'd1;
rece_finish <=1'b0;
end
end
else begin
recedata_r <=recedata_r;
rece_finish <=1'b0;
rece_count_r<=rece_count_r;
end
end
else begin
recedata_r <=recedata_r;
rece_count_r <=4'd0;
rece_finish <=1'b0;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
receive_valid <=1'b0;
end
else begin
if(receive_valid==1'b0&&uart_rx_i==1'b0)begin
receive_valid <=1'b1;
end
else begin
if(count_middle_valid_s&&rece_count_r >=4'd9)begin
receive_valid <=1'b0;
end
else begin
receive_valid <=receive_valid;
end
end
end
end
//------------------------时钟计数器
Clock_Count #(
.COUNT_MAX (32'd434)
) clock_count_rx(
.clk_i (sys_clk),
.rst_n_i (sys_rstn),
.count_start_i (receive_valid),
.count_middle_valid_o (count_middle_valid_s),
.count_end_valid_o ()
);
endmodule
对于UART的发送端的设计,就更加简单,UART发送的TX模块设计代码如下。
module Uart_Tx(
sys_clk,
sys_rstn,
uart_tx_o,
uart_tx_data,
uart_tx_data_i
);
input sys_clk;
input sys_rstn;
output reg uart_tx_o;
input [7:0] uart_tx_data;
input uart_tx_data_i;
reg uart_trans_valid_r;
reg [3:0] uart_trans_count_r;
reg [9:0] uart_trans_data_r;
reg uart_data_update_r;
wire uart_trans_clock_valid;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
uart_trans_count_r <=4'd0;
uart_tx_o <=1'b1;
end
else begin
if(uart_trans_valid_r)begin
if(uart_trans_clock_valid)begin
if(uart_trans_count_r>=4'd9)begin
uart_trans_count_r <=4'd0;
uart_tx_o <=uart_trans_data_r[uart_trans_count_r];
end
else begin
uart_trans_count_r <=uart_trans_count_r+4'd1;
uart_tx_o <=uart_trans_data_r[uart_trans_count_r];
end
end
else begin
uart_trans_count_r <=uart_trans_count_r;
uart_tx_o <=uart_tx_o;
end
end
else begin
uart_trans_count_r <=4'd0;
uart_tx_o <=1'b1;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
uart_trans_valid_r <=1'b0;
end
else begin
if(uart_data_update_r&&uart_trans_valid_r==1'b0)begin
uart_trans_valid_r <=1'b1;
end
else begin
if(uart_trans_clock_valid&&uart_trans_count_r==4'd9)begin
uart_trans_valid_r <=1'b0;
end
else begin
uart_trans_valid_r <=uart_trans_valid_r;
end
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
uart_trans_data_r <=10'b1_00000000_0;
uart_data_update_r <=1'b0;
end
else begin
if(uart_tx_data_i)begin
uart_data_update_r <=1'b1;
uart_trans_data_r <={1'b1,uart_tx_data,1'b0};
end
else begin
uart_data_update_r <=1'b0;
uart_trans_data_r <=uart_trans_data_r;
end
end
end
Clock_Count #(
.COUNT_MAX (32'd434)
) clock_count_rx(
.clk_i (sys_clk),
.rst_n_i (sys_rstn),
.count_start_i (uart_trans_valid_r),
.count_middle_valid_o (),
.count_end_valid_o (uart_trans_clock_valid)
);
endmodule
对于顶层,代码如下,由于只进行环回,因此只需要将TX的数据线与RX数据线相连即可。
module Uart_Top(
input sys_clk,
input sys_rstn,
input uart_rx_i,
output uart_tx_o
);
wire [7:0] uart_rx_dat_s;
wire uart_rx_valid_s;
Uart_Rx UART_RX_TOP(
.sys_clk (sys_clk),//50Mhz
.sys_rstn (sys_rstn),
.uart_rx_i (uart_rx_i),
.uart_rx_dat (uart_rx_dat_s),
.uart_rx_valid (uart_rx_valid_s)
);
Uart_Tx UART_TX_TOP(
.sys_clk (sys_clk),
.sys_rstn (sys_rstn),
.uart_tx_o (uart_tx_o),
.uart_tx_data (uart_rx_dat_s),
.uart_tx_data_i (uart_rx_valid_s)
);
endmodule
3.1.2、环回测试
将以上程序编译烧录后,FPGA板卡的UART与电脑进行连接,打开XCOM软件,配置波特率为115200bps,方可进行环回测试,环回测试结果如下,当发送HelloWorld后,上位机成功接收HelloWorld。
3.2、SPI协议
SPI协议运用广泛,很多速度较快的串口ADC、DAC芯片,或者是串口的显示屏,基本用的都是SPI协议。只是这些芯片和显示屏的SPI传输的格式有所不同。SPI协议可以说是UART协议的改进版,在拥有TX、RX的传输线的基础上,引入SCLK时钟线和CS片选线构成SPI协议。因此标准的SPI协议由4根线构成:SCLK时钟线、MOSI(主输出,从输入)线、MISO(主输入,从输出)线以及CS片选线。大多数情况用FPGA编写的均为主机SPI总线,用于对从机芯片进行读写寄存器或者数据。SPI协议时序例子图见下图所示,当MOSI发送8bit地址后,MISO立马传输8bit数据,表示数据读出。MOSI发送的数据在时钟下降沿读取,接收的数据在时钟上升沿时传输,因此在下降沿时接收数据。
3.2.1、SPI读写
由于目前手头没有SPI协议的外设,因此以上述的时序为例,使用VIVADO编写SPI的读写测试程序,通过编写TestBench进行仿真验证SPI的Master读写的功能完整性。
3.2.1.1、SPI主机读写时序编写
SPI主设备代码如下
module Spi_Master_Wr#(
parameter [9:0] clock_dev =10'd50
)(
sys_clk,// system clock ,50Mhz,
sys_rstn,// sys_reset
spi_csn,// cs .low valid
spi_sclk,//output spi sclk
spi_miso,//spi master input
spi_mosi,//spi master output
spi_wden,//spi write data enable
spi_rden,//spi read data enable
spi_waddr,//spi write address
spi_raddr,//spi read address
spi_rdata,//spi read data
spi_wdata,//spi write data
spi_trans_busy,//spi trans busy signal
spi_rdata_valid//rdata_valid
);
input sys_clk;
input sys_rstn;
output spi_csn;
output spi_sclk;
input spi_miso;
output spi_mosi;
input spi_wden;
input spi_rden;
input [7:0] spi_waddr;
input [7:0] spi_raddr;
output [7:0] spi_rdata;
output spi_rdata_valid;
input [7:0] spi_wdata;
output spi_trans_busy;
localparam [1:0] SPI_INIT =3'b00;
localparam [1:0] SPI_SEND_DATA =3'b01;
localparam [1:0] SPI_END =3'b11;
reg [15:0] spi_send_data_r;
reg [3:0] spi_send_count_r;
reg [7:0] spi_read_data_r;
reg spi_read_data_valid_r;
reg spi_read_data_en;
reg [1:0] spi_st;
//clock devide
reg clock_en_r;
reg [9:0] clock_count_r;
reg spi_clock_r;
reg spi_mosi_r;
assign spi_trans_busy = clock_en_r;
assign spi_mosi = spi_mosi_r;
assign spi_rdata = spi_read_data_r;
assign spi_rdata_valid = spi_read_data_valid_r;
assign spi_sclk = spi_clock_r;
assign spi_csn = ~clock_en_r;
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
spi_st <= SPI_INIT;
end
else begin
case (spi_st)
SPI_INIT:begin
if(spi_rden||spi_wden)begin
spi_st <= SPI_SEND_DATA;
end
else begin
spi_st <= SPI_INIT;
end
end
//--------------------------------------------------SPI_INIT
SPI_SEND_DATA:begin
if(clock_count_r==(clock_dev>>1)&&spi_send_count_r==4'd0)begin
spi_st <= SPI_END;
end
else begin
spi_st <= SPI_SEND_DATA;
end
end
//------------------------------------------------SPI_SEND_DATA
SPI_END:begin
if(clock_count_r==(clock_dev>>1)-1'b1)begin
spi_st <= SPI_INIT;
end
else begin
spi_st <= SPI_END;
end
end
default: spi_st <= SPI_INIT;
endcase
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
spi_read_data_r <=8'd0;
spi_read_data_valid_r<=1'd0;
end
else begin
if(spi_read_data_en&&clock_count_r==0&&spi_send_count_r<=4'd6&&spi_st==SPI_SEND_DATA)begin
spi_read_data_r <={spi_read_data_r[6:0],spi_miso};
spi_read_data_valid_r <=1'b0;
end
else begin
if(spi_read_data_en&&clock_count_r==0&&spi_st==SPI_END)begin
spi_read_data_r <={spi_read_data_r[6:0],spi_miso};
spi_read_data_valid_r <=1'b1;
end
else begin
spi_read_data_r <=spi_read_data_r;
spi_read_data_valid_r <=1'b0;
end
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
spi_mosi_r <= 1'b0;
end
else begin
if(spi_st==SPI_SEND_DATA&&clock_count_r==(clock_dev>>1))begin
spi_mosi_r <= spi_send_data_r[15];
end
else if(spi_st==SPI_END&&clock_count_r==(clock_dev>>1)-1'b1)begin
spi_mosi_r <= 1'b0;
end
else begin
spi_mosi_r <= spi_mosi_r;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
spi_send_data_r <= 16'd0;
spi_send_count_r <= 4'd15;
clock_en_r <=1'b0;
end
else begin
case (spi_st)
SPI_INIT:begin
spi_send_count_r <= 4'd15;
if(spi_rden)begin
spi_send_data_r <= {spi_raddr,8'd0};
clock_en_r <= 1'b1;
end
else if (spi_wden)begin
spi_send_data_r <= {spi_waddr,spi_wdata};
clock_en_r <= 1'b1;
end
else begin
spi_send_data_r <= spi_send_data_r;
clock_en_r <= 1'b0;
end
end
//---------------------------------------------SPI_INIT
SPI_SEND_DATA:begin
clock_en_r <= 1'b1;
if(clock_count_r==(clock_dev>>1))begin
spi_send_data_r <= {spi_send_data_r[14:0],1'b0};
if(spi_send_count_r==4'd0)begin
spi_send_count_r <=4'd15;
end
else begin
spi_send_count_r <=spi_send_count_r-4'd1;
end
end
else begin
spi_send_data_r <= spi_send_data_r;
spi_send_count_r <= spi_send_count_r;
end
end
//--------------------------------------------- SPI_SEND_DATA
SPI_END:begin
spi_send_count_r <=4'd15;
spi_send_data_r <= spi_send_data_r;
if(clock_count_r==(clock_dev>>1)-1'b1)begin
clock_en_r =1'b0;
end
else begin
clock_en_r =1'b1;
end
end
default:;
endcase
end
end
//----------------------------------------------spi_read_data_en
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
spi_read_data_en <=1'b0;
end
else begin
if(spi_st==SPI_INIT&&spi_rden)begin
spi_read_data_en <=1'b1;
end
else begin
if(clock_en_r==1'b0)begin
spi_read_data_en <=1'b0;
end
end
end
end
///////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
clock_count_r <=10'd0;
spi_clock_r <=1'b0;
end
else begin
if(clock_en_r)begin
if(clock_count_r<(clock_dev>>1))begin
clock_count_r <=clock_count_r+1'b1;
spi_clock_r <=1'b0;
end
else
begin
spi_clock_r <=1'b1;
if(clock_count_r>=clock_dev-1'b1)begin
clock_count_r <=10'd0;
end
else begin
clock_count_r <=clock_count_r+1'b1;
end
end
end
else begin
spi_clock_r <=1'b0;
clock_count_r <=10'd0;
end
end
end
endmodule
经过仿真测试,获取读数据8‘h34的仿真图如下:
四、数字电路设计
4.1、JK触发器
在 FPGA 上直接实现 JK 触发器时,由于其输出时钟频率过高,超出了仿真器的观测能力,因此采用了 D 触发器来模拟 JK 触发器的功能(需注意:此方法并非标准实现,会引入额外延迟)。
标准 JK 触发器的电路设计需使用两个与门和四个与非门。基于此结构在 FPGA 上实现了对应的 JK 触发器设计代码。
module JK(
sys_clk,
clk_jk,
jk_j_i,
jk_k_i,
jk_q_o,
jk_q_n_o,
voltage_high_en
);
input sys_clk;
input clk_jk;//JK触发时钟
input jk_j_i;//jk触发J输入
input jk_k_i;//jk触发K输入
output jk_q_o;//jk触发q输出
output jk_q_n_o;//jk触发q非输出
input voltage_high_en;
reg jk_step_1=1'b0;
reg jk_step_2=1'b0;
reg G3=1'b1;
reg G4=1'b1;
reg G1=1'b1;
reg G2=1'b1;
//此处屏蔽无法进行仿真的理论JK触发器搭建
/*
assign jk_step_1= (~voltage_high_en)?1'b0: jk_j_i&jk_q_n_o;
assign jk_step_2= (~voltage_high_en)?1'b0: jk_k_i&jk_q_o;
assign G3 = (~voltage_high_en)?1'b1: ~(clk_jk&jk_step_1);
assign G4 = (~voltage_high_en)?1'b1: ~(clk_jk&jk_step_2);
assign G1 = (~voltage_high_en)?1'b1: ~(jk_q_n_o&G3);
assign G2 = (~voltage_high_en)?1'b1: ~(jk_q_n_o&G4);*/
assign jk_q_o =G1;
assign jk_q_n_o =G2;
always @(posedge sys_clk)begin
jk_step_1 <=(~voltage_high_en)?1'b0: jk_j_i&jk_q_n_o;
jk_step_2 <=(~voltage_high_en)?1'b0: jk_k_i&jk_q_o;
G3 <= (~voltage_high_en)?1'b1: ~(clk_jk&jk_step_1);
G4 <=(~voltage_high_en)?1'b1: ~(clk_jk&jk_step_2);
G1 <=(~voltage_high_en)?1'b1: ~(jk_q_n_o&G3);
G2 <=(~voltage_high_en)?1'b1: ~(jk_q_n_o&G4);
end
endmodule
使用Vivado自带的仿真进行仿真结果如下,可以发现,使用D触发器搭建的Jk触发存在部分功能错误:1.在输入j、k均为1时,输出并未完全保持;2.在输入j=0,k=1时,输出并为完全为低电平;其余功能均正常。
4.2、译码器、编码器
FPGA可以通过查找表轻易实现3-8译码器和8-3编码器的功能,以下代码实现了3-8译码再编码的过程。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/08/11 11:39:18
// Design Name:
// Module Name: Encode_Decode
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module Encode_Decode(
sys_clk,
sys_rstn,
encode_i,//3_8译码器输入
encode_o,//3-8译码器//8-3编码器输出输入
decode_o//8-3编码器输出
);
input sys_clk;
input sys_rstn;
input[2:0] encode_i;
output reg[7:0] encode_o;
output reg[2:0] decode_o;
//--------------------------3-8译码器
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
encode_o <=8'b0;
end
else begin
case (encode_i)
3'd0:encode_o <=8'b0000000_1;
3'd1:encode_o <=8'b000000_1_0;
3'd2:encode_o <=8'b00000_1_00;
3'd3:encode_o <=8'b0000_1_000;
3'd4:encode_o <=8'b000_1_0000;
3'd5:encode_o <=8'b00_1_00000;
3'd6:encode_o <=8'b0_1_000000;
3'd7:encode_o <=8'b_1_0000000;
default:encode_o<=8'b00000000;
endcase
end
end
//---------------------------------------8-3编码器
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
decode_o <=3'd0;
end
else begin
if(encode_o[0])begin
decode_o <=3'd0;
end
else if (encode_o[1])begin
decode_o <=3'd1;
end
else if (encode_o[2])begin
decode_o <=3'd2;
end
else if (encode_o[3])begin
decode_o <=3'd3;
end
else if (encode_o[4])begin
decode_o <=3'd4;
end
else if (encode_o[5])begin
decode_o <=3'd5;
end
else if (encode_o[6])begin
decode_o <=3'd6;
end
else begin
decode_o <=3'd7;
end
end
end
endmodule
其仿真结果如下图所示。
4.3、积分器
数字积分器的实现较为简单:当积分器使能时,对输入信号执行乘积累加运算即可。在常规项目中,通常通过直接调用乘法器 IP 核实现信号的乘法操作来构建积分器。其实现代码如下所示。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/07/31 09:58:43
// Design Name:
// Module Name: Integral_Calculator
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module Integral_Calculator(
sys_clk,//100Mhz
sys_rstn,
data_input,
data_output
);
input sys_clk;
input sys_rstn;
input [15:0] data_input;
output [15:0] data_output;
//28bit
parameter [31:0] time_internal =32'd268435;//积分时间间隔
wire [47:0] data_internal_s;
reg [47:0] data_internal_s_add;
assign data_output=data_internal_s_add[43:28];
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
data_internal_s_add <=48'd0;
end
else begin
data_internal_s_add <=data_internal_s_add+data_internal_s;//积分相乘累加
end
end
//乘法器
mult_gen_0 Mul (
.CLK(sys_clk), // input wire CLK
.A(data_input), // input wire [15 : 0] A
.B(time_internal), // input wire [31 : 0] B
.P(data_internal_s) // output wire [47 : 0] P
);
endmodule
仿真对积分器输入三角波,三角波积分后成功变为正弦波(实际一个周期的波形是二次函数)。
4.4、微分器
数字微分器的实现,实际上是对当前采集的数据以及上一次采集的数据做差分即减法,其实现代码如下图所示。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/07/31 13:12:05
// Design Name:
// Module Name: Differentiator
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module Differentiator(
sys_clk,
sys_rstn,
data_input,
data_output,
Magnification_factor,
mag_step
);
input sys_clk;
input sys_rstn;
input [15:0] data_input;//输入微分数据
output [15:0] data_output;//输出放大微分数据
input [31:0] Magnification_factor;//放大倍数
input [31:0] mag_step;//采集抽样点数
wire [63:0] mult_out_r;//放大数据
reg [31:0] data_input_add;//数据位宽扩容寄存器
reg [31:0] data_input_before;//上一次采集数据
reg [31:0] data_add_r;//差分数据
reg [31:0] mag_count;//抽样计数器
wire en_r;//抽样使能
assign en_r=(mag_count>=mag_step-1'b1)?1'b1:1'b0;
assign data_output=mult_out_r[16:0];
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
mag_count <=32'd0;
end
else begin
if(mag_count>=mag_step-1'b1)begin
mag_count <=32'd0;
end
else begin
mag_count <=mag_count+32'd1;
end
end
end
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
data_input_add <=32'd0;
data_input_before <=32'd0;
data_add_r <=32'd0;
end
else begin
if(en_r)begin
data_input_add <={((data_input[15]==1'b1)?16'hffff:16'd0),data_input};
data_input_before <=data_input_add;
data_add_r <=data_input_add-data_input_before;
end
end
end
mult_gen_1 your_instance_name (
.CLK (sys_clk), // input wire CLK
.A (Magnification_factor), // input wire [15 : 0] A
.B (data_add_r), // input wire [15 : 0] B
.P (mult_out_r) // output wire [31 : 0] P
);
endmodule
仿真输入三角波,连接积分器后,积分器输出连接微分器输入,仿真可以成功得出三角波积分后的正弦波(实际二次函数周期波),以及正弦波微分后的还原三角波(高频分量有损)。
4.5、IIR无限响应滤波器
以构建4阶切比雪夫II型无限冲激响应(IIR)数字滤波器为例。在搭建滤波器之前,首先需要获取其设计参数。这些参数可以通过MATLAB或其他专业滤波器设计软件生成。本文以FilterSolution软件为例:设计一个截止频率为100 kHz的4阶切比雪夫II型滤波器,设置采样频率和截止频率后,软件将生成系统Z变换传递函数,从而导出滤波器参数。
下图展示了所获取的系统Z变换传递函数。根据图示,该4阶IIR滤波器的系数为:
分子系数 (b): 0.01559, 0.02297, 0.01477, 0.02297, 0.01559
分母系数 (a): 2.32, -2.244, 1.01, -0.1784
为在FPGA中实现,需将上述浮点系数进行定点量化。采用32位和16位定点化后,系数结果分别为:
分子系数 (b): 1022, 1505, 968, 1505, 1022
分母系数 (a): 152043, -147063, 66191, -11692
该滤波器在FPGA上的具体实现方法见下文代码。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/07/30 13:47:09
// Design Name:
// Module Name: Fir_solution
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module Fir_solution(
sys_clk,//50Mhz系统时钟
sys_rstn,//系统复位
data_1Mhz_valid,//1Mhz使能
data_input,//数据输入
data_fir_output//数据输出
);
input sys_clk;
input sys_rstn;
input data_1Mhz_valid;
input [15:0] data_input;
output [15:0] data_fir_output;
//滤波器系数
parameter [31:0] data_x_1 = 32'd1022;
parameter [31:0] data_x_2 = 32'd1505;
parameter [31:0] data_x_3 = 32'd968;
parameter [31:0] data_x_4 = 32'd1505;
parameter [31:0] data_x_5 = 32'd1022;
parameter [31:0] data_y_1 = 32'd152043 ;
parameter [31:0] data_y_2 = 32'd0-32'd147063;
parameter [31:0] data_y_3 = 32'd66191;
parameter [31:0] data_y_4 = 32'd0-32'd11692;
reg [15:0] data_x_1_r;
reg [15:0] data_x_2_r;
reg [15:0] data_x_3_r;
reg [15:0] data_x_4_r;
reg [15:0] data_x_5_r;
reg [15:0] data_y_out_r;
reg [15:0] data_y_1_r;
reg [15:0] data_y_2_r;
reg [15:0] data_y_3_r;
reg [15:0] data_y_4_r;
reg [15:0] input_multply_data;//输入乘法数据
reg [31:0] filter_data;//滤波数据
reg [39:0] data_1Mhz_valid_delay;//时分复用时间延时
wire [47:0] output_mult_data;//输出乘法数据
reg [47:0] process_raw_data_r;//乘法反馈原始数据
assign data_fir_output=data_y_out_r;
//--------------------------------------时分复用输出
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
data_y_out_r <=16'd0;
end
else begin
if(data_1Mhz_valid_delay[38])begin
data_y_out_r <=(process_raw_data_r[31:16]);
end
else begin
end
end
end
//-------------------------------------------时分复用结果累加
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
process_raw_data_r <=48'd0;
end
else begin
if(data_1Mhz_valid_delay[5]|data_1Mhz_valid_delay[9]|data_1Mhz_valid_delay[13]|data_1Mhz_valid_delay[17]|data_1Mhz_valid_delay[21]|data_1Mhz_valid_delay[25]
|data_1Mhz_valid_delay[29]|data_1Mhz_valid_delay[33]|data_1Mhz_valid_delay[37]
)begin
process_raw_data_r <= process_raw_data_r+output_mult_data;
end
else if (data_1Mhz_valid_delay[38])begin
process_raw_data_r <=48'd0;
end
else begin
process_raw_data_r <=process_raw_data_r;
end
end
end
//-----------------------------------------------------乘法器的时分复用
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
input_multply_data <=16'd0;
filter_data <=32'd0;
end
else begin
if(data_1Mhz_valid_delay[0])begin
input_multply_data <=data_x_1_r;
filter_data <=data_x_1;
end
else if (data_1Mhz_valid_delay[4])begin
input_multply_data <=data_x_2_r;
filter_data <=data_x_2;
end
else if (data_1Mhz_valid_delay[8])begin
input_multply_data <=data_x_3_r;
filter_data <=data_x_3;
end
else if (data_1Mhz_valid_delay[12])begin
input_multply_data <=data_x_4_r;
filter_data <=data_x_4;
end
else if (data_1Mhz_valid_delay[16])begin
input_multply_data <=data_x_5_r;
filter_data <=data_x_5;
end
else if (data_1Mhz_valid_delay[20])begin
input_multply_data <=data_y_1_r;
filter_data <=data_y_1;
end
else if (data_1Mhz_valid_delay[24])begin
input_multply_data <=data_y_2_r;
filter_data <=data_y_2;
end
else if (data_1Mhz_valid_delay[28])begin
input_multply_data <=data_y_3_r;
filter_data <=data_y_3;
end
else if (data_1Mhz_valid_delay[32])begin
input_multply_data <=data_y_4_r;
filter_data <=data_y_4;
end
else begin
input_multply_data <=input_multply_data;
filter_data <=filter_data;
end
end
end
//----------------------------------------时分复用移位寄存器
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
data_1Mhz_valid_delay <=40'd0;
end
else begin
data_1Mhz_valid_delay <={data_1Mhz_valid_delay[38:0],data_1Mhz_valid};
end
end
//----------------------------------------------D触发器
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
data_x_1_r <=16'd0;
data_x_2_r <=16'd0;
data_x_3_r <=16'd0;
data_x_4_r <=16'd0;
data_x_5_r <=16'd0;
data_y_1_r <=16'd0;
data_y_2_r <=16'd0;
data_y_3_r <=16'd0;
data_y_4_r <=16'd0;
end
else begin
if(data_1Mhz_valid)begin
data_x_1_r <= data_input;
data_x_2_r <=data_x_1_r;
data_x_3_r <=data_x_2_r;
data_x_4_r <=data_x_3_r;
data_x_5_r <=data_x_4_r;
end
if(data_1Mhz_valid_delay[39])begin
data_y_1_r <=data_y_out_r;
data_y_2_r <=data_y_1_r;
data_y_3_r <=data_y_2_r;
data_y_4_r <=data_y_3_r;
end
end
end
//-----------乘法器,理论应用9个乘法器,时分复用仅需一个
mult_gen_0 your_instance_name (
.CLK (sys_clk), // input wire CLK
.A (input_multply_data), // input wire [15 : 0] A
.B (filter_data), // input wire [31 : 0] B
.P (output_mult_data) // output wire [47 : 0] P
);
endmodule
输入100Khz的方波进行仿真,可正确得出相同频率的正弦波,仿真图如下所示。
4.6、快速傅里叶变换
在 FPGA 上实现傅里叶变换时,可采用手动搭建基于蝶形运算的快速傅里叶变换(FFT)结构,或直接调用 XFFT IP 核。本设计采用 XFFT IP 核实现 FFT,并通过编写 TestBench 验证其输出频谱的正确性。
4.6.1、XFFT ip核的配置
为实现对输入波形的实时快速傅里叶变换(FFT),XFFT IP 核的配置界面(下图所示)设置如下:
最大变换点数: 2048
采样频率: 50 MHz
架构模式: 流水线(Streaming I/O)
启用运行时可配置变换点数: 勾选(Run Time Configurable Transform Length)
界面二配置数据格式为定点,缩放类型为不缩放,输出顺序为自然顺序后,生成ip核,即可进行使用。
4.6.2、Cordic ip核配置
CORDIC IP 核在以下两个模块中使用:
TestBench 正弦波生成: 用于此模块的 CORDIC IP 核配置界面(如下图所示)设置如下:
功能选择: Sin and Cos
输出模式: Parallel
相位格式 (Phase Format): Scaled Radians
输入缩放相位范围: -1 至 1(对应未缩放相位范围:-π 至 π)
(配置完成后生成 IP 核。)
2. 平方根功能实现: XFFT IP 核输出的频域数据包含实部和虚部。为清晰观测波形的频谱特性,需计算其模值(Magnitude),该过程涉及平方根运算。
如下图所示,用于此功能的 CORDIC IP 核配置如下:
功能选择: Square Root (平方根)
数据格式: Unsigned Integer
(配置完成后生成 IP 核。)
4.6.3、代码编写
该功能模块负责对连续波形进行快速傅里叶变换(FFT),核心实现特性如下:
支持动态配置 FFT 点数: 最高 2048 点
输入数据位宽: 16 bit
输出频谱位宽: 24 bit
输出接口: AXI-Stream 总线
AXI-Stream 突发长度: 等于当前配置的 FFT 变换点数
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/08/12 08:47:20
// Design Name:
// Module Name: FFT
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module FFT(
s_axis_config_tdata,//XFFT 配置总线
s_axis_config_tvalid,//XFFT 配置总线
s_axis_config_tready,//XFFT 配置总线
fft_burst_length,//突发长度
sys_clk,//时钟
sys_rstn,//复位
data_input,//数据输入
data_input_valid,//数据输入有效
fft_data_output,//频谱输出
fft_data_valid,//频谱输出有效
fft_data_last,//频谱单次帧完成
fft_image,//频谱虚部,测试信号
fft_real//频谱实部,测试信号
);
input [31:0] s_axis_config_tdata;
input s_axis_config_tvalid;
output s_axis_config_tready;
input sys_clk;
input sys_rstn;
input [15:0] data_input;
input data_input_valid;
input [15:0] fft_burst_length;
output [23:0] fft_data_output;
output fft_data_valid;
output fft_data_last;
output [15:0] fft_image;
output [15:0] fft_real;
reg [31:0] s_axis_data_tdata;//AXI-Stream总线,输入XFFT
reg s_axis_data_tvalid;
wire s_axis_data_tready;
reg s_axis_data_tlast;
reg fifo_rden;//fifo读使能
reg [15:0] burst_length_count_r;//突发长度计数
reg [15:0] burst_length_count_out_r;//输出突发长度计数
wire [15:0] fifo_dout;//fifo 输出
wire fifo_empty;//fifo 空信号
wire [31:0] mul_real_out;//实部平方
wire [31:0] mul_image_out;//虚平方
reg [32:0] mul_final_add;//平方和
wire cordic_valid_i;//平方根输入有效
wire [39:0] cordic_data_i;//平方根输入
reg cordic_last_o;//平方根输出帧完成
wire cordic_valid_o;//平方根输出有效
wire [23:0] cordic_data_o;//平方根输出数据
reg [1:0] m_axis_data_tvalid_buf;//输出数据同步
wire [63:0] m_axis_data_tdata;//XFFT频谱输出
wire m_axis_data_tvalid;//XFFT频谱有效
wire m_axis_data_tready;//接收准备信号
wire m_axis_data_tlast;//帧完成信号
assign m_axis_data_tready=1'b1;
assign cordic_valid_i=m_axis_data_tvalid_buf[1];
assign cordic_data_i ={7'd0,mul_final_add};
assign fft_data_output=cordic_data_o;
assign fft_data_valid=cordic_valid_o;
assign fft_data_last=cordic_last_o;
assign fft_image=m_axis_data_tdata[59:44];
assign fft_real =m_axis_data_tdata[27:12];
//--------------------------------------------------------------Tlast output
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
burst_length_count_out_r <=10'd0;
cordic_last_o <=1'b0;
end
else begin
if(cordic_valid_o)begin
if(burst_length_count_out_r>=fft_burst_length)begin
burst_length_count_out_r <=10'd0;
cordic_last_o <=1'b1;
end
else begin
burst_length_count_out_r <=burst_length_count_out_r+10'd1;
cordic_last_o <=1'b0;
end
end
else begin
burst_length_count_out_r <=burst_length_count_out_r;
cordic_last_o <=1'b0;
end
end
end
//--------------------------------------------------------------------------sync
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
m_axis_data_tvalid_buf <=2'd0;
end
else begin
m_axis_data_tvalid_buf <={m_axis_data_tvalid_buf[0],m_axis_data_tvalid};
end
end
//------------------------------------------------------------------平方和
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
mul_final_add <=33'd0;
end
else begin
mul_final_add <=mul_real_out+mul_image_out;
end
end
//---------------------------------------------------------------------读fifo
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
s_axis_data_tlast <=1'b0;
end
else begin
if(fifo_rden&&fifo_empty==1'b0)begin
if(burst_length_count_r>=fft_burst_length)begin
s_axis_data_tlast <=1'b1;
end
else begin
s_axis_data_tlast <=1'b0;
end
end
else begin
s_axis_data_tlast <=s_axis_data_tlast;
end
end
end
//-----------------------------------------------------------------突发长度计数器
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
burst_length_count_r <=10'd0;
end
else begin
if(fifo_rden&&fifo_empty==1'b0)begin
if(burst_length_count_r>=fft_burst_length)begin
burst_length_count_r <=10'd0;
end
else begin
burst_length_count_r <=burst_length_count_r+10'd1;
end
end
else begin
burst_length_count_r <=burst_length_count_r;
end
end
end
//-----------------------------------------------------------XFFT输入
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
s_axis_data_tdata <=32'd0;
s_axis_data_tvalid <=1'b0;
end
else begin
if(fifo_rden&&fifo_empty==1'b0)begin
s_axis_data_tdata <={16'd0,fifo_dout};
s_axis_data_tvalid <=1'b1;
end
else begin
if(s_axis_data_tready==1'b0)begin
s_axis_data_tdata <= s_axis_data_tdata;
s_axis_data_tvalid <= 1'b0;
end
else begin
s_axis_data_tdata <=s_axis_data_tdata;
s_axis_data_tvalid <=s_axis_data_tvalid;
end
end
end
end
//-------------------------------------------------------------fifo读使能
always @(posedge sys_clk or negedge sys_rstn)begin
if(sys_rstn==1'b0)begin
fifo_rden <=1'b0;
end
else begin
if(s_axis_data_tready==1'b1&&fifo_empty==1'b0)begin
fifo_rden <=1'b1;
end
else begin
fifo_rden <=1'b0;
end
end
end
//-------------ip 核的调用
cordic cordic_square (
.aclk(sys_clk), // input wire aclk
.s_axis_cartesian_tvalid(cordic_valid_i), // input wire s_axis_cartesian_tvalid
.s_axis_cartesian_tdata(cordic_data_i), // input wire [39 : 0] s_axis_cartesian_tdata
.m_axis_dout_tvalid(cordic_valid_o), // output wire m_axis_dout_tvalid
.m_axis_dout_tdata(cordic_data_o) // output wire [23 : 0] m_axis_dout_tdata
);
mult Mult_Image (
.CLK(sys_clk), // input wire CLK
.A(m_axis_data_tdata[59:44]), // input wire [15 : 0] A
.B(m_axis_data_tdata[59:44]), // input wire [15 : 0] B
.P(mul_image_out) // output wire [31 : 0] P
);
mult Mult_Real (
.CLK(sys_clk), // input wire CLK
.A(m_axis_data_tdata[27:12]), // input wire [15 : 0] A
.B(m_axis_data_tdata[27:12]), // input wire [15 : 0] B
.P(mul_real_out) // output wire [31 : 0] P
);
fifo Fifo_Data (
.clk(sys_clk), // input wire clk
.srst(~sys_rstn), // input wire srst
.din(data_input), // input wire [15 : 0] din
.wr_en(data_input_valid), // input wire wr_en
.rd_en(fifo_rden), // input wire rd_en
.dout(fifo_dout), // output wire [15 : 0] dout
.full(), // output wire full
.empty(fifo_empty) // output wire empty
);
//-------------------------------------------------fft ip 核
xfft xfft (
.aclk(sys_clk), // input wire aclk
.s_axis_config_tdata(s_axis_config_tdata), // input wire [23 : 0] s_axis_config_tdata
.s_axis_config_tvalid(s_axis_config_tvalid), // input wire s_axis_config_tvalid
.s_axis_config_tready(s_axis_config_tready), // output wire s_axis_config_tready
.s_axis_data_tdata(s_axis_data_tdata), // input wire [31 : 0] s_axis_data_tdata
.s_axis_data_tvalid(s_axis_data_tvalid), // input wire s_axis_data_tvalid
.s_axis_data_tready(s_axis_data_tready), // output wire s_axis_data_tready
.s_axis_data_tlast(s_axis_data_tlast), // input wire s_axis_data_tlast
.m_axis_data_tdata(m_axis_data_tdata), // output wire [31 : 0] m_axis_data_tdata
.m_axis_data_tvalid(m_axis_data_tvalid), // output wire m_axis_data_tvalid
.m_axis_data_tready(m_axis_data_tready), // input wire m_axis_data_tready
.m_axis_data_tlast(m_axis_data_tlast), // output wire m_axis_data_tlast
.event_frame_started(), // output wire event_frame_started
.event_tlast_unexpected(), // output wire event_tlast_unexpected
.event_tlast_missing(), // output wire event_tlast_missing
.event_status_channel_halt(), // output wire event_status_channel_halt
.event_data_in_channel_halt(), // output wire event_data_in_channel_halt
.event_data_out_channel_halt() // output wire event_data_out_channel_halt
);
endmodule
TestBench如下,使用Cordic生成正弦波进行测试,也可以通过Matlab生成正弦波,将正弦波参数放入Txt文档,TestBench读取txt文件输入正弦波进行测试。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/08/12 09:29:22
// Design Name:
// Module Name: SIM
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module SIM(
);
reg [31:0] s_axis_config_tdata;
reg s_axis_config_tvalid;
wire s_axis_config_tready;
reg [15:0] fft_burst_length;
reg sys_clk;
reg sys_rstn;
wire [31:0] m_axis_dout_tdata;
wire m_axis_dout_tvalid;
reg rst_rdy;
reg s_axis_phase_tvalid;
reg [15:0] s_axis_phase_tdata;
wire [15:0] sin_wave;
wire [15:0] fft_image;
wire [15:0] fft_real;
wire [23:0] fft_data_output;
wire fft_data_valid;
wire fft_data_last;
reg [15:0] count;
assign sin_wave= m_axis_dout_tdata[15:0];
initial begin
s_axis_config_tdata ={23'd0,4'b1111,5'd11};
s_axis_config_tvalid =1'b0;
fft_burst_length =16'd2047;
count =16'd0;
sys_clk =1'b0;
rst_rdy =1'b0;
sys_rstn =1'b0;
s_axis_phase_tvalid =1'b0;
s_axis_phase_tdata =16'd0;
#100
sys_rstn =1'b1;
#500
while (!s_axis_config_tready)begin
end
s_axis_config_tvalid =1'b1;
#520
s_axis_config_tvalid =1'b0;
rst_rdy =1'b1;
end
always begin
#1
if(rst_rdy)begin
if(s_axis_phase_tdata==16'h2000)begin
s_axis_phase_tdata <=16'he000;
end
else begin
s_axis_phase_tdata <=s_axis_phase_tdata+16'd64;
end
end
end
always begin
#10
sys_clk=~sys_clk;
#0.0001
if(rst_rdy)begin
s_axis_phase_tvalid <=1'b1;
count <=count+16'd1;
end
else begin
s_axis_phase_tvalid <=1'b0;
end
end
sin sin_ou (
.aclk(sys_clk), // input wire aclk
.s_axis_phase_tvalid(s_axis_phase_tvalid), // input wire s_axis_phase_tvalid
.s_axis_phase_tdata(s_axis_phase_tdata), // input wire [15 : 0] s_axis_phase_tdata
.m_axis_dout_tvalid(m_axis_dout_tvalid), // output wire m_axis_dout_tvalid
.m_axis_dout_tdata(m_axis_dout_tdata) // output wire [31 : 0] m_axis_dout_tdata
);
FFT FFT_SIM(
.s_axis_config_tdata (s_axis_config_tdata),
.s_axis_config_tvalid (s_axis_config_tvalid),
.s_axis_config_tready (s_axis_config_tready),
.fft_burst_length (fft_burst_length),
.sys_clk (sys_clk),
.sys_rstn (sys_rstn),
.data_input (m_axis_dout_tdata[15:0]),
.data_input_valid (m_axis_dout_tvalid),
.fft_data_output (fft_data_output),
.fft_data_valid (fft_data_valid),
.fft_data_last (fft_data_last),
.fft_image (fft_image),
.fft_real (fft_real)
);
endmodule
4.6.4、仿真结果
仿真结果如下图所示,可以看到,仿真持续输入正弦波的同时,模块持续输出2048点的FFT变换,每2048点Tlast拉高代表当前一帧的频谱输出完成。由于输入的是持续不变的周期性正弦波,因此每一帧的频谱基本一致。