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
