設計思想與代碼規范均借鑒明德揚至簡設計法,有不足之處希望大家多提建議,真正做到至簡設計。本篇着重提出FPGA通用設計思想,以計數器為核心的代碼規范以及VIVADO debug操作流程。
此次試驗旨在通過串口試驗,講述FPGA的硬件設計思想和通用設計流程。串口是電子設計中非常常見,可以說掌握了串口數據收發,就明白了最基本的時序操作。串口的數據收發過程有其固定的數據格式。下面是本次實驗使用的數據格式,在滿足串口格式規范前提下是可變的:
空閑狀態下為高電平,當發送數據時,先發送低電平起始位,后從低位開始逐位發送有效數據比特,數據位位數由雙方約定,此處設定為8位。可在數據位后添加數據校驗位,但這不是必須的。發送完后發送高電平停止位並持續空閑狀態直至下一次發送。雖然本次實驗沒有用到,但這里簡要講一下奇偶校驗的原理:
奇偶校驗是一種非常簡單常用的數據校驗方式,分為奇校驗和偶校驗。奇校驗需要保證傳輸的數據總共有奇數個邏輯高電平,若是偶校驗則要保證傳輸的數據有偶數個邏輯高電平。即“奇偶”的意思就是數據中(包括該校驗位)中1的個數。例如:傳輸的數據位是0100_0011。如果是奇校驗,校驗位是0,偶校驗校驗位是1。
在串口通信中,波特率是一個非常重要的概念。串口通信中常用的波特率是9600、19200、38400、57600、115200。波特率是每個碼元傳輸的速率,在二進制數據傳輸中,和比特率相同,都是每個比特數據傳輸的速率,其倒數為1bit數據的位寬,也就是1bit數據持續的時間。有了這一時間段,就可用FPGA構造計數器實現比特周期的延時,從而實現特定的數據傳輸波特率。
有了這些預備知識,我們開始設計串口發送模塊。第一步要明確設計目的:要設計的模塊功能當一個時鍾周期使能信號有效時,將輸入數據通過串口發送給PC機。后續可以通過FIFO緩存數據,實現多個數據的發送。知道設計目的后,通常要開始根據大體功能進行模塊划分,模塊之間的接口定義以及各模塊內部的硬件設計。本次實驗只有一個模塊,所以直接從模塊接口定義開始。每個模塊都要有必要的時鍾和復位輸入,另外串口發送模塊需要確保數據不重復發送,因此要有發送使能信號。為了滿足不同速率需求,需要波特率設定輸入信號來選通不同的波特率。最重要的是待發送數據輸入端口。發送側要有數據串行輸出端口和發送完成指示輸出。綜上,串口發送模塊接口示意圖如下:
現在開始模塊內部功能的硬件實現。首先需要一個參數可變的分頻計數器滿足不同波特率要求。為此需要一個查找表結構對輸入的波特率設定指令進行譯碼,改變計數器參數。然后要將數據進行並串轉換可以通過一個比特位計數器控制數據選擇器實現,這樣可以將發送比特位數與待發送數據位數相對應。至於發送完成指示信號只需根據比特計數器的數值改變即可。在設計代碼前先畫出主要信號的時序波形圖有助於理清思路:(此處假設比特計數器每個時鍾周期計數一次便於畫圖)
到目前為止最重要的設計工作已經做完了,接下來的代碼編寫也就沒有任何難度可言。
串口發送模塊代碼:
1 `timescale 1ns / 1ps 2 3 module uart_tx( 4 input clk, 5 input rst_n, 6 input [2:0] baud_set, 7 input send_en, 8 input [7:0] data_in, 9 10 output reg data_out, 11 output tx_done 12 ); 13 14 reg [15:0] CYC; 15 reg [15:0] cnt_div; 16 (*mark_debug = "true"*)reg [3:0] cnt_bit; 17 reg add_flag; 18 19 wire add_cnt_div; 20 (*mark_debug = "true"*)wire end_cnt_div; 21 wire add_cnt_bit,end_cnt_bit; 22 23 //分頻計數器 24 always@(posedge clk or negedge rst_n)begin 25 if(!rst_n) 26 cnt_div <= 0; 27 else if(add_cnt_div)begin 28 if(end_cnt_div) 29 cnt_div <= 0; 30 else 31 cnt_div <= cnt_div + 1'b1; 32 end 33 end 34 35 assign add_cnt_div = add_flag; 36 assign end_cnt_div = add_cnt_div && cnt_div == CYC - 1; 37 38 //比特位數計數器 39 always@(posedge clk or negedge rst_n)begin 40 if(!rst_n) 41 cnt_bit <= 0; 42 else if(add_cnt_bit)begin 43 if(end_cnt_bit) 44 cnt_bit <= 0; 45 else 46 cnt_bit <= cnt_bit + 1'b1; 47 end 48 end 49 50 assign add_cnt_bit = end_cnt_div; 51 assign end_cnt_bit = add_cnt_bit && cnt_bit == 10 - 1; 52 53 //發送使能后分頻計數器開始計數,直到將起始位、數據位、停止位發送完成為止 54 always@(posedge clk or negedge rst_n)begin 55 if(!rst_n) 56 add_flag <= 0; 57 else if(send_en) 58 add_flag <= 1; 59 else if(end_cnt_bit) 60 add_flag <= 0; 61 end 62 //波特率查找表 63 always@(*)begin 64 case(baud_set) 65 3'b000:CYC <= 20833;//9600 66 3'b001:CYC <= 10417;//19200 67 3'b010:CYC <= 5208;//38400 68 3'b011:CYC <= 3472;//57600 69 3'b100:CYC <= 1736;//115200 70 default:CYC <= 20833;//9600 71 endcase 72 end 73 //根據比特計數器得到對應比特位 74 always@(posedge clk or negedge rst_n)begin 75 if(!rst_n) 76 data_out <= 1; 77 else if(send_en) 78 data_out <= 0; 79 else if(add_cnt_bit && cnt_bit >= 0 && cnt_bit < 8) 80 data_out <= data_in[cnt_bit]; 81 else if((add_cnt_bit && cnt_bit == 8) || end_cnt_bit) 82 data_out <= 1;//結束位或者空閑狀態均為高電平 83 end 84 85 assign tx_done = end_cnt_bit; 86 87 endmodule
現編寫測試激勵,觀察仿真波形是否與預期一致:
1 `timescale 1ns / 1ps 2 3 module uart_tx_tb; 4 5 reg clk,rst_n; 6 reg [2:0] baud_set; 7 reg send_en; 8 reg [7:0] data_in; 9 10 wire data_out; 11 wire tx_done; 12 13 uart_tx uart_tx( 14 .clk(clk), 15 .rst_n(rst_n), 16 .baud_set(baud_set),//[2:0] 17 .send_en(send_en), 18 .data_in(data_in),//[7:0] 19 20 .data_out(data_out), 21 .tx_done(tx_done) 22 ); 23 24 parameter CYCLE = 5, 25 RST_TIME = 2; 26 27 initial begin 28 clk = 0; 29 forever #(CYCLE / 2) clk = ~clk; 30 end 31 32 initial begin 33 rst_n = 1; 34 #1; 35 rst_n = 0; 36 #(CYCLE * RST_TIME); 37 rst_n = 1; 38 end 39 40 initial begin 41 baud_set = 3'b000; 42 send_en = 0; 43 data_in = 0; 44 #1; 45 #(CYCLE * RST_TIME); 46 #(CYCLE * 10); 47 send_en = 1; 48 data_in = 8'b0101_0110; 49 #(CYCLE * 1); 50 send_en = 0; 51 #2_000_000; 52 $stop; 53 end 54 55 endmodule
仿真波形如下:
可以看出該模塊真確將待發送數據8'b0101_0110 按照串口數據格式發送了出去,分頻計數器計數完成后分別發送了0_0110_1010_1.此刻,串口發送模塊邏輯功能驗證完畢。為了在開發板中運行,添加按鍵消抖模塊,將按鍵有效輸出信號作為發送模塊的發送使能,並建立頂層模塊。按鍵消抖模塊在上一篇博文中已詳細講述,僅稍作改動調用。下面是頂層模塊:
1 `timescale 1ns / 1ps 2 3 module send_data_top( 4 input sys_clk_p, 5 input sys_clk_n, 6 input rst_n, 7 input key, 8 output dout, 9 output tx_done_out 10 ); 11 (*mark_debug = "true"*)wire tx_done; 12 (*mark_debug = "true"*)wire key_en; 13 // 差分時鍾轉單端時鍾 14 // IBUFGDS是IBUFG差分形式,當信號從一對差分全局時鍾引腳輸入時,必須使用IBUFGDS作為全局時鍾輸入緩沖 15 wire sys_clk_ibufg; 16 IBUFGDS # 17 ( 18 .DIFF_TERM ("FALSE"), 19 .IBUF_LOW_PWR ("FALSE") 20 ) 21 u_ibufg_sys_clk 22 ( 23 .I (sys_clk_p), //差分時鍾的正端輸入,需要和頂層模塊的端口直接連接 24 .IB (sys_clk_n), // 差分時鍾的負端輸入,需要和頂層模塊的端口直接連接 25 .O (sys_clk_ibufg) //時鍾緩沖輸出 26 ); 27 28 key_jitter key_jitter( 29 .clk(sys_clk_ibufg), 30 .rst_n(rst_n), 31 .key_i(key), 32 .key_vld(key_en) 33 ); 34 35 uart_tx uart_tx( 36 .clk(sys_clk_ibufg), 37 .rst_n(rst_n), 38 .baud_set(3'b000),//[2:0] 39 .send_en(key_en), 40 .data_in(8'h32),//[7:0] 41 42 .data_out(dout), 43 .tx_done(tx_done)); 44 45 assign tx_done_out = ~tx_done; 46 47 48 endmodule
打開分析后的設計原理圖,方便地觀察設計整體結構:
HDL代碼設計完畢,后需添加約束文件,這里只需為每個端口添加對應的端口號和電平標准即可。注意:當某個信號為多個位時,在后邊的方括號內需要用大括號把每一位信號括起來,如:set_property PACKAGE A5 [{led[0]}]
仿真只是通過軟件來模擬硬件的場景,尤其在只做了最理想情況下的行為仿真時,並不能完全的體現出所有硬件特性,這時就要進行“在線調試”,也就是使用嵌入式邏輯分析儀,直接抓取芯片內部真實運行的信號數值。它的基本原理是通過IP核的形式嵌入到FPGA芯片內部,不斷將要觀測數據存入RAM中,當觸發條件有效時,停止檢測並將信號數據以類似仿真波形的形式顯示出來。那么如何選擇所要觀測的信號呢?觀察上面的HDL代碼會發現,某些信號定義之前有(*mark_debug = "true"*)。這就是“抓取信號”的方式,在信號定義之前加上這條語句之后,點擊Run synthesis,並打開綜合后的設計。打開調試界面,點擊Set Up Debug 執行ILA調試IP核的生成向導。之前被標注的信號已經自動添加了進來,當然,你可以添加更多的需要觀測的信號。
Run implementation並生成比特流后,打開硬件管理器,並自動連接開發板下載比特流。此時debug probles file也同時被加載進來:
下載完畢后debug界面自動打開:
按照圖中數字的順序依次完成抓取模式設置,設置觸發條件,啟動觸發,觀測波形。2中設置key_en為高電平時啟動觸發,觀察核心信號數據。
可以看出key_en高電平后發送“0”。由於設置RAM深度太小,導致沒有觀察到串口數據完整格式。再次將觸發條件改為tx_done高電平觸發,並修改觸發條件所在觀測窗口的位置:
tx_done高電平之前比特計數器正確計數到9,tx_done高電平之后一個時鍾周期計數值變為0,證明內部邏輯功能正常運行。也可以自行回到綜合后界面,再次打開Set Up Debug界面修改數據采樣深度觀察完整波形:
此時觀察串口調試助手,設置好波特率和數據格式,將顯示方式設定為16進制。打開串口后,按下按鍵並松手后,串口調試助手接收到一個8位數據,這里固定讓其發送數字8'h32,以下是按兩次按鍵收到的數據:
到此,串口發送模塊已設計完畢,將ILA IP核的標注和相關約束去掉可節省邏輯資源。