一
Verliog語法基礎
基本的語法略過,主要想寫一些關于框架,規范,技術難點的博文,這樣對于我們養成好的編碼習慣是有好處的,就定這樣一個flag吧.希望大家可以一起好好學習,共同進步.
接口時序設計規范
模塊和模塊之間的通過模塊的接口實現關聯, 因此規范的時序設計, 對于程序設計的過程, 以及程序的維護, 團隊之間的溝通都是非常必要的。
命名規則
1、 頂層文件
對象+功能+top
比如:video_oneline_top
2、 邏輯控制文件
介于頂層和驅動層文件之間
對象+ctr
比如:ddr_ctr.v
3、 驅動程序命名
對象+功能+dri
比如:lcd_dri.v、 uart_rxd_dri.v
4、 參數文件命名
對象+para
比如:lcd_para.v
5、 模塊接口命名:文件名+u
比如 lcd_dir lcd_dir_u(........)
6、 模塊接口命名:特征名+文件名+u
比如 mcb_read c3_mcb_read_u
7、 程序注釋說明
/*****************************************************************/
// Company:
// Engineer:
// WEB:
// BBS:
// Create Date: 0750 07/31/2019
// Design Name: FPGA STREAM
// Module Name: FPGA_USB
// Project Name: FPGA STREAM
// Target Devices: XC6SLX16-FTG256/XC6SLX25-FTG256 Mis603
// Tool versions: ISE14.7
// Description: CY7C68013A SLAVE FIFO comunication with fpga
// Revision: V1.0
// Additional Comments:
//1) _i input
//2) _o output
//3) _n activ low
//4) _dg debug signal
//5) _r delay or register
//6) _s state mechine
/*****************************************************************/
8、 端口注釋 input Video_vs_i,//輸入場同步入
9、 信號命名 命名總體規則:對象+功能(+極性) +特性
10、 時鐘信號 對象+功能+特性 比如:phy_txclk_i、 sys_50mhz_i
11、 復位信號 對象+功能+極性+特性 比如:phy_rst_n_i、 sys_rst_n_i
12、 延遲信號 對象+功能+特性 1+特征 2 比如:fram_sync_i_r0、 fram_sync_i_r1
13、 特定功能計數器
對象+cnt 比如:line_cnt、 div_cnt0、 div_cnt1 功能+cnt 比如:wr_cnt、 rd_cnt 對象+功能+cnt 比如:fifo_wr_cnt、 mcb_wr_cnt、 mem_wr_cnt 對象+對象+cnt 比如:video_line_cnt、 video_fram_cnt
14、 一般計數器 cnt+序號 用于不容易混淆的計數 比如:cnt0、 cnt1、 cnt2
15、 時序同步信號 對象+功能+特性 比如:line_sync_i、 fram_sysc_i
16、 使能信號 功能+en 比如:wr_en、 rd_en 對象+功能+en 比如:fifo_wr_en、 mcb_wr_en
Verilog 最最基礎語法
C 語言和 Verilog 的關鍵詞和結構對比:
C 語言和 Verilog 運算符對比:
關鍵字
信號部分:input 關鍵詞, 模塊的輸入信號, 比如 input Clk, Clk 是外面關鍵輸入的時鐘信號;
output 關鍵詞, 模塊的輸出信號, 比如 output[3:0]Led; 這個地方正好是一組輸出信號。其中[3:0]表示 0~3 共 4 路信號。
inout 模塊輸入輸出雙向信號。這種類型, 我們的例子 24LC02 中有使用。數總線的通信中, 這種信號被廣泛應用;
wire 關鍵詞, 線信號。例如:wire C1_Clk; 其中 C1_Clk 就是 wire 類型的信號;
線信號,三態類型, 我們一般常用的線信號類型有input,output,inout,wire;
reg 關鍵詞, 寄存器。和線信號不同, 它可以在 always 中被賦值, 經常用于時序邏輯中。比如 reg[3:0]Led;表示了一組寄存器。
結構部分:
module()… endmodule代表一個模塊, 我們的代碼寫在這個兩個關鍵字中間
always@()括號里面是敏感信號。這里的 always@(posedge Clk)敏感信號是 posedge Clk 含義是在上升沿的時候有效, 敏感信號還可以 negedge Clk 含義是下降沿的時候有效, 這種形式一般時序邏輯都會用到。還可以是*這個一符號, 如果是一個*則表示一直是敏感的, 一般用于組合邏輯。
assign 用來給 output,inout 以及 wire 這些類型進行連線。assign 相當于一條連線, 將表達式右邊的電路直接通過 wire(線)連接到左邊, 左邊信號必須是 wire 型(output 和 inout 屬于 wire 型) 。當右邊變化了左邊立馬變化, 方便用來描述簡單的組合邏輯。
符號部分:
這里重點講解一下“<=” 賦值符號,非阻塞賦值?,“=” 阻塞賦值,“{}”
“<=” 賦值符號, 非阻塞賦值, 在一個 always 模塊中, 所有語句一起更新。它也可以表示小于等于, 具體是什么含義編譯環境根據當前編程環境判斷, 如果“<=” 是用在一個 if 判斷里如:if(a <= 10);當然就表示小于等于了。
“=” 阻塞賦值, 或者給信號賦值, 如果在 always 模塊中, 這條語句被立刻執行。阻塞賦值和非阻塞賦值將再后面詳細舉例說明。
“{} ” 在 Verilog 中表示拼接符, {a,b}這個的含義是將括號內的數按位并在一起, 比如:{1001,1110}表示的是 10011110。拼接是 Verilog 相對于其他語言的一大優勢, 在以后的編程中請慢慢體會。
參數部分:
parameter
parameter a = 180;//十進制, 默認分配長度 32bit(編譯器默認)parameter a = 8’d180;//十進制parameter a = 8’haa; //十六進制parameter a = 8’b1010_1010; //二進制
預處理命令
`include file1.v`define X = 1;`deine Y;`ifdef YZ=1;`elseZ=0;`endif
Verilog 中數值表示的方式
如果我們要表示一個十進制是 180 的數值, 在 Verilog 中的表示方法如下:
二進制:8’ b1010_1010; //其中“_” 是為了容易觀察位數, 可有可無。
十進制:8’ d180;
16 進制:8’ hAA;
講到這里,具備這些基礎知識,需要通過代碼來學習Veriog 語言。最后, 筆者提一點建議, 學習 Verilog 多看別人寫的優秀的代碼, 多看官方提供的代碼和文檔。其中官方提供的代碼, 很多時候代表了最新的用法, 或者推薦的用法。讀者學習, 首先把最最基礎的掌握好, 這樣, 在項目中遇到了問題, 也能快速學習, 快速解決。
對于理論知識的學習, 沒必要一開始就研究得那么深刻, 只是搞理論學習, 對于學習Verilog 語言, 或者 FPGA 開發是不實際的, 要聯系理論和實踐結合。多仿真, 多驗證, 多問題, 多學習, 多改進.
三
淺談狀態機
01. 前言
狀態機是FPGA設計中一種非常重要、非常根基的設計思想,堪稱FPGA的靈魂,貫穿FPGA設計的始終。
02.狀態機簡介
什么是狀態機:狀態機通過不同的狀態遷移來完成特定的邏輯操作(時序操作)狀態機是許多數字系統的核心部件, 是一類重要的時序邏輯電路。通常包括三個部分:
-
下一個狀態的邏輯電路
-
存儲狀態機當前狀態的時序邏輯電路
-
輸出組合邏輯電路
03. 狀態機分類
通常, 狀態機的狀態數量有限, 稱為有限狀態機(FSM) 。由于狀態機所有觸發器的時鐘由同一脈沖邊沿觸發, 故也稱之為同步狀態機。
根據狀態機的輸出信號是否與電路的輸入有關分為 Mealy 型狀態機和 Moore 型狀態機
3.1,Mealy 型狀態機
電路的輸出信號不僅與電路當前狀態有關, 還與電路的輸入有關
3.2,Moore 型狀態機
電路的輸出僅僅與各觸發器的狀態, 不受電路輸入信號影響或無輸入
狀態機的狀態轉移圖, 通常也可根據輸入和內部條件畫出。一般來說, 狀態機的設計包含下列設計步驟:
-
根據需求和設計原則, 確定是 Moore 型還是 Mealy 型狀態機;
-
分析狀態機的所有狀態, 對每一狀態選擇合適的編碼方式, 進行編碼;
-
根據狀態轉移關系和輸出繪出狀態轉移圖;
-
構建合適的狀態機結構, 對狀態機進行硬件描述。
04. 狀態機描述
狀態機的描述通常有三種方法, 稱為一段式狀態機, 二段式狀態機和三段式狀態機。
狀態機的描述通常包含以下四部分:
-
利用參數定義語句 parameter 描述狀態機各個狀態名稱, 即狀態編碼。狀態編碼通常有很多方法包含自然二進制編碼, One-hot 編碼,格雷編碼碼等;
-
用時序的 always 塊描述狀態觸發器實現狀態存儲;
-
使用敏感表和 case 語句(也采用 if-else 等價語句) 描述狀態轉換邏輯;
-
描述狀態機的輸出邏輯。
下面根據狀態機的三種方法來具體說明
4.1,一段式狀態機
module detect_1(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//狀態聲明和狀態編碼
reg [1:0] state;
parameter [1:0] S0=2‘b00;
parameter [1:0] S1=2’b01;
parameter [1:0] S2=2‘b10;
parameter [1:0] S3=2’b11;
always@(posedge clk_i)
begin
if(!rst_n_i)begin
state《=0;
out_r《=1‘b0;
end
else
case(state)
S0 :
begin
out_r《=1’b0;
state《= S1;
end
S1 :
begin
out_r《=1‘b1;
state《= S2;
end
S2 :
begin
out_r《=1’b0;
state《= S3;
end
S3 :
begin
out_r《=1‘b1;
end
endcase
end
assign out_o=out_r;
endmodul
一段式狀態機是應該避免使用的, 該寫法僅僅適用于非常簡單的狀態機設計。
4.2,兩段式狀態機
module detect_2(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//狀態聲明和狀態編碼
reg [1:0] Current_state;
reg [1:0] Next_state;
parameter [1:0] S0=2‘b00;
parameter [1:0] S1=2’b01;
parameter [1:0] S2=2‘b10;
parameter [1:0] S3=2’b11;
//時序邏輯:描述狀態轉換
always@(posedge clk_i)
begin
if(!rst_n_i)
Current_state《=0;
else
Current_state《=Next_state;
end
//組合邏輯:描述下一狀態和輸出
always@(*)
begin
out_r=1‘b0;
case(Current_state)
S0 :
begin
out_r=1’b0;
Next_state= S1;
end
S1 :
begin
out_r=1‘b1;
Next_state= S2;
end
S2 :
begin
out_r=1’b0;
Next_state= S3;
end
S3 :
begin
out_r=1‘b1;
Next_state=Next_state;
end
endcase
end
assign out_o = out_r;
endmodule
兩段式狀態機采用兩個 always 模塊實現狀態機的功能, 其中一個 always 采用同步時序邏輯描述狀態轉移, 另一個 always 采用組合邏輯來判斷狀態條件轉移。
4.3,三段式狀態機
module detect_3(
input clk_i,
input rst_n_i,
output out_o
);
reg out_r;
//狀態聲明和狀態編碼
reg [1:0] Current_state;
reg [1:0] Next_state;
parameter [1:0] S0=2‘b00;
parameter [1:0] S1=2’b01;
parameter [1:0] S2=2‘b10;
parameter [1:0] S3=2’b11;
//時序邏輯:描述狀態轉換
always@(posedge clk_i)
begin
if(!rst_n_i)
Current_state《=0;
else
Current_state《=Next_state;
end
//組合邏輯:描述下一狀態
always@(*)
begin
case(Current_state)
S0:
Next_state = S1;
S1:
Next_state = S2;
S2:
Next_state = S3;
S3:
begin
Next_state = Next_state;
end
default :
Next_state = S0;
endcase
end
//輸出邏輯: 讓輸出 out, 經過寄存器 out_r 鎖存后輸出, 消除毛刺
always@(posedge clk_i)
begin
if(!rst_n_i)
out_r《=1‘b0;
else
begin
case(Current_state)
S0,S2:
out_r《=1’b0;
S1,S3:
out_r《=1‘b1;
default :
out_r《=out_r;
endcase
end
end
assign out_o=out_r;
endmodule
三段式狀態機在第一個 always 模塊采用同步時序邏輯方式描述狀態轉移, 第二個always 模塊采用組合邏輯方式描述狀態轉移規律, 第三個 always 描述電路的輸出。通常讓輸出信號經過寄存器緩存之后再輸出, 消除電路毛刺。
05. 狀態機優缺點
1、一段式狀態機:只涉及時序電路,沒有競爭與冒險,同時消耗邏輯比較少。
但是如果狀態非常多,一段式狀態機顯得比較臃腫,不利于維護。
2、兩段式狀態機:當一個模塊采用時序(狀態轉移),一個模塊采用組合時候(狀態機輸出),組合邏輯電路容易造成競爭與冒險;當兩個模塊都采用時序,可以避免競爭與冒險的存在,但是整個狀態機的時序上會延時一個周期。
兩段式狀態機是推薦的狀態機設計方法。
3、三段式狀態機:三段式狀態機在狀態轉移時采用組合邏輯電路+格雷碼,避免了組合邏輯的競爭與冒險;狀態機輸出采用了同步寄存器輸出,也可以避免組合邏輯電路的競爭與冒險;采用這兩種方法極大的降低了競爭冒險。并且在狀態機的采用這種組合邏輯電路+次態寄存器輸出,避免了兩段式狀態機的延時一個周期(三段式狀態機在上一狀態中根據輸入條件判斷當前狀態的輸出,從而在不插入額外時鐘節拍的前提下,實現寄存器的輸出)。
三段式狀態機也是比較推崇的,主要是由于維護方便, 組合邏輯與時序邏輯完全獨立。
06. 總結
靈活選擇狀態機,不一定要拘泥理論,怎樣方便怎樣來
07.擴展
四段式不是指三個always代碼,而是四段程序。使用四段式的寫法,可參照明德揚GVIM特色指令Ztj產生的狀態機模板。
明·德·揚四段式狀態機符合一次只考慮一個因素的設計理念。
-
第一段代碼,照抄格式,完全不用想其他的。
-
第二段代碼,只考慮狀態之間的跳轉,也就是說各個狀態機之間跳轉關系。
-
第三段代碼,只考慮跳轉條件。
-
第四段,每個信號逐個設計。
四
Test bench文件結構一覽無余
01,前言
Verilog測試平臺是一個例化的待測(MUT)模塊,重要的是給它施加激勵并觀測其輸出。邏輯模塊與其對應的測試平臺共同組成仿真模型,應用這個模型可以測試該模塊能否符合自己的設計要求。
編寫TESTBENCH的目的是為了對使用硬件描述語言設計的電路進行仿真驗證,測試設計電路的功能、性能與設計的預期是否相符。通常,編寫測試文件的過程如下:
-
產生模擬激勵(波形);
-
將產生的激勵加入到被測試模塊中并觀察其響應;
-
將輸出響應與期望值相比較。
02,完成的Test bench文件結構
通常,一個完整的測試文件其結構為
-
module Test_bench();//通常無輸入無輸出
-
信號或變量聲明定義
-
邏輯設計中輸入對應reg型
-
邏輯設計中輸出對應wire型
-
使用initial或always語句產生激勵
-
例化待測試模塊
-
監控和比較輸出響應
-
endmodule
03,時鐘激勵設計
下面列舉出一些常用的封裝子程序, 這些是常用的寫法, 在很多應用中都能用到。
3.1,時鐘激勵產生方法一
50%占空比時鐘
parameterClockPeriod=10;initialbeginclk_i=0;forever#(ClockPeriod/2)clk_i=~clk_i;end
3.2,時鐘激勵產生方法二
50%占空比時鐘
initialbeginclk_i=0;always#(ClockPeriod/2)clk_i=~clk_i;end
3.3,時鐘激勵產生方法三:
產生固定數量的時鐘脈沖
initialbeginclk_i=0;repeat(6)#(ClockPeriod/2)clk_i=~clk_i;end
3.4,時鐘激勵產生方法四
產生非占空比為50%的時鐘
initialbeginclk_i=0;foreverbegin#((ClockPeriod/2)-2)clk_i=0;#((ClockPeriod/2)+2)clk_i=1;end04,復位信號設計
4.1,復位信號產生方法一
異步復位
initialbeginrst_n_i=1;#100;rst_n_i=0;#100;rst_n_i=1;end
4.2,復位信號產生方法二
同步復位
initialbeginrst_n_i=1;@(negedgeclk_i)rst_n_i=0;#100;//固定時間復位repeat(10)@(negedgeclk_i);//固定周期數復位@(negedgeclk_i)rst_n_i=1;end
4.3復位信號產生方法三
復位任務封裝
taskreset;input[31:0]reset_time;//復位時間可調,輸入復位時間RST_ING=0;//復位方式可調,低電平或高電平beginrst_n=RST_ING;//復位中#reset_time;//復位時間rst_n_i=~RST_ING;//撤銷復位,復位結束endendtask05,雙向信號設計
5.1,雙向信號描述一
inout在testbench中定義為wire型變量
//為雙向端口設置中間變量inout_reg作為inout的輸出寄存,其中inout變//量定義為wire型,使用輸出使能控制傳輸方向//inoutbir_port;wirebir_port;regbir_port_reg;regbi_port_oe;assignbi_port=bi_port_oe?bir_port_reg:1'bz;
5.2雙向信號描述二
強制force
//當雙向端口作為輸出口時,不需要對其進行初始化,而只需開通三態門//當雙向端口作為輸入時,只需要對其初始化并關閉三態門,初始化賦值需//使用wire型數據,通過force命令來對雙向端口進行輸入賦值//assigndinout=(!en)din:16'hz;完成雙向賦值initialbeginforcedinout=20;#200forcedinout=dinout-1;end06,特殊信號設計
6.1特殊激勵信號產生描述一
輸入信號任務封裝
taski_data;input[7:0]dut_data;begin@(posedgedata_en);send_data=0;@(posedgedata_en);send_data=dut_data[0];@(posedgedata_en);send_data=dut_data[1];@(posedgedata_en);send_data=dut_data[2];@(posedgedata_en);send_data=dut_data[3];@(posedgedata_en);send_data=dut_data[4];@(posedgedata_en);send_data=dut_data[5];@(posedgedata_en);send_data=dut_data[6];@(posedgedata_en);send_data=dut_data[7];@(posedgedata_en);send_data=1;#100;endendtask//調用方法:i_data(8'hXX);
6.2特殊激勵信號產生描述二
多輸入信號任務封裝
taskmore_input;input[7:0]a;input[7:0]b;input[31:0]times;output[8:0]c;beginrepeat(times)//等待times個時鐘上升沿@(posedgeclk_i)c=a+b;//時鐘上升沿a,b相加endendtask//調用方法:more_input(x,y,t,z);//按聲明順序
6.3,特殊激勵信號產生描述三
輸入信號產生,一次SRAM寫信號產生
initialbegincs_n=1;//片選無效wr_n=1;//寫使能無效rd_n=1;//讀使能無效addr=8'hxx;//地址無效data=8'hzz;//數據無效#100;cs_n=0;//片選有效wr_n=0;//寫使能有效addr=8'hF1;//寫入地址data=8'h2C;//寫入數據#100;cs_n=1;wr_n=1;#10;addr=8'hxx;data=8'hzz;end
Testbench中@與wait
//@使用沿觸發//wait語句都是使用電平觸發initialbeginstart=1'b1;wait(en=1'b1);#10;start=1'b0;end
07,仿真控制語句及系統任務描述
7.1,仿真控制語句及系統任務描述
$stop//停止運行仿真,modelsim中可繼續仿真$stop(n)//帶參數系統任務,根據參數0,1或2不同,輸出仿真信息$finish//結束運行仿真,不可繼續仿真$finish(n)//帶參數系統任務,根據參數0,1或2不同,輸出仿真信息//0:不輸出任何信息//1:輸出當前仿真時刻和位置//2:輸出當前仿真時刻、位置和仿真過程中用到的memory以及CPU時間的統計$random//產生隨機數$random%n//產生范圍-n到n之間的隨機數{$random}%n//產生范圍0到n之間的隨機數?
7.2,仿真終端顯示描述
$monitor//仿真打印輸出,大印出仿真過程中的變量,使其終端顯示/*$monitor($time,,,"clk=%dreset=%dout=%d",clk,reset,out);*/$display//終端打印字符串,顯示仿真結果等/*$display(”Simulationstart!");$display(”Attime%t,inputis%b%b%b,outputis%b",$time,a,b,en,z);*/$time//返回64位整型時間$stime//返回32位整型時間$realtime//實行實型模擬時間
7.3文本輸入方式
$readmemb/$readmemh//激勵具有復雜的數據結構//verilog提供了讀入文本的系統函數$readmemb/$readmemh("<數據文件名>",<存儲器名>);$readmemb/$readmemh("<數據文件名>",<存儲器名>,<起始地址>);$readmemb/$readmemh("<數據文件名>",<存儲器名>,<起始地址>,<結束地址>);$readmemb:/*讀取二進制數據,讀取文件內容只能包含:空白位置,注釋行,二進制數數據中不能包含位寬說明和格式說明,每個數字必須是二進制數字。*/$readmemh:/*讀取十六進制數據,讀取文件內容只能包含:空白位置,注釋行,十六進制數數據中不能包含位寬說明和格式說明,每個數字必須是十六進制數字。*//*當地址出現在數據文件中,格式為@hh...h,地址與數字之間不允許空白位置,可出現多個地址*/modulereg[7:0]memory[0:3];//聲明8個8位存儲單元integeri;initialbegin$readmemh("mem.dat",memory);//讀取系統文件到存儲器中的給定地址//顯示此時存儲器內容for(i=0;i<4;i=i+1)$display("Memory[%d]=%h",i,memory[i]);endendmodule
?
/*mem.dat文件內容
@001ABCD@003A1*/
//仿真輸出為
Memory[0]=xx;Memory[1]=AB;Memory[2]=CD;Memory[3]=A1;
08,總結
一個完整的設計,除了好的功能描述代碼,對于程序的仿真驗證是必不可少的。學會如何去驗證自己所寫的程序,即如何調試自己的程序是一件非常重要的事情。而RTL邏輯設計中,學會根據硬件邏輯來寫測試程序,即Testbench是尤其重要的。
五
【很重要】Testbenth前仿真全過程
01. 前言
在FPGA 高手養成記-Test bench文件結構一覽無余只是簡單的例舉了常用的 testbench 寫法,在工程應用中基本能夠滿足我們需求, 至于其他更為復雜的 testbench 寫法, 大家可參考其他書籍或資料。
testbench沒有像RTL代碼設計那樣嚴謹,我們可以在符合語法規則的前提下,隨意編寫我們的測試文件,有些在RTL代碼中不可綜合的語句,我們可以在testbench中實現。大體流程如下:
02.測試模塊設計
要測試我們的cpu需要ROM和RAM模塊,這就需要我們先做好這兩個模塊
這里定義了一個 1024 x 8 的RAM
再定義一個8192x 8 的ROM
ROM和RAM都還沒有裝入數據,等會我們會調用函數給他們裝數據,接下來是地址譯碼器,來控制ROM和RAM的打開與關閉。
各模塊建立好之后我們就開始仿真了。
03.仿真
這次教學我們用的是modelsim SE 10.0 版本進行教學,直接先在quartus II中建一個.v文件將其保存在原來的工程文件目錄中,并命名為cpu_top.v,直接在這里寫測試代碼
下面大家可以來完成cpu 的仿真過程了
3.1,模塊包含
首先,我們需要將我們剛寫好的那幾個模塊包含進去,即CPU模塊,ROM模塊,RAM模塊,地址譯碼器模塊,并寫好時間測量度,見下圖
3.2,定義頂層模塊
由于我們的設計只有兩個輸入,即時鐘模塊和復位模塊,凡是輸入信號在testbench中統一定義成reg型變量,凡是輸出或者雙向輸入輸出信號統一定義成wire型變量,我們的設計只有輸入沒有輸出,故只定義輸入和連線即可
下圖便是我們要組成的測試頂層模塊圖,我們定義的wire型變量,實際就是我們頂層模塊中,模塊模塊與模塊間的連線。而這些連線就是我們cpu的輸出,這樣我們就可以用我們的測試模塊來測試我們的cpu是否能正確工作
3.3. 元件例化
就是將各個模塊連接起來即可,這里就不做太多的說明了,因為以前都寫過很多次了
3.4.測試激勵的書寫
先寫好時鐘產生模塊和復位模塊.并將復位模塊用task任務封裝,這樣我們在測試過程中就可以隨時調用復位任務進行復位
時鐘為50Mhz,復位時間為20ns
然后,我們再用task封裝我們需要的模塊,我們來想一下,上電后,CPU會從ROM中讀兩個時鐘周期的數據是吧,但是我們的ROM現在還是空的,所以我們需要一個任務是往ROM中裝入程序,給ROM中裝數據我們可以用系統函數$readmemb,即打開一個文件,并將其中的數據送到我們之前定義的ROM中去
而test1.pro文件是需要我們自己定義的,我們可以在quartusII中再新建一個.v文件,在里面寫上我們自己定義的程序,并將其保存為.pro文件即可,至于寫什么程序,是我們隨便定義的.
裝完ROM和RAM的數據之后,按說就可以了進行波形仿真了,因為cpu是自動讀取數據的,下面我們先來做第一步仿真,我先把之后的代碼注釋掉,大家先看沒有被注釋掉的代碼
里面都是我們之前封裝好的函數,剛開始進行復位,然后進行第一步測試,之后停止,將其保存之后,并默認為用其打開,打開后見下圖
然后,file——new——library——ok即建好一個庫
點擊左上角的編譯按鈕,將我們之前寫好的所有.v文件全部都編譯進去
看到transcript一欄顯示編譯成功后即可,若沒有transcript一欄,可以選擇菜單中的view——transcript即可,若顯示有紅色錯誤,那就請讀者按照它的要求進行修改代碼,這說明你的代碼有問題,一般是連接問題
3.5,波形仿真
編譯成功后,雙擊cpu_top就可以開始波形仿真了
進入仿真頁面后,我們右擊cpu模塊將其加入至波形
大家先看兩個圖,等會結合這兩個圖給大家細細講解仿真過程
我們先來看第一個過程
上電后,cpu先從ROM中讀回兩個周期的數據,是從ROM的0地址開始的,再對比我們之前定義好的ROM,數據讀取正確,讀回的數據的前三位是111,即指令碼JMP,后13位003c為地址碼,JMP指令是將讀回的數據作為新的地址碼來讀取相應地址的數據。那么,下一步,cpu應該是從ROM的003c地址處讀數據才對,再看一下波形
對比波形后可知,cpu正好是從003c處讀取數據,讀到的數據指令碼位111即JMP,地址碼位0006,再到ROM的0006地址處看
這次讀回的指令碼位101,即LDA,也就是說將后13位地址碼對應的RAM中的數據讀回,送到累加器中,想一下,這時的RAM應該是打開的,而且雙向輸入輸出口的數據總線上應該是來自RAM的8位數據,由于ROM0006地址處的地址碼為1800是13位的,而RAM的地址是9位的,因此實際上我們從RAM中讀回的數據是從RAM的0地址讀回的,即我們之前給RAM寫好的0000_0000,再看一下波形
正如我們所想的一樣,數據總線上是0000_0000,RAM是打開的,地址為1800
就這樣,讀者可以自己再試一下,看看我們的cpu是不是按照我們之前給他的程序運行的,在這里我就不再給大家一一介紹了
雖然波形仿真很直觀,但是看久了就會令人眼花繚亂,尤其是數據很多的時候,我們只能看其中一部分,不能講所有數據看完整,這時候我們單單是用波形來仿真就遠遠不夠了,下面介紹用系統任務仿真的過程
3.6,系統任務仿真
再回到我們的代碼,注釋掉了一些代碼吧,我們把那些代碼給加上,以其中一個過程為例
假設讀回的指令碼位101,即LDA,如果我在fentch_8的高電平期間且在cpu輸出地址為奇數的時候記錄一下此時的時間、指令、地址、目的地址、數據的話就可以不用看波形,讓電腦來幫助我們來分析了,因此作如下處理
這里我延時60ns,是因為第一次記錄的時候數據總線上還沒有數據,只有延時一會才會有數據,即上面那張波形圖右邊那根黃色的線處記錄一下數據,并將其顯示。我們也可以加上一下標注,來幫助我們觀察
這樣我們再來仿真的時候就不用看波形了,直接打開transcript一欄觀察記錄即可
這樣便可以為我們省下大量的仿真時間
04. 總結
這里提出以下幾點建議供大家參考:
? 封裝有用且常用的 testbench, testbench 中可以使用 task 或 function 對代碼進行封裝, 下次利用時靈活調用即可;
? 如果待測試文件中存在雙向信號(inout)需要注意, 需要一個 reg 變量來表示輸入, 一個 wire 變量表示輸出;
? 單個 initial 語句不要太復雜,可分開寫成多個 initial 語句, 便于閱讀和修改;
? Testbench 說到底是依賴 PC 軟件平臺, 必須與自身設計的硬件功能相搭配。
六
串行口通信電路設計
1、頂層模塊
寫程序都一樣,不能多有的程序都寫在一個模塊里,那樣看起來很麻煩,出了錯誤也不好維護,對于一些小的程序我們可以寫在一個模塊里,但程序一旦復雜起來還是要懂得模塊化編程的,對于頂層模塊,最好是只寫接口就好了,例如:
這段代碼中,rx_232是我們的底層模塊名,后面跟著的那個rx呢是我們自己取的名字,是任意的。后面的一大串呢就是接口,為了直觀呢,建議大家采用我的這種寫法,看上去比較清楚明白,括號里面的接口是我們頂層文件的接口,括號外面的是我們調用底層模塊的接口,這些接口要一一對應正確才能保證數據之間的傳輸。
在頂層模塊中,我們只定義了數據輸入接口,用來接收數據,數據輸出接口,用于發送數據,時鐘接口,和復位接口。這四個接口是有輸入輸出關系的,對于其他的接口,是屬于我們整個模塊內部的接口,是模塊與模塊之間的接口,既非輸入,也非輸出,相當于一根導線一樣,所以我們把他們定義成wire型變量
2、波特率選擇模塊
單片機或者計算機在串口通信時的傳輸速率用波特率表示,9600bps表示的就是每秒鐘傳送9600位的數據,這里之所以計數到5027,在這里算一下。
1秒傳送9600位,那么傳送一位的時間就可以算出,即1s=1000_000_000ns,所以傳送一位數據需要1000_000_000/9600=
104166ns,而我們的時鐘周期為20ns,因此需要計數到104166/20=5028個時鐘周期
下面是串口通信時序圖
我再來解釋一下這個圖吧,我當時學單片機的時候還真是沒怎么重視這張圖,只知道只要一個指令就可以發送,沒有真正搞清楚是怎么發送和接受的,那就在這里復習一下吧,計算機和單片機之間進行通信,這里用的是rs232通信方式,即通信之前,計算機和單片機之前要設定好相同的波特率,只有波特率相同了才能進行通信。
其次,計算機發送數據時要先發送一個起始位,一般是低電平,后面跟著的是8位數據位,奇偶校驗位,停止位等,當起始位低電平信號傳送到我們的接收端口時,在接收模塊中會發送一個命令給波特率時鐘計數器,開始計時,計時到一半的時候會產生一個采樣高脈沖信號,當接收模塊檢測到這個高脈沖之后就會將數據存到寄存器中,當檢測到第11個脈沖信號時,也就是代表一幀的數據接收完畢,發送模塊就給波特率選擇模塊發送一個停止信號告訴它停止計時。
同時,當數據接收完畢之后也會產生一個信號告訴發送模塊,信號已經接收完畢,準備發送,這個時候發送模塊再給波特率計時模塊發送一個信號開始計時,計數到某一位的中間時產生一個采樣信號,當發送模塊檢測到采樣信號之后就將寄存器里的數據送到發送端,每次只送一位,這樣就實現了數據的接收與發送。
下面是波特率計時模塊的主要程序部分
3、數據接收模塊
在接收模塊中,為了準確的檢測計算機發送來的數據起始位的那個低電平信號,用到了邊沿脈沖檢測法,可以有效的避免毛刺現象帶來的問題
下面是發送部分的主要程序段
4、數據發送模塊
發送模塊原理上和接受模塊是一樣的,不同點就是接收模塊通過邊沿檢測法檢測起始位低電平信號來啟動接收數據,而發送模塊是通過檢測數據發送完畢后,我們認為得置一個低電平信號,發送模塊通過檢測這個低電平信號來啟動發送。見下圖
下面是生成的RTL視圖
下面是測試結果
七
手把手解析時序邏輯乘法器代碼
下面是一段16位乘法器的代碼,大家可以先瀏覽一下,之后我再做詳細解釋
module mux16(clk,rst_n,start,ain,bin,yout,done);input clk; //芯片的時鐘信號。input rst_n; //低電平復位、清零信號。定義為0表示芯片復位;定義為1表示復位信號無效。input start; //芯片使能信號。定義為0表示信號無效;定義為1表示芯片讀入輸入管腳得乘數和被乘數,并將乘積復位清零。input[15:0] ain; //輸入a(被乘數),其數據位寬為16bit.input[15:0] bin; //輸入b(乘數),其數據位寬為16bit.output[31:0] yout; //乘積輸出,其數據位寬為32bit.output done; //芯片輸出標志信號。定義為1表示乘法運算完成.reg[15:0] areg; //乘數a寄存器reg[15:0] breg; //乘數b寄存器reg[31:0] yout_r; //乘積寄存器reg done_r;reg[4:0] i; //移位次數寄存器//------------------------------------------------//數據位控制always @(posedge clk or negedge rst_n)if(!rst_n) i <= 5'd0;else if(start && i < 5'd17) i <= i+1'b1;else if(!start) i <= 5'd0;//------------------------------------------------//乘法運算完成標志信號產生always @(posedge clk or negedge rst_n)if(!rst_n) done_r <= 1'b0;else if(i == 5'd16) done_r <= 1'b1; //乘法運算完成標志else if(i == 5'd17) done_r <= 1'b0; //標志位撤銷assign done = done_r;//------------------------------------------------//專用寄存器進行移位累加運算always @(posedge clk or negedge rst_n) beginif(!rst_n) beginareg <= 16'h0000;breg <= 16'h0000;yout_r <= 32'h00000000;endelse if(start) begin //啟動運算if(i == 5'd0) begin //鎖存乘數、被乘數areg <= ain;breg <= bin;endelse if(i > 5'd0 && i < 5'd16) beginif(areg[i-1]) yout_r = {1'b0,yout[30:15]+breg,yout_r[14:1]}; //累加并移位else yout_r <= yout_r>>1; //移位不累加endelse if(i == 5'd16 && areg[15]) yout_r[31:16] <= yout_r[31:16]+breg; //累加不移位endendassign?yout?=?yout_r;endmodule
要理解這段代碼,首先要弄明白幾個點。
1、我們通常寫的十進制的乘法豎式,同樣適用于二進制。下面我們就以這個算式為例:1011 x 0111 =0100_1101。
2、兩個16位的數相乘,結果是32位的,沒有32位要在高位補零。
3、計算兩個16位的數相乘需要移位15次。
例如:
1 0 1 1
x 0 1 1 1
------------------------------------
1 0 1 1
1 0 1 1
1 0 1 1
0 0 0 0
------------------------------------
1 0 0 1 1 0 1
前三次計算是移位的,最后一次沒有移位
4、兩個16位的數相加,結果是17位的,不夠17位最高位補零。
例如語句yout[30:15]+breg,結果是17位的。
知道了這些,我們就開始看代碼了
1)、接口部分注釋寫的很清楚,這里就不提了
2)、數據位控制部分
always @(posedge clk or negedge rst_n)if(!rst_n) i <= 5'd0;else if(start && i < 5'd17) i <= i+1'b1;else if(!start) i <= 5'd0;
當start為1時,芯片讀入兩個數,此時開始計數,計數16次,乘法運算開始
3)、乘法運算完成標志信號產生
always @(posedge clk or negedge rst_n)if(!rst_n) done_r <= 1'b0;else if(i == 5'd16) done_r <= 1'b1; //乘法運算完成標志else if(i == 5'd17) done_r <= 1'b0; //標志位撤銷assign done = done_r;
這部分也很好理解
4)、專用寄存器進行移位累加運算
這里為了簡單,就用15到18位代替15到30位
以上部分是最主要的計算部分,其他地方相對來說還比較簡單,例如當乘數某一位為0時,不用累加,直接右移,當i計數到16時,此時就不用再移位了,可以直接用位數表示,直接累加即可。
下面是仿真圖
八
基于FIFO的串口發送機設計全流程
首先來解釋一下FIFO的含義,FIFO就是First Input First Output的縮寫,就是先入先出的意思,按照我的理解就是,先進去的數據先出,例如一個數組的高位先進,那么讀出來的時候也就高位先出。下面是百度百科的解釋。
FIFO一般用于不同時鐘域之間的數據傳輸,比如FIFO的一端是AD數據采集,另一端是計算機的PCI總線,假設其AD采集的速率為16位 100K SPS,那么每秒的數據量為100K×16bit=1.6Mbps,而PCI總線的速度為33MHz,總線寬度32bit,其最大傳輸速率為1056Mbps,在兩個不同的時鐘域間就可以采用FIFO來作為數據緩沖。另外對于不同寬度的數據接口也可以用FIFO,例如單片機為8位數據輸出,而DSP可能是16位數據輸入,在單片機與DSP連接時就可以使用FIFO來達到數據匹配的目的。
我們將這三個模塊分別定義為dataoutput塊,fifo_ctrl塊和uart_ctrl塊。現在考慮連線,具體到每一根連線,這樣根據圖來寫代碼要比直接用腦子構圖要方便的多。三個模塊,先考慮時鐘和復位信號線,三個模塊都有,然后,數據產生模塊要將產生的數據發給FIFO模塊,所以要有數據寫入線,我們定義它為wr-datain,數據寫入FIFO塊后總要輸出,這些數據就是我們要發送的數據,所以定義輸出數據線tx_data,先不管FIFO,我們再來定義數據發送模塊的連線,數據發送總要有個啟動信號,所以我們定義變量tx_start,之后,還要有一個輸出端給PC機,我們定義這個輸出端位rs232。
對于FIFO模塊的例化過程很簡單就不做過多的說明,只把接口說一下,FIFO模塊除了時鐘,復位信號外,還有數據輸入端口,這個端口要和之前的數據產生模塊的數據輸出端口相連,還有寫請求端口,高電平有效,數據發送模塊每隔1秒鐘產生一個16位的數據,并發送寫請求命令給FIFO,還有讀請求命令,高電平有效數據發送模塊在發送數據時要發送一個讀請求給FIFO,從中讀取數據后再發送給PC機,還有空信號empty,只要檢測到FIFO中有數據,empty就為低電平,我們可用這個信號來啟動數據發送模塊。這樣一來,我們的整體框架就出來了有了這個整體框架,再寫代碼就容易多了。
下面是RTL視圖
按照這個框架,先把接口定義出來,中間的連線用wire型
設計完端口之后我們就來設計底層模塊,先設計數據產生模塊dataoutput,這個部分主要是產生數據,可用一個分頻電路實現每1s發送一次的數據,產生這16位數據的時候,需要16個時鐘,每個時鐘數據自加1,總體來說比較簡單
寫完一個模塊之后養成好習慣,馬上把端口例化
數據產生以后就要進入緩沖器FIFO,由于這段代碼我們是調用的,所以只要例化接口就好了,只需要將產生的fifo_ctrl_inst文件中例化好的代碼拷貝粘貼就好
最后我們要寫數據發送部分,之前已經講過,數據發送部分還要包括兩個子模塊,一個是波特率匹配模塊,一個是發送模塊,既然又包括兩個子模塊,那么我們還要構建一個框圖
按照之前的例子,當FIFO當中有數據時empty就會拉低,我們把它取反后送給發送模塊,告訴發送模塊準備發送,這樣,發送模塊就會產生一個波特率計數器啟動信號bps_start給波特率匹配模塊,波特率匹配模塊收到信號后立馬開始匹配計數,并產生采集信號,將采集信號傳給發送模塊,發送模塊根據采集信號,將數據一位一位發送出去。知道了這個原理之后,我們構建起這樣一個框架
根據這個框圖,我們定義端口和線
定義完端口之后,開始寫發送模塊,用邊沿脈沖檢測法檢測啟動信號tx_start信號的上升沿來啟動發送部分,波特率配置模塊具體代碼在前面也文章中有給出,就不在說明,寫完之后例化端口,這兩個模塊作為數據發送模塊的子模塊,要在數據發送模塊下例化
這樣一來,我們整個設計就完成了,看上去很簡單,但是從我自己實踐的角度來說還是有點挑戰的,包括中間出現的各種問題,下面就來分享一下我在做這個設計時遇到的問題
1.例化問題
在例化端口時,要注意括號里面的才是本層模塊的端口,也就是說在本層模塊上面已經定義過的變量,括號外面的才是被調用模塊的端口,也在下層模塊的頂部被聲明,我在寫這段程序的時候將二者顛倒了,導致連線不成功,最終是通過查看RTL視圖知道了哪根線有問題才修改成功的
2.同一個變量不能在多個always語句中被賦值
我們可能習慣這么寫
那么,num的值在其他always語句中就不允許再被賦值或者清零,我在寫的時候在其他always語句中將num 清零了,導致編譯不成功
3.定義變量之前不要出現該變量,即使后面又定義了
例如,我先進行num的運算,之后再定義num,reg [3:0] num,這樣寫的話雖然編譯沒有錯誤,但是在調用modelsim仿真的時候它會出現編譯錯誤,所以為了規范,不要這樣寫
4. 在邊沿脈沖檢測的時候,習慣于檢測下降沿,而這里是檢測tx_en 的上升沿,所以我在復位清零的時候錯誤的將兩級寄存器賦值為0,實際上在檢測上升沿時要對兩級寄存器復位時置一,再把最后一級寄存器取反后與上一級相與。
5.在發送數據部分,由于受到上次寫接收部分程序的影響,沒有將起始位發送出去,因為在接收部分,是不需要接收起始位的,是從第一位開始,而在發送部分只有先發送起始位才能和上位機握手通信,還有在發送完數據后要發送停止位,其他情況下都發送高電平來阻止通信的進行
6.最后一個問題是最棘手的問題,我找了好大一半天也沒發現,最后還是根據源代碼找出來的,不過我還是不知道將這兩條語句顛倒了對程序有什么影響,只知道顛倒后數據會一直在發送,不會像預設一樣,每隔一秒發送一次,至今還是搞不清楚,希望大神指點迷津
總結
語法上的錯誤到不至于太難,寫的多了就不會出錯了,關鍵是邏輯上的錯誤很隱蔽,也很難發現,可以通過RTL視圖來檢測連線上是否正確,還可以借助仿真工具。
審核編輯 :李倩
-
FPGA
+關注
關注
1626文章
21678瀏覽量
602043 -
寄存器
+關注
關注
31文章
5325瀏覽量
120054
原文標題:工程師深度:FPGA 高手養成記
文章出處:【微信號:zhuyandz,微信公眾號:FPGA之家】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論