本系列旨在提供100%准确的数字IC设计/验证手撕代码环节的题目,原理,RTL设计,Testbench和参考仿真波形,每篇文章的内容都经过仿真核对。快速导航链接如下:
奇数分频
偶数分频
半整数分批
小数/分数分频
序列检测器
模三检测器
饮料机
异步复位,同步释放
边沿检测(上升沿,下降沿,双边沿)
全加器,半加器
格雷码转二进制
单bit跨时钟域(打两拍,边沿同步,脉冲同步)
奇偶校验
伪随机数生成器[线性反馈移位寄存器]
同步FIFO
应当说,手撕代码环节是面试流程中既重要又简单的一个环节,跟软件类的岗位相比起来,数字IC的手撕代码题目固定,数量有限,属于整个面试中必得分的一个环节,在这个系列以外,笔者同样推荐数字IC求职者使用“HdlBits”进行代码的训练
链接如下
HDLBits — Verilog Practice
这篇开始之前,先安利大家两篇文章,都是Clifford E. Cummings写的,分别是《Simulation and Synthesis Techniques for Asynchronous FIFO Design》和《Simulation and Synthesis Techniques for Asynchronous FIFO Design with Asynchronous Pointer Comparisons》,感兴趣的朋友可以baidu或者google搜来看看,这两篇文章涵盖了有关FIFO的所有关键问题,语言生动,深入浅出,加一起一共四十页左右。
而作为《数字IC手撕代码篇》,我们想要讨论的同步FIFO一方面继承自异步FIFO,另一方面也应该是所有面试中可能会遇到的最为复杂的手撕代码题,具体的内容如下
1.使用verilog,完成同步FIFO的设计,其中数据位宽为8位,FIFO的深度为16,其中输入端口为clk,rst_n(复位信号),write_en(写使能),read_en(读使能),data_in,输出端口为empty(空信号),full(满信号),data_out。
有关什么是FIFO之类的问题就不讨论了,我们重点讨论几个关键的问题
以上是异步FIFO的结构图,可以发现,对于异步FIFO来说,它由几部分组成,最中间的是一块双口RAM,RAM的左边是写控制模块, 右边是读控制模块,下面的两级寄存器起到了格雷码状态下的地址信号跨时钟域作用,而对于同步FIFO来说,因为读写共用同一个时钟频率,因此不需要寄存器同步的那一部分。为了最大程度的贴近于异步FIFO的设计,作者依旧采用格雷码的比较来产生空满信号,当然,使用一个计数器来判断空满信号也是可行的,读者感兴趣可以在评论区讨论一下。
2.FIFO深度的约束
在格雷码转二进制中,我们已经讨论了“gray_code= binary_code ^ (binary_code >> 1)”这种格雷码的产生方式的限制,即只适用于2^N的情况,这里对FIFO的深度约束依旧成立,假如我们设计一个14位深度的fifo,地址由14跳回1不满足单bit变化,会产生采样错误的问题,因此本篇我们依旧对偶数情况的格雷码按下不表,只讨论2的N次方的深度问题
3.空满信号如何产生
设置读指针与写指针,在格雷码的情况下,若读写指针相等,证明FIFO为空,这个很好理解,那么什么情况下,FIFO为满呢?写指针转了一圈,追上了读指针的时候,FIFO应该是满状态,这里如果拿格雷码表格来举例的话,假设FIFO的深度为8位,只需要3位二进制编码就能表示,我们多使用一位,用4位的二进制编码来额外加上写指针追读指针的情况,那么以下的截图,读写指针为1+9,2+10,3+11以此类推的情况都是应该产生满信号的情况,找下规律,发现对于4bit格雷码来说,前两位相反,之后的位相同,就是verilog逻辑中判断满信号产生的条件
4.数据通路和控制通路是否都需要复位
在作者的设计中,并不是所有的寄存器块都进行了复位的,比如说描述data_in所在的块,作者没有进行复位,但这并不影响FIFO的正常工作,这是因为我们设计追寻的原则为“控制通路必须包含复位”,数据通路“是否复位可以选择”,即读写指针所在的通路rst_n到来的时候都需要复位,但data_in作为数据通路可以选择不带复位信号。具体涉及到的代码如下:
always@(posedge clk)
if(write_en && !full)
data[wr_point] <= data_in;
else
data[wr_point] <= data[wr_point];
module fifo(clk,rst_n,write_en,read_en,data_in,empty,full,data_out);
input clk;
input rst_n;
input write_en;
input read_en;
input [7:0] data_in;
output empty;
output full;
output reg [7:0] data_out;
reg [7:0] data [0:15];
reg [4:0] wr_point;
reg [4:0] rd_point;
wire [4:0] wr_gray_point;
wire [4:0] rd_gray_point;
assign wr_gray_point = wr_point ^ (wr_point>>1);
assign rd_gray_point = rd_point ^ (rd_point>>1);
assign empty = (wr_gray_point == rd_gray_point) ? 1 : 0;
assign full = (wr_gray_point[2:0] == rd_gray_point[2:0]
&& wr_gray_point[4] == !rd_gray_point[4]
&& wr_gray_point[3] == !rd_gray_point[3] ) ? 1 : 0 ;
always@(posedge clk or negedge rst_n)
if(!rst_n)
wr_point <= 5'b0_0000;
else if (write_en && !full)
begin
if(wr_point < 5'b1_1111)
wr_point <= wr_point + 1'b1;
else
wr_point <= 5'b0_0000;
end
else
wr_point <= wr_point;
always@(posedge clk or negedge rst_n)
if(!rst_n)
rd_point <= 5'd0;
else if (read_en && !empty)
begin
if(rd_point <5'b1_1111)
rd_point <= rd_point + 1'b1;
else
rd_point <= 5'b0_0000;
end
else
rd_point <= rd_point;
always@(posedge clk)
if(write_en && !full)
data[wr_point] <= data_in;
else
data[wr_point] <= data[wr_point];
always@(posedge clk or negedge rst_n)
if(!rst_n)
data_out <= 8'h00;
else if(read_en && !empty)
data_out <= data[rd_point];
else
data_out <= 8'h00;
endmodule
module fifo_tb();
reg clk;
reg rst_n;
reg write_en;
reg read_en;
reg [7:0] data_in;
wire empty;
wire full;
wire [7:0] data_out;
fifo u1(.clk(clk),
.rst_n(rst_n),
.write_en(write_en),
.read_en(read_en),
.data_in(data_in),
.empty(empty),
.full(full),
.data_out(data_out)
);
always #5 clk = !clk;
always #10.01 data_in = $random;
initial
begin
clk = 0;
rst_n = 1;
#10
rst_n =0;
#14
rst_n = 1;
write_en = 1;
#200
write_en = 0;
read_en = 1;
#400
$stop;
end
endmodule
我们可以看到,之前按顺序输入的数据,之后按照相同顺序输出,同时空满信号的产生正常,设计成立。