RTL级的基本要素和设计步骤

描述

RTL和综合的概念

RTL(Register Transfer Level,寄存器传输级)指:不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓扑结构等),通过描述寄存器到寄存器之间的逻辑功能描述电路的HDL层次。RTL级是比门级更高的抽象层次,使用RTL级语言描述硬件电路一般比门级描述简单高效得多。

  • RTL级语言的最重要的特性是:RTL级描述是可综合的描述层次。

  • 综合(Synthesize)是指将HDL语言、原理图等设计输入翻译成由与、或、非门等基本逻辑单元组成的门级连接(网表),并根据设计目标与要求(约束条件)优化所生成的逻辑连接,输出门级网表文件。RTL级综合指将RTL级源码翻译并优化为门级网表。

RTL级的基本要素和设计步骤

典型的RTL设计包含一下3个部分

  • 时钟域描述:描述所使用的所有时钟,时钟之间的主从与派生关系,时钟域之间的转换;

  • 时序逻辑描述(寄存器描述):根据时钟沿的变换,描述寄存器之间的数据传输方式;

  • 组合逻辑描述:描述电平敏感信号的逻辑组合方式与逻辑功能。

书中推荐的设计步骤:

  • 功能定义与模块划分:根据系统功能的定义和模块划分准则划分各个功能模块;
  • 定义所有模块的接口:首先清晰定义每个模块的接口,完成每个模块的信号列表,这种思路与Modular Design(模块化设计方法)一致,利于模块重用、调试、修改;
  • 设计时钟域:根据设计的时钟复杂程度定义时钟之间的派生关系,分析设计中有哪些时钟域,是否存在异步时钟域之间的数据交换;对于PLD器件设计,还需要确认全局时钟是否使用PLL/DLL完成时钟的分频、倍频、移相等功能,哪些时钟使用全局时钟资源布线,哪些时钟使用第二全局时钟资源布线;全局时钟的特点是:几乎没有Clock Skew(时钟倾斜),有一定的Clock Delay(时钟延迟),驱动能力最强;第二全局时钟的特点是:有较小的Clock Shew,较小的Clock Delay,时钟驱动能力较强;

补充:时钟抖动(Clock Jitter):指芯片的某一个给定点上时钟周期发生暂时性变化,使得时钟周期在不同的周期上可能加长或缩短。时钟偏移(Clock Skew):是由于布线长度及负载不同引起的,导致同一个时钟信号到达相邻两个时序单元的时间不一致。区别:Jitter是在时钟发生器内部产生的,和晶振或者PLL内部电路有关,布线对其没有影响。Skew是由不同布线长度导致的不同路径的时钟上升沿到来的延时不同。

  • 考虑设计的关键路径:关键路径是指设计中时序要求最难以满足的路径,设计的时序要求主要体现在频率、建立时间、保持时间等时序指标上,;在设计初期,设计者可以根据系统的频率要求,粗略的分析出设计的时序难点(如最高频率路径、计数器的最低位、包含复杂组合逻辑的时序路径等),通过一些时序优化手段(如Pipeline、Retiming、逻辑复制等)从代码上缓解设计的时序压力,这种方法以但依靠综合与布线工具的自动优化有效的多;

  • 顶层设计:RTL设计推荐使用自顶而下的设计方法,因为这种设计方法与模块规划的顺序一致,而且更有利于进行Modular Design,可以并行开展设计工作,提高模块复用率;

  • FSM设计:FSM是逻辑设计最重要的内容之一;

  • 时序逻辑设计:首先根据时钟域规划好寄存器组,然后描述各个寄存器组之间的数据传输方式;

  • 组合逻辑设计:一般来说,大段的组合逻辑最好与时序逻辑分开描述,这样更有利于时序约束和时序分析,使综合器和布局布线器达到更好的优化效果。

常用RTL级建模

非阻塞赋值、阻塞赋值、连续赋值

  • 对于时序逻辑,即always块的敏感信号列表为边沿敏感信号,统一使用非阻塞赋值“<=”;
  • 对于always块敏感信号列表为电平敏感的组合逻辑,统一使用阻塞赋值“=”;
  • 对于assign关键字描述的组合逻辑,统一使用阻塞赋值“=”,变量被定义为wire型信号。

寄存器电路建模

寄存器和组合逻辑是数字逻辑电路的两大基本要素,寄存器一般和同步时序逻辑关联,其特点是仅当时钟的边沿到达时,才有可能发生输出的改变。

  • 寄存器变量声明:寄存器定义为reg型,但要注意的是,反之不一定成立;
  • 时钟输入:在每个时钟的正沿或负沿对数据从进行处理。
  • 异步复位/置位:绝大多数目标器件的寄存器模型都包含异步复位/置位端;
  • 同步复位/置位:任何寄存器都可以实现同步复位/置位功能;
  • 同时使用时钟上升沿和下降沿的问题:有时因为数据采样或者调整数据相位等需求,设计者会在一个always的敏感信号列表中同时使用时钟的posedge和negedge,或者在两个always的敏感信号列表中分别使用posedge和nesedge对某个寄存器电路操作;这两种描述下,时钟上升沿和下降沿到来时,寄存器电路都会做相应的操作,这个双边沿电路等同于使用了原来时钟的倍频时钟的单边沿操作电路,这种操作是不推荐的;芯片内部的PLL/DLL和一些时钟电路往往只能对一个边沿有非常好的指标,而另一个沿的抖动、偏移、斜率等指标不见得非常优化,有时同时使用时钟的正负边沿会因为时钟的抖动、偏斜、占空比、斜率等问题造成一定的性能恶化;一般推荐将原时钟通过PLL/DLL倍频,然后使用倍频时钟的单边沿进行操作。

组合逻辑建模

  • always 模块的敏感信号列表为电平敏感信号的组合逻辑电路always模块的敏感信号列表为所有判定条件和输入信号,在使用这种结构描述组合逻辑时一定要将敏感列表列写完整。在always块中可以使用高级编程语言,使用阻塞赋值“=”,虽然信号被定义位reg型,但最终综合实现结果并不是寄存器,而是组合逻辑,定义为reg型是纯语法需要。

  • assign 等语句描述的组合逻辑电路

这种形式描述组合逻辑电路适用于描述那些相对简单的组合逻辑,信号一般被定义位wire型。

  • 双向端口与三态信号建模

所有的双向总线应该在顶层模块定义为三态信号,禁止在顶层以外的其他子层次定义双向端口。为了避免仿真和综合实现结果不一致,并便于维护,强烈建议仅在顶层定义双向总线和例化三态信号,禁止在除顶层以外的其他层次赋值高阻态"Z",在顶层将双向信号分为输入和输出信号两种类型,然后根据需要分别传递到不同的子模块中,这样做的另一个好处是便于描述仿真激励。

module bibus (clk, rst, sel, data_bus, addr);
input       clk, rst, sel;
input [7:0] addr;
inout [7:0] data_bus;
wire [7:0] data_in, data_out;

assign data_in = data_bus;
assign data_bus = (sel) ? data_out : 8'bZ;

decode decode_inst (.clock (clk),
                    .reset (rst),
                    .data_bus_in (data_in),
                    .addr_bus (addr),
                    .data_bus_out (data_out)
                    );
endmodule

如果三态总线的使能关系比较复杂,不是单一信号,此时可以使用嵌套的问号表达式,或者使用case语句描述。

  • 嵌套的问号表达式
module complex_bibus (clk, rst, sel1, sel2, sel3, data_bus, addr);
input       clk, rst;
input       sel1, sel2, sel3;
input [7:0] addr;
inout [7:0] data_bus;

wire [7:0] data_in;
//wire [7:0] data_out; //use wire type
wire [7:0] decode_out;
wire [7:0] cnt_out;

assign data_in = data_bus;
assign data_bus = (sel1)? decode_out : ((sel2)? cnt_out : ((sel3)? 8'b11111111: 8'bZZZZZZZZ)); 

decode decode_inst (.clock (clk),
                    .reset (rst),
                    .data_bus_in (data_in),
                    .addr_bus (addr),
                    .data_bus_out (decode_out)
                    );
                    
counter counter_inst (.clock (clk),
                    .reset (rst),
                    .data_bus_in (data_in),
                    .cnt_out (cnt_out)
                    );            
endmodule
  • case语句(如果使能情况比较复杂,通过case进行罗列,更清晰)
input sel1, sel2, sel3;
input [7:0] addr;
inout [7:0] data_bus;

wire [7:0] data_in;
reg  [7:0] data_out; //use reg type, but not registers
wire [7:0] decode_out;
wire [7:0] cnt_out;

assign data_in = data_bus;

decode decode_inst (.clock (clk),
                    .reset (rst),
                    .data_bus_in (data_in),
                    .addr_bus (addr),
                    .data_bus_out (decode_out)
                    );
                    
counter counter_inst (.clock (clk),
                    .reset (rst),
                    .data_bus_in (data_in),
                    .cnt_out (cnt_out)
                    );
                    
always @ (decode_out or cnt_out or sel1 or sel2 or sel3)
    begin
       case ({sel1, sel2, sel3})
         3'b100:  data_out = decode_out;
         3'b010:  data_out = cnt_out;
         3'b001:  data_out = 8'b11111111;
         default: data_out = 8'bZZZZZZZZ;
       endcase
    end
assign data_bus = data_out;                                       
endmodule
  • mux 建模

简单的使用assign和?,相对复杂的使用always和if…else、case等条件判断语句建模。

  • 存储器建模

逻辑电路设计经常使用一些单口RAM、双口RAM和ROM等存储器。Verilog 语法中基本的存储单元定义格式为:

reg [datawidth] MemoryName [addresswidth]

如定义一个数据位宽为8bit,地址为63为的RAM8x64:

reg [7:0] RAM8x64 [0:63];

在使用存储单元时,不能直接操作存储器某地址的某位,需要先将存储单元赋值给某个寄存器,然后再对该存储器的某位进行相关操作。

module ram_basic (clk, CS, WR, addr, data_in, data_out, en);
input         clk;
input         CS;  //CS = 1, RAM enable
input         WR;  //WR =1 then WRite enable; WR = 0 then read enable
input         en;  //data_out enable, convert the data sequency
input  [5:0]  addr;
input  [7:0]  data_in;
output [7:0]  data_out;

 reg [7:0] RAM8x64 [0:63];
 reg [7:0] mem_data;
 
always @ (posedge clk)
    if (WR && CS) //WRite
        RAM8x64 [addr] <= data_in [7:0];
    else if (~WR && CS ) // read
        mem_data <= RAM8x64 [addr]; 
         
assign data_out = (en)? mem_data[7:0] : {~mem_data[7], mem_data[6:0]};
endmodule
  • FPGA中内嵌的RAM资源分为两类:块RAM(Block RAM)资源和分布式RAM(Distributed RAM)资源,BRAM作为FPGA内部硬件资源,使用时不会占用其他逻辑资源,分布式RAM是通过查找表和触发器实现的RAM结构。
  • 使用RAM等资源时通常不使用这种Verilog语言进行建模,一般使用厂商提供IP核通过GUI完成相关参数配置,并生成相关IP。

简单的时钟分频电路

偶数分频十分简单,只需要用高速时钟驱动一个同步计数器;

module clk_div_phase (rst, clk_200K, clk_100K, clk_50K, clk_25K);
input        clk_200K;
input        rst;
output       clk_100K, clk_50K, clk_25K;
wire         clk_100K, clk_50K, clk_25K;

reg [2:0] cnt;  

always @ (posedge clk_200K or negedge rst)
   if (!rst)
      cnt <= 3'b000;
   else
      cnt <= cnt + 1;

assign clk_100K = ~cnt [0];//2分频
assign clk_50K  = ~cnt [1];//4分频
assign clk_25K  = ~cnt [2];//8分频

endmodule

上例通过对计数器每个bit的反向,完成了所有分频后的时钟调整,保证了3个分频后时钟的相位严格同相,也与源时钟同相,有共同的上升沿。

奇数分频

module clk_3div (clk,reset,clk_out);
input clk, reset;
output clk_out;
reg[1:0] state;
reg clk1;
always @(posedge clk or negedge reset)
if(!reset)
    state<=2'b00;
else
    case(state)
        2'b00:state<=2'b01;
        2'b01:state<=2'b11;
        2'b11:state<=2'b00;
        default:state<=2'b00;
    endcase

always @(negedge clk or negedge reset)
    if(!reset)
        clk1<=1'b0;
    else
        clk1<=state[0];

assign clk_out=state[0]&clk1;
endmodule 

串/并转换建模

根据数据的排序和数量的要求,可以选用移位寄存器、RAM等实现;对于数量比较小的设计可以采用移位寄存器完成串/并转换(串转并:先移位,再并行输出;并转串:先加载并行数据,再移位输出);对于排列顺序有规律的串/并转换,可以使用case语句进行判断实现;对于复杂的串/并转换,还可以用状态机实现。

同步复位与异步复位

同步复位建模

module syn_rst (clk, rst_, cnt1, cnt2);
input        clk;
input        rst_;
output [4:0] cnt1 , cnt2;
reg    [4:0] cnt1 , cnt2;

always @ (posedge clk)
   if (!rst_)
      begin
        cnt1 <= 4'b0;
        cnt2 <= 4'b0;
      end
   else
      begin
        if (cnt1 < 2'b11)
            cnt1 <= cnt1 + 1;
        else
            cnt1 <= cnt1;                
        cnt2 <= cnt1 - 1;      
      end
endmodule

很多目标器件的触发器本身本身并不包含同步复位端口,则同步复位可以通过下图结构实现:

RTLRTL

优点

  • 同步复位利于基于周期机制的仿真器仿真;
  • 使用同步复位可以设计100%的同步时序电路,利于时序分析,其综合结果的频率往往更高;
  • 同步复位仅在时钟的上升沿生效,可以有效的避免因复位电路毛刺造成的亚稳态和错误;在进行复位和释放复位信号时,都是仅当时钟沿采到复位电平变化时才进行相关操作,如果复位信号树的组合逻辑出现了某些毛刺,此时时钟边沿采集到毛刺的概率非常低,通过时钟沿采样,可以十分有效地过滤复位电路的组合逻辑毛刺,增强电路的稳定性。

缺点

  • 很多目标器件的触发器本身不包含同步复位端口,使用同步复位会增加很多逻辑资源;

  • 同步复位的最大问题在于必须保证复位信号的有效时间足够长,才能保证所有触发器都有效复位,所以同步复位信号的持续时间必须大于设计的最长时钟周期,以保证所有时钟的有效沿都能采样到同步复位信号。

  • 其实仅仅保证同步复位信号的持续时间大于最慢的时钟周期还是不够的,设计中还要考虑到同步复位信号树通过所有组合逻辑路径的延时以及由于时钟布线产生的偏斜(skew),只有同步复位大于时钟最大周期加上同步信号穿过的组合逻辑路径延时加上时钟偏斜时,才能保证同步复位可靠、彻底。

RTL

上图中,假设同步复位逻辑树组合逻辑的延时为t1,复位信号传播路径的最大延迟为t2,最慢时钟的周期为Period max,时钟的skew为clk2-clk1,则同步复位的周期Tsys_rst应满足:Tsys_rst > Period max + (clk2-clk1) + t1 + t2;

异步复位建模

module asyn_rst (clk, rst_, cnt1, cnt2);
input        clk;
input        rst_;
output [4:0] cnt1 , cnt2;
reg    [4:0] cnt1 , cnt2;

always @ (posedge clk or negedge rst_)
   if (!rst_)
      begin
        cnt1 <= 4'b0;
        cnt2 <= 4'b0;
      end
   else
      begin
        if (cnt1 < 2'b11)
            cnt1 <= cnt1 + 1;
        else
            cnt1 <= cnt1;                
        cnt2 <= cnt1 - 1;      
      end
endmodule

优点

  • 多数器件包含异步复位端口,异步复位会节约逻辑资源;
  • 异步复位设计简单;
  • 大多数FPGA,都有专用的全局复位/置位资源(GSR,Globe Set Reset),使用GSR资源,异步复位达到所有寄存器的偏斜(skew)最小。

缺点

  • 异步复位的作用和释放与时钟沿没有直接关系,在异步复位神效时问题并不明显,但当异步复位释放时,如果异步复位释放时间和时钟的有效沿到达时间几乎一致,则容易造成触发器输出亚稳态,造成逻辑错误;
  • 如果异步复位逻辑树的组合逻辑产生了毛刺,则毛刺的有效沿会使触发器误复位,造成逻辑错误。

推荐的复位电路设计方式——异步复位,同步释放

  • 推荐的复位电路设计方式是异步复位,同步释放,这种方式可以有效的继承异步复位设计简单的优势,并克服异步复位的风险与缺陷;相较于纯粹的异步复位,降低了异步复位信号释放导致的亚稳态的可能性,相较于同步复位,能够识别到同步复位中检测不到的复位信号。
  • 在FPGA中使用异步复位,同步释放可以节约器件资源,并获得稳定可靠的复位效果。
  • 异步复位同步释放,既能很快的检测到复位信号,不需要复位保持超过一个时钟周期,又能解决释放时的亚稳态问题(降低亚稳态发生的概率)。
RTL
  • 异步复位,同步释放的具体设计方法很多,关键是如何保证同步地释放复位信号,本例举例的方法是在复位信号释放时,用系统时钟采样后再将复位信号送到寄存器的异步复位端。
  • 所谓“异步复位”是针对D触发器的复位端口,它是异步的,但是设计中已经同步了异步复位信号,所以笔者(Crazybingo)认为这只是某种意义上的“异步复位”。
  • 所谓“同步释放”,实际上是由于我们设计了同步逻辑电路,外部复位信号不会在出现释放时与clk信号竞争,整个系统将与全局时钟clk信号同步。
  • 使用时钟将外部输入的异步复位信号寄存一个节拍后,再送到触发器异步复位端口的设计方法的另一个好处在于:做STA(静态时序分析)分析时,时序工具会自动检查同步后的异步复位信号和时钟的到达(Recovery)/撤销(Removal)时间关系,如果因布线造成的skew导致该到达/撤销时间不能满足,STA工具会上报该路径,帮助设计者进一步分析问题。
module system_ctrl     //异步复位,同步释放——by 特权同学
//==================<端口>==================================================
(
//globel clock ----------------------------------
input  wire                 clk                 , //时钟,50Mhz
input  wire                 rst_n               , //复位,低电平有效
//user interface --------------------------------
input  wire                 a                   , //输入信号
output reg                  b                     //输出信号
);

//==========================================================================
//==    异步复位的同步化设计
//==========================================================================
reg        sys_rst_n_r;
reg        sys_rst_n;

always @(posedge clk or negedge rst_n) 
begin
    if(!rst_n) begin
        sys_rst_n_r <= 1'b0;
        sys_rst_n   <= 1'b0;
    end
    else begin
        sys_rst_n_r <= 1'b1;
        sys_rst_n   <= sys_rst_n_r; //注意这里的rst_sync_n才是我们真正对系统输出的复位信号
    end
end

always @(posedge clk or negedge sys_rst_n)   //注意这里将同步后的信号仍作为异步复位信号进行处理,Altera推荐
begin
    if(!sys_rst_n)
        b <= 0;
    else
        b <= a;
end

endmodule
RTL

上图是Altera推荐的异步复位,同步释放示意图

module reset_gen ( output rst_sync_n, input clk, rst_async_n);  //此模块对应前一个黄框中的逻辑,输出信号在后级电路中仍作为异步复位信号进行处理
reg rst_s1, rst_s2;
wire rst_sync_n ;

always @ (posedge clk, posedge rst_async_n)
    if (rst_async_n)
         begin 
            rst_s1 <= 1'b0;
            rst_s2 <= 1'b0;
        end
    else 
        begin
            rst_s1 <= 1'b1;  //针对Altera FPGA
            rst_s2 <= rst_s1;
        end

assign rst_sync_n = rst_s2; //注意这里的rst_sync_n才是我们真正对系统输出的复位信号

endmodule

Xilinx的FPGA,高电平复位其Filp-Flop同时支持同步/异步复位,复位准则:

  • 尽量少使用复位,特别是少用全局复位,能不用复位就不用,一定要用复位的使用局部复位;
  • 如果必须要复位,在同步和异步复位上,则尽量使用同步复位(BRAM DSP48不支持异步复位),一定要用异步复位的地方,采用“异步复位、同步释放”;
  • 复位电平选择高电平复位;

只要存在复位都会增加布局布线的负担,因为复位会像时钟一样连接到每一个寄存器上,是相当复杂的工程,会增加时序收敛的难度。

对于同一个触发器逻辑,因为同时支持异步和同步复位,所以异步复位并不会节省资源;对于其他的资源,比如 DSP48 等,同步复位更加节省资源。

首先,对于 DSP48,其内部还带有一些寄存器(只支持同步复位),如果使用异步复位,则会额外使用外部 Slice 中带异步复位的寄存器,而使用同步复位时,可以利用 DSP48 内部的寄存器;Xilinx 的 FPGA,对于 DSP48、BRAM 资源,使用同步复位比异步复位更节省资源。

对于高电平复位,使用异步复位同步释放,则第一个寄存器的 D 输入是 0,这里使用了 4 个触发器打拍同步。

RTL
always @(posedge clk or posedge rst_async)
begin
    if(rst_async == 1'b1) begin
        rst_sync_reg1 <= 1'b1; //Xilinx的FPGA高电平复位
        rst_sync_reg2 <= 1'b1;
        rst_sync_reg3 <= 1'b1;
        rst_sync_reg4 <= 1'b1;
    end
    else begin
        rst_sync_reg1 <= 1'b0;
        rst_sync_reg2 <= rst_sync_reg1;
        rst_sync_reg3 <= rst_sync_reg2;
        rst_sync_reg4 <= rst_sync_reg3;
    end
end  

wire sys_rst;
assign sys_rst = rst_sync_reg4;

always @(posedge clk)    //同步后的信号当作同步复位信号处理
begin
    if(sys_rst == 1'b1) begin
        data_out_rst_async <= 1'b0;
    end
    else begin
        data_out_rst_async <= a & b & c & d;
    end
end

同步后的信号如果作为同步复位信号进行处理:

rst_async异步复位一旦给出,用于同步的4个寄存器rst_sync_reg1~4立刻输出高电平“1”,在下一个时钟上升沿检测到同步复位并将输出data_out_rst_async复位;

异步复位信号释放后,经过同步的sys_rst经过一定周期后在时钟边沿同步释放;

同步后的信号如果作为异步复位信号进行处理:

区别在于异步复位信号rst_async一旦产生,输出立刻复位,且同样是同步释放,好像这种处理才更符合异步复位、同步释放。

那么为什么Xilinx白皮书还是将sys_rst按照同步复位去做的呢?综合考虑可能有这样的因素:

  • 当作同步复位的差别只在于复位时间会稍晚一些,要在时钟的下一个边沿检测到,但是还是能够识别到输入的rst_async异步复位信号,所以从复位角度来说,都能够后实现复位效果;

  • 根据Xilinx复位准则,我们知道同步复位相比异步复位有很多好处,具体参见:Xilinx FPGA 复位策略白皮书(WP272) 公众号-FPGA探索者做了翻译可以参考,既然两者对后级复位没有功能上的差别,那么优先选择同步复位;

Xilinx 推荐的复位准则:

  • 尽量少使用复位,特别是少用全局复位,能不用复位就不用,一定要用复位的使用局部复位;

  • 如果必须要复位,在同步和异步复位上,则尽量使用同步复位,一定要用异步复位的地方,采用“异步复位、同步释放”;

  • 复位电平选择高电平复位;

Altera

Altera的FPGA,低电平复位,其触发器只有异步复位端口,所以如果想要用同步复位,需要额外的资源来实现,这也是“异步复位节省资源”这一说法的原因。

具体电路及代码见上文

可综合的Verilog语法子集

在RTL建模时,使用可综合的Verilog语法是整个Verilog语法中的非常小的一个子集。其实可综合的Verilog常用关键字非常有限,这恰恰体现了Verilog语言是硬件描述语言的本质,Verilog作为HDL,其本质在于把电路流畅、合理的转换为语言形式,而使用较少的一些关键字就可以有效的将电路转换到可综合的RTL语言结构。常用的RTL语法结构列举:

  • 模块声明:module…endmodule;
  • 端口声明:input、outpu、inout;
  • 信号类型:wire、reg、tri等,integer通常用于for语句中索引;
  • 参数定义:parameter
  • 运算操作符:逻辑操作、移位操作、算术操作;
  • 比较判断:case…endcase(casex/casez)、if…else;
  • 连续赋值:assign、问号表达式
  • always模块:建模时序和组合逻辑
  • 语法分割符:begin…end
  • 任务定义:task…endtask
  • 循环语句:for

CPU读/写PLD寄存器接口设计实例

RTLRTL
  • CS:片选(低有效、input)
  • OE:输出使能信号(低有效、input)
  • WR:读/写指示,低-读数据,高-写数据(input)
  • Address:地址总线(input)
  • Data:双向数据总线(inout)

地址译码器电路

module decode (CS_, OE_, WR_, Addr, my_wr, my_rd, CS_reg1, CS_reg2, CS_reg3);

input        CS_, OE_, WR_;
input  [7:0] Addr;

output       my_wr, my_rd;
output       CS_reg1, CS_reg2, CS_reg3;

reg          CS_reg1, CS_reg2, CS_reg3;

assign my_wr = (!WR_) && (!CS_) && (!OE_);
assign my_rd = (WR_)  && (!CS_) && (!OE_);

always @ (Addr or CS_)
  if (!CS_)
     begin
       case (Addr)
          8'b 11110000: CS_reg1 <= 1'b1;
          8'b 00001111: CS_reg2 <= 1'b1;
          8'b 10100010: CS_reg3 <= 1'b1;
          default:     begin
                           CS_reg1 <= 1'b0;
                           CS_reg2 <= 1'b0;
                           CS_reg3 <= 1'b0;
                       end
       endcase
     end

               
endmodule

读寄存器

module read_reg (clk, rst, data_out,  my_rd, CS_reg1, CS_reg2, CS_reg3, reg1, reg2, reg3);

input        clk, rst, my_rd, CS_reg1, CS_reg2, CS_reg3;
input  [7:0] reg1, reg2, reg3;
output [7:0] data_out;
reg    [7:0] data_out;

  always @ (posedge clk or negedge rst)
      if (!rst)
         data_out <= 8'b0;
      else
         begin
            if (my_rd)
                begin
                     if (CS_reg1)
                         data_out <= reg1;
                     else if (CS_reg2)
                         data_out <= reg2;
                     else if (CS_reg3)
                         data_out <= reg3;
                end
            else
                data_out <= 8'b0;              
         
         end

endmodule

写寄存器

module write_reg (clk, rst, data_in,  my_wr, CS_reg1, CS_reg2, CS_reg3, reg1, reg2, reg3);

input        clk, rst, my_wr, CS_reg1, CS_reg2, CS_reg3;
input  [7:0] data_in;
output [7:0] reg1, reg2, reg3;
reg    [7:0] reg1, reg2, reg3;

  always @ (posedge clk or negedge rst)
      if (!rst)
         begin
              reg1 <= 8'b0;
              reg2 <= 8'b0;
              reg3 <= 8'b0;         
         end
      else
         begin
            if (my_wr)
                begin
                     if (CS_reg1)
                         reg1 <= data_in;
                     else if (CS_reg2)
                         reg2 <= data_in;
                     else if (CS_reg3)
                         reg3 <= data_in;
                end
            else
                begin
                     reg1 <= reg1;
                     reg2 <= reg2;
                     reg3 <= reg3;
                end                
         
         end

endmodule

顶层

module top (clk_cpu, rst, CS_, OE_, WR_, Addr, data_bus);

input   clk_cpu, rst;
input   CS_, OE_, WR_;
input [7:0] Addr;
inout [7:0] data_bus;

wire [7:0] data_in;
wire [7:0] data_out;
wire       my_wr, my_rd;
wire       CS_reg1, CS_reg2, CS_reg3; // the register selection
wire [7:0] reg1, reg2, reg3;          // the register to be read and written


assign data_in = data_bus;
assign data_bus = ((!CS_) && (!OE_))? data_out : 8'bZZZZZZZZ;

decode decode_u1  (.CS_(CS_),
                   .OE_(OE_),
                   .WR_(WR_),
                   .Addr(Addr),
                   .my_wr(my_wr),
                   .my_rd(my_rd),
                   .CS_reg1(CS_reg1),
                   .CS_reg2(CS_reg2),
                   .CS_reg3(CS_reg3)
                   );

write_reg write_reg_u1 ( .clk(clk_cpu),
                         .rst(rst),
                         .data_in(data_in),
                         .my_wr(my_wr),
                         .CS_reg1(CS_reg1),
                         .CS_reg2(CS_reg2),
                         .CS_reg3(CS_reg3),
                         .reg1(reg1),
                         .reg2(reg2),
                         .reg3(reg3)
                         );

read_reg read_reg_u1  ( .clk(clk_cpu),
                         .rst(rst),
                         .data_out(data_out),
                         .my_rd(my_rd),
                         .CS_reg1(CS_reg1),
                         .CS_reg2(CS_reg2),
                         .CS_reg3(CS_reg3),
                         .reg1(reg1),
                         .reg2(reg2),
                         .reg3(reg3)
                         );
                    
endmodule

使用OE/WR边沿读写

使用OE或WR的沿读写寄存器的描述看起来比前面介绍的使用CPU时钟同步读写寄存器的描述简单,但是读者必须明确这种方式正常工作有两个前提条件:

  • OE的上升沿可以有效地采样数据总线,即OE的上升沿采样数据总线时Setup和Hold都能保证满足;
  • WR和CS信号都比OE信号宽,即OE上升沿读写寄存器时,CS和WR信号始终保持有效。

只有这两个条件同时满足的前提下,才能保证使用OE的沿读写PLD寄存器电路是可靠的。

/******************************************/
module decode (CS_, WR_, Addr, my_wr, my_rd, CS_reg1, CS_reg2, CS_reg3);

input        CS_, WR_;
input  [7:0] Addr;

output       my_wr, my_rd;
output       CS_reg1, CS_reg2, CS_reg3;

reg          CS_reg1, CS_reg2, CS_reg3;

assign my_wr = (!WR_) && (!CS_);
assign my_rd = (WR_)  && (!CS_);

always @ (Addr or CS_)
  if (!CS_)
     begin
       case (Addr)
          8'b 11110000: CS_reg1 <= 1'b1;
          8'b 00001111: CS_reg2 <= 1'b1;
          8'b 10100010: CS_reg3 <= 1'b1;
          default:     begin     
                           CS_reg1 <= 1'b0;   
                           CS_reg2 <= 1'b0;  
                           CS_reg3 <= 1'b0;                   
                       end
       endcase
     end               
endmodule
/******************************************/
module read_reg (OE_, rst, data_out,  my_rd, CS_reg1, CS_reg2, CS_reg3, reg1, reg2, reg3);

input        OE_, rst, my_rd, CS_reg1, CS_reg2, CS_reg3;
input  [7:0] reg1, reg2, reg3;
output [7:0] data_out;
reg    [7:0] data_out;

  always @ (posedge OE_ or negedge rst)
      if (!rst)
         data_out <= 8'b0;
      else
         begin
            if (my_rd)
                begin
                     if (CS_reg1)
                         data_out <= reg1;
                     else if (CS_reg2)
                         data_out <= reg2;
                     else if (CS_reg3)
                         data_out <= reg3;
                end
            else
                data_out <= 8'b0;              
         
         end
endmodule
/******************************************/
module write_reg (OE_, rst, data_in,  my_wr, CS_reg1, CS_reg2, CS_reg3, reg1, reg2, reg3);

input        OE_, rst, my_wr, CS_reg1, CS_reg2, CS_reg3;
input  [7:0] data_in;
output [7:0] reg1, reg2, reg3;
reg    [7:0] reg1, reg2, reg3;

  always @ (posedge OE_ or negedge rst)
      if (!rst)
         begin
              reg1 <= 8'b0;
              reg2 <= 8'b0;
              reg3 <= 8'b0;         
         end
      else
         begin
            if (my_wr)
                begin
                     if (CS_reg1)
                         reg1 <= data_in;
                     else if (CS_reg2)
                         reg2 <= data_in;
                     else if (CS_reg3)
                         reg3 <= data_in;
                end
            else
                begin
                     reg1 <= reg1;
                     reg2 <= reg2;
                     reg3 <= reg3;
                end                
         
         end
         
endmodule
/******************************************/
module top (rst, CS_, OE_, WR_, Addr, data_bus);

input       rst;
input        CS_, OE_, WR_;
input [7:0] Addr;
inout [7:0] data_bus;

wire [7:0] data_in;
wire [7:0] data_out;
wire       my_wr, my_rd;
wire       CS_reg1, CS_reg2, CS_reg3; // the register selection
wire [7:0] reg1, reg2, reg3;          // the register to be read and written

assign data_in = data_bus;
assign data_bus = ((!CS_) && (!OE_))? data_out : 8'bZZZZZZZZ;

decode decode_u1  (.CS_(CS_), 
                 //  .OE_(OE_), 
                   .WR_(WR_), 
                   .Addr(Addr), 
                   .my_wr(my_wr), 
                   .my_rd(my_rd), 
                   .CS_reg1(CS_reg1), 
                   .CS_reg2(CS_reg2), 
                   .CS_reg3(CS_reg3)
                   );

write_reg write_reg_u1 ( .OE_(OE_), 
                         .rst(rst),
                         .data_in(data_in),  
                         .my_wr(my_wr), 
                         .CS_reg1(CS_reg1), 
                         .CS_reg2(CS_reg2), 
                         .CS_reg3(CS_reg3), 
                         .reg1(reg1), 
                         .reg2(reg2), 
                         .reg3(reg3)
                         );

read_reg read_reg_u1  (  .OE_(OE_),
                         .rst(rst),
                         .data_out(data_out),  
                         .my_rd(my_rd), 
                         .CS_reg1(CS_reg1), 
                         .CS_reg2(CS_reg2), 
                         .CS_reg3(CS_reg3), 
                         .reg1(reg1), 
                         .reg2(reg2), 
                         .reg3(reg3)
                         );
                    
endmodule
/******************************************/

如果译码电路是组合逻辑,则其译码结果就有可能带有毛刺,另外由于CPU总线的时序在电压、温度、环境变化的情况下时序可能遭到破坏,造成OE,WR,CS等信号的时序余量恶化,如果此时使用译码结果的电平做电平敏感的always模块,进行读写寄存器操作(如Example-4-21 asyn_bad目录下的read_reg.v和write_reg.v),则会因为毛刺和错误电平造成读写错误。所以不同将OE或WR的电平作为敏感信号来进行读写。

审核编辑 :李倩
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分