目录
在看这篇文章之前,希望各位读者先看一下上一篇文章,sram_core的实现,否则的话会有很多理解不了的地方
https://blog.csdn.net/qq_44055255/article/details/126505243
仔细看一下ahb协议也是很有必要的,代码也包含在了这个链接里
https://download.csdn.net/download/qq_44055255/86479698
下面正式开始。
一、ahb_sramc_if的总框架
思路清晰,先说功能:
- 能够处理错误情况(在sram_core的文章中有提到);
- 能够按照htrans做出相应的反应;
- 在该插入等待周期(wait state)的时候插入等待周期;
- 根据ahb协议的时序向sram_core读写控制和使能信号;
- 向sram_core提供私有地址(private_addr);
- 根据ahb协议的时序给出hreadyout和应答信号hresp;
对于某些功能,后面会展开说明。
动手开敲前的构思
在sram_core的文章中提到过,如果总线地址没有根据位宽对齐的话,应视为错误处理,在这种情况下ahb_sramc_if应当忽略此次操作,并驱动hreadyout和hresp按照ahb协议的时序给出错误应答。
在ahb协议手册中有说明如果htrans=IDLE或BUSY时,从机应当忽略此次操作请求,并给出OKAY应答,ahb_sramc_if要能够处理这种情况。
现在要考虑一下这种情况:
如上图所示,总线在T0周期发起了一个对地址A的写操作请求,随后在T1周期紧跟了一个对地址B的读操作请求。我们分析一下这种情况下会发生什么:从机在T1上升沿采样到地址A和写请求,然后在T2上升沿采样对地址A的写数据DATA(A),并将DATA(A)写入到sram中,但与此同时从机也采样到了对地址B的写操作请求,在sram_core的文章中我们就已经分析过ahb的读写时序,从机要在采样到读请求和地址后要立刻开始提供读数据,这时我们发现在T2上升沿时刻同时发生了对sram的写和读。经过以上分析我们发现,ahb总线的流水线的工作方式存在一个不得不处理的情况:每次写请求后紧跟一个读请求都会导致同时对memory进行读和写。我想到了两个处理方式:第一个是使用真-双端口sram,能够同时进行读和写,但仔细想一下如果地址A=地址B的话,那就会发生同时对一个地址进行读和写,这不还是一个BUG;第二个就是插入wait state延长读操作周期(如上图所示)。最后决定采用第二种处理方式。
在后面的代码分析可以发现,正是这个插入wait state使得设计难度提高了几个等级,这不仅是判断何时插入这么简单,还要考虑wait state时怎样控制读写信号,wait state跟BUSY和IDLE传输撞在一起了要怎么处理,还有地址和应答信号,几乎ahb_sramc_if每一个功能的实现都要考虑到wait state的影响。
ahb_sramc_if的框图和信号描述
下面给出ahb_sramc_if的框图和信号描述,ahb_sramc_if的逻辑比较复杂,其框图不好画,所以我干脆直接摆烂,随便画了一个框图:
signal | I/O | width | descriptions |
hrstn | input | 1 | 总线复位信号,同步复位,低有效 |
hclk | input | 1 | 总线时钟 |
hsel_sram | input | 1 | sramc选中信号 |
hready | input | 1 | 总线可用信号,从机只能在hready为1时从总线采样 |
hwrite | input | 1 | 读写控制信号1写0读 |
htrans | input | 2 | 传输类型,详见AHB协议 |
hsize | input | 3 | 传输位宽,详见AHB协议 |
haddr | input | AHB_ADDR_WIDTH | 总线地址 |
hsel_sram_out | output | 1 | 传给sram_core的使能信号 |
write | output | 1 | 传给sram_core的写控制信号 |
read | output | 1 | 传给sram_core的读控制信号 |
hreadyout | output | 1 | 从机传输是否完成 |
hresp | output | 1 | 从机传输状态应答信号 |
private_addr | output | AHB_ADDR_WIDTH | sramc的私有地址 |
private_addr_reg1 | output | AHB_ADDR_WIDTH | 寄存一拍的private_addr |
二、Verilog代码和代码分析
ahb_sramc_if.v:
`timescale 1 ns/1 ns
module ahb_sramc_if
#(parameter AHB_ADDR_WIDTH = 32, AHB_DATA_WIDTH = 32, PRIVATE_ADDR_WIDTH = 32,
SLAVE_BASE_ADDR = 32'h00000000, IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11)
(
hclk,
hrstn,
hready,
hsel_sram,
hwrite,
hsize,
htrans,
haddr,
hwdata,
hreadyout,
hresp,
hsel_sram_out,
write,
read,
private_addr,
private_addr_reg1,
hwdata_out
);
input hclk, hrstn, hready, hsel_sram, hwrite;
input[2:0] hsize;
input[1:0] htrans;
input[AHB_ADDR_WIDTH-1:0] haddr;
input[AHB_DATA_WIDTH-1:0] hwdata;
output hreadyout;
output hresp;
output reg hsel_sram_out;
output write;
output read;
output[PRIVATE_ADDR_WIDTH-1:0] private_addr;
output reg[PRIVATE_ADDR_WIDTH-1:0] private_addr_reg1;//对于写操作,写入是在地址周期的下一周期发生,另外读出的数据需要维持一个周期,所以地址要寄存一拍,相应的,下面的写操作相应的控制信号和写数据也要寄存一拍
output [AHB_DATA_WIDTH-1:0] hwdata_out;
wire[PRIVATE_ADDR_WIDTH-1:0] offset;
reg lasttrans_unvalid;
wire presenttrans_unvalid;
wire if_wait;
reg if_wait_reg1;
reg wait_state;
reg if_error;
reg if_error_reg1;
reg write_reg1;
reg write_in_wait_reg1;
reg hsel_sram_reg1;
wire hwrite_in_wait;
wire[AHB_ADDR_WIDTH-1:0] haddr_in_wait;
assign hwdata_out = hwdata;
assign hwrite_in_wait = wait_state ? hwrite_in_wait : hwrite;
assign presenttrans_unvalid = (htrans==IDLE || htrans==BUSY);
assign if_wait = hsel_sram ? (!lasttrans_unvalid && !presenttrans_unvalid && !hwrite && write_reg1) : 0;
always@(posedge hclk) begin
if(!hrstn || !hsel_sram) begin
lasttrans_unvalid <= 0;
if_wait_reg1 <= 0;
if_error_reg1 <= 0;
write_reg1 <= 0;
write_in_wait_reg1 <= 0;
hsel_sram_reg1 <= 0;
end
else begin
lasttrans_unvalid <= presenttrans_unvalid;
if_wait_reg1 <= if_wait;
if_error_reg1 <= if_error;
write_reg1 <= hwrite;
write_in_wait_reg1 <= hwrite_in_wait;
hsel_sram_reg1 <= hsel_sram;
end
end
always@(*) begin
if(if_wait || !hreadyout)
wait_state = 1;
else
wait_state = 0;
end
/***********************读写信号控制逻辑**********************/
assign read = (if_wait || presenttrans_unvalid) ? 0 : ~hwrite_in_wait; //如果在本周期确定要请求等待,则忽略本周期的读操作
assign write = lasttrans_unvalid ? 0 : write_in_wait_reg1;
/********************地址处理************************/
assign haddr_in_wait = wait_state ? haddr_in_wait : haddr;
assign offset = haddr_in_wait - SLAVE_BASE_ADDR;
assign private_addr = wait_state ? private_addr_reg1 : offset;
always@(posedge hclk) begin
if(hsel_sram && hready)
private_addr_reg1 <= offset;
end
/********************hreadyout和hresp信号的控制逻辑**********************/
always@(*) begin
if(hsel_sram) begin
case(hsize)
3'b000: if_error = 0;
3'b001: if_error = haddr[0];
3'b010: if_error = haddr[1] || haddr[0];
default: if_error = 1;
endcase
end
else
if_error = 0;
end
assign hreadyout = !(if_wait_reg1 || if_error);
assign hresp = (if_error || if_error_reg1);
/*********************SRAM CORE使能信号控制逻辑*******************/
always@(*) begin
if(if_error_reg1 || !(hsel_sram && hready) ||
({hsel_sram, hsel_sram_reg1}==2'b01 && !write) ||
{hsel_sram, hsel_sram_reg1}==2'b00)
hsel_sram_out = 0;
else
hsel_sram_out = 1;
end
endmodule
assign hwrite_in_wait = wait_state ? hwrite_in_wait : hwrite;
assign presenttrans_unvalid = (htrans==IDLE || htrans==BUSY);
assign if_wait = hsel_sram ? (!lasttrans_unvalid && !presenttrans_unvalid && !hwrite && write_reg1) : 0;
always@(posedge hclk) begin
if(!hrstn || !(hsel_sram && hready))
lasttrans_unvalid <= 0;
else
lasttrans_unvalid <= presenttrans_unvalid;
end
always@(*) begin
if(if_wait || !hreadyout)
wait_state = 1;
else
wait_state = 0;
end
先来看看这一部分组合逻辑,这些内部信号后面都用的到。lasttrans_unvalid和presenttrans_unvalid用于记录上次传输和本次传输是否有效,前面说过当htrans=IDLE或BUSY时要忽略此次传输,即为无效传输,这两个信号的控制逻辑简单直白,无需多讲。
if_wait是等待周期的触发信号,当if_wait=1是就会出发ahb_sramc_if产生wait state。if_wait的控制逻辑是:如果本周期是读操作,上周期是写操作(write_reg1是hwrite寄存一拍)并且本次和上次传输都不是无效传输,if_wait就置1。
wait_state用于判断是否还处于等待周期,if_wait只是触发等待周期的信号,不足以判断是否还在等待周期,观察上面的插入wait state的时序图可以发现,if_wait和hready_out(hready可暂时理解为就是hreadyout)结合就能判断是否还处于等待周期。
hwrite_in_wait作用就是锁存住刚进入等待周期的hwrite,其他情况下hwrite_in_wait=hwrite。从时序图可以看出,进入等待周期后hwrite会发生改变,所以需要锁存。同样也需要锁存刚进入等待周期时的地址信号。
还需要注意一点就是当sramc没被选中(ahb协议中有说明,只能在hready为高时对地址和控制信号进行采样,所以hready为0时也视为没被选中)时也会对lasttrans_unvalid复位。这是因为每次选中sramc时,我们不希望此次选中的第一次传输受上次选中的最后一次传输的影响,所以不只是lasttrans_unvalid,所有寄存控制信号的信号在sramc没被选中时都要被复位到默认值。
/***********************读写信号控制逻辑**********************/
always@(posedge hclk) begin //写操作相关控制信号需要寄存一拍,原因如上述
if(!hrstn || !(hsel_sram && hready)) begin
write_reg1 <= 0;
write_in_wait_reg1 <= 0;
end
else begin
write_reg1 <= hwrite;
write_in_wait_reg1 <= hwrite_in_wait;
end
end
assign read = (if_wait || presenttrans_unvalid) ? 0 : ~hwrite_in_wait; //如果在本周期确定要请求等待,则忽略本周期的读操作
assign write = lasttrans_unvalid ? 0 : write_in_wait_reg1;
接下来是传给sram_core的读写信号的控制逻辑,有几点需要注意:如果在当前周期触发等待周期,直接忽略当前周期的读操作,这是因为首先要完成上个周期写操作(这里强调一下只有写请求后紧跟一个读请求才会触发等待周期);如果上个周期的传输是无效传输,则直接忽略上个周期写操作(我们已经多次分析过,对于ahb协议,写操作真正发生在从机采样到写请求和地址的下一个上升沿)。
/********************地址处理************************/
assign haddr_in_wait = wait_state ? haddr_in_wait : haddr;
assign offset = haddr_in_wait - SLAVE_BASE_ADDR;
assign private_addr = wait_state ? private_addr_reg1 : offset;
always@(posedge hclk) begin
if(hsel_sram && hready)
private_addr_reg1 <= offset;
end
然后要对haddr进行处理,了解过微机原理就会知道,总线寻址方式一定是基地址加上一个偏移地址,这个偏移地址可以理解为从机的私有地址,即从机在其内部寻址时使用的地址。在sram_core的文章中分析过,sram_core需要当前地址和寄存一拍的地址,所以这里将私有地址寄存一拍。从这块逻辑的第三个assign语句,我们发现如果处于等待周期,传给sram_core的地址就只有寄存一拍的私有地址,这是因为等待周期将读操作的发生向后推迟了一个时钟周期(在这里说明一下,对于我们的sramc,其等待周期只会有两个时钟周期,不会多也不会少,第一个时钟周期触发等待周期并完成上个时钟周期的写操作,第二个时钟周期完成被推迟的读操作),并且在等待周期期间总线上的地址和控制信号是会变的。这里也对刚进入等待周期的地址进行了锁存。
/********************hreadyout和hresp信号的控制逻辑**********************/
always@(*) begin
if(hsel_sram) begin
case(hsize)
3'b000: if_error = 0;
3'b001: if_error = haddr[0];
3'b010: if_error = haddr[1] || haddr[0];
default: if_error = 1;
endcase
end
else
if_error = 0;
end
always@(posedge hclk) begin
if(!hrstn || !hsel_sram) begin
if_wait_reg1 <= 0;
if_error_reg1 <= 0;
end
else begin
if_wait_reg1 <= if_wait;
if_error_reg1 <= if_error;
end
end
assign hreadyout = !(if_wait_reg1 || if_error);
assign hresp = (if_error || if_error_reg1);
还有hreadyout和hresp这两个应答信号的控制逻辑。对于hresp,从ahb协议手册里可以看出,只要不发生错误,hresp都为0,即OKAY状态,二hreadyout需要在触发等待周期之后的上升沿拉低。在sram_core的文章中说过,如果出现传输地址没有按照传输位宽对齐的情况,就视为错误传输,检测到这种情况就会将if_error置1,触发错误周期。if_error的控制逻辑很简单,如果你真的理解“地址按照位宽对齐”这句话的话,代码一看便懂。ahb协议手册给出了错误情况的应答时序:
可以看出对于hreadyout,在触发错误周期时拉低,然后在紧接着的上升沿拉高,另外在等待周期时,hreadyout在触发等待周期后的上升沿时拉低,所以if_wait_reg1(if_wait寄存一拍的信号)和if_error就能正确控制hreadyout(在这里说明一下if_wait被置1只会持续一个时钟周期,因为对于两个连续的传输请求只存在读读,写写,读写,写读这四种组合,而只有写读会触发等待周期)。对于hresp,需要在触发错误周期时拉高,并在下一个时钟周期内维持住,所以将if_error寄存一拍,与if_error结合就能正确控制hresp。
/*********************SRAM CORE使能信号控制逻辑*******************/
always@(posedge hclk) begin //写操作相关控制信号需要寄存一拍,原因如上述
if(!hrstn || !(hsel_sram && hready)) begin
hsel_sram_reg1 <= 0;
end
else begin
hsel_sram_reg1 <= hsel_sram;
end
end
always@(*) begin
if(if_error_reg1 || !(hsel_sram && hready) ||
({hsel_sram, hsel_sram_reg1}==2'b01 && !write) ||
{hsel_sram, hsel_sram_reg1}==2'b00)
hsel_sram_out = 0;
else
hsel_sram_out = 1;
end
最后是传给sram_core的使能信号的控制逻辑,只有一点需要说的:在hsel_sram_out置0的判断条件里有一条 ({hsel_sram, hsel_sram_reg1}==2'b01 && !write)。考虑一下一次选中的最后一次传输,如果最后一次传输是个读操作,读出数据在当前时钟周期就给出了,所以在当前时钟周期就可以停止对sram_core的使能,而如果最后一次传输是个写操作,那就需要延迟一个时钟周期再停止对sram_core的使能,因为写操作的真正发生是在从机采样到控制信号的下一个上升沿。
三、sramc的仿真和验证
sramc.v:
`timescale 1 ns/1 ns
module sramc
#(parameter AHB_ADDR_WIDTH = 32, AHB_DATA_WIDTH = 32, PRIVATE_ADDR_WIDTH = 32,
SLAVE_BASE_ADDR = 32'h00000000, IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11)
(
hclk,
hrstn,
hready,
hsel_sram,
hwrite,
hsize,
htrans,
haddr,
hwdata,
hreadyout,
hresp,
hrdata
);
input hclk, hrstn, hready, hsel_sram, hwrite;
input[2:0] hsize;
input[1:0] htrans;
input[AHB_ADDR_WIDTH-1:0] haddr;
input[AHB_DATA_WIDTH-1:0] hwdata;
output hreadyout;
output hresp;
output[AHB_DATA_WIDTH-1:0] hrdata;
wire hsel_sram_out;
wire write, read;
wire[PRIVATE_ADDR_WIDTH-1:0] private_addr;
wire[PRIVATE_ADDR_WIDTH-1:0] private_addr_reg1;
wire[AHB_DATA_WIDTH-1:0] hwdata_in;
ahb_sramc_if ahb_sramc_if1 (
.hclk(hclk),
.hrstn(hrstn),
.hready(hready),
.hsel_sram(hsel_sram),
.hwrite(hwrite),
.hsize(hsize),
.htrans(htrans),
.haddr(haddr),
.hwdata(hwdata),
.hreadyout(hreadyout),
.hresp(hresp),
.hsel_sram_out(hsel_sram_out),
.write(write),
.read(read),
.private_addr(private_addr),
.private_addr_reg1(private_addr_reg1),
.hwdata_out(hwdata_in)
);
sram_core sram_core1 (
.clk(hclk),
.hsel_sram(hsel_sram_out),
.write(write),
.read(read),
.hsize(hsize),
.private_addr(private_addr),
.private_addr_reg1(private_addr_reg1),
.hwdata(hwdata),
.hrdata(hrdata)
);
endmodule
sramc就是作为一个顶层模块例化ahb_sram_if和sram_core。
sramc_test.v:
`timescale 1 ns/1 ns
module sramc_test();
parameter AHB_ADDR_WIDTH = 32, AHB_DATA_WIDTH = 32, PRIVATE_ADDR_WIDTH = 32, SLAVE_BASE_ADDR = 32'h00000000, IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
reg hclk, hrstn, hready, hsel_sram, hwrite;
reg[2:0] hsize;
reg[1:0] htrans;
reg[AHB_ADDR_WIDTH-1:0] haddr;
reg[AHB_DATA_WIDTH-1:0] hwdata;
wire hreadyout;
wire hresp;
wire[AHB_DATA_WIDTH-1:0] hrdata;
integer i;
sramc sramc1 (
.hclk(hclk),
.hrstn(hrstn),
.hready(hready),
.hsel_sram(hsel_sram),
.hwrite(hwrite),
.hsize(hsize),
.htrans(htrans),
.haddr(haddr),
.hwdata(hwdata),
.hreadyout(hreadyout),
.hresp(hresp),
.hrdata(hrdata)
);
always #5 hclk = ~hclk;
initial begin
hclk=1; hrstn=0; hready=1; hsel_sram=1; hwrite=1; hsize=3'd0; htrans=NONSEQ;
haddr=32'h0000_0000; hwdata=32'h0000_0000;
#11 hrstn=1; hsel_sram=1; hwrite=1; hsize=3'd1; htrans=NONSEQ;
haddr=32'h0000_0000; hwdata=32'h0000_0000;
for(i=0; i<3; i=i+1) begin
#10 haddr = haddr + 2; hwdata = hwdata + 1;
end
#10 hsel_sram=1; hwrite=0; hsize=3'd1; htrans=NONSEQ;
haddr=32'h0000_0000;
#10 haddr=32'h0000_0002;
#10 haddr=32'h0000_0002;
#10 haddr=32'h0000_0004;
#10 haddr=32'h0000_0006;
#20 hsel_sram=1; hwrite=1; hsize=3'd1; htrans=IDLE;
haddr=32'h0000_0000; hwdata=32'h0000_000f;
#10 hsel_sram=1; hwrite=0; hsize=3'd1; htrans=NONSEQ;
haddr=32'h0000_0000;
#100 $finish;
end
initial begin
$fsdbDumpfile("sramc_test.fsdb");
$fsdbDumpvars(0);
end
endmodule
仿真代码就是从地址0开始,先连续进行四次写操作,然后紧跟四次读操作,最后再进行一次IDLE的写操作紧跟一个有效的读操作。还是那句话,这一次仿真肯定不够,我进行过很多仿真,但本人验证不精,就不讲验证代码了。
下面是仿真结果:
四、结语
从读ahb协议手册开始,然后构思,敲代码,仿真调试,到最后写文章,前后用了一个星期,我终于算是完成了sram控制器的全部工作。以前我写代码都是写完就算完事了,不会留下任何文档,我以前的那些宝贵调试经验和构思也都随时间消逝,just like tears, in the rain,这对我来说是个巨大的损失,这也是我一直逃避的问题。而这次,我终于切实的做出了改变,再加上这是我写的第一篇博客,这篇文章对我来说便更加具有里程碑意义。不奢求各位打赏,如果能高抬贵手点个赞就是对我莫大的鼓励。
最后祝我自己能怒火重燃,不断突破自己的舒适区,然后摆脱停滞再次踏上征程。
正所谓生命不息,战斗不止,也祝诸君武运昌隆!