写在前面:
博主耗费了四天!!!完成了FIFO核的第二部分。
在这个部分,博主遇到了很多问题,对着代码不停修改,询问学长和老师,好在终于没有报错,可以向大家交作业了!当然,我会在本文详细的帮助大家学习FIFO核的代码编写,带着大家剖析每个部分,尽可能地通俗易懂。
因为淋过雨,所以想撑开你们的伞…
一. 核心代码
1.1 总代码
还是老样子,我直接给出我所编写的总代码,方便大家学习。
module fifo_ip(
input clk,
input rst_n
);
parameter CLK_FREQ = 26'd1250_000; //25ms
reg wr_en_reg; //写使能
reg rd_en_reg; //读使能
reg [3:0] cnt_dly; //延迟计数器
reg [25:0] cnt; //计数器
reg [1:0] sys_state; //定义状态机,位数2位,四种状态
reg [7:0] wr_deep; //写深度
reg [7:0] rd_deep; //读深度
reg [7:0] wr_data_reg; //写数据
reg [7:0] rd_data_reg; //读数据
wire wr_rd_flag; //寄存实时状态
wire [7:0] wr_data;
wire wr_en;
wire [7:0] rd_data;
wire rd_en;
wire full; //写满
wire empty; //读空
//定义状态机的四种状态---> 休息,写入,读取,延迟读出
parameter STATE_REST = 2'b00;
parameter STATE_WRITE = 2'b01;
parameter STATE_READ = 2'b10;
parameter STATE_DLY = 2'b11;
//定义计时器模块
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 1'b0;
end
else begin
if(cnt >= (CLK_FREQ - 1'b1))
cnt <= 1'b0;
else
cnt <= cnt + 1'b1;
end
end
//定义延时读出模块,类似于计时器模块
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_dly <= 1'b0;
end
else begin
if(cnt_dly > 4'b1010)
cnt_dly <= 1'b0;
else
cnt_dly <= cnt_dly + 1'b1;
end
end
//给出状态机的状态
assign wr_rd_flag = ( sys_state[1:0] == STATE_REST)? 1'b0 : 1'b1; //0不工作;1工作
//定义状态机
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
sys_state <= STATE_REST;
wr_deep <= 1'b0;
rd_deep <= 1'b0;
end
else begin
if(cnt == (CLK_FREQ - 1'b1)) begin
sys_state <= STATE_WRITE;
wr_deep <= 1'b0;
end
else begin
case(sys_state)
STATE_REST: begin
wr_deep <= 1'b0;
rd_deep <= 1'b0;
end
STATE_WRITE: begin
//记录写入数据的深度
if(wr_deep >= 8'd255) begin
sys_state <= STATE_DLY;
rd_deep <= 1'b0;
end
else
wr_deep <= wr_deep + 1'b1;
end
STATE_DLY: begin
if(cnt_dly == 4'b1010) begin
sys_state <= STATE_READ;
end
else
sys_state <= sys_state;
end
STATE_READ: begin
//记录读取数据的深度
if(rd_deep >= 8'd255) begin
sys_state <= STATE_REST;
end
else
rd_deep <= rd_deep + 1'b1;
end
default: sys_state <= STATE_REST;
endcase
end
end
end
//定义写入数据的内容
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
else begin
if(cnt == (CLK_FREQ - 1'b1)) begin
wr_en_reg <= 1'b1;
end
else begin
case(sys_state)
STATE_REST: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
STATE_WRITE: begin
wr_data_reg <= wr_data_reg + 1'b1;
wr_en_reg <= 1'b1;
rd_en_reg <= 1'b0;
end
STATE_DLY: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
STATE_READ: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b1;
end
endcase
end
end
end
//例化FIFO核
assign wr_data[7:0] = wr_data_reg;
assign rd_data[7:0] = rd_data_reg;
assign wr_en = wr_en_reg;
assign rd_en = rd_en_reg;
fifo_generator_0 u_fifo_generator_0 (
.clk (clk),
.srst (~rst_n),
.din (wr_data),
.wr_en (wr_en),
.rd_en (rd_en),
.dout (rd_data),
.full (full),
.empty (empty)
);
//例化ILA模块
wire [26:0] probe0;
assign probe0[0] = clk; // 可能会抓取不到信号,因为频率不匹配
assign probe0[1] = rst_n;
assign probe0[2] = wr_rd_flag;
assign probe0[3] = wr_en;
assign probe0[4] = rd_en;
assign probe0[12:5] = wr_data[7:0];
assign probe0[20:13] = rd_data[7:0];
assign probe0[21] = full;
assign probe0[22] = empty;
assign probe0[26:23] = cnt_dly[3:0];
ila_0 u_ila_0 (
.clk (clk), // input wire clk
.probe0 (probe0) // input wire [26:0] probe0
);
endmodule
二. 剖析代码
2.1 状态机模块
- 根据上一节的知识,咱们首先要确定FIFO核应该有几个状态。
肯定有写入,读出两个状态。为了让整个代码顺畅且富有逻辑,FIFO核应还具有休息状态(也就是什么都不干),和延迟读出状态(防止写入的数据FIFO核没反应过来,给它一点反应时间)。所以一共有四种状态。
//定义状态机的四种状态---> 休息,写入,读取,延迟
parameter STATE_REST = 2'b00;
parameter STATE_WRITE = 2'b01;
parameter STATE_READ = 2'b10;
parameter STATE_DLY = 2'b11;
由此可见,状态机的位数应该是2位(00,01,10,11,刚好代表了四种状态)
reg wr_en_reg;
reg rd_en_reg;
reg [3:0] cnt_dly;
reg [25:0] cnt;
reg [1:0] sys_state; //定义状态机,位数2位,四种状态
reg [7:0] wr_deep;
reg [7:0] rd_deep;
reg [7:0] wr_data_reg;
reg [7:0] rd_data_reg;
- 当状态机为REST状态,FIFO核不工作,即没有写入,也没有读出,那么此时的写深度和读深度就应该都为0.
STATE_REST: begin
wr_deep <= 1'b0;
rd_deep <= 1'b0;
end
当状态机是WRITE状态时,如果wr_deep显示满了,就让状态机进入延迟读出,暂时休息休息。如果没满,就让wr_deep加1,表示写了一位。(毕竟到写入状态了,写深度肯定要加一次的)
STATE_WRITE: begin
//记录写入数据的深度
if(wr_deep >= 8'd255) begin
sys_state <= STATE_DLY;
rd_deep <= 1'b0;
end
else
wr_deep <= wr_deep + 1'b1;
end
当状态机是DLY状态时,FIFO核写入读出都在“怠工”,让FIFO核一人刷新数据着呢。当刷新完成时,FIFO核进入READ状态,如果没有刷新好呢,就让FIFO核保持现在状态就行
STATE_DLY: begin
if(cnt_dly == 4'b1010) begin
sys_state <= STATE_READ;
end
else
sys_state <= sys_state;
end
当状态机是READ状态时,和WRITE类似,读深度要好好考虑一下。读完了就让FIFO核休息去吧,没读完,就让他读深度加1.继续读。
STATE_READ: begin
//记录读取数据的深度
if(rd_deep >= 8'd255) begin
sys_state <= STATE_REST;
end
else
rd_deep <= rd_deep + 1'b1;
end
这个时候四大状态已经全部编写好了,状态机雏形已经具备。但为了防止有其他情况,最后再加一个逻辑判断,避免锁相环,占用太多空间。
default: sys_state <= STATE_REST;
那怎么让状态机启动呢,状态机的起始状态是什么呢。
给他一个always时序,给他一个初始化。(超大声!
好了,不开玩笑了。
逻辑地讲,在系统开始,应该先让状态机到REST状态,先休息着。此时没读没写,读深度,写深度自然为0。假设到了一定的时间(博主把这个时间定义为clk有1250000个上升沿的时间,即25ms。大家也可以根据自己需要来配置这个时间),后,FIFO核开始写入,即状态机到达WRITE状态。
所以,代码如下:
//定义状态机
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
sys_state <= STATE_REST;
wr_deep <= 1'b0;
rd_deep <= 1'b0;
end
else begin
if(cnt == (CLK_FREQ - 1'b1)) begin
sys_state <= STATE_WRITE;
wr_deep <= 1'b0;
end
......
......
......
至此,状态机模块基本成型!下面开始另一个模块的剖析
2.2 写入数据模块
写入数据必须要考虑到写的数据是什么,什么时候写,什么时候不写。
由此看来,这个模块也需要状态机的加持,只不过不像上一个模块,专门给大家讲解状态机罢了,此时我们把重点放在代码的逻辑上。
复位的时候,理所应当的,写数据应该为0,写使能应该为0,读使能应该为0.(没有人质疑吧~
//定义写入数据的内容
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
......
......
......
当状态机WRITE状态启动的时候,写使能此时就要立刻拉高以配合写入数据。不然他俩就要差一个时钟位。读出的数据就不会从0开始,而从1开始。
if(cnt == (CLK_FREQ - 1'b1)) begin
wr_en_reg <= 1'b1;
当状态机为REST状态,FIFO核不工作,也就是不写入,不读出。即wr_data,wr_en,rd_en仍为0.
STATE_REST: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
当状态机为WRITE状态,FIFO核开始写入数据。根据实验目的,写数据的内容应该从0到255连续加1.记得此时要打开写使能,读使能依旧为0.因为此时不读出数据,只写入数据
STATE_WRITE: begin
wr_data_reg <= wr_data_reg + 1'b1;
wr_en_reg <= 1'b1;
rd_en_reg <= 1'b0;
end
当状态机为DLY状态,FIFO核暂时休息,写使能读使能均为0.
STATE_DLY: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b0;
end
当状态机为READ状态,FIFO核开始读出数据。此时要关闭写使能,打开读使能。
STATE_READ: begin
wr_data_reg <= 1'b0;
wr_en_reg <= 1'b0;
rd_en_reg <= 1'b1;
end
2.3 其他细节
2.3.1 FIFO核状态展示
在代码的运行过程中,为了清楚的知道此时FIFO核处于什么状态,我们专门引入一个变量来寄存实时状态
//给出状态机的状态
assign wr_rd_flag = ( sys_state[1:0] == STATE_REST)? 1'b0 : 1'b1; //0不工作;1工作
2.3.2 例化
本文设计了两个IP核的例化,分别是FIFO核和ILA核。
(ILA是方便板子上电的时候查看波形图的,其类似于仿真图。同时ILA具有debug功能,可以直观查看错误。不久博主就要专门写一篇来讲解Vivado里的特殊功能,其中就包含例化。)
因为例化中只接受wire类型的变量,所以博主定义了大量的wire变量,并且通过一定的assign语句由reg变量向wire变量赋值,以达到两个模块完美相连的功能。
(相当于C语言数组在两个函数间的值传递)
注意:只有reg变量才能在always模块中被赋值!!
reg wr_en_reg; //写使能
reg rd_en_reg; //读使能
reg [3:0] cnt_dly; //延迟计数器
reg [25:0] cnt; //计数器
reg [1:0] sys_state; //定义状态机,位数2位,四种状态
reg [7:0] wr_deep; //写深度
reg [7:0] rd_deep; //读深度
reg [7:0] wr_data_reg; //写数据
reg [7:0] rd_data_reg; //读数据
wire wr_rd_flag; //寄存实时状态
wire [7:0] wr_data;
wire wr_en;
wire [7:0] rd_data;
wire rd_en;
wire full;
wire empty;
//例化FIFO核
assign wr_data[7:0] = wr_data_reg;
assign rd_data[7:0] = rd_data_reg;
assign wr_en = wr_en_reg;
assign rd_en = rd_en_reg;
fifo_generator_0 u_fifo_generator_0 (
.clk (clk),
.srst (~rst_n),
.din (wr_data),
.wr_en (wr_en),
.rd_en (rd_en),
.dout (rd_data),
.full (full),
.empty (empty)
);
//例化ILA模块
wire [26:0] probe0;
assign probe0[0] = clk; // 可能会抓取不到信号,因为频率不匹配
assign probe0[1] = rst_n;
assign probe0[2] = wr_rd_flag;
assign probe0[3] = wr_en;
assign probe0[4] = rd_en;
assign probe0[12:5] = wr_data[7:0];
assign probe0[20:13] = rd_data[7:0];
assign probe0[21] = full;
assign probe0[22] = empty;
assign probe0[26:23] = cnt_dly[3:0];
ila_0 u_ila_0 (
.clk (clk), // input wire clk
.probe0 (probe0) // input wire [26:0] probe0
);
大概就是这么个道理 (类似于媒介)
三. 结果展示
3.1 写入数据
仔细观察,发现写入数据并不是从0开始的。这个没有关系,博主试了几次,它每次的初始值都不固定,咱们只要能保证读出的数据第一个是0就OK了。因为FIFO核最大的特点就是先入先出。先出去的0一定是先写进去的.
3.2 full信号
写到255时,full信号拉高,证明此时已经写满,即满足实验要求之一。
3.3 读出数据
full拉低时,证明开始读出了。可见第一个读出的确实是0,证明咱们写入数据是没有问题的。实验要求达成.
实验结束!!!
四. 问题总结
4.1 仿真
细心的同学就会发现,这个实验我没有给大家仿真代码,也没有仿真图。不是因为博主懒,不想仿真,而是因为这个实验就无法仿真!
在仿真的过程中,为了看到仿真图,必然要在module主模块中加入input变量,如下图:
这些变量,不像clk,是系统时钟,板子自带的。这些输入变量是没有外来输入值的,都是自己在代码中利用状态机去给值的。那么就会使这些接口成为悬空接口,进一步在生成仿真波形图时,导致rd_data无法读出具体的数字,只能是X态。(其他数据为什么可以在仿真图中展示呢,是因为其他数据我在状态机中已经赋值了,只有rd_data从来没有赋值,自然只能读出来x态。)
并且,即使不仿真,在生成bit文件时,也会出现多重驱动器驱动一个变量的错误。多达100个。
切记切记:
为了看见波形图,此处只能选择利用ILA进行查看!!!
4.2 always 语句
不允许对同一个变量在两个always时序语句中赋值。文章来源:https://www.toymoban.com/news/detail-769877.html
哪怕你的时序,你的逻辑都是OK的,也不允许!否则会在布局布线的时候被优化掉,导致出错。文章来源地址https://www.toymoban.com/news/detail-769877.html
到了这里,关于FPGA 学习分享-- 04 FIFO核的使用(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!