RTL(Register transfer Level)级和综合(Synthesize)的概念
在之前我们已经谈过,HDL语言有五个层次:系统级,行为级,RTL级,门级,晶体管级。而我们主要也是在RTL级使用Verilog语言。
RTL正如它名字说的那样,主要描述的是寄存器到寄存器之间逻辑功能的实现,它不具体关心到底使用了多少逻辑门,因而比门级更为简单和高效。
RTL级的重要特点:可综合。
何谓综合?综合即将原理图或者HDL语言描述的电路转换成逻辑门的连接,门级网表。
RTL级的基本要素和设计步骤
典型的RTL级的设计包含三个部分:时钟域描述,时序逻辑描述,组合逻辑描述。
较为推荐的设计步骤如下:
1.功能定义与模块拆分
2.定义所有模块接口
3.设计时钟域:注意全局时钟资源几乎没有时钟偏斜(Clock Skew)但时延(Clock Delay)大,驱动能力强;第二全局时钟偏斜小,但时延小,驱动能力次之。
4.考虑设计中的关键路径:关键路径即时序要求最紧张的路径,主要由频率、建立时间(Tsetup)、保持时间(Thold)等制约,同时可以用pipeline或者逻辑复用等方法缓解。
5.顶层设计:推荐使用自顶向下的设计,这同模块规划是一致的。
6.FSM状态机:后续有专门介绍
7.时序逻辑设计
8.组合逻辑设计
常用RTL级建模
非阻塞赋值、阻塞赋值、连续赋值
这里再次提到了这三个概念,可见其非常重要。
为了避免错误:推荐在组合逻辑电路中仅使用阻塞赋值,在时序逻辑设计中统一使用非阻塞赋值。
//cnt1================
reg [3:0] cnt_out;
always@(posedge clk)
cnt_out <= cnt_out + 1;
//cnt2================
reg [3:0] cnt_out_plus;
always@(cnt_out)
cnt_out_plus = cnt_out +1;
//cnt3================
wire [3:0] cnt_out_plus;
assign cnt_out_plus = cnt_out + 1;
注意:在cnt2和cnt3中不能使用 cnt_out = cnt_out + 1;原因在于他们都是组合逻辑电路,这样写必然造成组合逻辑的闭环,可能产生竞争冒险,而时序逻辑在时钟控制下看似存在电气连接但是实际上不会连续赋值(即输出在输入改变立刻改变)。
寄存器电路建模
寄存器和组合逻辑是数字逻辑电路中的两大基本要素。
注意:
1.不是所有的reg型声明实际上都是寄存器,正如上例cnt2那样。只有既是reg类型变量又是由时钟控制的综合实现时才是寄存器。
2.在always敏感表中时钟沿是通过posedge、negedge来表示的。
3.异步复位/置位:无论时钟沿是否到达,复位信号一经产生就会触发复位。需要注意的是:实际上虽然异步复位看起来类似组合电路的输出瞬时随着输入变化,但是考虑到触发器的特性,它也要满足一定的removal和recovery条件,否则会产生亚稳态。
4.同步复位/置位:复位信号即使到达了,也需要等待时钟信号到达才能生效。避免异步复位可能产生亚稳态的问题,但是要求复位信号能持续一个时钟周期以上,这是不利的。实际上我们之后会具体讨论二者优劣,而真正使用的则是异步复位,同步释放。
//同步复位============
reg reset;
always@(posedge clk)
if(!reset)
cnt = 4'b0000;
......
//异步复位============
always@(posedge clk or negedge reset)
if(!reset)
cnt = 4'b0000;
......
5.同时使用时钟上升沿和下降沿:如果同时使用了上升沿和下降沿,实际上就是采用了一个倍频时钟,需要注意,往往时钟的上升沿和下降沿信号的质量是不同的,所以并不推荐这么做,不如直接在时钟信号上做倍频然后取一个质量好的边沿。
下面第一个电路非常重要,不可以直接使用cnt去加,必然会产生竞争冒险,需要用一个数据选择器再接出来,用时钟的电平控制输出。
//既使用了上升沿也使用了下降沿,相当于倍频电路
reg[3:0] temp1,temp2;
wire[3:0] cnt;
always@(posedge clk_100M or negedge rst)
begin
if(~rst)
temp1 = 0;
else
temp1 <= temp2 + 1;
end
always@(negedge clk_100M or negedge rst)//同一个时钟的两个边沿不能放在一个always语句中
begin
if(~rst)
temp2 = 0;
else
temp2 <= temp1 + 1;
end
assign cnt = (clk)? temp1 : temp2;
always@(negedge clk_100M or negedge reset)
......
//这个电路和上面那个是等效的
always@(posedge clk_50M or negedge reset)
......
组合逻辑建模
组合逻辑的特点就是与时钟无关,随输入电平变化而变化。RTL级主要包含always模块与assign等关键字描述这两种组合逻辑电路。
1.always模块中需要注意,必须把敏感表写完整,其次推荐使用阻塞赋值"="因为虽然变量是reg但是纯粹是处于语法需要,综合时实际仍然是作为线网实现的。
2.assign结构比较适合描述那些较为简单的逻辑,如果逻辑复杂,用assign描述会使得代码可读性不强。
//实际上是一个低电平片选译码电路
//通过always模块实现
reg cs1,cs2,cs3,cs4;//use reg type, but not registers
always@(cs or addr[7:6])
if(cs)
{cs1,cs2,cs3,cs4} = 4'b1111;
else
begin
case(addr[7:6]):
chip1_decode:{cs1,cs2,cs3,cs4} = 4'b0111;
chip2_decode:{cs1,cs2,cs3,cs4} = 4'b1011;
chip3_decode:{cs1,cs2,cs3,cs4} = 4'b1101;
chip4_decode:{cs1,cs2,cs3,cs4} = 4'b1110;
endcase//建议加上default因为亚稳态存在
end
//也可以通过assign实现===========
reg cs1,cs2,cs3,cs4;
assign cs1 = (!cs && (addr[7:6]==chip1_decode)) 0 : 1;
assign cs2 = (!cs && (addr[7:6]==chip2_decode)) 0 : 1;
assign cs3 = (!cs && (addr[7:6]==chip3_decode)) 0 : 1;
assign cs4 = (!cs && (addr[7:6]==chip4_decode)) 0 : 1;
//实际上仍然存在一些隐患,且描述较为复杂,易读性差
双向端口与三态信号建模
首先,必须明确的是,为了避免仿真结果和综合实现结果不一致为了便于维护,三台总线应该一律放在顶层,禁止在顶层以外的地方赋值高阻态“Z”。
同样,若总线结构较为复杂(显然大多数情况是这样的),应该使用case语句而非assign语句去描述总线。
inout [7:0] data_bus;
wire [7:0] data_in;
reg [7:0] data_out;
wire [7:0] cnt_out;
always@(decode_out or cnt_out or sel1 or sel2 or sel3)
begin
case({sel1,sel2,sl3})
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
对于中间变量wire应该注意到:是因为inout只能是一个wire型或者tri型不能再always中直接赋值。
MUX建模
多路选择器是很常见也很常用的一种组合逻辑电路,也有两种通常的方式对它进行建模,其一就是用assign和“?:”结构,其二就是用case。实际上之前的例子中也已经出现了,就不再举例了。
存储器建模
逻辑电路中经常使用一些单口RAM,双口RAM和ROM等存储器。以下举例说明:
reg [7:0] RAM8x64 [63:0];//定义了一个
reg [7:0] mem;
always@(posedge clk)
if(WR && CS)
RAM8x64[addr] <= data_in[7:0];
else if(!WR && CS)
mem <= RAM8x64[addr];
存储器的读写有以下特点:不能单独存储某一位,必须通过地址来操作一个存储单元即这里是1字节,写入时可以直接写入,但是要操作时必须经过一个寄存器读出然后在寄存器内操作。
以上说的是一般方法,但是许多开发平台中都已经内嵌了存储器类型,只需要调用并赋值参数即可。
简单时钟分频电路
时钟电路是PLD的核心,对于大多数的PLD来说都内嵌有相应的时钟模块,但是有时我们需要实现一些特定的功能的时候,往往不能直接依靠给定的时钟模块实现。
时钟处理中需要注意的是分频和调相。
对于偶数分频,往往不难实现,但是奇数分频同时还要进行调相和控制占空比,这就有一定的难度了。
//把一个时钟进行2,4,8分频,要求分频后的时钟同相
reg[3:0] cnt;
always@(posedge clk)begin
if(reset)
cnt <= 3'b0;
else begin
cnt <= cnt + 3'b1;
end
end
assign clk_2 = ~cnt[1];//开始时应该都是pos即上升
assign clk_4 = ~cnt[2];
assign clk_8 = ~cnt[3];
//三分频模块,使用三段式状态机实现,书上是一段式
reg[1:0] state;
reg[1:0] next_state;
always@(*)begin
case(state)
2'b00: next_state = 2'b01;
2'b01: next_state = 2'b10;
2'b10: next_state = 2'b00;
default:next_state = 2'b00;
endcase
end
always@(posedge clk or negedge reset)begin
if(~reset)
state <= 2'b00;
else
state <= next_state;
end
always@(posedge clk)begin
if(next_state==2'b01)
clk_3 <= ~clk_3;
end
串/并联转换建模
有多种实现方式,可以通过移位寄存器,RAM转换,也可以写一个状态机来进行转换。
reg[7:0] out;
always@(posedge clk or posedge reset)begin
if(reset)
out <= 8'b0;
else
out <= {out, pal_in};
end
同步复位与异步复位
复位信号是硬件电路中重要的信号,因为往往在开机工作时,难以确定内部电路究竟是什么情况,需要依靠复位电路来进行复位,从而实现初始态已知。通常复位信号为低电平,同时也会上拉电阻以提高抗干扰性能。
复位主要有两种:同步复位与异步复位。
同步复位
同步复位指的就是在always敏感表中是不含复位信号的,因此只有在clk指定边沿的时候复位信号才会发生作用。
优点:可以有效过虑毛刺信号(很短时间的信号)的干扰,同时可以使得时序百分百同步,便于分析和仿真。
缺点:同步复位对复位信号是有要求的,即复位信号必须要保持一个周期以上,以保证复位信号能够在极端情况下(刚好一个指定边沿才过去时复位信号来了)还能起作用。同时在FPGA等触发器中并不包含同步复位信号,使用同步复位会占用资源。
always@(posedge clk)begin
if(~resetn)
...
end
异步复位
异步复位指的是在always敏感表中是存在复位信号的,这样复位信号的作用就不受到时钟的制约,一旦复位信号生效,输出立即发生改变。
优点:电路简单,多数PLD中都预置有异步复位。
缺点:释放和时钟无关,很可能在释放时产生数据冒险,而且一旦组合逻辑产生毛刺,会在输出中反映出来。
always@(posedge clk or negedge resetn)begin
if(~resetn)
...
end
异步复位同步释放
这是推荐采用的方式,这样既不会对电路的输出时序产生影响,也能对异步的信号敏感。
简单说就是先存储进复位信号,然后在下一个周期在送往寄存器的异步复位端。
用case 和 if...else建模
关于这两者的差别,主要是case语句在执行时,各分支是并行执行的,而对于if...else语句他是带有优先级的,先判断哪个再判断哪个。
另外要说明的是,在组合电路中,如果case不给default以及if不给else很可能会出现锁存器(Latch)如果并非刻意制造锁存器,一定要避免。
具体的问题还是应该通过习题去练习。
主要内容和示例来源:《轻松称为设计高手 Verilog HDL实用精解》;北京航空航天大学出版社;
推荐练习网站:https://hdlbits.01xz.net/wiki