片上存贮器综合综述(网络文摘)

      在我们进行设计当中,经常会使用到各种各样的存贮器来存贮信息。当我们保存的状态很少的时候,使用寄存器或者锁存器就足够了,而当我们需要存贮大量信息,并且这些信息的存取不是很随机的时候(如果完全随机,那就变成一个N*1的存贮器了,实际成了寄存器列),我们就可以考虑使用存贮器了。对于大量的存贮,我们往往可以得到面积、功耗、速度等方面的好处。特别是FPGA当中,因为可用的DFF较少(顺带提一下Xilinx的FPGA每个CLB包含4个Slice,每个Slice才包含2个Logic Cell,每个Logic Cell包括一个4输入查找表,1个DFF,1/4 tri buffer,1/4 SOP Chain,1/4 Shift Chain,一个Logic Cell算100门左右,所以100万门FPGA可提供1万个DFF),所以应该多利用LUT构建存贮器(一个LUT提供12bit存贮)或者使用Block RAM。而对于ASIC,DFF/SDFF面积更大了,并且大量的互连会引入极大的延迟和占用很多面积,并使得P&R工具难于达到Timing Closure,不如使用Memory Compiler生成存贮器划算,当然,如果数量本来比较小(例如单端口1Kbits以下,双端口128bits以下),那么就没有必要使用Memory Compiler了,因为生成的存贮器往往需要加一圈dummy cell,加block halo,面积反而可能增加,并且timing不像使用dff或者latch可以随便增强驱动能力,也可能更坏。

总的来说,存贮器无非是能够保存状态信息的存贮单元以及对这些存贮单元进行存取的地址译码逻辑。对于用DFF之类搭建的存贮器来说(也就是综合器对一般行为描述综合出的结果,本文主要讨论用DFF或者LATCH搭建的存贮器,而不是使用Memory Compiler生成或者使用FPGA片上已有的存贮器,因为后者基本没有什么选择的余地了),地址译码实际就是一个多路选择器。存贮器的输出实际就是多路选择器(multiplexer)从多个存贮字当中选择一个字输出到数据线;而存贮器的写入则是利用地址译码器(demultiplexer)使能某一个存贮单元的时钟使能信号(cen),从而将数据写入。从前面的描述可以看到,输入数据线必须接入每一个存贮字,扇出是非常大的;而每个存贮字都要输出到数据线,这个多路选择器的扇入也是很大的;而写入的地址译码器则是n-->2^n译码,面积的开销也是很大的。这里必须注意到,如果虽然读操作用的mux和写操作用的decoder面积大致差不多,但是读取的mux必须重复字宽那么多次,造成面积很大的开销。这里存在一种解决方案就是利用三态输出,在使用三态输出的时候,存贮器中所有的字都通过三态输出接到输出数据总线,从而读操作也是一个n-->2^n的decoder,在单端口的时候就可以和写操作共同使用一个decoder,当然这里三态会有一些问题,不过注意避免就好了,面积和timing方面的收益更大。

存贮器虽然结构看上去很简单,但是也可以有很多变化,可不是那么简单的哦。为了介绍方便起见,先说一下几个一般存贮器都有的信号:clk, wen(写使能,无效则为读操作), oen(输出使能,无效则输出高阻), cen(时钟使能,用于降低功耗),addr_i, data_i, data_o等。注意:存贮器的数据线也有采用双向端口的,不过因为采用双向数据总线的同步存贮器有bus turn-around缺点,在片上较少采用,毕竟ASIC片上布线的资源还是很丰富的,不会象外部总线那样由于增加pin而导致封装面积加大,成本大增,同时pin过多也会造成PCB布线困难,片上没有必要搞这样的双向端口,并且双向端口实际是一个三态输出控制,FPGA上三态资源并不多,Altera好像甚至不提供三态资源(三态I/O端口除外),而ASIC上面使用三态也会对Timing、可测性(三态是不可观测的)等造成不良影响,如果高阻态被错误的当作输入进行锁存可能造成电路或者逻辑功能失效,总之不是一个好东西了。

(什么是bus turn-around?: 对于同步的RAM在从读变为写的时候因为上一个周期的读操作在下一个周期才会在数据总线上给出数据,如果采用双向数据总线,则此时数据总线就被占用了,所以就不能马上进行写操作,当然继续读还是可以的,这样就会消耗一个周期,高性能的存贮器例如ZBT RAM就专门针对这个问题进行处理,达到zero bus turn-around)

首先,从功用来看,可以分成RAM和ROM,本篇只说RAM;其次,从访问的端口数目来看,可以分成单端口和多端口(多端口的通常也叫作寄存器文件,因为通常处理器当中的寄存器堆就是一个多访问端口的存贮器),下面先说单端口RAM:

1.   从读取访问的方式来看,可以分成同步存贮器和异步的单端口存贮器。读取同步存贮器的时候,比第一个时钟沿(可以是上升沿也可以是下降沿)至少提前t_setup给出地址,wen无效;然后同步RAM在第一个时钟沿锁存地址,wen信号,并根据锁存的地址进行译码,t_pdout后给出数据,如果oen无效,则输出高阻,如果oen有效,那么输出至少在oen有效之后t_oenlz输出数据才有效,在第二个时钟沿就可以从数据总线锁存读取出来的数据了,期间地址和cen,wen均只需要保持t_hold一段时间,中间随便变化也不会影响读出来的值,而地址、wen、cen等信号显然是第0个时钟沿给出的,所以这种读取都要花费两个周期;读取异步存贮器的时候,不管时钟,只管oen,只要输入地址变化,那么隔t_pdout后数据总线上就要变化了,因此这种读取只要一个时钟周期就可以完成,异步RAM这种特性可以很方便就实现前半周期写后半周期读的功能(但是Xilinx FPGA上实现异步RAM必须要使用Logic Cell里面的DFF来做,连Distributed RAM也不可以,代价高昂,所以我们的PE当中使用的是同步Block RAM)。下面分别给出对应的verilog描述:
module mem( //异步ram
  input clk, wen,
  input[1 : 0] addr,
  input[1 : 0] data_i,
  output[1 : 0] data_o
);

reg[1 : 0] mem[3 : 0];

assign data_o = mem[addr]; //异步读取,输出随着输入地址改变

always @ (posedge clk)
begin
  if (wen)
        mem[addr] = data_i;
end

endmodule

module mem( //同步ram,读地址锁存,写地址不锁存,OR1200中寄存器文件采用这种方式。
  input clk, wen,
  input[1 : 0] addr,
  input[1 : 0] data_i,
  output[1 : 0] data_o
);

reg[1 : 0] int_addr;
reg[1 : 0] mem[3 : 0];

assign data_o = mem[int_addr];
 
always @ (posedge clk)
begin
  int_addr <= addr;
  if (wen)
        mem[addr] <= data_i;
end

endmodule

DC综合的网表分别如图。

同步的RAM还有另外一种方式,就是在第一个上升沿不锁存地址,而是锁存数据,这种方式要求在第一个上升沿之前t_setup+t_pdout的时间就在输入地址总线上出现有效地址。其verilog描述如下:
module mem( //同步ram,读写地址均不锁存,读出数据锁存,OR1200中双端口存贮器用这种描述。不过OR1200中容量很大的存贮器用DFF搭建的时候居然还使用mux,ft...真是不管面积了, timing也差!!
  input clk, wen,
  input[1 : 0] addr,
  input[1 : 0] data_i,
  output reg[1 : 0] data_o
);

reg[1 : 0] mem[3 : 0];

always @ (posedge clk)
begin
  if (wen)
        mem[addr] <= data_i;
end

always @ (posedge clk)
  data_o <= mem[addr];
//如果把这句话放到上面的always块中if的后面就不对了,单放if前面到对,晕..怎么回事?

endmodule

2.   从写访问的方式来看,因为写操作是对存贮单元的操作,所以一般都是和时钟边沿同步的(当然如果存贮单元为latch,写操作也可能不是边沿同步的)。我们可以看到,在上面给出的三种描述当中,写操作都是和时钟边沿同步的,并且写操作的地址都是没有锁存的,可见写操作的时候的地址必须在t_setup+t_decoder_delay+t_cen_clk之前给出(其中t_cen_clk是cen必须比clk上升沿提前有效的时间)。从前面的描述我们已经了解到译码器的延迟是比较大的,该时延跟读操作要求的时延相差比较多(即便是使用第二种同步读的方式,decoder和mux之间的延迟也是不一样的,如果使用三态延迟都使用decoder,那么读操作对地址有效的时间要求就比写操作提前t_ptribuf(即三态门的传输时间)。这种不一致给综合带来了困难,因为综合器不知道到底是读还是写操作。为了避免这种困难,我们可以在写的时候也将地址锁存,我们可以在下降沿将写的数据锁入,也可以使用latch,那么只要在latch的clk变低之前得到地址译码的结果就可以了。如果半个时钟周期不能满足译码要求,那么我们可以在存贮单元的时钟加上一些延迟缓冲(这里有些timing borrowing的意味,注意一般只能借半个周期)。写地址进行锁存后对外部来说读写操作对cen,wen,oen,data, addr等信号的时序要求就完全一样了,如果译码延迟非常大,那么译码则可能需要进行流水化,因为通常一个流水级包含6-10级门(延时1个纳秒以下),足够对10位地址进行译码了,这么大的时候早就该使用Memory Compiler之类的了,哪个时候就是全定制的事情了,呵呵,所以大可不必考虑。第二种方式的描述如下:

module mem( //同步ram,Cadence的Generic Library中双端口memory compiler生成的行为级模型就是如此
  input clk, wen,
  input[1 : 0] addr,
  input[1 : 0] data_i,
  output[1 : 0] data_o
);

reg[1 : 0] int_addr;
reg[1 : 0] int_data;
reg int_wen;
reg[1 : 0] mem[3 : 0];

assign data_o = mem[int_addr];
 
always @ (posedge clk)
begin
  int_addr <= addr;
  int_wen <= wen;
  int_data <= data_i;
end

always @ (negedge clk)
  if (int_wen)
        mem[int_addr] <= int_data;
end
module

这里先小结一下,总之,存贮器的变化花样有:单/多端口,同/异步,单/双向数据总线,DFF/LATCH搭建,三态/非三态,读写地址锁存/不锁存,读出数据锁存/不锁存,这些东西任意组合一下就可以了,可见花样还是不少的哦,呵呵,那么我们通常用的是什么呢?是单端口、同步、双向数据总线、DFF搭建、非三态、读写地址锁存、读出数据不锁存的片上存贮器,另外异步的也很常用,例如对于2,3级的流水处理器,寄存器文件就只好使用异步的了;使用三态来节约面积也是很常用的;而往往寄存器文件的端口要么只读,要么只写,所以可以读端口锁存地址(或者锁存读出的数据,根据timing的需要而移动这个寄存器了),写的时候不锁存地址而使用第一个边沿(不过窃以为这样不好,因为你只是写嘛,过了边沿就在剩下的周期里闲着多不好,还是把地址先锁存,然后在这个周期里面慢慢译码,写入,不要让人家那么早的把地址赶出来么,呵呵。当然这样做可以满足同一个周期对同一个地址进行读写,并且读出的是写入的新值,如果写入的时候先锁存地址那么读出新数据可能稍微延迟一点,而寄存器文件应该早点给出数据,因为ALU是关键通路哦)。

posted on 2011-05-05 21:58  dodo_bear  阅读(843)  评论(0)    收藏  举报