整个博客写到最后就是个改错本,代码都是没问题的,可以正常仿真
1. FIFO介绍
2. 同步FIFO
读写用的是一个是时钟
模块实现
模块框图
端口介绍:
- sys_clk:50MHz系统时钟
- sys_rst_n:系统复位,低有效
- wr_en:写使能信号,高有效
- rd_en:读使能信号,高有效
- data_in:写入数据
- data_out:读出数据
- full:FIFO满信号,高有效
- empty:FIFO空信号,高有效
- count:FIFO数据计数器,当读写指针相等时,可能是写满,也可能是读空,根据计数器的值可以判断空满
功能描述:
- 以4*8大小的FIFO为例,数据位宽4bit,地址位宽3bit,[4-1 : 0] mem [{3{1’b1}} : 0] // 寻址范围为0~3’b111,即0 ~7
- 复位,将mem初始化为0
- 写数据,写使能并且未满,写入数据,计数器加1
- 读数据,读使能并且未空,读出数据,计数器减1
- 读写同时进行,只进行数据更新,计数器不变
代码:
// fifo_4x8,地址位宽为3bit
// 计数器工作范围0~7
// 当计数器为8,证明已经满了,为了计数器能到8,需要4bit
module fifo_sync
#(
parameter data_width = 4,
addr_width = 3
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire wr_en ,
input wire rd_en ,
input wire [data_width-1 : 0] data_in ,
output reg [data_width-1 : 0] data_out ,
output wire full ,
output wire empty ,
output reg [addr_width : 0] count
);
reg [addr_width-1 : 0] wr_addr ; // 写地址,指向下一个要写的地址
reg [addr_width-1 : 0] rd_addr ; // 读地址,指向下一个要读的地址
reg [data_width-1 : 0] mem [{addr_width{1'b1}} : 0]; // 声明存储器
integer i; // 用于初始化存储器
// mem初始化
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n) begin
for (i = 0; i <= {addr_width{1'b1}}; i = i + 1) // 寻址范围0~3'b111,即0~7
mem[i] <= {data_width{1'b0}}; // 数据初始化为4'd0000,即0x0
end
// 写操作
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
wr_addr <= 0;
else if (wr_en && (~full)) // 写使能 & 存储器未满
begin
mem[wr_addr] <= data_in;
wr_addr <= wr_addr + 1'b1;
end
else
wr_addr <= wr_addr;
// 读操作
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
begin
data_out <= {data_width-1{1'b0}}; // 输出对应数据位宽个0
rd_addr <= 0;
end
else if (rd_en && (~empty)) // 读使能 & 存储器未空
begin
data_out <= mem[rd_addr];
rd_addr <= rd_addr + 1'b1;
end
else
begin
data_out <= data_out;
rd_addr <= rd_addr;
end
// count计数器,产生空满标志
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
count <= 0;
else if (wr_en && (~full)) // 只进行写操作
count <= count + 1'b1;
else if (rd_en && (~empty)) // 只进行读操作
count <= count - 1'b1;
else
count <= count;
assign empty = (count == 0);
assign full = (count == {addr_width{1'b1}} + 1'b1);
endmodule
// fifo_4x8
// 数据位宽4bit,地址位宽为3bit
module sim_fifo_sync #(
parameter data_width = 4,
addr_width = 3
)();
reg sys_clk ;
reg sys_rst_n ;
reg wr_en ;
reg rd_en ;
reg [data_width-1 : 0] data_in ;
wire [data_width-1 : 0] data_out ;
wire full ;
wire empty ;
wire [addr_width : 0] count ;
integer i;
// 实例化fifo_sync
fifo_sync
#(
.data_width (data_width) ,
.addr_width (addr_width)
)
fifo_sync_inst
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.wr_en (wr_en) ,
.rd_en (rd_en) ,
.data_in (data_in) ,
.data_out (data_out) ,
.full (full) ,
.empty (empty) ,
.count (count)
);
// 50MHz
localparam clk_period_50M = 20;
always # (clk_period_50M /2 ) sys_clk = ~sys_clk;
initial begin
sys_clk = 0;
sys_rst_n = 0;
wr_en = 0;
rd_en = 0;
data_in = 0;
#20
sys_rst_n = 1; // 复位结束
wr_en = 1; // 写5个数据
#100
wr_en = 0; // 不写了
rd_en = 1; // 读5个数据
#100
rd_en = 0; // 不读了
#100
wr_en = 1; // 开始写
#100
rd_en = 1; // 一边写一边读
#100
wr_en = 0;
rd_en = 0;
#100
wr_en = 1;
#200
rd_en = 1;
end
initial begin
for (i = 0; i < 50; i=i+1)
#20 data_in = {$random} % 16;
end
endmodule
读写分开进行的时候没有问题,当边读边写的时候就有问题了
边读边写的时候期待计数器不变,只进行FIFO数据的更新,而对比波形,可以看到边读边写的时候优先进行了写操作,count模块里面用了if-else结构,本身就有优先级,会先判断是否进行写操作
修改一下count计数器代码
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
count <= 0;
else if ((wr_en && (~full)) && (rd_en && (~empty))) // 读写操作同时进行
count <= count;
else if (wr_en && (~full)) // 只进行写操作
count <= count + 1'b1;
else if (rd_en && (~empty)) // 只进行读操作
count <= count - 1'b1;
else
count <= count;
下板验证
- 验证思路:
所有模块的工作时钟都是50MHz,EGO1系统时钟是100MHz,在顶层模块进行二分频。
对读写控制按键先进行消抖处理,按键有效时仅输出一个时钟周期的高电平flag信号;
data_in要写入的4bit数据由开关确定;
将读出的数据data_out、写数据指针(地址下标)、读数据指针(地址下标)、数据个数计数器count用数码管进行动态显示
-
下板遇到的问题:
- 初始化操作和写操作分成两个always块进行:复位无效后,写数据写不进去,但其实data_in已经送过去了,读出来的数据永远都是初始化的值。(下板验证后的结果,不太懂)。或者直接注释掉初始化的always块,直接写,这样就能正常写入数据,跟下面的合并是一个结果
- 初始化操作和写操作合并在一个always块进行:数据可以正常由开关写入FIFO,读出来的数据也是后来写入的
- 复位后,默认会写入一个数据,这个数据就是当前开关控制的值,不太知道为什么会默认写,但确实发生了
除了第一个数据有一点点不受控制,其他情况都是正常写入、正常判断空满
-
key_filter消抖模块代码
module key_filter #( parameter CNT_MAX = 20'd999_999 //计数器计数最大值 ) ( input wire sys_clk , //系统时钟50Mhz input wire sys_rst_n , //全局复位 input wire key_in , //按键输入信号 output reg key_flag //key_flag为1时表示消抖后检测到按键被按下 //key_flag为0时表示没有检测到按键被按下 ); //reg define reg [19:0] cnt_20ms ; //计数器 //cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) cnt_20ms <= 20'b0; else if(key_in == 1'b1) cnt_20ms <= 20'b0; else if(cnt_20ms == CNT_MAX && key_in == 1'b0) cnt_20ms <= cnt_20ms; else cnt_20ms <= cnt_20ms + 1'b1; //key_flag:当计数满20ms后产生按键有效标志位 //且key_flag在999_999时拉高,维持一个时钟的高电平 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) key_flag <= 1'b0; else if(cnt_20ms == CNT_MAX - 1'b1) key_flag <= 1'b1; else key_flag <= 1'b0; endmodule
-
fifo_sync,在上面代码的基础上修改一下初始化
module fifo_sync #( parameter data_width = 4, addr_width = 3 ) ( input wire sys_clk , input wire sys_rst_n , input wire wr_en , input wire rd_en , input wire [data_width-1 : 0] data_in , output reg [data_width-1 : 0] data_out , output wire full , output wire empty , output reg [addr_width : 0] count , output reg [addr_width-1 : 0] wr_addr , output reg [addr_width-1 : 0] rd_addr ); // reg [addr_width-1 : 0] wr_addr ; // 写地址 // reg [addr_width-1 : 0] rd_addr ; // 读地址 reg [data_width-1 : 0] mem [{addr_width{1'b1}} : 0]; // 声明存储器 integer i; // 用于初始化存储器 // mem初始化 // always @ (posedge sys_clk or negedge sys_rst_n) // if (!sys_rst_n) begin // for (i = 0; i <= {addr_width{1'b1}}; i = i + 1) // 寻址范围0~3'b111,即0~7 // //mem[i] <= {data_width{1'b1}}; // 数据初始化为4'd0000,即0x0 // mem[i] <= 4'd8; // end // 写操作 // always @ (posedge sys_clk or negedge sys_rst_n) // if (!sys_rst_n) // wr_addr <= 0; // else if (wr_en && (~full)) // 写使能 & 存储器未满 // begin // mem[wr_addr] <= data_in; // wr_addr <= wr_addr + 1'b1; // end // else // wr_addr <= wr_addr; // 初始化和写操作 always @ (posedge sys_clk or negedge sys_rst_n) if (!sys_rst_n) begin wr_addr <= 0; for (i = 0; i <= {addr_width{1'b1}}; i = i + 1) // 寻址范围0~3'b111,即0~7 mem[i] <= {data_width{1'b0}}; // 数据初始化为4'd0000,即0x0 end else if (wr_en && (~full)) // 写使能 & 存储器未满 begin mem[wr_addr] <= data_in; wr_addr <= wr_addr + 1'b1; end else wr_addr <= wr_addr; // 读操作 always @ (posedge sys_clk or negedge sys_rst_n) if (!sys_rst_n) begin data_out <= {data_width-1{1'b0}}; // 输出对应数据位宽个0 rd_addr <= 0; end else if (rd_en && (~empty)) // 读使能 & 存储器未空 begin data_out <= mem[rd_addr]; rd_addr <= rd_addr + 1'b1; end else begin data_out <= data_out; rd_addr <= rd_addr; end // count计数器,产生空满标志 always @ (posedge sys_clk or negedge sys_rst_n) if (!sys_rst_n) count <= 0; else if ((wr_en && (~full)) && (rd_en && (~empty))) // 读写操作同时进行 count <= count; else if (wr_en && (~full)) // 只进行写操作 count <= count + 1'b1; else if (rd_en && (~empty)) // 只进行读操作 count <= count - 1'b1; else count <= count; // always @ (posedge sys_clk or negedge sys_rst_n) // 读写同时进行就有小问题 // if (!sys_rst_n) // count <= 0; // else if (wr_en && (~full)) // 只进行写操作 // count <= count + 1'b1; // else if (rd_en && (~empty)) // 只进行读操作 // count <= count - 1'b1; // else // count <= count; assign empty = (count == 0) ? 1'b1 : 1'b0; assign full = (count == {addr_width{1'b1}} + 1'b1) ? 1'b1 : 1'b0; endmodule
-
数码管动态显示
链接: 数码管动态显示.module led_dynamic( output reg [7:0] seg, output reg [3:0] an, input wire sys_clk, input wire sys_rst_n, input wire [3:0] in3, in2, in1, in0 ); parameter _0 = ~8'hc0; parameter _1 = ~8'hf9; parameter _2 = ~8'ha4; parameter _3 = ~8'hb0; parameter _4 = ~8'h99; parameter _5 = ~8'h92; parameter _6 = ~8'h82; parameter _7 = ~8'hf8; parameter _8 = ~8'h80; parameter _9 = ~8'h90; parameter _a = ~8'h88; parameter _b = ~8'h83; parameter _c = ~8'hc6; parameter _d = ~8'ha1; parameter _e = ~8'h86; parameter _f = ~8'h8e; parameter _err = ~8'hcf; parameter N = 18; reg [N-1 : 0] regN; reg [3:0] hex_in; always @ (posedge sys_clk or posedge sys_rst_n) begin if (sys_rst_n == 1'b0) begin regN <= 0; end else begin regN <= regN + 1; end end always @ (*) begin case (regN[N-1: N-2]) 2'b00: begin an <= 4'b0001; hex_in <= in0; end 2'b01: begin an <= 4'b0010; hex_in <= in1; end 2'b10: begin an <= 4'b0100; hex_in <= in2; end 2'b11: begin an <= 4'b1000; hex_in <= in3; end default: begin an <= 4'b1111; hex_in <= in3; end endcase end always @ (*) begin case (hex_in) 4'h0: seg <= _0; 4'h1: seg <= _1; 4'h2: seg <= _2; 4'h3: seg <= _3; 4'h4: seg <= _4; 4'h5: seg <= _5; 4'h6: seg <= _6; 4'h7: seg <= _7; 4'h8: seg <= _8; 4'h9: seg <= _9; 4'ha: seg <= _a; 4'hb: seg <= _b; 4'hc: seg <= _c; 4'hd: seg <= _d; 4'he: seg <= _e; 4'hf: seg <= _f; default:seg <= _err; endcase end endmodule
-
顶层模块
// 4X8,数据位宽4bit,地址位宽3bit // 使用EGO1板子下板,系统时钟是100MHz,调用模块需要注意工作时钟的选择 module top_fifo_sync #( parameter data_width = 4, addr_width = 3 )( input wire sys_clk , // 100MHz input wire sys_rst_n, input wire key_wr, input wire key_rd, input wire [data_width-1 : 0] data_in, output wire [7:0] seg, output wire [3:0] an, output wire full, output wire empty ); wire wr_en ; wire rd_en ; wire [data_width-1 : 0] data_out; wire [addr_width : 0] count ; wire [addr_width-1 : 0] wr_addr ; wire [addr_width-1 : 0] rd_addr ; reg sys_clk_50M; always @ (posedge sys_clk or negedge sys_rst_n) if (!sys_rst_n) sys_clk_50M = 0; else if (sys_clk == 1) sys_clk_50M = ~sys_clk_50M; key_filter key_filter_wr_inst ( .sys_clk (sys_clk_50M), .sys_rst_n (sys_rst_n), .key_in (key_wr), .key_flag (wr_en) ); key_filter key_filter_rd_inst ( .sys_clk (sys_clk_50M), .sys_rst_n (sys_rst_n), .key_in (key_rd), .key_flag (rd_en) ); fifo_sync #( .data_width (data_width) , .addr_width (addr_width) ) fifo_sync_inst ( .sys_clk (sys_clk_50M), .sys_rst_n (sys_rst_n), .wr_en (wr_en), .rd_en (rd_en), .data_in (data_in), .data_out (data_out), .full (full), .empty (empty), .count (count), .wr_addr (wr_addr), .rd_addr (rd_addr) ); led_dynamic led_dynamic_inst ( .sys_clk (sys_clk_50M), .sys_rst_n (sys_rst_n), .in3 (data_out), .in2 (wr_addr), .in1 (rd_addr), .in0 (count), .seg (seg), .an (an) ); endmodule
-
约束文件
set_property -dict {PACKAGE_PIN P17 IOSTANDARD LVCMOS33} [get_ports sys_clk ] set_property -dict {PACKAGE_PIN P15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n ] set_property -dict {PACKAGE_PIN V1 IOSTANDARD LVCMOS33} [get_ports {key_wr}] set_property -dict {PACKAGE_PIN R11 IOSTANDARD LVCMOS33} [get_ports {key_rd}] set_property -dict {PACKAGE_PIN R2 IOSTANDARD LVCMOS33} [get_ports {data_in[3]}] set_property -dict {PACKAGE_PIN M4 IOSTANDARD LVCMOS33} [get_ports {data_in[2]}] set_property -dict {PACKAGE_PIN N4 IOSTANDARD LVCMOS33} [get_ports {data_in[1]}] set_property -dict {PACKAGE_PIN R1 IOSTANDARD LVCMOS33} [get_ports {data_in[0]}] set_property -dict {PACKAGE_PIN G2 IOSTANDARD LVCMOS33} [get_ports {an[3]}] set_property -dict {PACKAGE_PIN C2 IOSTANDARD LVCMOS33} [get_ports {an[2]}] set_property -dict {PACKAGE_PIN C1 IOSTANDARD LVCMOS33} [get_ports {an[1]}] set_property -dict {PACKAGE_PIN H1 IOSTANDARD LVCMOS33} [get_ports {an[0]}] set_property -dict {PACKAGE_PIN B4 IOSTANDARD LVCMOS33} [get_ports {seg[0]}] set_property -dict {PACKAGE_PIN A4 IOSTANDARD LVCMOS33} [get_ports {seg[1]}] set_property -dict {PACKAGE_PIN A3 IOSTANDARD LVCMOS33} [get_ports {seg[2]}] set_property -dict {PACKAGE_PIN B1 IOSTANDARD LVCMOS33} [get_ports {seg[3]}] set_property -dict {PACKAGE_PIN A1 IOSTANDARD LVCMOS33} [get_ports {seg[4]}] set_property -dict {PACKAGE_PIN B3 IOSTANDARD LVCMOS33} [get_ports {seg[5]}] set_property -dict {PACKAGE_PIN B2 IOSTANDARD LVCMOS33} [get_ports {seg[6]}] set_property -dict {PACKAGE_PIN D5 IOSTANDARD LVCMOS33} [get_ports {seg[7]}] set_property -dict {PACKAGE_PIN F6 IOSTANDARD LVCMOS33} [get_ports {full}] set_property -dict {PACKAGE_PIN G4 IOSTANDARD LVCMOS33} [get_ports {empty}]
3. 异步FIFO
读写用的是不同的时钟
思路补充
补充:$clog2()系统函数使用
// 这里依旧以4X8大小的FIFO为例
// 在同步FIFO的例子中,给定数据位宽4bit,地址位宽3bit,然后寻址范围根据地址位宽来确定:[{addr_width{1'b1}} : 0]
parameter data_width = 4,
parameter addr_width = 3
reg [data_width-1 : 0] mem [{addr_width{1'b1}} : 0];
reg [addr_width-1 : 0] wr_addr ; // 写地址
reg [addr_width-1 : 0] rd_addr ; // 读地址
// 如果使用$clog2()系统函数
// 给定数据位宽4bit,给定数据深度为8,2^3 = 8
parameter data_width = 4;
parameter data_depth = 8;
reg [data_width-1 : 0] mem [data_depth : 0];
reg [$clog(data_depth)-1 : 0] wr_addr ; // 写地址 [3-1 : 0]
reg [$clog(data_depth)-1 : 0] rd_addr ; // 读地址 [3-1 : 0]
这里是补充跨时钟域的问题
参考这博客:
谈谈跨时钟域传输问题(CDC)——李锐博恩.
FPGA基础知识极简教程(4)从FIFO设计讲起之异步FIFO篇——李锐博恩.
补充:
- fast to slow: 快时钟信号保持时间短,慢时钟可能采样不到,这就需要对快时钟下的输入信号进行展宽,让它保持时间久一点,然后采样完了得到一个反馈信号,用于拉低展宽信号
- slow to fast: 快时钟一定能采样到慢时钟,慢时钟的信号至少会在快时钟下保持一个时钟周期
- 同步FIFO可以用计数器的方法判断空满,但是异步不可以,因为读写指针不在一个时钟域,计数器不能在这样的情况下计数,需要将读写指针同步一下
读写指针同步到一个时钟域:
- 判满:读指针同步到写时钟域
- 读数据完成后读指针+1,将结果值转换为格雷码形式,寄存一拍,然后用写时钟两级同步,将同步过来的信号转换为二进制编码,寄存一拍与写指针比较
- 当读指针转换为格雷码形式以及同步到写时钟域的过程中,读写数据可能都在进行,读写指针也可能都还在递增,等同步后的读写指针相等时,实际的读指针可能已经变了,这样的话还有空间没有写满(被读出)
- 判空:写指针同步到读时钟域
- 写数据完成后写指针+1,将结果值转换为格雷码性质,寄存一拍,然后用读时钟两级同步,将同步过来的信号转换为二进制编码,寄存一拍与读指针比较
- 当写指针转换为格雷码形式以及同步到写时钟域的过程中,读写数据可能都在进行,读写指针也可能都还在递增,等同步后的读写指针相等时,实际的写市镇可能已经变了,这样的话相当于多写了
格雷码转换的应用:
-
格雷码是一种安全码,因为相邻的格雷码只有一位不同,和二进制不同,二进制一般相邻的都有多位不同。格雷码在传输中,因为相邻只有一位不同,所以其误码率比二进制低得多。
在同步时,出现亚稳态的概率也比二进制低。// 二进制转格雷码 gray = binary ^ (binary >>> 1)
-
格雷码在任意两个相邻的数之间转换时,只有1个bit发生了变化,所以它有效的避免了寄存器由一个数值到下一个数值时的不稳定态。并且由于格雷码中最大数与最小数之间也仅1个bit不同,因此通常又被称作循环二进制码或者反射二进制码。
-
这里以深度为8的FIFO为例,实际地址宽度为[2:0],3bit就足够了,但是应用中宽度多加1位,定义位[3:0]。
- 观察二进制数,宽度多一位对指针是没有影响的,当地址为3bit的时候,全1之后再加1,就变成全0;当地址为4bit的时候,低三位也是全1之后再加1,就变成全0。都是在3b’111~3’b000循环,不会出现溢出越界。
- 观察格雷码,横着看一行,大小相差8的一对码值,高两位相反,其余位都相等。比如5和6,对应的格雷码为4’d0111和4’d1011。
- 多出来的这一位,可以将0 ~ 7、8 ~ 15分成两个组(本来也只用0 ~ 7的地址位置),可以用格雷码的特性进行空满判断。
- 格雷码全部位相等,即为空。读写指针相等,并且在一个组,证明读指针追上了写指针,读空了
- 格雷码高两位相反,其余位相等,即为满。读写指针相等,分别在两个组,证明写指针追上了读指针,写满了
草率的代码实现,以及好不容易出结果的仿真测试
full和empty信号分别用了时序逻辑和组合逻辑实现,最后的问题分析提到了
// fifo_8x32
module fifo_async
#(
parameter data_width = 8,
data_depth = 32
)
(
input wire wr_clk ,
input wire wr_rst_n,
input wire wr_en ,
input wire [data_width-1 : 0] data_in ,
output reg full ,
input wire rd_clk ,
input wire rd_rst_n,
input wire rd_en ,
output reg [data_width-1 : 0] data_out,
output wire empty
);
// 定义FIFO 8x32
reg [data_width-1 : 0] mem [0 : data_depth-1];
// 定义寄存器
reg [$clog2(data_depth) : 0] wr_addr = 0 ; // 写指针 [5:0]
reg [$clog2(data_depth) : 0] rd_addr = 0 ; // 读指针 [5:0]
// 对比mem[{wr_addr[$clog2(data_depth)-1 : 0]}]和mem[wr_addr],后面的问题分析讲到这两种寻址方式的差异
// wire [data_width-1 : 0] s1;
// wire [data_width-1 : 0] s2;
// assign s1 = mem[{wr_addr[$clog2(data_depth)-1 : 0]}]; // 可以正确读出FIFO的数据
// assign s2 = mem[wr_addr]; // 读出的都是高阻态,证明该位置没有写入数据
// 写操作
always @ (posedge wr_clk or negedge wr_rst_n)
if (!wr_rst_n)
wr_addr <= 0;
// else if (wr_en && (~full))
else if (wr_en && ~((~rd_b2g_rr[$clog2(data_depth) : $clog2(data_depth)-1] == wr_b2g[$clog2(data_depth) : $clog2(data_depth)-1])
&& (rd_b2g_rr[$clog2(data_depth)-2 : 0] == wr_b2g[$clog2(data_depth)-2 : 0])))
begin
wr_addr <= wr_addr + 1'b1;
// 这里因为wr_addr比实际位宽多定义了一位,这里摘取除了最高位的地址
// 以深度为16举例,mem[0:15][7:0],比如第一组数据写完了,回过头要给第一个地址写新的数据,此时wr_addr =5'b 1_0000,直接mem[wr_addr] <= data_in,数据其实没有写到想要写的位置
mem[{wr_addr[$clog2(data_depth)-1 : 0]}] <= data_in;
// mem[wr_addr] <= data_in;
end
else
wr_addr <= wr_addr;
// 读操作
always @ (posedge rd_clk or negedge rd_rst_n)
if (!rd_rst_n)
begin
rd_addr <= 0;
data_out <= 0;
end
else if (rd_en && (~empty))
begin
rd_addr <= rd_addr + 1'b1;
data_out <= mem[rd_addr];
end
else
begin
rd_addr <= rd_addr;
data_out <= data_out;
end
wire [$clog2(data_depth) : 0] wr_b2g;
wire [$clog2(data_depth) : 0] rd_b2g;
// 读写指针二进制转格雷码 采用组合逻辑,减少打拍次数
assign wr_b2g = wr_addr ^ (wr_addr >> 1);
assign rd_b2g = rd_addr ^ (rd_addr >> 1);
// 两级寄存器进行打拍 这里直接用两个寄存器进行同步,需要注意CDC问题
reg [$clog2(data_depth) : 0] wr_b2g_r ;
reg [$clog2(data_depth) : 0] wr_b2g_rr ;
reg [$clog2(data_depth) : 0] rd_b2g_r ;
reg [$clog2(data_depth) : 0] rd_b2g_rr ;
// 写指针同步到读时钟域,用于判空
// always @ (posedge rd_clk or negedge rd_rst_n)
always @ (rd_clk)
if (!rd_rst_n)
begin
wr_b2g_r <= 0;
wr_b2g_rr <= 0;
end
else
begin
wr_b2g_r <= wr_b2g;
wr_b2g_rr <= wr_b2g_r;
end
// always @ (posedge rd_clk or negedge rd_rst_n) // 时序逻辑
// if (!rd_rst_n)
// empty <= 0;
// // 所有位都相等,判空
// else if (wr_b2g_rr == rd_b2g)
// empty <= 1;
// else
// empty <= 0;
assign empty = (wr_b2g_rr == rd_b2g) ? 1 : 0; // 组合逻辑
// 读指针同步到写时钟域,用于判满
// always @ (posedge wr_clk or negedge wr_rst_n)
always @ (wr_clk)
if (!wr_rst_n)
begin
rd_b2g_r <= 0;
rd_b2g_rr <= 0;
end
else
begin
rd_b2g_r <= rd_b2g;
rd_b2g_rr <= rd_b2g_r;
end
always @ (posedge wr_clk or negedge wr_rst_n)
if (!wr_rst_n)
full <= 0;
// 高两位相反,其余位相等,判满
else if ( (~rd_b2g_rr[$clog2(data_depth) : $clog2(data_depth)-1] == wr_b2g[$clog2(data_depth) : $clog2(data_depth)-1])
&& (rd_b2g_rr[$clog2(data_depth)-2 : 0] == wr_b2g[$clog2(data_depth)-2 : 0]) )
full <= 1;
else
full <= 0;
endmodule
`define clk_period_wr 50
`define clk_period_rd 20
module sim_fifo_async ();
parameter data_width = 8;
parameter data_depth = 16;
reg wr_clk ;
reg wr_rst_n;
reg wr_en ;
reg [data_width-1 : 0] data_in ;
wire full ;
reg rd_clk ;
reg rd_rst_n;
reg rd_en ;
wire [data_width-1 : 0] data_out;
wire empty ;
fifo_async #(
.data_width (data_width),
.data_depth (data_depth)
)
fifo_async_inst (
.wr_clk (wr_clk),
.wr_rst_n (wr_rst_n),
.wr_en (wr_en),
.data_in (data_in),
.full (full),
.rd_clk (rd_clk),
.rd_rst_n (rd_rst_n),
.rd_en (rd_en),
.data_out (data_out),
.empty (empty)
);
//always #(`clk_period_wr/2) wr_clk = ~wr_clk;
//always #(`clk_period_rd/2) rd_clk = ~rd_clk;
always #(4/2) wr_clk = ~wr_clk;
always #(10/2) rd_clk = ~rd_clk;
initial begin
wr_clk = 0;
rd_clk = 0;
wr_rst_n = 0;
rd_rst_n = 0;
wr_en = 0;
rd_en = 0;
#10
wr_rst_n = 1;
rd_rst_n = 1;
// 开始写,6个数据
#10
wr_en = #(0.2) 1'b1;
data_in = #(0.2) $random;
repeat(5) begin
@(posedge wr_clk);
data_in = #(0.2) $random;
end
@(posedge wr_clk);
wr_en = #(0.2) 1'b0;
data_in = #(0.2) $random;
// 开始读,读空
#10
rd_en = #(0.2) 1'b1;
repeat(5) begin
@(posedge rd_clk);
end
@(posedge rd_clk);
rd_en = #(0.2) 1'b0;
// 将FIFO写满,会看到full标志信号
#50
wr_en = #(0.2) 1'b1;
data_in = #(0.2) $random;
repeat(19) begin
@(posedge wr_clk);
data_in = #(0.2) $random;
end
@(posedge wr_clk);
wr_en = #(0.2) 1'b0;
data_in = #(0.2) $random;
end
endmodule
遇到的问题,以及片面的分析:
-
有关empty信号是否可以正常工作,仿真给出的激励信号必须有讲究,同样的写入一组数据,但因为信号变化的时间不太一样,就影响到了相关寄存器的状态,也就影响了空满信号。
-
有关wr_addr多定义一位的影响,上面说了没有影响,但是对mem 的寻址还是有一点影响的
-
有关读写标志的跨时钟域问题,这里也没有很好的解决,这里能跨时钟域后能正常得出结果,有一点勉强的感觉。测试的时候,修改了很多组周期,从T(wr_clk) = 4, T(rd_clk) = 10,到T(wr_clk) = 300, T(rd_clk) = 20,都能正确读写,并且产生正确的空满标志
这个博客完了又有新的目标了,重新整理一下跨时钟域
单比特数据为例
- slow2fast,快时钟一定能采样到慢时钟,慢时钟的信号至少会在快时钟下保持一个时钟周期
- fast2slow,快时钟下的信号保持时间短,慢时钟可能会采样不到,代码中用的寄存器打两拍的方法,就不太适用,需要其他方法解决,比如对输入信号进行展宽呀啥的
-
有关读写操作的请求控制,以写数据为例,
// 写操作 always @ (posedge wr_clk or negedge wr_rst_n) if (!wr_rst_n) wr_addr <= 0; else if (wr_en && (~full)) begin wr_addr <= wr_addr + 1'b1; mem[{wr_addr[$clog2(data_depth)-1 : 0]}] <= data_in; end else wr_addr <= wr_addr; // 判满 always @ (posedge wr_clk or negedge wr_rst_n) if (!wr_rst_n) full <= 0; // 高两位相反,其余位相等,判满 else if ( (~rd_b2g_rr[$clog2(data_depth) : $clog2(data_depth)-1] == wr_b2g[$clog2(data_depth) : $clog2(data_depth)-1]) && (rd_b2g_rr[$clog2(data_depth)-2 : 0] == wr_b2g[$clog2(data_depth)-2 : 0]) ) full <= 1; else full <= 0;
仿真结果:这一大段我一直在写数据,确实full信号也输出了高电平,但也只是短暂的输出了一个周期 ,大无语。
full信号是使用时序逻辑求得的,如果用(wr_en && (~full))作为控制条件,wr_en是实时激励的,而full相对于晚一拍,她们俩短暂的相匹配(红色箭头的位置),然后又错过了
修改了一下控制条件如下,else if (wr_en && ~((~rd_b2g_rr[$clog2(data_depth) : $clog2(data_depth)-1] == wr_b2g[$clog2(data_depth) : $clog2(data_depth)-1]) && (rd_b2g_rr[$clog2(data_depth)-2 : 0] == wr_b2g[$clog2(data_depth)-2 : 0]))) begin wr_addr <= wr_addr + 1'b1; mem[{wr_addr[$clog2(data_depth)-1 : 0]}] <= data_in; end
猜想:如果full信号是wire型,用的assign语句赋值,那么它就是实时的,可以if((wr_en && (~full))),下面我试着这样用一下empty信号
试过了,也是正确的
回顾问题1,说到的empty信号没有正常工作,和问题4是一个原因,修改了就都好了。
总结
- 组合逻辑还是为了辅助时序逻辑,要注意节拍的同步问题
- 别人的博客都是技术分享,我的博客像是一个改错本,遇到各种问题,然后各种打补丁修改
终于写完了
4.29到5.18