文章目录
- 前言
- 一、VX_cache.sv代码部分解读2——buffering/initialize
- 1.1 core response buffering与VX_elastic_buffer模块解读
- 1.1.1 VX_pipe_buffer模块解读
- 1.1.1.1 一种握手信号的解释
- 1.1.1.2 世界线收束——VX_pipe_buffer的核心代码解释
- 1.1.1.3 VX_pipe_register模块解读与分析握手协议
- 1.1.2 VX_skid_buffer模块解读
- 1.1.2.1 VX_toggle_buffer模块解读与分析握手协议
- 1.1.2.2 VX_stream_buffer模块解读与分析握手协议
- 1.1.3 VX_fifo_queue模块解读
- 1.1.3.1 该模块的FIFO接口
- 1.1.3.2 额外延申同步FIFO
- 1.1.3.3 额外延申异步FIFO
- 1.1.3.4 VX_dp_ram模块讲解
- 1.1.4 为什么又出现一个VX_elastic_buffer模块?
- 1.2 memory request buffering
- 1.3 memory response buffering
- 1.4 cache initialize
- 1.4.1 VX_cache_init模块解读
- 总结
前言
上一篇的Vortex GPGPU的硬件代码分析(Cache篇1)已经分析了VX_cache.sv
代码的一部分,主要包括参数、变量和接口。
这一篇接着分析VX_cache.sv
的代码。
一、VX_cache.sv代码部分解读2——buffering/initialize
1.1 core response buffering与VX_elastic_buffer模块解读
// Core response buffering
wire [NUM_REQS-1:0] core_rsp_valid_s;
wire [NUM_REQS-1:0][`CS_WORD_WIDTH-1:0] core_rsp_data_s;
wire [NUM_REQS-1:0][TAG_WIDTH-1:0] core_rsp_tag_s;
wire [NUM_REQS-1:0] core_rsp_ready_s;
`RESET_RELAY (core_rsp_reset, reset);
for (genvar i = 0; i < NUM_REQS; ++i) begin
VX_elastic_buffer #(
.DATAW (`CS_WORD_WIDTH + TAG_WIDTH),
.SIZE (CORE_REQ_BUF_ENABLE ? `TO_OUT_BUF_SIZE(CORE_OUT_BUF) : 0),
.OUT_REG (`TO_OUT_BUF_REG(CORE_OUT_BUF))
) core_rsp_buf (
.clk (clk),
.reset (core_rsp_reset),
.valid_in (core_rsp_valid_s[i]),
.ready_in (core_rsp_ready_s[i]),
.data_in ({core_rsp_data_s[i], core_rsp_tag_s[i]}),
.data_out ({core_bus_if[i].rsp_data.data, core_bus_if[i].rsp_data.tag}),
.valid_out (core_bus_if[i].rsp_valid),
.ready_out (core_bus_if[i].rsp_ready)
);
end
这里的2个常量定义如下:
// 其中XLEN值为32
parameter WORD_SIZE = `XLEN/8
`define CS_WORD_WIDTH (8 * WORD_SIZE) // 值为32
parameter UUID_WIDTH = 0,
parameter TAG_WIDTH = UUID_WIDTH + 1, // 值为1
例化模块中的3个parameter
如下:
// DATAW为33
.DATAW (`CS_WORD_WIDTH + TAG_WIDTH),
// CORE_REQ_BUF_ENABLE为1,因此SIZE为2
.SIZE (CORE_REQ_BUF_ENABLE ? `TO_OUT_BUF_SIZE(CORE_OUT_BUF) : 0),
// OUT_REG为2
.OUT_REG (`TO_OUT_BUF_REG(CORE_OUT_BUF))
// ps:相关变量:
// Core response output register
parameter CORE_OUT_BUF = 0
localparam CORE_REQ_BUF_ENABLE = (NUM_BANKS != 1) || (NUM_REQS != 1);
// Number of banks
parameter NUM_BANKS = 1,
// Number of Word requests per cycle
parameter NUM_REQS = 4,
因此core request buffer
的size
为2
,也就是允许接受2
个core request
。
其中关于TO_OUT_BUF_REG
见hw/rtl/VX_platform.vh
,如下:
// size(x): 0 -> 0, 1 -> 1, 2 -> 2, 3 -> 2, 4-> 2
`define TO_OUT_BUF_SIZE(out_reg) `MIN(out_reg, 2)
先不管其余变量,先看VX_elastic_buffer
模块:
`include "VX_platform.vh"
`TRACING_OFF
module VX_elastic_buffer #(
parameter DATAW = 1,
parameter SIZE = 1,
parameter OUT_REG = 0,
parameter LUTRAM = 0
) (
input wire clk,
input wire reset,
input wire valid_in,
output wire ready_in,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
input wire ready_out,
output wire valid_out
);
if (SIZE == 0) begin
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign valid_out = valid_in;
assign data_out = data_in;
assign ready_in = ready_out;
end else if (SIZE == 1) begin
VX_pipe_buffer #(
.DATAW (DATAW)
) pipe_buffer (
.clk (clk),
.reset (reset),
.valid_in (valid_in),
.data_in (data_in),
.ready_in (ready_in),
.valid_out (valid_out),
.data_out (data_out),
.ready_out (ready_out)
);
end else if (SIZE == 2) begin
VX_skid_buffer #(
.DATAW (DATAW),
.HALF_BW (OUT_REG == 2),
.OUT_REG (OUT_REG)
) skid_buffer (
.clk (clk),
.reset (reset),
.valid_in (valid_in),
.data_in (data_in),
.ready_in (ready_in),
.valid_out (valid_out),
.data_out (data_out),
.ready_out (ready_out)
);
end else begin
wire empty, full;
wire [DATAW-1:0] data_out_t;
wire ready_out_t;
wire push = valid_in && ready_in;
wire pop = ~empty && ready_out_t;
VX_fifo_queue #(
.DATAW (DATAW),
.DEPTH (SIZE),
.OUT_REG (OUT_REG == 1),
.LUTRAM (LUTRAM)
) fifo_queue (
.clk (clk),
.reset (reset),
.push (push),
.pop (pop),
.data_in(data_in),
.data_out(data_out_t),
.empty (empty),
.full (full),
`UNUSED_PIN (alm_empty),
`UNUSED_PIN (alm_full),
`UNUSED_PIN (size)
);
assign ready_in = ~full;
VX_elastic_buffer #(
.DATAW (DATAW),
.SIZE (OUT_REG == 2)
) out_buf (
.clk (clk),
.reset (reset),
.valid_in (~empty),
.data_in (data_out_t),
.ready_in (ready_out_t),
.valid_out (valid_out),
.data_out (data_out),
.ready_out (ready_out)
);
end
endmodule
`TRACING_ON
其中关于UNUSED_VAR
的定义见于hw/rtl/VX_platform.vh
,具体如下:
`define UNUSED_VAR(x) if (1) begin \
/* verilator lint_off UNUSED */ \
wire [$bits(x)-1:0] __x = x; \
/* verilator lint_on UNUSED */ \
end
该函数的功能是将x
赋给__x
,其中$bits
是SV
内置函数,用于获取变量的位宽。其实就是一个初始化的wire
语句。
因此关于SIZE=0
的这段条件体内容:
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign valid_out = valid_in;
assign data_out = data_in;
assign ready_in = ready_out;
其实就是走个过场,表明不接受任何core request
。现在size=2
,所以关键在接下来两个pipe_buffer
和skid_buffer
。
1.1.1 VX_pipe_buffer模块解读
该模块见于hw/rtl/libs/VX_pipe_buffer.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_pipe_buffer #(
parameter DATAW = 1,
parameter PASSTHRU = 0
) (
input wire clk,
input wire reset,
input wire valid_in,
output wire ready_in,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
input wire ready_out,
output wire valid_out
);
if (PASSTHRU != 0) begin
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign ready_in = ready_out;
assign valid_out = valid_in;
assign data_out = data_in;
end else begin
wire stall = valid_out && ~ready_out;
VX_pipe_register #(
.DATAW (1 + DATAW),
.RESETW (1)
) pipe_register (
.clk (clk),
.reset (reset),
.enable (~stall),
.data_in ({valid_in, data_in}),
.data_out ({valid_out, data_out})
);
assign ready_in = ~stall;
end
endmodule
`TRACING_ON
在这里还是能看到几个熟悉的信号的!
input valid_in
output ready_in
output valid_out
input ready_out
1.1.1.1 一种握手信号的解释
先回顾一个握手协议(该图来自牛客的数据累加输出
):
其中信号的输入输出关系:
input valid_a
output ready_a
output valid_b
input ready_b
时序图含有的信息较多,观察时序图需要注意:
1.data_out
是在已接收到4个数据后产生输出;
2.在data_out
准备好,valid_b
拉高时,如果下游的ready_b
为低,表示下游此时不能接收本模块的数据,那么,将会拉低ready_a
,以反压上游数据输入;
3.当下游ready_b
拉高,且valid_b
为高,表示模块与下游握手成功,valid_b
在下一个时钟周期拉低;
4.当下游ready_b
拉高,本来由于之前ready_b
为低而反压上游的ready_a
立即拉高,开始接收上游数据,注意,此细节也是体现了要求的数据传输无气泡。如果ready_a
不是立即拉高,而是在下一个时钟周期拉高,那么本模块将会在下游握手成功后空一个时钟周期
,才能开始接收上游数据,这样是不满足要求的。
5.要实现4个输入数据的累加,要用1个寄存器将先到达的数据累加之后进行缓存。当上游握手成功,将输入数据累加进寄存器;当累加完4个输入数据,且下游握手成功,将新的输入数据缓存进寄存器。注意,之所以这样设计,是为了不造成性能损失,而之前的累加结果,已经传给了下游。
6.需要计数器来计数接收到的数据数量,计数器在0-3之间循环。计数器初始值是0,每接收一个数据,计数器加1,当计数器再次循环到0时,表示已经接收到4个数据,可以输出累加结果。
因此其中关于4个信号之间的关系
:
`timescale 1ns/1ns
module valid_ready(
input clk ,
input rst_n ,
input [7:0] data_in ,
input valid_a ,
input ready_b ,
output ready_a ,
output reg valid_b ,
output reg [9:0] data_out
);
reg [1:0] count;
always @(posedge clk or negedge rst_n)
begin
if (!rst_n) count <= 0;
else if (valid_a && ready_a) count <= (count==2'd3) ? 0 : count + 1;
else count <= count;
end
always @(posedge clk or negedge rst_n)
begin
if (!rst_n) valid_b <= 0;
else begin
if (count==2'd3 && valid_a && ready_a) valid_b <= 1; // 注意和ready_b没关系
else if (valid_b && ready_b) valid_b <= 0; // 这一步代表握手
end
end
assign ready_a = !valid_b | ready_b; // 注意这个信号和valid_a无关!!!
always @(posedge clk or negedge rst_n)
begin
if (!rst_n) data_out <= 0;
else begin
if (valid_a && ready_a && count==2'd0 && ready_b) data_out <= data_in;
else if (valid_a && ready_a) data_out <= data_out + data_in;
end
end
endmodule
所以总结下来就是input ready_b
是用于确定下游模块是否(为1
表示是,否则否)可以接受输出数据
,而input valid_a
是表示上游模块是否(为1
表示是,否则否)准备好输入数据
。
那么中间两个输出信号则是为了建立握手
,其中output ready_a
表示可以接受输入数据
传入(但不代表下游模块可以接受
,因为还得等到输入数据
全部传入完成以后,进入一个停止传入输入数据
的状态才行),当且仅当input valid_a
和output ready_a
之间均为高时可以传入输入数据
。
所以有:
1、input valid_a为高时,开始传输数据到内部,但是等到计数器满足条件时,output ready_a被拉低,表明数据已经传入完毕(也就是被ready_b反压的信号);
2、此处关于output ready_a后续被拉高是特殊的,因为题目要求无气泡。当output valid_b和input ready_b都拉高时,表明4个内部数据已经全部处理完毕,且这个过程只需要1拍,那么在处理这些数据的同时其实可以允许输入数据进入内部,所以在input ready_b被拉高的同时,同时拉高output ready_a。
同理,output valid_b
表示输入数据
传送好了,等待下游模块
来取(意味着已经进入了停止传输输入数据
的状态,但不意味着下游模块
可以取了)。当且仅当,output valid_b
和input ready_b
同时拉高时才可以让下游模块
接受已经传入的
数据。当然下游模块
接受结束以后,output valid_b
要被拉低,因为此时内部已经没有传入的数据了
。
所以有:
1、input valid_a 和 output ready_a 均为高,且计数到一定值,output valid_b被拉高,表明数据已经传入完毕;
2、output valid_b 和 input ready_b 均为高,下一拍output valid_b必须拉低,表明数据已经处理完了。
在基本理解握手协议
以后,世界线收束回来!
1.1.1.2 世界线收束——VX_pipe_buffer的核心代码解释
VX_pipe_buffer
中的核心代码则是:
wire stall = valid_out && ~ready_out;
VX_pipe_register #(
.DATAW (1 + DATAW),
.RESETW (1)
) pipe_register (
.clk (clk),
.reset (reset),
.enable (~stall),
.data_in ({valid_in, data_in}),
.data_out ({valid_out, data_out})
);
assign ready_in = ~stall;
其中stall
指的就是output valid_out
和input ready_out
之间前者拉高
、后者拉低
时的一段时间。也就是下游模块尚未准备好接受数据
!
其中的:
wire stall = valid_out && ~ready_out;
assign ready_in = ~stall;
刚和下图中的valid_b
、ready_b
与ready_a
对应!
按照后面的程序来看,valid_out
的数据来源是valid_in
。
其实后面的程序和上面那个传4个数据
类似,但是只传输1个数据
,且valid_out
信号只是valid_in
延迟一拍,仅在~stall
阶段传输。
1.1.1.3 VX_pipe_register模块解读与分析握手协议
关于VX_pipe_register
见于hw/rtl/libs/VX_pipe_register.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_pipe_register #(
parameter DATAW = 1,
parameter RESETW = 0,
parameter DEPTH = 1
) (
input wire clk,
input wire reset,
input wire enable,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out
);
if (DEPTH == 0) // DEPTH 为 0
begin
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
`UNUSED_VAR (enable)
assign data_out = data_in;
end
else if (DEPTH == 1) // DEPTH 为 1
begin
if (RESETW == 0) begin
`UNUSED_VAR (reset)
reg [DATAW-1:0] value;
always @(posedge clk) begin
if (enable) begin
value <= data_in;
end
end
assign data_out = value;
end
else if (RESETW == DATAW) begin
reg [DATAW-1:0] value;
always @(posedge clk) begin
if (reset) begin
value <= RESETW'(0);
end else if (enable) begin
value <= data_in;
end
end
assign data_out = value;
end
else begin // 是这个
reg [DATAW-RESETW-1:0] value_d;
reg [RESETW-1:0] value_r;
always @(posedge clk) begin
if (reset) begin
value_r <= RESETW'(0);
end else if (enable) begin
value_r <= data_in[DATAW-1:DATAW-RESETW];
end
end
always @(posedge clk) begin
if (enable) begin
value_d <= data_in[DATAW-RESETW-1:0];
end
end
assign data_out = {value_r, value_d};
end
end
else begin // DEPTH ≥ 2
wire [DEPTH:0][DATAW-1:0] data_delayed;
assign data_delayed[0] = data_in;
for (genvar i = 1; i <= DEPTH; ++i) begin
VX_pipe_register #(
.DATAW (DATAW),
.RESETW (RESETW)
) pipe_reg (
.clk (clk),
.reset (reset),
.enable (enable),
.data_in (data_delayed[i-1]),
.data_out (data_delayed[i])
);
end
assign data_out = data_delayed[DEPTH];
end
endmodule
`TRACING_ON
满足条件的核心代码是:
reg [DATAW-RESETW-1:0] value_d;
reg [RESETW-1:0] value_r;
always @(posedge clk) begin
if (reset) begin
value_r <= RESETW'(0);
end else if (enable) begin
value_r <= data_in[DATAW-1:DATAW-RESETW];
end
end
always @(posedge clk) begin
if (enable) begin
value_d <= data_in[DATAW-RESETW-1:0];
end
end
assign data_out = {value_r, value_d};
按照前面的参数:
value_r <= data_in[DATAW-1:DATAW-RESETW];
//其实就是
value_r <= data_in[W+1-1:W+1-1]; // 也就是最高位的valid_in
value_d <= data_in[DATAW-RESETW-1:0];
//其实就是
value_d <= data_in[W+1-1-1:0]; // 也就是后面拼接的data_in
刚好对应:
.data_in ({valid_in, data_in}),
.data_out ({valid_out, data_out})
根据reset
最高位的valid_in
被拉低,经过stall
的wire
语句后拉低,底下那个enable
被拉高,所以允许数据传输:
1、如果此时valid_in
为低,那么下一拍valid_out
为低,表示接着往内部传输数据。
2、如果此时valid_in
为高,那么下一拍valid_out
为高,下一拍的stall
由ready_out
决定,如果ready_out
为0
表示下游模块没准备好接收数据,此时stall
为1
,出现阻塞,数据不再传入内部,等待ready_out
为1
。如果ready_out
为1
表示下游模块开始接收数据,此时stall
为0
,表示上游模块可以接着往内部送入数据。
下游模块接收数据
和上游模块送入数据
之间不存在气泡
。
关于为什么又出现VX_pipe_register
模块的例化,这个问题放在1.1.4
展开。
1.1.2 VX_skid_buffer模块解读
关于VX_skid_buffer
模块的代码见于hw/rtl/libs/VX_skid_buffer.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_skid_buffer #(
parameter DATAW = 32,
parameter PASSTHRU = 0,
parameter HALF_BW = 0,
parameter OUT_REG = 0
) (
input wire clk,
input wire reset,
input wire valid_in,
output wire ready_in,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
input wire ready_out,
output wire valid_out
);
if (PASSTHRU != 0) begin
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign valid_out = valid_in;
assign data_out = data_in;
assign ready_in = ready_out;
end else if (HALF_BW != 0) begin
VX_toggle_buffer #(
.DATAW (DATAW)
) toggle_buffer (
.clk (clk),
.reset (reset),
.valid_in (valid_in),
.data_in (data_in),
.ready_in (ready_in),
.valid_out (valid_out),
.data_out (data_out),
.ready_out (ready_out)
);
end else begin
VX_stream_buffer #(
.DATAW (DATAW),
.OUT_REG (OUT_REG)
) stream_buffer (
.clk (clk),
.reset (reset),
.valid_in (valid_in),
.data_in (data_in),
.ready_in (ready_in),
.valid_out (valid_out),
.data_out (data_out),
.ready_out (ready_out)
);
end
endmodule
`TRACING_ON
其中3个parameter
的情况如下:
.DATAW (DATAW),
.HALF_BW (OUT_REG == 2),
.OUT_REG (OUT_REG)
// 开头分析过:
// DATAW为33
.DATAW (`CS_WORD_WIDTH + TAG_WIDTH),
// CORE_REQ_BUF_ENABLE为1,因此SIZE为2
.SIZE (CORE_REQ_BUF_ENABLE ? `TO_OUT_BUF_SIZE(CORE_OUT_BUF) : 0),
// OUT_REG为2
.OUT_REG (`TO_OUT_BUF_REG(CORE_OUT_BUF))
那么满足条件的模块VX_toggle_buffer
。
1.1.2.1 VX_toggle_buffer模块解读与分析握手协议
VX_toggle_buffer
模块见于hw/rtl/libs/VX_toggle_buffer.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_toggle_buffer #(
parameter DATAW = 1,
parameter PASSTHRU = 0
) (
input wire clk,
input wire reset,
input wire valid_in,
output wire ready_in,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
input wire ready_out,
output wire valid_out
);
if (PASSTHRU != 0) begin
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign ready_in = ready_out;
assign valid_out = valid_in;
assign data_out = data_in;
end else begin
reg [DATAW-1:0] buffer;
reg has_data;
always @(posedge clk) begin
if (reset) begin
has_data <= 0;
end else begin
if (~has_data) begin
has_data <= valid_in;
end else if (ready_out) begin
has_data <= 0;
end
end
if (~has_data) begin
buffer <= data_in;
end
end
assign ready_in = ~has_data;
assign valid_out = has_data;
assign data_out = buffer;
end
endmodule
`TRACING_ON
这里又是一个握手协议
!
首先在reset
为高的情况下,has_data
被拉低为0
。
在has_data
被拉低为0的情况下,ready_in
为高,valid_out
为低,这俩信号是合理的,因为ready_in
为高表明上游模块正在将输入数据传送到内部,valid_out
为低说明此时上游模块的数据还没到送给下游模块的时候。
与此同时,has_data
因为是0
,所以下一拍data_in
会送到内部(也就是buffer
),同时has_data
的下一拍也被注定了,也就是使用valid_in
的当前值,按照握手协议,数据传送只发生在valid_in
和ready_in
同时拉高的情况下,因此has_data
下一拍只能为高。同时下一拍buffer
会把数据交给data_out
。
如果此刻has_data
为高,ready_in
被拉低,valid_out
被拉高,这是合理的,因为上游模块的输入数据已经进入到buffer
内,valid_out
被拉高是为了提示下游模块可以准备接收数据了,此时ready_out
为1,且在这一拍buffer
刚好把数据交给data_out
。
在ready_out
被拉高后,has_data
会被拉低。又开始重复reset
的操作!
顺带解读涉及的另一个模块VX_stream_buffer
模块。
1.1.2.2 VX_stream_buffer模块解读与分析握手协议
关于VX_stream_buffer
模块见于hw/rtl/libs/VX_stream_buffer.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_stream_buffer #(
parameter DATAW = 1,
parameter OUT_REG = 0,
parameter PASSTHRU = 0
) (
input wire clk,
input wire reset,
input wire valid_in,
output wire ready_in,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
input wire ready_out,
output wire valid_out
);
if (PASSTHRU != 0) begin // 第一层分支
`UNUSED_VAR (clk)
`UNUSED_VAR (reset)
assign ready_in = ready_out;
assign valid_out = valid_in;
assign data_out = data_in;
end else begin
if (OUT_REG != 0) begin // 第二层分支
reg [DATAW-1:0] data_out_r;
reg [DATAW-1:0] buffer;
reg valid_out_r;
reg use_buffer;
wire push = valid_in && ready_in;
wire stall_out = valid_out_r && ~ready_out;
always @(posedge clk) begin
if (reset) begin
valid_out_r <= 0;
use_buffer <= 0;
end else begin
if (ready_out) begin
use_buffer <= 0;
end else if (valid_in && valid_out) begin
use_buffer <= 1;
end
if (~stall_out) begin
valid_out_r <= valid_in || use_buffer;
end
end
end
always @(posedge clk) begin
if (push) begin
buffer <= data_in;
end
if (~stall_out) begin
data_out_r <= use_buffer ? buffer : data_in;
end
end
assign ready_in = ~use_buffer;
assign valid_out = valid_out_r;
assign data_out = data_out_r;
end else begin // 第三层分支
reg [1:0][DATAW-1:0] shift_reg;
reg valid_out_r, ready_in_r, rd_ptr_r;
wire push = valid_in && ready_in;
wire pop = valid_out_r && ready_out;
always @(posedge clk) begin
if (reset) begin
valid_out_r <= 0;
ready_in_r <= 1;
rd_ptr_r <= 1;
end else begin
if (push) begin
if (!pop) begin
ready_in_r <= rd_ptr_r;
valid_out_r <= 1;
end
end else if (pop) begin
ready_in_r <= 1;
valid_out_r <= rd_ptr_r;
end
rd_ptr_r <= rd_ptr_r ^ (push ^ pop);
end
end
always @(posedge clk) begin
if (push) begin
shift_reg[1] <= shift_reg[0];
shift_reg[0] <= data_in;
end
end
assign ready_in = ready_in_r;
assign valid_out = valid_out_r;
assign data_out = shift_reg[rd_ptr_r];
end
end
endmodule
`TRACING_ON
这里也有2
个握手协议
,我们分开来讲。按照代码中3个分支,我们只看第2个分支和第3个分支。
先看第2
个分支:
reg [DATAW-1:0] data_out_r;
reg [DATAW-1:0] buffer;
reg valid_out_r;
reg use_buffer;
wire push = valid_in && ready_in;
wire stall_out = valid_out_r && ~ready_out;
always @(posedge clk) begin
if (reset) begin
valid_out_r <= 0;
use_buffer <= 0;
end else begin
if (ready_out) begin
use_buffer <= 0;
end else if (valid_in && valid_out) begin
use_buffer <= 1;
end
if (~stall_out) begin
valid_out_r <= valid_in || use_buffer;
end
end
end
always @(posedge clk) begin
if (push) begin
buffer <= data_in;
end
if (~stall_out) begin
data_out_r <= use_buffer ? buffer : data_in;
end
end
assign ready_in = ~use_buffer;
assign valid_out = valid_out_r;
assign data_out = data_out_r;
这一行的wire push = valid_in && ready_in;
表示入栈
,也就是允许data_in
进入buffer
。
第一拍:在检测reset
为高时,valid_out_r
下一拍为0,use_buffer
下一拍也为0。
// 现在过了一拍
第二拍:valid_out_r
此时为0,use_buffer
此时为0。此时stall_out
为0,valid_out
为0,ready_in
为1。
下一拍会发生啥?
首先,valid_out_r
下一拍为1;
其次,由于valid_in
为1,push
被拉高为1,那么buffer
下一拍被赋予这一拍的data_in
;
随后,data_out_r
下一拍被赋予data_in
,因为use_buffer
不是1;
之后,use_buffer
下一拍保持为0。
//现在又过了一拍
第三拍:valid_out_r
为1,data_out_r
为上一拍的data_in
,buffer
为上一拍的data_in
,data_out
为上一拍的data_in
,use_buffer
为0
,ready_in
为1,valid_out
为1。stall_out
因为valid_out_r
为1和~ready_out
为1,可以出现1。
第一种可能:push
因为valid_in
为1和ready_in
为1,可以继续为1。
补充——第二种可能:push
因为valid_in
为0和ready_in
为1,为0。
下一拍会发生啥?
首先,use_buffer
下一拍为1,valid_out_r
保持;
其次,这一拍data_in
的数据赋予buffer
;(补充第二种可能的结果:buffer
保持不变且为第二拍的data_in
)
随后,data_out_r
下一拍被赋予这一拍的data_in
,因为use_buffer
为0。
//现在又过了一拍
第四拍:valid_out_r
为1,data_out_r
是上一拍的data_in
,buffer
为第三拍的data_in
(补充第二种可能的结果:buffer
保持为第二拍的data_in
),data_out
为上一拍的data_in
,use_buffer
为1
(注意了!),ready_in
为0,valid_out
为1。push
为0(其实很好理解,此刻valid_in
不管怎么样,ready_in
被拉低,说明输入数据
被限制送到内部,ready_in
被反压了)!stall_out
完全取决于~ready_out
了。
假如此时ready_out
为1,这说明下游模块准备接收内部数据了,那么stall_out
为0。
下一拍会发生啥?
首先,use_buffer
下一拍为0,valid_out_r
下一拍为1;
其次,下一拍data_out_r
是数据来源就是这一拍的buffer
;
随后,buffer
数据保持。
//现在又过了一拍
第五拍:valid_out_r
为1,data_out_r
就是上一拍的buffer
,buffer
还是第三拍(补充可能性:第二拍)的data_in
,data_out
就是上一拍的buffer
(也就是第二拍或者第三拍的data_in
),use_buffer
为0
(注意了!),ready_in
为1,valid_out
为1。push
等待valid_in
拉高。接下来的节奏和第三拍
一样了。之后不再分析!
上面还是太复杂,还是简化以后看更加明白!
//reg [DATAW-1:0] data_out_r;
reg [DATAW-1:0] buffer;
//reg valid_out_r;
//reg use_buffer;
//wire push = valid_in && ready_in;
//wire stall_out = valid_out_r && ~ready_out;
always @(posedge clk) begin
if (reset) begin
valid_out <= 0;
ready_in <= 1;
end else begin
if (ready_out) begin
ready_in <= 1;
end else if (valid_in && valid_out) begin
ready_in <= 0;
end
if (~valid_out || ready_out) begin
valid_out <= valid_in || (~ready_in);
end
end
end
always @(posedge clk) begin
if (valid_in && ready_in) begin
buffer <= data_in;
end
if (~valid_out || ready_out) begin
data_out <= ~ready_in ? buffer : data_in;
end
end
//assign ready_in = ~use_buffer;
//assign valid_out = valid_out_r;
//assign data_out = data_out_r;
其实实现了从data_in
到buffer
内,至少隔了2拍(更多拍数取决于ready_out
何时为1)后再传送给data_out
。但是还是会出现data_in
直接到data_out
,所以作者的方案分为use_buffer=1
和use_buffer=0
这两种情况!
再来看看第3
个分支:
reg [1:0][DATAW-1:0] shift_reg;
reg valid_out_r, ready_in_r, rd_ptr_r;
wire push = valid_in && ready_in;
//wire pop = valid_out_r && ready_out;
wire pop = valid_out && ready_out;
always @(posedge clk) begin
if (reset) begin
valid_out <= 0;
ready_in <= 1;
rd_ptr_r <= 1;
end else begin
if (valid_in && ready_in) begin
if (~valid_out || ~ready_out) begin
ready_in <= rd_ptr_r;
valid_out <= 1;
end
end
else if (valid_out && ready_out) begin
ready_in <= 1;
valid_out <= rd_ptr_r;
end
rd_ptr_r <= rd_ptr_r ^ (push ^ pop);
end
end
always @(posedge clk) begin
if (valid_in && ready_in) begin
shift_reg[1] <= shift_reg[0];
shift_reg[0] <= data_in;
end
end
//assign ready_in = ready_in_r;
//assign valid_out = valid_out_r;
assign data_out = shift_reg[rd_ptr_r];
end
end
简析一下:
rd_ptr_r <= rd_ptr_r ^ (push ^ pop);
首先,push
和pop
这两个操作在任何时候最多只能有一个1。
- 当push ^ pop = 1时,rd_ptr_r为1,则下一拍为0,否则下一拍为1。换言之,只要push或者pop有一个为1,那么rd_ptr_r将在0和1之间不断跳转。
- 当push ^ pop = 0时,rd_ptr_r为1,则下一拍为1,否则下一拍为0。换言之就是push和pop都为0(也就是既不入栈,也不出栈)时,rd_ptr_r保持不变。
那么分析就变得容易了,还是一套握手协议!
1.1.3 VX_fifo_queue模块解读
VX_fifo_queue
模块见于hw/rtl/libs/VX_fifo_queue.sv
,代码如下:
`include "VX_platform.vh"
`TRACING_OFF
module VX_fifo_queue #(
parameter DATAW = 1,
parameter DEPTH = 2,
parameter ALM_FULL = (DEPTH - 1),
parameter ALM_EMPTY = 1,
parameter OUT_REG = 0,
parameter LUTRAM = 1,
parameter SIZEW = `CLOG2(DEPTH+1)
) (
input wire clk,
input wire reset,
input wire push,
input wire pop,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
output wire empty,
output wire alm_empty,
output wire full,
output wire alm_full,
output wire [SIZEW-1:0] size
);
localparam ADDRW = `CLOG2(DEPTH);
`STATIC_ASSERT(ALM_FULL > 0, ("alm_full must be greater than 0!"))
`STATIC_ASSERT(ALM_FULL < DEPTH, ("alm_full must be smaller than size!"))
`STATIC_ASSERT(ALM_EMPTY > 0, ("alm_empty must be greater than 0!"))
`STATIC_ASSERT(ALM_EMPTY < DEPTH, ("alm_empty must be smaller than size!"))
`STATIC_ASSERT(`IS_POW2(DEPTH), ("size must be a power of 2!"))
if (DEPTH == 1) begin
reg [DATAW-1:0] head_r;
reg size_r;
always @(posedge clk) begin
if (reset) begin
head_r <= '0;
size_r <= '0;
end else begin
`ASSERT(~push || ~full, ("runtime error: writing to a full queue"));
`ASSERT(~pop || ~empty, ("runtime error: reading an empty queue"));
if (push) begin
if (~pop) begin
size_r <= 1;
end
end else if (pop) begin
size_r <= '0;
end
if (push) begin
head_r <= data_in;
end
end
end
assign data_out = head_r;
assign empty = (size_r == 0);
assign alm_empty = 1'b1;
assign full = (size_r != 0);
assign alm_full = 1'b1;
assign size = size_r;
end else begin
reg empty_r, alm_empty_r;
reg full_r, alm_full_r;
reg [ADDRW-1:0] used_r;
wire [ADDRW-1:0] used_n;
always @(posedge clk) begin
if (reset) begin
empty_r <= 1;
alm_empty_r <= 1;
full_r <= 0;
alm_full_r <= 0;
used_r <= '0;
end else begin
`ASSERT(~(push && ~pop) || ~full, ("runtime error: incrementing full queue"));
`ASSERT(~(pop && ~push) || ~empty, ("runtime error: decrementing empty queue"));
if (push) begin
if (~pop) begin
empty_r <= 0;
if (used_r == ADDRW'(ALM_EMPTY))
alm_empty_r <= 0;
if (used_r == ADDRW'(DEPTH-1))
full_r <= 1;
if (used_r == ADDRW'(ALM_FULL-1))
alm_full_r <= 1;
end
end else if (pop) begin
full_r <= 0;
if (used_r == ADDRW'(ALM_FULL))
alm_full_r <= 0;
if (used_r == ADDRW'(1))
empty_r <= 1;
if (used_r == ADDRW'(ALM_EMPTY+1))
alm_empty_r <= 1;
end
used_r <= used_n;
end
end
if (DEPTH == 2) begin
assign used_n = used_r ^ (push ^ pop);
if (0 == OUT_REG) begin
reg [1:0][DATAW-1:0] shift_reg;
always @(posedge clk) begin
if (push) begin
shift_reg[1] <= shift_reg[0];
shift_reg[0] <= data_in;
end
end
assign data_out = shift_reg[!used_r[0]];
end else begin
reg [DATAW-1:0] data_out_r;
reg [DATAW-1:0] buffer;
always @(posedge clk) begin
if (push) begin
buffer <= data_in;
end
if (push && (empty_r || (used_r && pop))) begin
data_out_r <= data_in;
end else if (pop) begin
data_out_r <= buffer;
end
end
assign data_out = data_out_r;
end
end else begin
assign used_n = $signed(used_r) + ADDRW'($signed(2'(push) - 2'(pop)));
if (0 == OUT_REG) begin
reg [ADDRW-1:0] rd_ptr_r;
reg [ADDRW-1:0] wr_ptr_r;
always @(posedge clk) begin
if (reset) begin
rd_ptr_r <= '0;
wr_ptr_r <= '0;
end else begin
wr_ptr_r <= wr_ptr_r + ADDRW'(push);
rd_ptr_r <= rd_ptr_r + ADDRW'(pop);
end
end
VX_dp_ram #(
.DATAW (DATAW),
.SIZE (DEPTH),
.LUTRAM (LUTRAM)
) dp_ram (
.clk(clk),
.read (1'b1),
.write (push),
`UNUSED_PIN (wren),
.waddr (wr_ptr_r),
.wdata (data_in),
.raddr (rd_ptr_r),
.rdata (data_out)
);
end else begin
wire [DATAW-1:0] dout;
reg [DATAW-1:0] dout_r;
reg [ADDRW-1:0] wr_ptr_r;
reg [ADDRW-1:0] rd_ptr_r;
reg [ADDRW-1:0] rd_ptr_n_r;
always @(posedge clk) begin
if (reset) begin
wr_ptr_r <= '0;
rd_ptr_r <= '0;
rd_ptr_n_r <= 1;
end else begin
wr_ptr_r <= wr_ptr_r + ADDRW'(push);
if (pop) begin
rd_ptr_r <= rd_ptr_n_r;
if (DEPTH > 2) begin
rd_ptr_n_r <= rd_ptr_r + ADDRW'(2);
end else begin // (DEPTH == 2);
rd_ptr_n_r <= ~rd_ptr_n_r;
end
end
end
end
wire going_empty;
if (ALM_EMPTY == 1) begin
assign going_empty = alm_empty_r;
end else begin
assign going_empty = (used_r == ADDRW'(1));
end
VX_dp_ram #(
.DATAW (DATAW),
.SIZE (DEPTH),
.LUTRAM (LUTRAM)
) dp_ram (
.clk (clk),
.read (1'b1),
.write (push),
`UNUSED_PIN (wren),
.waddr (wr_ptr_r),
.wdata (data_in),
.raddr (rd_ptr_n_r),
.rdata (dout)
);
always @(posedge clk) begin
if (push && (empty_r || (going_empty && pop))) begin
dout_r <= data_in;
end else if (pop) begin
dout_r <= dout;
end
end
assign data_out = dout_r;
end
end
assign empty = empty_r;
assign alm_empty = alm_empty_r;
assign full = full_r;
assign alm_full = alm_full_r;
assign size = {full_r, used_r};
end
endmodule
`TRACING_ON
这段代码只配了一套时钟和复位,因此是同步FIFO
。
1.1.3.1 该模块的FIFO接口
该模块的FIFO
接口如下:
input wire clk,
input wire reset,
input wire push,
input wire pop,
input wire [DATAW-1:0] data_in,
output wire [DATAW-1:0] data_out,
output wire empty,
output wire alm_empty,
output wire full,
output wire alm_full,
output wire [SIZEW-1:0] size
出现了alm_empty
和alm_full
,大概就是almost empty
和almost full
两个信号!almost_full
和alomst_empty
信号在FIFO差一个深度就满/空
的时候拉高。
一个粗糙的例子:
//增加almost信号判断满
always @(posedge clk or negedge rstn) begin
if(!rstn)
full <= 0;
else if(al_full && wr_en && !rd_en)
full <= 1;
//else if(al_full && rd_en && !wr_en)
else if(al_full && rd_en)
full <= 0;
end
always@(posedge clk or negedge rstn)begin
if(!rstn)
empty <= 0;
else if(al_empty && rd_en && !wr_en)
empty <= 1;
//else if(al_empty && wr_en && !rd_en)
else if(al_empty && wr_en)
empty <= 0;
end
我们还是先用牛客上的例子来回顾同步FIFO
和异步FIFO
,该例子也就大概能看明白了。也可以补充参考FIFO相关。
1.1.3.2 额外延申同步FIFO
首先,对于读写指针而言,通过计数器来确定2个指针之间的距离。
/**********************addr bin gen*************************/
reg [ADDR_WIDTH:0] waddr;
reg [ADDR_WIDTH:0] raddr;
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
waddr <= 'd0;
end
else if(!wfull && winc)begin
waddr <= waddr + 1'd1;
end
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
raddr <= 'd0;
end
else if(!rempty && rinc)begin
raddr <= raddr + 1'd1;
end
end
随后基于waddr
和raddr
两个计数器来确定空满情况。
/**********************full empty gen*************************/
wire [ADDR_WIDTH : 0] fifo_cnt;
assign fifo_cnt = (waddr[ADDR_WIDTH] == raddr[ADDR_WIDTH]) ? (waddr[ADDR_WIDTH:0] - raddr[ADDR_WIDTH:0]) :
(DEPTH + waddr[ADDR_WIDTH-1:0] - raddr[ADDR_WIDTH-1:0]);
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
wfull <= 'd0;
rempty <= 'd0;
end
else if(fifo_cnt == 'd0)begin
rempty <= 1'd1;
end
else if(fifo_cnt == DEPTH)begin
wfull <= 1'd1;
end
else begin
wfull <= 'd0;
rempty <= 'd0;
end
end
这里的fifo_cnt
是因为考虑到计数器相比地址而言,多用了一位。高位用于指示是否出现套圈的情况。
之后就是利用空满信息确定write enable
和read enable
。
/**********************RAM*************************/
wire wen ;
wire ren ;
wire wren;//high write
assign wen = winc & !wfull;
assign ren = rinc & !rempty;
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH(WIDTH)
)dual_port_RAM(
.wclk (clk),
.wenc (wen),
.waddr(waddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.wdata(wdata), //数据写入
.rclk (clk),
.renc (ren),
.raddr(raddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.rdata(rdata) //数据输出
);
关于dual port RAM
的verilog:
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
给出完整的同步FIFO
的代码:
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output reg wfull ,
output reg rempty ,
output wire [WIDTH-1:0] rdata
);
parameter ADDR_WIDTH = $clog2(DEPTH);
/**********************addr bin gen*************************/
reg [ADDR_WIDTH:0] waddr;
reg [ADDR_WIDTH:0] raddr;
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
waddr <= 'd0;
end
else if(!wfull && winc)begin
waddr <= waddr + 1'd1;
end
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
raddr <= 'd0;
end
else if(!rempty && rinc)begin
raddr <= raddr + 1'd1;
end
end
/**********************full empty gen*************************/
wire [ADDR_WIDTH : 0] fifo_cnt;
assign fifo_cnt = (waddr[ADDR_WIDTH] == raddr[ADDR_WIDTH]) ? (waddr[ADDR_WIDTH:0] - raddr[ADDR_WIDTH:0]) :
(DEPTH + waddr[ADDR_WIDTH-1:0] - raddr[ADDR_WIDTH-1:0]);
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
wfull <= 'd0;
rempty <= 'd0;
end
else if(fifo_cnt == 'd0)begin
rempty <= 1'd1;
end
else if(fifo_cnt == DEPTH)begin
wfull <= 1'd1;
end
else begin
wfull <= 'd0;
rempty <= 'd0;
end
end
/**********************RAM*************************/
wire wen ;
wire ren ;
wire wren;//high write
assign wen = winc & !wfull;
assign ren = rinc & !rempty;
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH(WIDTH)
)dual_port_RAM(
.wclk (clk),
.wenc (wen),
.waddr(waddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.wdata(wdata), //数据写入
.rclk (clk),
.renc (ren),
.raddr(raddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.rdata(rdata) //数据输出
);
endmodule
1.1.3.3 额外延申异步FIFO
异步FIFO
略微麻烦点!但记住下图之后其实处理起来没那么麻烦!
异步FIFO结构如上图所示:
1.第1部分是双口RAM,用于数据的存储。
2.第2部分是数据写入控制器
3.第3部分是数据读取控制器
4.读指针同步器:使用写时钟的两级触发器采集读指针,输出到数据写入控制器。
5. 写指针同步器:使用读时钟的两级触发器采集写指针,输出到数据读取控制器。
本题解采用的空满判断
的方式是用格雷码的比较
来产生空满信号
。
如上图所示,使用4位格雷码作为深度为8的FIFO的读写指针。将格雷码转换成四位二进制数,使用二进制数低三位作为访问RAM的地址。与同步FIFO类似,当读写指针相等时,得出FIFO为空。
当写指针比读指针多循环RAM一周时,此时读写指针的最高位和次高位都相反,其余位相同,FIFO为满。
首先还是dual port RAM
的verilog:
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
接下来就是异步FIFO
:
module asyn_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input wclk ,
input rclk ,
input wrstn ,
input rrstn ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output wire wfull ,
output wire rempty ,
output wire [WIDTH-1:0] rdata
);
parameter ADDR_WIDTH = $clog2(DEPTH);
/**********************addr bin gen*************************/
reg [ADDR_WIDTH:0] waddr_cnt;
reg [ADDR_WIDTH:0] raddr_cnt;
always @(posedge wclk or negedge wrstn) begin
if(~wrstn) begin
waddr_cnt <= 'd0;
end
else if(!wfull && winc)begin
waddr_cnt <= waddr_cnt + 1'd1;
end
end
always @(posedge rclk or negedge rrstn) begin
if(~rrstn) begin
raddr_cnt <= 'd0;
end
else if(!rempty && rinc)begin
raddr_cnt <= raddr_cnt + 1'd1;
end
end
/**********************addr gray gen*************************/
wire [ADDR_WIDTH:0] waddr_gray;
wire [ADDR_WIDTH:0] raddr_gray;
reg [ADDR_WIDTH:0] wptr;
reg [ADDR_WIDTH:0] rptr;
assign waddr_gray = waddr_cnt ^ (waddr_cnt>>1);
assign raddr_gray = raddr_cnt ^ (raddr_cnt>>1);
always @(posedge wclk or negedge wrstn) begin
if(~wrstn) begin
wptr <= 'd0;
end
else begin
wptr <= waddr_gray;
end
end
always @(posedge rclk or negedge rrstn) begin
if(~rrstn) begin
rptr <= 'd0;
end
else begin
rptr <= raddr_gray;
end
end
/**********************syn addr gray*************************/
reg [ADDR_WIDTH:0] wptr_buff;
reg [ADDR_WIDTH:0] wptr_syn;
reg [ADDR_WIDTH:0] rptr_buff;
reg [ADDR_WIDTH:0] rptr_syn;
always @(posedge wclk or negedge wrstn) begin
if(~wrstn) begin
rptr_buff <= 'd0;
rptr_syn <= 'd0;
end
else begin
rptr_buff <= rptr;
rptr_syn <= rptr_buff;
end
end
always @(posedge rclk or negedge rrstn) begin
if(~rrstn) begin
wptr_buff <= 'd0;
wptr_syn <= 'd0;
end
else begin
wptr_buff <= wptr;
wptr_syn <= wptr_buff;
end
end
/**********************full empty gen*************************/
assign wfull = (wptr == {~rptr_syn[ADDR_WIDTH:ADDR_WIDTH-1],rptr_syn[ADDR_WIDTH-2:0]});
assign rempty = (rptr == wptr_syn);
/**********************RAM*************************/
wire wen ;
wire ren ;
wire wren;//high write
wire [ADDR_WIDTH-1:0] waddr;
wire [ADDR_WIDTH-1:0] raddr;
assign wen = winc & !wfull;
assign ren = rinc & !rempty;
assign waddr = waddr_cnt[ADDR_WIDTH-1:0];
assign raddr = raddr_cnt[ADDR_WIDTH-1:0];
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH(WIDTH)
)dual_port_RAM(
.wclk (wclk),
.wenc (wen),
.waddr(waddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.wdata(wdata), //数据写入
.rclk (rclk),
.renc (ren),
.raddr(raddr[ADDR_WIDTH-1:0]), //深度对2取对数,得到地址的位宽。
.rdata(rdata) //数据输出
);
endmodule
上方注释比较好,且对异步FIFO
做了比较详细的阐述,因此理解难度不大。
1.1.3.4 VX_dp_ram模块讲解
不管是同步FIFO
还是异步FIFO
,都涉及这么三大部分:
1、读计数器、写计数器;
2、基于以上2个计数器判断空满状态(如果是异步,首先转格雷码,随后需要通过2级触发器组成的同步器来跨时钟域同步格雷码计数结果)
3、将基于2得到的空满信号和其他信号共同例化ram模块
所以回到VX_fifo_queue模块
中有VX_dp_ram
也挺好理解的!甚至我们可以大胆猜测dp
就是dual port
。回到该模块的具体代码:
`include "VX_platform.vh"
`TRACING_OFF
module VX_dp_ram #(
parameter DATAW = 1,
parameter SIZE = 1,
parameter WRENW = 1,
parameter OUT_REG = 0,
parameter NO_RWCHECK = 0,
parameter LUTRAM = 0,
parameter INIT_ENABLE = 0,
parameter INIT_FILE = "",
parameter [DATAW-1:0] INIT_VALUE = 0,
parameter ADDRW = `LOG2UP(SIZE)
) (
input wire clk,
input wire read,
input wire write,
input wire [WRENW-1:0] wren,
input wire [ADDRW-1:0] waddr,
input wire [DATAW-1:0] wdata,
input wire [ADDRW-1:0] raddr,
output wire [DATAW-1:0] rdata
);
localparam WSELW = DATAW / WRENW;
`STATIC_ASSERT((WRENW * WSELW == DATAW), ("invalid parameter"))
`define RAM_INITIALIZATION \
if (INIT_ENABLE != 0) begin \
if (INIT_FILE != "") begin \
initial $readmemh(INIT_FILE, ram); \
end else begin \
initial \
for (integer i = 0; i < SIZE; ++i) \
ram[i] = INIT_VALUE; \
end \
end
`UNUSED_VAR (read)
`ifdef SYNTHESIS
if (WRENW > 1) begin
`ifdef QUARTUS
if (LUTRAM != 0) begin
if (OUT_REG != 0) begin
reg [DATAW-1:0] rdata_r;
`USE_FAST_BRAM reg [WRENW-1:0][WSELW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i] <= wdata[i * WSELW +: WSELW];
end
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
`USE_FAST_BRAM reg [WRENW-1:0][WSELW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end
end else begin
if (OUT_REG != 0) begin
reg [DATAW-1:0] rdata_r;
reg [WRENW-1:0][WSELW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i] <= wdata[i * WSELW +: WSELW];
end
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
if (NO_RWCHECK != 0) begin
`NO_RW_RAM_CHECK reg [WRENW-1:0][WSELW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end else begin
reg [WRENW-1:0][WSELW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end
end
end
`else
// default synthesis
if (LUTRAM != 0) begin
`USE_FAST_BRAM reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
if (OUT_REG != 0) begin
reg [DATAW-1:0] rdata_r;
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i * WSELW +: WSELW] <= wdata[i * WSELW +: WSELW];
end
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i * WSELW +: WSELW] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end
end else begin
if (OUT_REG != 0) begin
reg [DATAW-1:0] ram [SIZE-1:0];
reg [DATAW-1:0] rdata_r;
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i * WSELW +: WSELW] <= wdata[i * WSELW +: WSELW];
end
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
if (NO_RWCHECK != 0) begin
`NO_RW_RAM_CHECK reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i * WSELW +: WSELW] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end else begin
reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
for (integer i = 0; i < WRENW; ++i) begin
if (wren[i])
ram[waddr][i * WSELW +: WSELW] <= wdata[i * WSELW +: WSELW];
end
end
end
assign rdata = ram[raddr];
end
end
end
`endif
end else begin
// (WRENW == 1)
if (LUTRAM != 0) begin
`USE_FAST_BRAM reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
if (OUT_REG != 0) begin
reg [DATAW-1:0] rdata_r;
always @(posedge clk) begin
if (write) begin
ram[waddr] <= wdata;
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
always @(posedge clk) begin
if (write) begin
ram[waddr] <= wdata;
end
end
assign rdata = ram[raddr];
end
end else begin
if (OUT_REG != 0) begin
reg [DATAW-1:0] ram [SIZE-1:0];
reg [DATAW-1:0] rdata_r;
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
ram[waddr] <= wdata;
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
if (NO_RWCHECK != 0) begin
`NO_RW_RAM_CHECK reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
ram[waddr] <= wdata;
end
end
assign rdata = ram[raddr];
end else begin
reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
always @(posedge clk) begin
if (write) begin
ram[waddr] <= wdata;
end
end
assign rdata = ram[raddr];
end
end
end
end
`else
// RAM emulation
reg [DATAW-1:0] ram [SIZE-1:0];
`RAM_INITIALIZATION
wire [DATAW-1:0] ram_n;
for (genvar i = 0; i < WRENW; ++i) begin
assign ram_n[i * WSELW +: WSELW] = ((WRENW == 1) | wren[i]) ? wdata[i * WSELW +: WSELW] : ram[waddr][i * WSELW +: WSELW];
end
if (OUT_REG != 0) begin
reg [DATAW-1:0] rdata_r;
always @(posedge clk) begin
if (write) begin
ram[waddr] <= ram_n;
end
if (read) begin
rdata_r <= ram[raddr];
end
end
assign rdata = rdata_r;
end else begin
reg [DATAW-1:0] prev_data;
reg [ADDRW-1:0] prev_waddr;
reg prev_write;
always @(posedge clk) begin
if (write) begin
ram[waddr] <= ram_n;
end
prev_write <= (| wren);
prev_data <= ram[waddr];
prev_waddr <= waddr;
end
if (LUTRAM || !NO_RWCHECK) begin
`UNUSED_VAR (prev_write)
`UNUSED_VAR (prev_data)
`UNUSED_VAR (prev_waddr)
assign rdata = ram[raddr];
end else begin
assign rdata = (prev_write && (prev_waddr == raddr)) ? prev_data : ram[raddr];
end
end
`endif
endmodule
`TRACING_ON
这里面出现了很多条件分支:
ifdef SYNTHESIS:这是一个条件编译指令,它指示编译器在进行综合时执行相应的代码路径。在这里,它处理了不同的综合工具(如 Quartus)和一些参数的情况。
if (WRENW > 1):检查写使能端口 WRENW 的宽度是否大于 1。如果是,进入下一级条件。
ifdef QUARTUS:这是对 Quartus 综合工具的特定条件检查。 // 这是基于Vortex GPGPU仿真平台的考量
if (LUTRAM != 0):检查是否使用 LUT RAM。 // 熟悉vivado中会出现LUTRAM,但是不清楚怎么控制使用还是不使用LUTRAM资源
if (OUT_REG != 0):检查是否需要输出寄存器。
else 分支(RAM emulation):如果编译器未定义 SYNTHESIS,则执行 RAM 的仿真模式。
基本功能可以参考我额外提到的同步FIFO
和异步FIFO
中的dual port ram
的代码。
1.1.4 为什么又出现一个VX_elastic_buffer模块?
还想再问一个问题,为什么VX_elastic_buffer
模块里面可以实例化VX_elastc_buffer
?以前我没接触过这个操作!
在Verilog代码中,可以在模块内部实例化同一类型的模块。这种方式被称为递归实例化
或者自实例化(self-instantiation)
。
如果 SIZE == 0,它直接将输入信号连接到输出信号,没有任何缓冲。
如果 SIZE == 1,它实例化了 VX_pipe_buffer。
如果 SIZE == 2,它实例化了 VX_skid_buffer。
对于其他情况,它实现了一个深度为 SIZE 的 FIFO 队列 (VX_fifo_queue),并在 FIFO 的基础上实现了一个更复杂的弹性缓冲器。
1.2 memory request buffering
// Memory request buffering
wire mem_req_valid_s;
wire [`CS_MEM_ADDR_WIDTH-1:0] mem_req_addr_s;
wire mem_req_rw_s;
wire [LINE_SIZE-1:0] mem_req_byteen_s;
wire [`CS_LINE_WIDTH-1:0] mem_req_data_s;
wire [MEM_TAG_WIDTH-1:0] mem_req_tag_s;
wire mem_req_ready_s;
VX_elastic_buffer #(
.DATAW (1 + LINE_SIZE + `CS_MEM_ADDR_WIDTH + `CS_LINE_WIDTH + MEM_TAG_WIDTH),
.SIZE (MEM_REQ_BUF_ENABLE ? `TO_OUT_BUF_SIZE(MEM_OUT_BUF) : 0),
.OUT_REG (`TO_OUT_BUF_REG(MEM_OUT_BUF))
) mem_req_buf (
.clk (clk),
.reset (reset),
.valid_in (mem_req_valid_s),
.ready_in (mem_req_ready_s),
.data_in ({mem_req_rw_s, mem_req_byteen_s, mem_req_addr_s, mem_req_data_s, mem_req_tag_s}),
.data_out ({mem_bus_if.req_data.rw, mem_bus_if.req_data.byteen, mem_bus_if.req_data.addr, mem_bus_if.req_data.data, mem_bus_if.req_data.tag}),
.valid_out (mem_bus_if.req_valid),
.ready_out (mem_bus_if.req_ready)
);
assign mem_bus_if.req_data.atype = '0;
该模块的结构和1.1
类似,参考1.1
。
1.3 memory response buffering
// Memory response buffering
wire mem_rsp_valid_s;
wire [`CS_LINE_WIDTH-1:0] mem_rsp_data_s;
wire [MEM_TAG_WIDTH-1:0] mem_rsp_tag_s;
wire mem_rsp_ready_s;
VX_elastic_buffer #(
.DATAW (MEM_TAG_WIDTH + `CS_LINE_WIDTH),
.SIZE (MRSQ_SIZE),
.OUT_REG (MRSQ_SIZE > 2)
) mem_rsp_queue (
.clk (clk),
.reset (reset),
.valid_in (mem_bus_if.rsp_valid),
.ready_in (mem_bus_if.rsp_ready),
.data_in ({mem_bus_if.rsp_data.tag, mem_bus_if.rsp_data.data}),
.data_out ({mem_rsp_tag_s, mem_rsp_data_s}),
.valid_out (mem_rsp_valid_s),
.ready_out (mem_rsp_ready_s)
);
该模块的结构和1.1
类似,参考1.1
。
1.4 cache initialize
wire [`CS_LINE_SEL_BITS-1:0] init_line_sel;
wire init_enable;
// this reset relay is required to sync with bank initialization
`RESET_RELAY (init_reset, reset);
VX_cache_init #(
.CACHE_SIZE (CACHE_SIZE),
.LINE_SIZE (LINE_SIZE),
.NUM_BANKS (NUM_BANKS),
.NUM_WAYS (NUM_WAYS)
) cache_init (
.clk (clk),
.reset (init_reset),
.addr_out (init_line_sel),
.valid_out (init_enable)
);
1.4.1 VX_cache_init模块解读
VX_cache_int
模块见于w/rtl/cache/VX_cache_init.sv
,其代码如下:
`include "VX_cache_define.vh"
module VX_cache_init #(
// Size of cache in bytes
parameter CACHE_SIZE = 1024,
// Size of line inside a bank in bytes
parameter LINE_SIZE = 16,
// Number of banks
parameter NUM_BANKS = 1,
// Number of associative ways
parameter NUM_WAYS = 1
) (
input wire clk,
input wire reset,
output wire [`CS_LINE_SEL_BITS-1:0] addr_out,
output wire valid_out
);
reg enabled;
reg [`CS_LINE_SEL_BITS-1:0] line_ctr;
always @(posedge clk) begin
if (reset) begin
enabled <= 1;
line_ctr <= '0;
end else begin
if (enabled) begin
if (line_ctr == ((2 ** `CS_LINE_SEL_BITS)-1)) begin
enabled <= 0;
end
line_ctr <= line_ctr + `CS_LINE_SEL_BITS'(1);
end
end
end
assign addr_out = line_ctr;
assign valid_out = enabled;
endmodule
初始化模块的功能还是直观。时钟上升沿触发,当 reset 信号为高时,初始化 enabled 和 line_ctr。如果 enabled 为高,每个时钟周期增加 line_ctr 的值,直到达到最大值 ((2 ** CS_LINE_SEL_BITS)-1`)。当 line_ctr 达到最大值时,将 enabled 置为低,表示初始化完成。
值得一提的是CS_LINE_SEL_BITS(1)
,其中(1)
的使用方式通常表示一个强制类型转换。举个例子,假设CS_LINE_SEL_BITS
的值是4
,那么2 ** CS_LINE_SEL_BITS-1
将被解析为2 ** 4 - 1
,即15
。在这种情况下,CS_LINE_SEL_BITS'(1)
实际上是在1
的左边加上了4'b
,这表示将1
转换为一个 4
位的二进制数。
总结
本文接着对VX_cache.sv
展开介绍,并对涉及的模块做出了解释。