RTL(Register Transfer Level,寄存器传输级)指:不关注寄存器和组合逻辑的细节(如使用了多少逻辑门,逻辑门之间的连接拓扑结构等),通过描述寄存器到寄存器之间的逻辑功能描述电路的HDL层次。RTL级是比门级更高的抽象层次,使用RTL级语言描述硬件电路一般比门级描述简单高效得多。
RTL级语言的最重要的特性是:RTL级描述是可综合的描述层次。
综合(Synthesize)是指将HDL语言、原理图等设计输入翻译成由与、或、非门等基本逻辑单元组成的门级连接(网表),并根据设计目标与要求(约束条件)优化所生成的逻辑连接,输出门级网表文件。RTL级综合指将RTL级源码翻译并优化为门级网表。
时钟域描述:描述所使用的所有时钟,时钟之间的主从与派生关系,时钟域之间的转换;
时序逻辑描述(寄存器描述):根据时钟沿的变换,描述寄存器之间的数据传输方式;
组合逻辑描述:描述电平敏感信号的逻辑组合方式与逻辑功能。
补充:时钟抖动(Clock Jitter):指芯片的某一个给定点上时钟周期发生暂时性变化,使得时钟周期在不同的周期上可能加长或缩短。时钟偏移(Clock Skew):是由于布线长度及负载不同引起的,导致同一个时钟信号到达相邻两个时序单元的时间不一致。区别:Jitter是在时钟发生器内部产生的,和晶振或者PLL内部电路有关,布线对其没有影响。Skew是由不同布线长度导致的不同路径的时钟上升沿到来的延时不同。
考虑设计的关键路径:关键路径是指设计中时序要求最难以满足的路径,设计的时序要求主要体现在频率、建立时间、保持时间等时序指标上,;在设计初期,设计者可以根据系统的频率要求,粗略的分析出设计的时序难点(如最高频率路径、计数器的最低位、包含复杂组合逻辑的时序路径等),通过一些时序优化手段(如Pipeline、Retiming、逻辑复制等)从代码上缓解设计的时序压力,这种方法以但依靠综合与布线工具的自动优化有效的多;
顶层设计:RTL设计推荐使用自顶而下的设计方法,因为这种设计方法与模块规划的顺序一致,而且更有利于进行Modular Design,可以并行开展设计工作,提高模块复用率;
FSM设计:FSM是逻辑设计最重要的内容之一;
时序逻辑设计:首先根据时钟域规划好寄存器组,然后描述各个寄存器组之间的数据传输方式;
组合逻辑设计:一般来说,大段的组合逻辑最好与时序逻辑分开描述,这样更有利于时序约束和时序分析,使综合器和布局布线器达到更好的优化效果。
寄存器和组合逻辑是数字逻辑电路的两大基本要素,寄存器一般和同步时序逻辑关联,其特点是仅当时钟的边沿到达时,才有可能发生输出的改变。
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
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
简单的使用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
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
很多目标器件的触发器本身本身并不包含同步复位端口,则同步复位可以通过下图结构实现:
很多目标器件的触发器本身不包含同步复位端口,使用同步复位会增加很多逻辑资源;
同步复位的最大问题在于必须保证复位信号的有效时间足够长,才能保证所有触发器都有效复位,所以同步复位信号的持续时间必须大于设计的最长时钟周期,以保证所有时钟的有效沿都能采样到同步复位信号。
其实仅仅保证同步复位信号的持续时间大于最慢的时钟周期还是不够的,设计中还要考虑到同步复位信号树通过所有组合逻辑路径的延时以及由于时钟布线产生的偏斜(skew),只有同步复位大于时钟最大周期加上同步信号穿过的组合逻辑路径延时加上时钟偏斜时,才能保证同步复位可靠、彻底。
上图中,假设同步复位逻辑树组合逻辑的延时为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
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
上图是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
只要存在复位都会增加布局布线的负担,因为复位会像时钟一样连接到每一个寄存器上,是相当复杂的工程,会增加时序收敛的难度。
对于同一个触发器逻辑,因为同时支持异步和同步复位,所以异步复位并不会节省资源;对于其他的资源,比如 DSP48 等,同步复位更加节省资源。
首先,对于 DSP48,其内部还带有一些寄存器(只支持同步复位),如果使用异步复位,则会额外使用外部 Slice 中带异步复位的寄存器,而使用同步复位时,可以利用 DSP48 内部的寄存器;Xilinx 的 FPGA,对于 DSP48、BRAM 资源,使用同步复位比异步复位更节省资源。
对于高电平复位,使用异步复位同步释放,则第一个寄存器的 D 输入是 0,这里使用了 4 个触发器打拍同步。
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一旦产生,输出立刻复位,且同样是同步释放,好像这种处理才更符合异步复位、同步释放。
当作同步复位的差别只在于复位时间会稍晚一些,要在时钟的下一个边沿检测到,但是还是能够识别到输入的rst_async异步复位信号,所以从复位角度来说,都能够后实现复位效果;
根据Xilinx复位准则,我们知道同步复位相比异步复位有很多好处,具体参见:Xilinx FPGA 复位策略白皮书(WP272) 公众号-FPGA探索者做了翻译可以参考,既然两者对后级复位没有功能上的差别,那么优先选择同步复位;
尽量少使用复位,特别是少用全局复位,能不用复位就不用,一定要用复位的使用局部复位;
如果必须要复位,在同步和异步复位上,则尽量使用同步复位,一定要用异步复位的地方,采用“异步复位、同步释放”;
复位电平选择高电平复位;
Altera的FPGA,低电平复位,其触发器只有异步复位端口,所以如果想要用同步复位,需要额外的资源来实现,这也是“异步复位节省资源”这一说法的原因。
具体电路及代码见上文
在RTL建模时,使用可综合的Verilog语法是整个Verilog语法中的非常小的一个子集。其实可综合的Verilog常用关键字非常有限,这恰恰体现了Verilog语言是硬件描述语言的本质,Verilog作为HDL,其本质在于把电路流畅、合理的转换为语言形式,而使用较少的一些关键字就可以有效的将电路转换到可综合的RTL语言结构。常用的RTL语法结构列举:
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的沿读写寄存器的描述看起来比前面介绍的使用CPU时钟同步读写寄存器的描述简单,但是读者必须明确这种方式正常工作有两个前提条件:
只有这两个条件同时满足的前提下,才能保证使用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的电平作为敏感信号来进行读写。
全部0条评论
快来发表一下你的评论吧 !