在之前STM32的学习中,认识到无论实现什么功能,有几个基础功能是必不可少的,缺失这些功能则无法将代码以我们想要的逻辑运行下去,STM32因为STM32CubeMX的存在,使得这些功能能够通过图形化配置,建立代码框架,但我们知道,FPGA是硬件电路代码化,其本身并没有已经固化好的功能,比如串口、IIC、SPI等,甚至连RAM和ROM都没有,但这也就是FPGA可以是任何东西的原因,其并未将功能固化到引脚上的同时,各个功能也没有了像STM32一样的引脚束缚。 但由于上述功能的概念大众较为认可,Vivado同样也集成了类似的图形化配置功能,其命名为IP核。 故该章主要讲述的就是IP核的三个大功能,分别是:
- PLL锁相环【时钟相关】
- RAM随机存储器【活动内存相关】
- FIFO先进先出【数据传输相关】
IP核之MMCM/PLL
MMCM/PLL通俗的将就是将晶振的主频提高或降低的核心部件,为不同的功能提供不同的时钟信号,可以理解为STM32CubeMX的时钟树配置的部分,只是STM32CubeMX的时钟树需要兼顾多个部件的时钟供应,故存在诸多限制,但MMCM/PLL则不存在此限制,直接配置即可
目标功能
使用Clocking Wizard IP核产生4个时钟100MHz、100MHz_180deg、50M、25M,连接到U20、T20、P19、N18四个引脚。
IP核配置
新建Vivado项目后,点击Project Manager下的IP Catalog,即可进入IP核的配置界面 在IP核配置界面搜索clock,找到Clocking Wizard 首先配置时钟源,时钟源为FPGA的板上晶振提供的时钟信号,我是用的板子时钟为50MHz晶振,则将时钟源配置为50MHz 配置完成时钟源后,进行输出时钟配置,我们的配置目标是100MHz、100MHz_180deg、50M、25M4个时钟,其中180deg的意义为相位后移180° Clock_Wizard的IP核存在一个控制信号为locked,在Port Renaming中可对此信号重命名,若无特殊要求则默认即可 MMCM Settings为而配置总览 完成上一步后,若确认无异常,则可直接点击OK,点击后会出现Generate Output Products界面,直接点击Generate即可 之后在Source串口的IP Source菜单即可找到clk_wiz_0.veo,双击打开后可找到clk_wiz_0的例化接口。 接口如下:
clk_wiz_0 instance_name
(
.clk_out1_100m(clk_out1_100m),
.clk_out2_100m_180(clk_out2_100m_180),
.clk_out3_50m(clk_out3_50m),
.clk_out4_25m(clk_out4_25m),
.reset(reset),
.locked(locked),
.clk_in1(clk_in1));
代码编辑
为验证内核功能,需要建立一个verilog文件将ip_wiz_0功能例化,名称为MMCM_PLL.v内容如下:
`timescale 1ns / 1ps
module MMCM_PLL(
input sys_clk ,
input sys_rst_n ,
output clk_100m ,
output clk_100m_180deg,
output clk_50m ,
output clk_25m
);
wire locked;
clk_wiz_0 clk_wiz_0
(
.clk_out1_100m (clk_100m),
.clk_out2_100m_180 (clk_100m_180deg),
.clk_out3_50m (clk_50m),
.clk_out4_25m (clk_25m),
.reset (~sys_rst_n),
.locked (locked),
.clk_in1 (sys_clk)
);
endmodule
除了逻辑代码外,还需要添加约束代码【用于堆芯逻辑输出和硬件物理引脚】 约束文件命名为MMCM_PLL.xdc 若各位码哥有逻辑分析仪,则直接生成代码下载测试,如果没有的话则需要在Vivado或者Modelsim中进行仿真,仿真的玩法基本一致,这里图方便就在Vivado实现了;
仿真配置
首先建立仿真verilog文件: 将新建立的仿真文件命名为tb_MMCM_PLL
需要建立一个verilog文件进行仿真,名称为tb_MMCM_PLL.v,代码如下:
`timescale 1ns / 1ps
module tb_MMCM_PLL();
reg sys_clk;
reg sys_rst_n;
wire clk_100m;
wire clk_100m_180deg;
wire clk_50m;
wire clk_25m;
always #10 sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
end
MMCM_PLL u_MMCM_PLL(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.clk_100m (clk_100m ),
.clk_100m_180deg (clk_100m_180deg ),
.clk_50m (clk_50m ),
.clk_25m (clk_25m )
);
endmodule
该仿真程序的核心就是仿真出时钟信号sys_clk和复位信号sys_rst_n。
接下来就是进行仿真,首先左键点击项目管理的Run Simulation,在弹出的菜单中点击第一项RUN Behavioral Simulation即可开始仿真 按照下图红框中配置好仿真的时长,并开始仿真 由图可见生成的各个时钟的波形,与预期一致,到此,IP的内核功能已经实现!
IP核之RAM
RAM随机存储器,它可以随时把数据写入任一指定地 址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度是由时钟频率决定的。可以理解为STM32在编写代码时,定义的变量在赋值或使用时所对应的硬件地址所在的存储表。
目标功能
定义一块RAM空间,对RAM空间内的数据进行写入的读取;仅使用时钟输入和复位输入;
IP核配置
新建Vivado工程,在IP核的配置界面搜索Block Memory 配置如下图所示,配置总线类型为Native、存储器类型为单端口RAM【读写只能通过一个端口实现】 接下来配置端口A,读取数据宽度8bit深度32,写入数据宽度8bit深度32,读写模式使用不变模式No Change【不允许同时读写】,取消Primitives Output Register,防止在仿真时,数据后滞一个时钟周期,不利于学习过程的分析,选中该选项是打开输出流水寄存器,可改善时序性能,正常使用时一般选择打开; Other Options在该实验暂时不用修改,故跳过,之后在Summary中进行全局检查,无问题则直接点击OK。 跳出生成界面直接点击生成(Generate)即可 在与MMCM_PLL的IP核相同的位置可找到RAM的例化端口 复制例化端口后进行编码;
blk_mem_gen_0 your_instance_name (
.clka(clka),
.ena(ena),
.wea(wea),
.addra(addra),
.dina(dina),
.douta(douta)
);
代码编辑
建立使用RAM的功能模块,命名为ram_rw.v,该模块和IP核为同层级; 代码如下:
`timescale 1ns / 1ps
module ram_rw(
input clk ,
input rst_n ,
output ram_en ,
output ram_wea ,
output reg [4:0] ram_addr ,
output reg [7:0] ram_wr_data,
input [7:0] ram_rd_data
);
reg [5:0] rw_cnt;
assign ram_en = rst_n;
assign ram_wea = (rw_cnt <= 6'd31 && ram_en == 1'b1) ? 1'b1 : 1'b0;
always @ (posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)
rw_cnt <= 1'b0;
else if(rw_cnt == 6'd63)
rw_cnt <= 1'b0;
else
rw_cnt <= rw_cnt + 1'b1;
end
always @ (posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)
ram_wr_data <= 1'b0;
else if(rw_cnt <= 6'd31)
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 1'b0;
end
always@(posedge clk or negedge rst_n)begin
if(rst_n == 1'b0)
ram_addr <= 1'b0;
else if(rw_cnt == 6'd31)
ram_addr <= 1'b0;
else
ram_addr <= ram_addr + 1'b1;
end
endmodule
新建高层级的模块,调用IP核和刚刚写好的模块ip_ram.v,端口仅为时钟信号和复位信号,代码如下:
`timescale 1ns / 1ps
module ip_ram(
input sys_clk ,
input sys_rst_n
);
wire ram_en ;
wire ram_wea ;
wire [4:0] ram_addr ;
wire [7:0] ram_wr_data ;
wire [7:0] ram_rd_data ;
ram_rw u_ram_rw(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.ram_en (ram_en ),
.ram_wea (ram_wea ),
.ram_addr (ram_addr ),
.ram_wr_data(ram_wr_data),
.ram_rd_data(ram_rd_data)
);
blk_mem_gen_0 blk_mem_gen_0(
.clka (sys_clk ) ,
.ena (ram_en ) ,
.wea (ram_wea ) ,
.addra (ram_addr ) ,
.dina (ram_wr_data) ,
.douta (ram_rd_data)
);
endmodule
接下来建立约束文件,命名为ip_ram.xdc,代码如下:
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
仿真配置
RAM的读写由于与物理引脚无关,故需要进行仿真验证,确定RAM的IP核功能是否正确,新建访问文件为tb_ip_ram.v,具体代码如下:
`timescale 1ns / 1ps
module tb_ip_ram();
reg sys_clk;
reg sys_rst_n;
always#10 sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
end
ip_ram u_ip_ram(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n )
);
endmodule
之后与仿真MMCM_PLL一样的操作进行仿真: 进行仿真 仿真结果放大后,结果如下: 当ram_wea信号拉高后进行写操作,写入数据与地址一一对应; 当ram_wea信号拉低后进行读操作,读取数据与地址一一对应;
至此测试完成。
IP核之FIFO
目标功能
使用异步方式将数据先写入FIFO区,再从FIFO区读取; 其大致思路如下图所示,建立一个FIFO的写入模块u_fifo_wr、建立一个FIFO的读取模块u_fifo_rd、建立一个FIFO的IP核fifo_generator_0,若要下载到FPGA测试则还需要建立一个虚拟逻辑分析仪IP核ila_0:
IP核配置
与之前的IP核方法一样,在IP核搜索栏搜索FIFO,找到FIFO_Generator 选在Native接口的独立时钟块的RAM“Independent Clocks Block RAM” 对FIFO接口进行配置,本次实验读写数据为8位,故数据宽度都为8位,数据深度暂定为256,读取写入每次都顺序操作255个数据,由于仅观察FIFO读写,故ResetPin可以取消勾选。 状态标记配置需要把快满和快空的标记勾选上 配置数据计数器,读写数据计数器均使能,且宽度定义为8 完成上述操作后直接点击OK,则会跳出IP核生成器,直接点击生成即可; FIFO的IP核例化端口可在下图所示位置实现
代码编辑
顶层模块代码文件命名为ip_fifo.v,代码如下:
`timescale 1ns / 1ps
module ip_fifo(
input sys_clk,
input sys_rst_n
);
wire fifo_wr_en ;
wire fifo_rd_en ;
wire [7:0] fifo_din ;
wire [7:0] fifo_dout ;
wire almost_full ;
wire almost_empty;
wire fifo_full ;
wire fifo_empty ;
wire [7:0] fifo_wr_data_count;
wire [7:0] fifo_rd_data_count;
fifo_generator_0 fifo_generator_0(
.wr_clk (sys_clk ),
.rd_clk (sys_clk ),
.wr_en (fifo_wr_en ),
.rd_en (fifo_rd_en ),
.din (fifo_din ),
.dout (fifo_dout ),
.almost_full (almost_full ),
.almost_empty (almost_empty ),
.full (fifo_full ),
.empty (fifo_empty ),
.wr_data_count (fifo_wr_data_count),
.rd_data_count (fifo_rd_data_count)
);
fifo_wr u_fifo_wr(
.clk ( sys_clk ),
.rst_n ( sys_rst_n ),
.fifo_wr_en ( fifo_wr_en ) ,
.fifo_wr_data ( fifo_din ) ,
.almost_empty ( almost_empty ),
.almost_full ( almost_full )
);
fifo_rd u_fifo_rd(
.clk ( sys_clk ),
.rst_n ( sys_rst_n ),
.fifo_rd_en ( fifo_rd_en ),
.fifo_dout ( fifo_dout ),
.almost_empty ( almost_empty ),
.almost_full ( almost_full )
);
ila_0 ila_0(
.clk (sys_clk ),
.probe0 (fifo_wr_en ),
.probe1 (fifo_rd_en ),
.probe2 (fifo_din ),
.probe3 (fifo_dout ),
.probe4 (fifo_empty ),
.probe5 (almost_empty ),
.probe6 (fifo_full ),
.probe7 (almost_full ),
.probe8 (fifo_wr_data_count),
.probe9 (fifo_rd_data_count)
);
endmodule
新建约束文件,命名为ip_dido.xdc,代码如下:
create_clock -period 20.000 -name clk [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
编写FIFO写入模块,命名为fifo_wr.v,代码如下:
`timescale 1ns / 1ps
module fifo_wr(
input clk ,
input rst_n ,
input almost_empty,
input almost_full ,
output reg fifo_wr_en ,
output reg [7:0]fifo_wr_data
);
reg [1:0] state ;
reg almost_empty_d0 ;
reg almost_empty_syn;
reg [3:0] dly_cnt ;
always@(posedge clk)begin
if(!rst_n)begin
almost_empty_d0 <= 1'b0;
almost_empty_syn <= 1'b0;
end
else begin
almost_empty_d0 <= almost_empty;
almost_empty_syn <= almost_empty_d0;
end
end
always @(posedge clk)begin
if(!rst_n)begin
fifo_wr_en <= 1'b0;
fifo_wr_data <= 8'b0;
state <= 2'b0;
dly_cnt <= 4'd0;
end
else begin
case(state)
2'd0:begin
if(almost_empty_syn) begin
state <= 2'd1;
end
else
state <= state;
end
2'd1:begin
if(dly_cnt == 4'd10) begin
dly_cnt <= 4'd0;
state <= 2'd2;
fifo_wr_en <= 1'b1;
end
else
dly_cnt <= dly_cnt + 4'd1;
end
2'd2:begin
if(almost_full) begin
fifo_wr_en <= 1'b0;
fifo_wr_data <= 8'd0;
state <= 2'd0;
end
else begin
fifo_wr_en <= 1'b1;
fifo_wr_data <= fifo_wr_data + 1'd1;
end
end
default : state <= 2'd0;
endcase
end
end
endmodule
编写FIFO的读取模块,命名为fifo_rd.v,代码如下:
`timescale 1ns / 1ps
module fifo_rd(
input clk ,
input rst_n ,
input [7:0]fifo_dout,
input almost_full ,
input almost_empty,
output reg fifo_rd_en
);
reg [1:0] state ;
reg almost_full_d0 ;
reg almost_full_syn;
reg [3:0] dly_cnt ;
always@(posedge clk)begin
if(!rst_n)begin
almost_full_d0 <= 1'b0;
almost_full_syn <= 1'b0;
end
else begin
almost_full_d0 <= almost_full;
almost_full_syn <= almost_full_d0;
end
end
always @(posedge clk)begin
if(!rst_n)begin
fifo_rd_en <= 1'b0;
state <= 2'b0;
dly_cnt <= 4'd0;
end
else begin
case(state)
2'd0:begin
if(almost_full_syn) begin
state <= 2'd1;
end
else
state <= state;
end
2'd1:begin
if(dly_cnt == 4'd10) begin
dly_cnt <= 4'd0;
state <= 2'd2;
end
else
dly_cnt <= dly_cnt + 4'd1;
end
2'd2:begin
if(almost_empty) begin
fifo_rd_en <= 1'b0;
state <= 2'd0;
end
else
fifo_rd_en <= 1'b1;
end
default : state <= 2'd0;
endcase
end
end
endmodule
因为读写FIFO的动作下载到FPGA上后,实验现象难以观测,故需要启动虚拟逻辑分析仪内核ILA实现。 首先还是在IP核中搜索ILA,选中下图所示的IP核 由于我们需要观测的数据为读写过程中的使能、数据、存满、读空、将满将空等数据,具体内容如下,则需要配置10路虚拟逻辑分析仪探针,数据深度设置为1024【深度越深,采集的过程信息越多,占用FPGA空间越多,需要适可而止】 对虚拟逻辑分析仪配置的10个端口的数据宽度,针对读取写入的具体数据需要配置的数据宽度为8,其余布尔量信号宽度为1即可。 配置完成之后直接点击OK,生成即可。
仿真配置
编辑仿真代码,命名为tb_ip_fifo.v,代码如下:
`timescale 1ns / 1ps
module tb_ip_fifo();
reg sys_clk;
reg sys_rst_n;
ip_fifo u_ip_fifo(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n)
);
parameter PERIOD = 20;
always begin
sys_clk = 1'b0;
#(PERIOD/2) sys_clk = 1'b1;
#(PERIOD/2);
end
initial begin
sys_rst_n = 0;
#100
sys_rst_n = 1;
end
endmodule
完成代码编辑后在Vivado直接进行仿真,完成写操作位置为下图黄线所示位置; 完成读操作,下图黄线位置为写完成 故测试成功;
|