【芯片前端】延迟一拍出数的握手型ram结构的一次探索_尼德兰的喵的博客-CSDN博客
上一次尝试对握手型的ram控制器进行了一次尝试,最后尝试给出的结构如下图所示:
之后呢我们会再拉出来这个结构来讨论他的优缺点,这篇文章呢是写一个更普遍更简单的结构,开始的起因是上一篇文章中的讨论:
当时我暂时把第三种思路放在了一边而研究了下第二种思路因此搞了上面的结构,这次呢来根据第三种思路规划代码。
双端口ram代码和之前一样:
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk ,
input wenc ,
input [$clog2(DEPTH)-1:0] waddr ,
input [WIDTH-1:0] wdata ,
input rclk ,
input renc ,
input [$clog2(DEPTH)-1:0] raddr ,
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
握手型双端口ram的接口以及写通路也和之前一样:
module hand_dp_ram #(
parameter DEPTH = 16,
parameter WIDTH = 8
)(
input clk,
input rst_n,
input wvalid,
output wready,
input [$clog2(DEPTH) -1:0]waddr,
input [WIDTH -1:0]wdata,
input arvalid,
output arready,
input [$clog2(DEPTH) -1:0]araddr,
output rvalid,
input rready,
output [WIDTH -1:0]rdata
);
//***********************************************
// ram inst
//***********************************************
wire ram_wenc;
wire [$clog2(DEPTH) -1:0]ram_waddr;
wire [WIDTH -1:0]ram_wdata;
wire ram_renc;
wire [$clog2(DEPTH) -1:0]ram_raddr;
wire [WIDTH -1:0]ram_rdata;
dual_port_RAM #(
.DEPTH(DEPTH),
.WIDTH(WIDTH))
u_ram(
.wclk(clk),
.wenc(ram_wenc),
.waddr(ram_waddr),
.wdata(ram_wdata),
.rclk(clk),
.renc(ram_renc),
.raddr(ram_raddr),
.rdata(ram_rdata)
);
//***********************************************
// write path
//***********************************************
assign ram_wenc = wvalid && wready;
assign ram_waddr = waddr;
assign ram_wdata = wdata;
endmodule
再来确定一下我们的思路:当数据没有被取出时,连续发起同一地址的renc,直到该地址的数据被读取。而有一个关键点,在renc的下一拍我们才能知道读出的数据是否取走,才能确定是要读取新的地址(假设又有新的读请求到来了)还是读取旧的地址。因此必须要有两个寄存器分别寄存旧的读使能和读地址。
//***********************************************
// read path
//***********************************************
reg arvalid_ff;
reg[$clog2(DEPTH) -1:0]araddr_ff;
arvalid_ff应当有两种情况需要跳变:第一种,当拍arvalid和arready握手向ram发出来读请求,因为不确定下一拍ram_data能否被取用,所以此时arvalid_ff应当在下一拍跳变为1表示当拍发起过ram读请求;第二种,rready置起为1时,这表示上一拍读的ram_data必然在此拍被取用,此时代表着上一拍发起读请求的标志arvalid_ff也就可以跳变为0了,当然这种情况的优先级是要低于场景一的。
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
arvalid_ff <= 1'b0;
else if(arvalid && arready)begin
arvalid_ff <= 1'b1;
end
else if(rready)begin
arvalid_ff <= 1'b0;
end
end
相对而言,araddr_ff不会参与控制逻辑,那么逻辑就简单了很多:
always @(posedge clk)begin
if(arvalid && arready)begin
araddr_ff <= araddr;
end
end
而后呢再做一个renc的打拍来标记数据读出的使能(给rvalid用):
reg ram_renc_ff;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
ram_renc_ff <= 1'b0;
else begin
ram_renc_ff <= ram_renc;
end
end
万事俱备,接下来做关键的端口逻辑!
先看arready吧:
assign arready = !arvalid_ff || rready;
如果rready为1则代表着这一笔读请求之前的请求在当拍必然可以取走,那么下一拍即使ready跳变为0,至少arvalid_ff/araddr_ff是可以将当拍的读取信息锁存并且持续读取当前地址的。!arvalid_ff的情况也一样,哪怕当前ready已经为0,arvalid_ff/araddr_ff可以寄存当前拍的信息。
之后是给ram的信号:
//to ram
assign ram_renc = arvalid || (arvalid_ff && rready == 1'b0);
assign ram_raddr = (rready == 1'b0 && arvalid_ff) ? araddr_ff : araddr;
理论上说,ram_renc应该用(arvalid && arready) || (arvalid_ff && rready == 1'b0),但是通过观察逻辑可以发现,当后面的逻辑不成立时,arready的值必然为1,所以前面的arready删除即可。ram_renc发起读的场景也是两个:第一种,ar接口握手成功,发起当前ar接口对应的读;第二种,前一拍的有请求(arvalid_ff == 1'b1)且这一拍读出的值没有被接收(rready == 1'b0)。进而,ram_addr的选择也就顺理成章。
最后是输出的信号:
//to r-port
assign rvalid = ram_renc_ff;
assign rdata = ram_rdata;
这个没啥好解释的。
在测试波形中,我在initial块中把ram的每一个地址都赋值为地址号,然后进行顺序的读取和随机的反压,rhand_en = rvalid && rready:
目测功能是没有问题的。
在之后的博文中我想要分析下这两种结构的优劣和时序优化的方向,今天太晚了先睡了。