九、基於串口獵人軟件的串口示波器
1、實驗介紹
本實驗,為芯航線開發板的綜合實驗,該實驗利用芯航線開發板上的ADC、獨立按鍵、UART等外設,搭建了一個具備豐富功能的數據采集卡,芯航線開發板負責進行數據的采集並將數據通過串口發送到PC機上,PC端,利用強大的串口調試工具——串口獵人,來實現數據的接收分析,並將數據分別以波形、碼表、柱狀圖的形式動態顯示出來,以讓使用者能夠直觀的看到ADC采集到的信號細節。同時,用戶也可以使用串口獵人通過串口給下位機(FPGA)發送指令,下位機將對接收到的指令進行解碼,然后依據解碼結果來配置FPGA中各個子模塊的控制寄存器,以實現通過串口控制FPGA中子模塊工作狀態的功能。
本實驗中,涉及到的應用模塊和知識點如下所示:
串口收發模塊的設計和使用;
串口收發模塊仿真模型的設計;
串口簡單數據幀的解碼;
串口幀轉Memory Mapped總線的設計;
Memory Mapped Slave模塊的設計;
線性序列機設計思想的應用(ADC驅動);
獨立按鍵消抖的分析與實現;
直接數字頻率合成(DDS)的設計與實現;
使能時鍾對系統間模塊協調工作的重要性;
串口獵人的詳細使用;
完整系統的仿真驗證設計;
頭文件在設計中的運用;
Quartus II軟件中可定制化存儲器ROM的使用;
本實驗不僅注重可綜合的代碼編寫,同時更注重代碼的仿真驗證。通過仿真,我們能夠尋找設計中可能存在的問題並修正。最終,在整個系統仿真無誤的基礎上,下載到開發板上一次性成功。
2、系統結構
下圖為本設計的框架結構圖:
系統采用模塊化設計,在模塊划分的過程中,重點考慮了系統的可擴展性,下表為對系統中各模塊功能的簡單介紹。
系統中各端口和信號的功能介紹如下:
本實驗為綜合性實驗,代碼量較大,因此這里只針對部分代碼進行講解。如果文檔中沒有講到的內容,大家可以參看代碼注釋。
模塊詳解
3.1 Tx_Bps_Gen
Tx_Bps_Gen為發送波特率生成模塊,每當有Byte_En信號到來時,即開始產生發送一個完整字節的數據需要的完整波特率時鍾信號。
本設計,波特率支持9600bps到921600bps。例如,需要產生的波特率時鍾為9600bps,即波特率時鍾頻率為9600Hz,周期為104.17us。生成9600Hz波特率時鍾的核心思想就是對系統時鍾進行計數,這里設定系統時鍾為50MHz,則一個時鍾的周期為20ns,我們只需要對系統時鍾計數5208次,每計數5208次產生一個時鍾周期的高電平脈沖,即可實現生成9600Hz波特率時鍾的功能。相應代碼如下所示:
018 parameter system_clk = 50_000_000; /*輸入時鍾頻率設定,默認50M*/ 019 020 /*根據輸入時鍾頻率計算生成各波特率時分頻計數器的計數最大值*/ 021 localparam bps9600 = system_clk/9600 - 1; 022 localparam bps19200 = system_clk/19200 - 1; 023 localparam bps38400 = system_clk/38400 - 1; 024 localparam bps57600 = system_clk/57600 - 1; 025 localparam bps115200 = system_clk/115200 - 1; 026 localparam bps230400 = system_clk/230400 - 1; 027 localparam bps460800 = system_clk/460800 - 1; 028 localparam bps921600 = system_clk/921600 - 1; 029 030 reg [31:0]BPS_PARA;/*波特率分頻計數器的計數最大值*/ 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 BPS_PARA <= bps9600;/*復位時波特率默認為9600bps*/ 035 end 036 else begin 037 case(Baud_Set)/*根據波特率控制信號選擇不同的波特率計數器計數最大值*/ 038 3'd0: BPS_PARA <= bps9600; 039 3'd1: BPS_PARA <= bps19200; 040 3'd2: BPS_PARA <= bps38400; 041 3'd3: BPS_PARA <= bps57600; 042 3'd4: BPS_PARA <= bps115200; 043 3'd5: BPS_PARA <= bps230400; 044 3'd6: BPS_PARA <= bps460800; 045 3'd7: BPS_PARA <= bps921600; 046 default: BPS_PARA <= bps9600; 047 endcase 048 end 049 050 //========================================================= 051 reg[12:0]Count; 052 053 reg n_state; 054 localparam IDEL_1 = 1'b0, 055 SEND = 1'b1; 056 057 reg BPS_EN; 058 059 /*-------波特率時鍾生成控制邏輯--------------*/ 060 always@(posedge Clk or negedge Rst_n) 061 if(!Rst_n)begin 062 BPS_EN <= 1'b0; 063 n_state <= IDEL_1; 064 end 065 else begin 066 case(n_state) 067 IDEL_1: 068 if(Byte_En)begin/*檢測到字節發送使能信號,則啟動波特率生成進程,同時進入發送狀態*/ 069 BPS_EN <= 1'b1; 070 n_state <= SEND; 071 end 072 else begin 073 n_state <= IDEL_1; 074 BPS_EN <= 1'b0; 075 end 076 SEND: 077 if(Tx_Done == 1)begin/*發送完成,關閉波特率生成進程,回到空閑狀態*/ 078 BPS_EN <= 1'b0; 079 n_state <= IDEL_1; 080 end 081 else begin 082 n_state <= SEND; 083 BPS_EN <= 1'b1; 084 end 085 default:n_state <= IDEL_1; 086 endcase 087 end 088 089 /*-------波特率時鍾生成定時器--------------*/ 090 always@(posedge Clk or negedge Rst_n) 091 if(!Rst_n) 092 Count <= 13'd0; 093 else if(BPS_EN == 1'b0) 094 Count <= 13'd0; 095 else begin 096 if(Count == BPS_PARA) 097 Count <= 13'd0; 098 else 099 Count <= Count + 1'b1; 100 end 101 102 /*輸出數據接收采樣時鍾*/ 103 //----------------------------------------------- 104 always @(posedge Clk or negedge Rst_n) 105 if(!Rst_n) 106 Bps_Clk <= 1'b0; 107 else if(Count== 1) 108 Bps_Clk <= 1'b1; 109 else 110 Bps_Clk <= 1'b0;
第18行“parameter system_clk = 50_000_000;”,這里用一個全局參數定義了系統時鍾,暫時設定為50M,可根據實際使用的板卡上的工作時鍾進行修改。
所謂波特率生成,就是用一個定時器來定時,產生頻率與對應波特率時鍾頻率相同的時鍾信號。例如,我們使用波特率為115200bps,則我們需要產生一個頻率為115200Hz的時鍾信號。那么如何產生這樣一個115200Hz的時鍾信號呢?這里,我們首先將115200Hz時鍾信號的周期計算出來,1秒鍾為1000_000_000ns,因此波特率時鍾的周期Tb= 1000000000/115200 =8680.6ns,即115200信號的一個周期為8680.6ns,那么,我們只需要設定我們的定時器定時時間為8680.6ns,每當定時時間到,產生一個系統時鍾周期長度的高脈沖信號即可。系統時鍾頻率為50MHz,即周期為20ns,那么,我們只需要計數8680/20個系統時鍾,就可獲得8680ns的定時,即bps115200=Tb/Tclk - 1=Tb*fclk - 1=fclk/115200-1。相應的,其它波特率定時值的計算與此類似,這里小梅哥就不再一一分析。20行至28行為波特率定時器定時值的計算部分。
為了能夠通過外部控制波特率,設計中使用了一個3位的波特率選擇端口:Baud_Set。通過給此端口不同的值,就能選擇不同的波特率,此端口控制不同波特率的原理很簡單,就是一個多路選擇器,第32行至第48行即為此多路選擇器的控制代碼, Baud_Set的值與各波特率的對應關系如下:
000 : 9600bps;
001 : 19200bps;
010 :38400bps;
011 :57600bps;
100 :115200bps;
101 :230400bps;
110 :460800bps;
111 :921600bps;
3.2 Uart_Byte_Tx
Uart_Byte_Tx為字節發送模塊,該模塊在波特率時鍾的節拍下,依照UART通信協議發送一個完整的字節的數據。當一個字節發送完畢后,Tx_Done產生一個高脈沖信號,以告知其它模塊或邏輯一個字節的數據已經傳輸完成,可以開始下一個字節的發送了。其發送一個字節數據的實現代碼如下:
33 /*計數波特率時鍾,11個波特率時鍾為一次完整的數據發送過程*/ 34 always@(posedge Clk or negedge Rst_n) 35 if(!Rst_n) 36 Bps_Clk_Cnt <= 4'b0; 37 else if(Bps_Clk_Cnt == 4'd11) 38 Bps_Clk_Cnt <= 4'b0; 39 else if(Bps_Clk) 40 Bps_Clk_Cnt <= Bps_Clk_Cnt + 1'b1; 41 else 42 Bps_Clk_Cnt <= Bps_Clk_Cnt; 43 44 /*生成數據發送完成標志信號*/ 45 always@(posedge Clk or negedge Rst_n) 46 if(!Rst_n) 47 Tx_Done <= 1'b0; 48 else if(Bps_Clk_Cnt == 4'd11) 49 Tx_Done <= 1'b1; 50 else 51 Tx_Done <= 1'b0; 52 53 /*在開始發送起始位的時候就讀取並寄存Data_Byte,以免Data_Byte變化導致數據的丟失*/ 54 always@(posedge Clk or negedge Rst_n) 55 if(!Rst_n) 56 Data = 8'd0; 57 else if(Bps_Clk & Bps_Clk_Cnt == 4'd1) 58 Data <= Data_Byte; 59 else 60 Data <= Data; 61 62 /*發送數據序列機*/ 63 always@(posedge Clk or negedge Rst_n) 64 if(!Rst_n) 65 Rs232_Tx <= 1'b1; 66 else begin 67 case(Bps_Clk_Cnt) 68 4'd1: Rs232_Tx <= 1'b0; 69 4'd2: Rs232_Tx <= Data[0]; 70 4'd3: Rs232_Tx <= Data[1]; 71 4'd4: Rs232_Tx <= Data[2]; 72 4'd5: Rs232_Tx <= Data[3]; 73 4'd6: Rs232_Tx <= Data[4]; 74 4'd7: Rs232_Tx <= Data[5]; 75 4'd8: Rs232_Tx <= Data[6]; 76 4'd9: Rs232_Tx <= Data[7]; 77 4'd10: Rs232_Tx <= 1'b1; 78 default:Rs232_Tx <= 1'b1; 79 endcase 80 end
在UART協議中,一個完整的字節包括一位起始位、8位數據位、一位停止位即總共十位數據,那么,要想完整的實現這十位數據的發送,就需要11個波特率時鍾脈沖,如下所示:
BPS_CLK信號的第一個上升沿到來時,字節發送模塊開始發送起始位,接下來的2到9個上升沿,發送8個數據位,第10個上升沿到第11個上升沿為停止位的發送。
3.3 Uart_Byte_Rx
單個串口接收模塊中實現串口數據接收的主要代碼如下所示:
025 always @ (posedge Clk or negedge Rst_n) 026 if(!Rst_n) begin 027 Rs232_Rx0 <= 1'b0; 028 Rs232_Rx1 <= 1'b0; 029 Rs232_Rx2 <= 1'b0; 030 Rs232_Rx3 <= 1'b0; 031 end 032 else begin 033 Rs232_Rx0 <= Rs232_Rx; 034 Rs232_Rx1 <= Rs232_Rx0; 035 Rs232_Rx2 <= Rs232_Rx1; 036 Rs232_Rx3 <= Rs232_Rx2; 037 end 038 039 wire neg_Rs232_Rx= Rs232_Rx3 & Rs232_Rx2 & ~Rs232_Rx1 & ~Rs232_Rx0; 040 041 assign Byte_En = neg_Rs232_Rx; 042 043 /*----------計數采樣時鍾--------------*/ 044 /*9倍波特率采樣時鍾,故一個完整的接收過程有90個波特率時鍾*/ 045 reg[6:0]Sample_Clk_Cnt; 046 always @ (posedge Clk or negedge Rst_n) 047 if(!Rst_n) 048 Sample_Clk_Cnt <= 7'd0; 049 else if(Sample_Clk)begin 050 if(Sample_Clk_Cnt == 7'd89) 051 Sample_Clk_Cnt <= 7'd0; 052 else 053 Sample_Clk_Cnt <= Sample_Clk_Cnt + 1'b1; 054 end 055 else 056 Sample_Clk_Cnt <= Sample_Clk_Cnt; 057 058 reg [1:0]Start_Bit; /*起始位,這里雖然定義,但並未使用該位來判斷接收數據的正確性,即默認接收都是成功的*/ 059 reg [1:0]Stop_Bit; /*停止位,這里雖然定義,但並未使用該位來判斷接收數據的正確性,即默認接收都是成功的*/ 060 reg [1:0] Data_Tmp[7:0];/*此部分較為復雜,請參看說明文檔中相關解釋*/ 061 062 always @ (posedge Clk or negedge Rst_n) 063 if(!Rst_n)begin 064 Data_Tmp[0] <= 2'd0; 065 Data_Tmp[1] <= 2'd0; 066 Data_Tmp[2] <= 2'd0; 067 Data_Tmp[3] <= 2'd0; 068 Data_Tmp[4] <= 2'd0; 069 Data_Tmp[5] <= 2'd0; 070 Data_Tmp[6] <= 2'd0; 071 Data_Tmp[7] <= 2'd0; 072 Start_Bit <= 2'd0; 073 Stop_Bit <= 2'd0; 074 end 075 else if(Sample_Clk)begin 076 case(Sample_Clk_Cnt) 077 7'd0: 078 begin 079 Data_Tmp[0] <= 2'd0; 080 Data_Tmp[1] <= 2'd0; 081 Data_Tmp[2] <= 2'd0; 082 Data_Tmp[3] <= 2'd0; 083 Data_Tmp[4] <= 2'd0; 084 Data_Tmp[5] <= 2'd0; 085 Data_Tmp[6] <= 2'd0; 086 Data_Tmp[7] <= 2'd0; 087 Start_Bit <= 2'd0; 088 Stop_Bit <= 2'd0; 089 end 090 7'd3,7'd4,7'd5: Start_Bit <= Start_Bit + Rs232_Rx; 091 7'd12,7'd13,7'd14:Data_Tmp[0] <= Data_Tmp[0] + Rs232_Rx; 092 7'd21,7'd22,7'd23:Data_Tmp[1] <= Data_Tmp[1] + Rs232_Rx; 093 7'd30,7'd31,7'd32:Data_Tmp[2] <= Data_Tmp[2] + Rs232_Rx; 094 7'd39,7'd40,7'd41:Data_Tmp[3] <= Data_Tmp[3] + Rs232_Rx; 095 7'd48,7'd49,7'd50:Data_Tmp[4] <= Data_Tmp[4] + Rs232_Rx; 096 7'd57,7'd58,7'd59:Data_Tmp[5] <= Data_Tmp[5] + Rs232_Rx; 097 7'd66,7'd67,7'd68:Data_Tmp[6] <= Data_Tmp[6] + Rs232_Rx; 098 7'd75,7'd76,7'd77:Data_Tmp[7] <= Data_Tmp[7] + Rs232_Rx; 099 7'd84,7'd85,7'd86:Stop_Bit <= Stop_Bit + Rs232_Rx; 100 default:; 101 endcase 102 end 103 else ;
根據串口發送協議,一個字節的數據傳輸是以一個波特率周期的低電平作為起始位的,因此,成功接收UART串口數據的核心就是准確檢測起始位。由於外部串口發送過來的數據與接收系統不在同一個時鍾域,因此不能直接使用該信號的下降沿來作為檢測標志,我們需要在fpga中,采用專用的邊沿檢測電路來實現,第25行至37行通過四個移位寄存器,存儲連續四個時鍾上升沿時外部發送數據線的狀態,第39行通過比較前兩個時鍾時數據線的狀態與后兩個時鍾時數據線的狀態,來得到該數據線的准確下降沿,以此保證起始位的准確檢測。
在簡單的串口接收中,我們通常選取一位數據的中間時刻進行采樣,因為此時數據最穩定,但是在工業環境中,存在着各種干擾,在干擾存在的情況下,如果采用傳統的中間時刻采樣一次的方式,采樣結果就有可能受到干擾而出錯。為了濾除這種干擾,這里采用多次采樣求概率的方式。如下圖,將一位數據平均分成9個時間段,對位於中間的三個時間段進行采樣。然后對三個采樣結果進行統計判斷,如果某種電平狀態在三次采樣結果中占到了兩次及以上,則可以判定此電平狀態即為正確的數據電平。例如4、5、6時刻采樣結果分別為1、1、0,那么就取此位解碼結果為1,否則,若三次采樣結果為0、1、0,則解碼結果就為0。
因為采樣一位需要9個時鍾上升沿,因此,采樣一個完整的數據需要10*9,即90個時鍾上升沿,這里,采樣時鍾為波特率時鍾的9倍。產生采樣時鍾的部分代碼如下所示:
089 /*-------波特率時鍾生成定時器--------------*/ 090 always@(posedge Clk or negedge Rst_n) 091 if(!Rst_n) 092 Count <= 10'd0; 093 else if(BPS_EN == 1'b0) 094 Count <= 10'd0; 095 else begin 096 if(Count == BPS_PARA) 097 Count <= 10'd0; 098 else 099 Count <= Count + 1'b1; 100 end 101 102 //===================================================== 103 /*輸出數據接收采樣時鍾*/ 104 always @(posedge Clk or negedge Rst_n) 105 if(!Rst_n) 106 Sample_Clk <= 1'b0; 107 else if(Count== 1) 108 Sample_Clk <= 1'b1; 109 else 110 Sample_Clk <= 1'b0;
這里,BPS_PARA的計算原理和前面Tx_Bps_Gen模塊中的BPS_PARA的計算原理一致,不過這里,因為采樣時鍾為波特率時鍾的9倍,所以,BPS_PARA為Tx_Bps_Gen模塊中的BPS_PARA的1/9。計算BPS_PARA的相關代碼如下:
018 parameter system_clk = 50_000_000; /*輸入時鍾頻率設定,默認50M*/ 019 020 /*根據輸入時鍾頻率計算生成各波特率時分頻計數器的計數最大值*/ 021 localparam bps9600 = system_clk/9600/9 - 1; 022 localparam bps19200 = system_clk/19200/9 - 1; 023 localparam bps38400 = system_clk/38400/9 - 1; 024 localparam bps57600 = system_clk/57600/9 - 1; 025 localparam bps115200 = system_clk/115200/9 - 1; 026 localparam bps230400 = system_clk/230400/9 - 1; 027 localparam bps460800 = system_clk/460800/9 - 1; 028 localparam bps921600 = system_clk/921600/9 - 1; 029 030 reg [31:0]BPS_PARA;/*波特率分頻計數器的計數最大值*/ 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 BPS_PARA <= bps9600; /*復位時波特率默認為9600bps*/ 035 end 036 else begin 037 case(Baud_Set) /*根據波特率控制信號選擇不同的波特率計數器計數最大值*/ 038 3'd0: BPS_PARA <= bps9600; 039 3'd1: BPS_PARA <= bps19200; 040 3'd2: BPS_PARA <= bps38400; 041 3'd3: BPS_PARA <= bps57600; 042 3'd4: BPS_PARA <= bps115200; 043 3'd5: BPS_PARA <= bps230400; 044 3'd6: BPS_PARA <= bps460800; 045 3'd7: BPS_PARA <= bps921600; 046 default: BPS_PARA <= bps9600;/*異常情況,恢復到9600的波特率*/ 047 endcase 048 end
3.4 CMD
CMD模塊為串口數據幀接收與解析模塊,該模塊負責對串口接收到的每一幀的數據進行解碼判斷,並從數據幀中提取出地址字節和數據字節。最后將地址字節和數據字節轉換為類似於Avalon-MM形式的總線,以實現對其它模塊的控制寄存器的讀寫,從而實現通過串口控制FPGA中各個模塊工作的目的。
在工業應用中,串口指令大多以數據幀的格式出現,包含幀頭、幀長、幀命令、幀內容、校驗和以及幀尾,不會只是單純的傳輸數據。在這個實驗中,小梅哥也使用了數據幀的形式來通過上位機向FPGA發送命令,不過這里我使用的幀格式非常簡單,幀格式以幀頭、幀長、幀內容以及幀尾組成,忽略了校驗部分內容,幀頭、幀長以及幀尾內容都是固定的,不固定的只是幀內容,以下為小梅哥的設計中一幀數據的格式:
由於數據幀本身結構簡單,因此數據幀的解析過程也相對簡潔,以下為小梅哥的數據幀解析狀態機設計,該狀態機分為幀頭解析、幀長解析、數據接收以及幀尾解析。默認時,狀態機處於幀頭解析狀態,一旦出現幀頭數據,則跳轉到幀長接收狀態,若下一個字節為幀長數據(這里嚴格意義上並不能算作幀長,因為長度固定,充其量只能算作幀頭,讀者不須過分糾結),則開始連續接收三個字節的數據,若非指定的幀長內容,則表明這是一次無關傳輸,狀態機將返回到幀頭解析狀態繼續等待新的數據幀到來。在幀尾解析狀態,若解析到的數據並非指定的幀尾數據,則表明此次數據幀非有效幀,則將此幀已解析到的數據舍棄。若為幀尾數據,則解析成功,產生命令有效標志信號(CMD_Valid),Memory Mapped 總線進程在檢測到此命令有效信號后,即產生寫外設寄存器操作。
命令解析的狀態機實現代碼如下所示:
017 localparam 018 Header = 8'hAA, /*幀頭*/ 019 Length = 8'd3, /*幀長*/ 020 Tail = 8'h88; /*幀尾*/ 021 022 /*----------狀態定義-----------------*/ 023 localparam 024 CMD_HEADER = 6'b00_0001, 025 CMD_LENGTH = 6'b00_0010, 026 CMD_DATAA = 6'b00_0100, 027 CMD_DATAB = 6'b00_1000, 028 CMD_DATAC = 6'b01_0000, 029 CMD_TAIL = 6'b10_0000; 030 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 reg_CMD_DATA <= 24'd0; 035 CMD_Valid <= 1'b0; 036 state <= CMD_HEADER; 037 end 038 else if(Rx_Int)begin 039 case(state) 040 CMD_HEADER: /*解碼幀頭數據*/ 041 if(Rx_Byte == Header) 042 state <= CMD_LENGTH; 043 else 044 state <= CMD_HEADER; 045 046 CMD_LENGTH: /*解碼幀長數據*/ 047 if(Rx_Byte == Length) 048 state <= CMD_DATAA; 049 else 050 state <= CMD_HEADER; 051 052 CMD_DATAA: /*解碼數據A*/ 053 begin 054 reg_CMD_DATA[23:16] <= Rx_Byte; 055 state <= CMD_DATAB; 056 end 057 058 CMD_DATAB: /*解碼數據B*/ 059 begin 060 reg_CMD_DATA[15:8] <= Rx_Byte; 061 state <= CMD_DATAC; 062 end 063 064 CMD_DATAC: /*解碼數據C*/ 065 begin 066 reg_CMD_DATA[7:0] <= Rx_Byte; 067 state <= CMD_TAIL; 068 end 069 070 CMD_TAIL: /*解碼幀尾數據*/ 071 if(Rx_Byte == Tail)begin 072 CMD_Valid <= 1'b1; /*解碼成功,發送解碼數據有效標志*/ 073 state <= CMD_HEADER; 074 end 075 else begin 076 CMD_Valid <= 1'b0; 077 state <= CMD_HEADER; 078 end 079 default:; 080 endcase 081 end 082 else begin 083 CMD_Valid <= 1'b0; 084 reg_CMD_DATA <= reg_CMD_DATA; 085 end
第23行到第29行為狀態機編碼,這里采用獨熱碼的編碼方式。狀態機的編碼方式有很多種,包括二進制編碼、獨熱碼、格雷碼等,二進制編碼最接近我們的常規思維,但是在FPGA內部,其譯碼電路較為復雜,且容易出現競爭冒險,導致使用二進制編碼的狀態機最高運行速度相對較低。獨熱碼的譯碼電路最簡單,因此采用獨熱碼方式編碼的狀態機運行速度較二進制編碼方式高很多,但是編碼會占用較多的數據位寬。格雷碼以其獨特的編碼特性,能夠非常完美的解決競爭冒險的問題,使狀態機綜合出來的電路能夠運行在很高的時鍾頻率,但是格雷碼編碼較為復雜,尤其對於位寬超過4位的格雷碼,編碼實現較二進制編碼和獨熱碼編碼要復雜的多。這里,詳細的關於狀態機的編碼問題,小梅哥不做過多的討論,更加細致的內容,請大家參看夏宇聞老師經典書籍《Verilog數字系統設計教程》中第12章相關內容。
Memory Mapped 總線進程根據命令有效標志信號產生寫外設寄存器操作的相關代碼如下所示:
087 /*------驅動總線寫外設寄存器--------*/ 088 always@(posedge Clk or negedge Rst_n) 089 if(!Rst_n)begin 090 m_wr <= 1'b0; 091 m_addr <= 8'd0; 092 m_wrdata <= 16'd0; 093 end 094 else if(CMD_Valid)begin 095 m_wr <= 1'b1; 096 m_addr <= reg_CMD_DATA[23:16]; 097 m_wrdata <= reg_CMD_DATA[15:0]; 098 end 099 else begin 100 m_wr <= 1'b0; 101 m_addr <= m_addr; 102 m_wrdata <= m_wrdata; 103 end
在本系統中,需要通過該Memory Mapped 總線配置的寄存器總共有12個,分別位於ADC采樣速率控制模塊(Sample_Ctrl)、串口發送控制模塊(UART_Tx_Ctrl)、直接數字頻率合成信號發生器模塊(DDS)中,各寄存器地址分配及物理意義如下所示:
指令使用說明:
例如,系統在上電后,各個模塊默認是沒有工作的,要想在上位機上看到數據,就必須先通過上位機發送控制命令。因為系統上電后默認選擇的數據通道為DDS生成的數據,為了以最快的方式在串口獵人上看到波形,一種可行的控制順序如下所示:
使能DDS生成數據(AA 03 06 00 01 88) —> 使能采樣DDS數據(AA 03 0C 00 01 88) —>使能串口發送數據(AA 03 04 00 01 88),
這里,為了演示方便,因此在系統中對數據采樣速率和DDS生成的信號的頻率初始值都做了設置,因此不設置采樣率和輸出頻率控制字這幾個寄存器也能在串口獵人上接收到數據。
經過此操作后,串口獵人的接收窗口中就會不斷的接收到數據了。當然,這離我們最終顯示波形還有一段距離,這部分內容我將放到文檔最后,以一次具體的使用為例,來step by step的介紹給大家。
關於Memory Mapped 總線如何實現各模塊寄存器的配置,這里小梅哥以ADC采樣控制模塊Sample_Ctrl中三個寄存器的配置來進行介紹。Sample_Ctrl中三個寄存器的定義及配置代碼如下所示:
14 reg [15:0]ADC_Sample_Cnt_Max_L;/*采樣分頻計數器計數最大值的低16位,ADDR = 8'd1*/ 15 reg [15:0]ADC_Sample_Cnt_Max_H;/*采樣分頻計數器計數最大值的高16位,ADDR = 8'd2*/ 16 reg ADC_Sample_En;/*采樣使能寄存器,ADDR = 8'd3*/ 17 18 /*-------設置采樣分頻計數器計數最大值---------*/ 19 always@(posedge Clk or negedge Rst_n) 20 if(!Rst_n)begin 21 ADC_Sample_Cnt_Max_H <= 16'd0; 22 ADC_Sample_Cnt_Max_L <= 16'd49999;/*默認設置采樣率為1K*/ 23 end 24 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_L))//寫采樣分頻計數器計數最大值的低16位 25 ADC_Sample_Cnt_Max_L <= m_wrdata; 26 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_H))//寫采樣分頻計數器計數最大值的高16位 27 ADC_Sample_Cnt_Max_H <= m_wrdata; 28 else begin 29 ADC_Sample_Cnt_Max_H <= ADC_Sample_Cnt_Max_H; 30 ADC_Sample_Cnt_Max_L <= ADC_Sample_Cnt_Max_L; 31 end 32 33 /*---------寫采樣使能寄存器-------------*/ 34 always@(posedge Clk or negedge Rst_n) 35 if(!Rst_n) 36 ADC_Sample_En <= 1'b0; 37 else if(m_wr && (m_addr == `ADC_Sample_En)) 38 ADC_Sample_En <= m_wrdata[0]; 39 else 40 ADC_Sample_En <= ADC_Sample_En;
采樣率的控制采用定時器的方式實現。使用一個計數器持續對系統時鍾進行計數,一旦計數滿設定時間,則產生一個時鍾周期的高脈沖信號,作為ADC采樣使能信號。這里,系統時鍾周期為20ns,因此,如果要實現采樣1K的采樣率(采樣周期為1ms),則需對系統時鍾計數50000次;若實現20K的采樣率(采樣周期為50us),則需要對系統時鍾計數2500次。以此類推,可知改變采樣率的實質就是改變計數器的計數最大值,因此,我們要想改變采樣速率,也只需要改變采樣率控制計數器的計數最大值即可。所以這里,我們設計了兩個16位的寄存器,分別存儲采樣率控制計數器的計數最大值的低16位和高16位,如第14、15行所示。當我們需要修改ADC的采樣率時,直接通過串口發送指令,修改這兩個寄存器中的內容即可。
這里,小梅哥使用自己設計的一個山寨版Memory Mapped 總線來配置各個寄存器,該總線包含三組信號,分別為:
寫使能信號:m_wr;
寫地址信號:m_addr;
寫數據信號:m_wrdata;
那么,這三組信號是如何配合工作的呢?我們以配置ADC_Sample_Cnt_Max_H和ADC_Sample_Cnt_Max_L這兩個寄存器來進行介紹,這里再貼上這部分代碼:
18 /*-------設置采樣分頻計數器計數最大值---------*/ 19 always@(posedge Clk or negedge Rst_n) 20 if(!Rst_n)begin 21 ADC_Sample_Cnt_Max_H <= 16'd0; 22 ADC_Sample_Cnt_Max_L <= 16'd49999;/*默認設置采樣率為1K*/ 23 end 24 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_L))//寫采樣分頻計數器計數最大值的低16位 25 ADC_Sample_Cnt_Max_L <= m_wrdata; 26 else if(m_wr && (m_addr == `ADC_S_Cnt_Max_H))//寫采樣分頻計數器計數最大值的高16位 27 ADC_Sample_Cnt_Max_H <= m_wrdata; 28 else begin 29 ADC_Sample_Cnt_Max_H <= ADC_Sample_Cnt_Max_H; 30 ADC_Sample_Cnt_Max_L <= ADC_Sample_Cnt_Max_L; 31 end
復位時,讓{ ADC_Sample_Cnt_Max_H,ADC_Sample_Cnt_Max_L }為49999,即設置默認采樣率為1K,每當m_wr為高且m_addr等於ADC_Sample_Cnt_Max_H寄存器的地址時,就將m_wrdata的數據更新到ADC_Sample_Cnt_Max_H寄存器中,同理,若當m_wr為高且m_addr等於ADC_Sample_Cnt_Max_L寄存器的地址時,就將m_wrdata的數據更新到ADC_Sample_Cnt_Max_L寄存器中。其他寄存器的配置原理與此相同,因此不再做闡述,相信大家舉一反三,便可理解了。
4、DDS基本原理
注:本文內容摘抄自周立功編寫的教材《EDA實驗與實踐》196~197頁。
DDS(Direct Digital Synthesizer)即數字合成器,是一種新型的頻率合成技術,具有相對帶寬大,頻率轉換時間短、分辨率高和相位連續性好等優點,很容易實現頻率,相位,和幅度的數控調制,廣泛應用於通信領域。
DDS的基本結構圖如圖1所示:
圖1 DDS的基本結構圖
主要由相位累加器,相位調制器,正弦數據表,和D/A轉換器構成,相位累加器由N位加法器與N位寄存器構成。每來一個時鍾,加法器就將頻率控制字,與累加寄存器輸出的相位數據相加,相加的結果又反饋至累加寄存器的數據輸入端,以使加法器在下一個時鍾脈沖的作用下繼續與頻率控制字相加,這樣,相位累加器在時鍾作用下,不斷對頻率控制字進行線性相位累加。由此可以看出,在每一個時鍾脈沖輸入時,相位累加器便把頻率控制字累加一次。相位累加器輸出的數據就是合成信號的相位,相位累加器的溢出頻率,就是DDS輸出的信號頻率,用相位累加器輸出的數據,作為波形存儲器的相位采樣地址,這樣就可以把存儲在波形存儲器里的波形采樣值經查表找出,完成相位到幅度的轉換,波形存儲器的付出送到D/A轉換器,由D/A轉換器將數字信號轉換成模擬信號輸出,DDS信號流程示意圖如圖4.51所示。
圖2 DDS信號流程示意圖
由於相位累加器為N位,相當於把正弦信號在相位上的精度定義為N位,(N的取值范圍一般為24~32),所以其分辨率為1/2N,若系統時鍾頻率為Fclk,頻率控制字fword為1,則輸出頻率為Fout=Fclk/2N,這個頻率相當於“基頻”,若fword為B,則輸出頻率為
當系統輸入時鍾頻率,Fclk不變時,輸出信號頻率由頻率控制字M所決定,由上式可得:
其中B為頻率字,注意B要取整,有時會有誤差,在本設計中,N取32位,系統時鍾頻率Fclk為120兆,
選取ROM的地址(即相位累加器的輸出數據)時,可以間隔選通,相位寄存器輸出的位數一般取10~16位,這種截取方法稱為截斷式用法,以減少ROM的容量,M太大會導致ROM容量的成倍上升,而輸出精度受D/A位數的限制未有很大改善,在本設計中M取12位。
以上為周立功《EDA實驗與實踐》一書中對DDS原理的介紹
DDS原理再解釋。
上面的對DDS原理的解釋,還是有部分同學反映不夠直觀,讀完之后還是不明白DDS究竟是怎么控制頻率和相位的,那么,這里小梅哥再用更加通俗的方式給大家講解一下。
如圖3,為一個完整周期的正弦信號的波形,總共有33個采樣點,其中第1點和第33點的
值相同,第33點為下一個周期的起始點,因此,實際一個周期為32個采樣點(1~32)。因為是在matlab中生成的,因此起始點為1,而不是我們常見的0,這里對我們理解DDS的原理沒有任何影響,因此不必過多糾結。
圖3 32個采樣點的正弦信號波形
圖4 16個采樣點的正弦信號波形
我們要使用FPGA控制DAC來輸出這樣一個周期的正弦信號,每1ms輸出一個數值。如果每個點都輸出,則總共輸出這一個完整的周期信號需要輸出32個點,因此輸出一個完整的信號需要32ms,則輸出信號的頻率為1000/32Hz。
假如,我們現在用這一組數據來輸出一個2*(1000/32)Hz的正弦信號,因為輸出信號頻率為2*(1000/32)Hz,那么輸出一個完整的周期的正弦波所需要的時間為32/2,即16ms,為了保證輸出信號的周期為16ms,那么,我們就需要對我們的輸出策略進行更改,上面輸出周期為32ms的信號時,我們采用的為逐點輸出的方式,以32個點來輸出一個完整的正弦信號,而我們FPGA控制DAC輸出信號的頻率固定為1ms,因此,我們要輸出周期為16ms的信號,只能輸出16個點來表示一個完整的周期。我們這里選擇以每隔一個點輸出一個數據的方式,例如,我們可以選擇輸出(1、3、5、7……29、31)這些點,因為采用這些點,我們還是能夠組成一個完整的周期的正弦信號,而輸出時間縮短為一半,則頻率提高了一倍。最終結果如上圖4所示。
如果我們需要輸出頻率為(1/2)*(1000/32)Hz,即周期為64ms,則只需要以此組數據為基礎,每2ms輸出一個數據即可,例如第1ms和第2ms輸出第一個點,第3ms和第4ms輸出第二個點,以此類推,第63ms和第64ms輸出第32個點,即可實現周期加倍,即頻率減半的效果。
對於相位的調整,則更加簡單,我們只需要在每個取樣點的序號上加上一個偏移量,便可實現相位的控制。例如,上面默認的是第1ms時輸出第一個點的數據,假如我們現在在第1ms時從第9個點開始輸出,則將相位左移了90度,這就是控制相位的原理。
實現DDS輸出時,將橫坐標上的數據作為ROM的地址,縱坐標上的數據作為ROM的輸出,那么指定不同的地址就可實現對應值的輸出。而我們DDS輸出控制頻率和相位,歸結到底就是控制ROM的地址。
了解了以上原理之后,再來設計DDS系統就很容易了,以下為DDS信號發生器的代碼:
4.1 DDS_Module
01 module DDS_Module( 02 Clk, 03 Rst_n, 04 EN, 05 Fword, 06 Pword, 07 DA_Clk, 08 DA_Data 09 ); 10 11 input Clk;/*系統時鍾*/ 12 input Rst_n;/*系統復位*/ 13 input EN;/*DDS模塊使能*/ 14 input [31:0]Fword;/*頻率控制字*/ 15 input [11:0]Pword;/*相位控制字*/ 16 17 output DA_Clk;/*DA數據輸出時鍾*/ 18 output [9:0]DA_Data;/*D輸出輸出A*/ 19 20 reg [31:0]Fre_acc; 21 reg [11:0]Rom_Addr; 22 23 /*---------------相位累加器------------------*/ 24 always @(posedge Clk or negedge Rst_n) 25 if(!Rst_n) 26 Fre_acc <= 32'd0; 27 else if(!EN) 28 Fre_acc <= 32'd0; 29 else 30 Fre_acc <= Fre_acc + Fword; 31 32 /*----------生成查找表地址---------------------*/ 33 always @(posedge Clk or negedge Rst_n) 34 if(!Rst_n) 35 Rom_Addr <= 12'd0; 36 else if(!EN) 37 Rom_Addr <= 12'd0; 38 else 39 Rom_Addr <= Fre_acc[31:20] + Pword; 40 41 /*----------例化查找表ROM-------*/ 42 ddsrom ddsrom( 43 .address(Rom_Addr), 44 .clock(Clk), 45 .q(DA_Data) 46 ); 47 48 /*----------輸出DA時鍾----------*/ 49 assign DA_Clk = (EN)?Clk:1'b1; 50 51 endmodule
5、仿真驗證:
以上分部分介紹了系統的各個關鍵模塊的設計。接下來,我們來對該設計進行仿真驗證。因為該實驗是基於串口的,為了實現仿真驗證,這里小梅哥分別編寫了一個串口發送的仿真模型(Uart_Tx_Model)和一個串口接收的仿真模型(Uart_Rx_Model),兩個仿真模型的設計都較為簡單,但是我們卻可以通過該模型模擬對我們的設計進行串口數據的發送和接收,並實時打印仿真模型發送的數據與接收到的數據。關於仿真模型的代碼,這里只貼上代碼,不做具體解釋。(此貼回復超過100條我就專門開文講解testbench的編寫技巧)
以下為串口接收仿真模型的代碼
001 `timescale 1ns/1ps 002 003 module Uart_RX_Model(Baud_Set,uart_rx); 004 005 input [2:0]Baud_Set;/*波特率選擇信號*/ 006 input uart_rx;/*仿真模型串口接收引腳*/ 007 008 reg Clk;/*仿真模型內部時鍾,50M*/ 009 reg Rst_n;/*仿真模型內部復位信號*/ 010 011 wire Mid_Flag_Receive;/*數據中點(采樣點)標志信號*/ 012 013 reg Receive_Baud_Start;/*接收波特率生成使能信號*/ 014 reg [7:0]rx_data;/*接收數據移位寄存器*/ 015 016 reg [7:0]Rx_Byte;/*最終接收結果*/ 017 018 initial Clk = 1; 019 always#10 Clk = ~Clk; 020 021 /*例化波特率設置模塊*/ 022 baud_select baud_select_Receive( 023 .Clk(Clk), 024 .Rst_n(Rst_n), 025 .Baud_Set(Baud_Set), 026 .Baud_Start(Receive_Baud_Start), 027 .Mid_Flag(Mid_Flag_Receive) 028 ); 029 030 initial begin 031 Rst_n = 0; 032 Rx_Byte = 0; 033 rx_data = 0; 034 #100 Rst_n = 1; 035 end 036 037 /*接收一個字節的數據*/ 038 initial begin 039 forever begin 040 @(negedge uart_rx) 041 begin 042 Receive_Baud_Start = 1; 043 @(posedge Mid_Flag_Receive); 044 @(posedge Mid_Flag_Receive)rx_data[0] = uart_rx; 045 @(posedge Mid_Flag_Receive)rx_data[1] = uart_rx; 046 @(posedge Mid_Flag_Receive)rx_data[2] = uart_rx; 047 @(posedge Mid_Flag_Receive)rx_data[3] = uart_rx; 048 @(posedge Mid_Flag_Receive)rx_data[4] = uart_rx; 049 @(posedge Mid_Flag_Receive)rx_data[5] = uart_rx; 050 @(posedge Mid_Flag_Receive)rx_data[6] = uart_rx; 051 @(posedge Mid_Flag_Receive)rx_data[7] = uart_rx; 052 @(posedge Mid_Flag_Receive)begin Receive_Baud_Start = 0;Rx_Byte = rx_data;end 053 $display("Master_receive Data = %0h",Rx_Byte); 054 end 055 end 056 end 057 058 endmodule
以下為串口發送仿真模型的設計代碼
001 `timescale 1ns/1ps 002 003 module Uart_Tx_Model(Baud_Set,Tx_Data,Tx_En,uart_tx,Tx_Done); 004 005 input [2:0]Baud_Set; /*波特率選擇信號*/ 006 input [7:0]Tx_Data; /*待發送數據字節*/ 007 input Tx_En; /*數據字節發送使能信號*/ 008 output reg uart_tx; /*仿真串口發送模型發送信號*/ 009 output reg Tx_Done; /*發送完成信號*/ 010 011 reg Clk; /*仿真模型內部工作時鍾*/ 012 reg Rst_n; /*仿真模型內部復位信號*/ 013 014 wire Bps_Clk; /*發送波特率時鍾波特率*/ 015 reg Bps_En; /*發送波特率使能信號*/ 016 017 initial Clk = 1; 018 always#10 Clk = ~Clk; 019 020 /*----例化發送波特率時鍾生成模塊-----*/ 021 TxModel_Bps_Gen TxModel_Bps_Gen_send( 022 .Clk(Clk), 023 .Rst_n(Rst_n), 024 .Baud_Set(Baud_Set), 025 .Tx_Done(Tx_Done), 026 .Bps_Clk(Bps_Clk), 027 .Byte_En(Bps_En) 028 ); 029 030 initial begin 031 Tx_Done = 0; 032 uart_tx = 1; 033 Rst_n = 0; 034 Bps_En = 0; 035 #100; 036 Rst_n = 1; 037 forever@(posedge Tx_En)/*每來一個發送使能信號即執行一次發送過程*/ 038 Uart_Send(Tx_Data); 039 end 040 041 /*執行一次字節數據的發送*/ 042 task Uart_Send; 043 input [7:0]Data; 044 begin 045 Bps_En = 1; 046 Tx_Done = 0; 047 $display("Uart_Send Data = %0h",Data);/*打印發送的數據*/ 048 @(posedge Bps_Clk) #0.1 uart_tx = 0; 049 @(posedge Bps_Clk) #0.1 uart_tx = Data[0]; 050 @(posedge Bps_Clk) #0.1 uart_tx = Data[1]; 051 @(posedge Bps_Clk) #0.1 uart_tx = Data[2]; 052 @(posedge Bps_Clk) #0.1 uart_tx = Data[3]; 053 @(posedge Bps_Clk) #0.1 uart_tx = Data[4]; 054 @(posedge Bps_Clk) #0.1 uart_tx = Data[5]; 055 @(posedge Bps_Clk) #0.1 uart_tx = Data[6]; 056 @(posedge Bps_Clk) #0.1 uart_tx = Data[7]; 057 @(posedge Bps_Clk) #0.1 uart_tx = 1; 058 @(posedge Bps_Clk) #0.1 ; 059 Tx_Done = 1; 060 Bps_En = 0; 061 #20 Tx_Done = 0; 062 end 063 endtask 064 065 endmodule
以下為仿真頂層模塊的設計
001 `timescale 1ns/1ns 002 `include "../rtl/header.v" 003 module uart_scope_tb; 004 localparam KEY_WIDTH = 3; 005 006 reg Clk; 007 reg Rst_n; 008 reg [KEY_WIDTH - 1:0]Key_in; 009 010 reg ADC_Din; 011 wire ADC_Clk; 012 wire ADC_Cs_n; 013 014 /*波特率設置總線,此處默認為9600bps,仿真不做波特率修改測試*/ 015 wire [2:0]Baud_Set; 016 reg [7:0]Tx_Data;/*串口發送仿真模型待發送數據字節*/ 017 reg Tx_En; /*串口發送仿真模型發送使能信號*/ 018 wire Rs232_MTSR; /*串口“主機(PC)發送-從機(FPGA)接收”信號*/ 019 wire Rs232_MRST; /*串口“主機(PC)接收-從機(FPGA)發送”信號*/ 020 wire Tx_Done; /*串口字節發送完成信號*/ 021 022 assign Baud_Set = 3'd0;/*設置波特率為固定的9600bps*/ 023 024 localparam 025 Header = 8'hAA, /*幀頭*/ 026 Length = 8'd3, /*幀長*/ 027 Tail = 8'h88; /*幀尾*/ 028 029 /*------例化串口示波器頂層模塊------*/ 030 uart_scope uart_scope( 031 .Clk(Clk), 032 .Rst_n(Rst_n), 033 .Rs232_Rx(Rs232_MTSR), 034 .Rs232_Tx(Rs232_MRST), 035 .Key_in(Key_in), 036 .ADC_Din(ADC_Din), 037 .ADC_Clk(ADC_Clk), 038 .ADC_Cs_n(ADC_Cs_n) 039 ); 040 041 /*------例化串口發送仿真模型------*/ 042 Uart_Tx_Model Uart_Tx_Model( 043 .Baud_Set(Baud_Set), 044 .Tx_Data(Tx_Data), 045 .Tx_En(Tx_En), 046 .uart_tx(Rs232_MTSR), 047 .Tx_Done(Tx_Done) 048 ); 049 050 /*------例化串口接收仿真模型------*/ 051 //該模型接收FPGA發送出來的數據並打印在modelsim的transcript窗口中 052 Uart_RX_Model Uart_RX_Model( 053 .Baud_Set(Baud_Set), 054 .uart_rx(Rs232_MRST) 055 ); 056 057 /*-------生成50M時鍾信號--------*/ 058 initial Clk = 0; 059 always #10 Clk = ~Clk; 060 061 /*-------生成ADC_Din數據-------*/ 062 /*此處不對ADC的采樣結果多做計較,只要求保 063 證ADC_Din上有數據即可,有興趣者可自己編寫仿真模型*/ 064 initial ADC_Din = 1; 065 always #1315 ADC_Din = ~ADC_Din; 066 067 initial begin 068 Rst_n = 1'b0; 069 Tx_En = 1'b0; 070 Tx_Data = 8'd0; 071 Key_in = 4'b1111; 072 #200; 073 Rst_n = 1'b1; /*釋放復位信號,系統即進入正常工作狀態*/ 074 #1000; 075 En_DDS_Run; /*使能DDS信號發生器生成信號數據*/ 076 #10000; 077 En_S_DDS; /*使能采樣ADC數據*/ 078 En_S_ADC; /*使能采樣DDS數據*/ 079 #10000; 080 En_UART_Send;/*使能串口發送,此時串口獵人軟件上將會開始持續接收到數據*/ 081 end 082 083 initial begin 084 #200_000_000;press_key(0); 085 #200_000_000;press_key(1); 086 #200_000_000; 087 $stop; 088 end 089 090 091 092 /*---發送命令幀數據任務-----*/ 093 task Send_CMD; 094 input [7:0]DATAA,DATAB,DATAC;/*用戶數據(地址、數據高字節,數據低字節)*/ 095 begin 096 Tx_Data = Header;/*需發送數據為幀頭*/ 097 Tx_En = 1; /*啟動發送*/ 098 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 099 @(posedge Tx_Done)/*等待發送完成信號*/ 100 #1000; 101 102 Tx_Data = Length;/*需發送數據為幀長,此處幀長只是數據內容的長度*/ 103 Tx_En = 1; /*啟動發送*/ 104 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 105 @(posedge Tx_Done)/*等待發送完成信號*/ 106 #1000; 107 108 Tx_Data = DATAA;/*需發送數據第一個字節,此數據代表外設寄存器的地址*/ 109 Tx_En = 1; /*啟動發送*/ 110 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 111 @(posedge Tx_Done)/*等待發送完成信號*/ 112 #1000; 113 114 Tx_Data = DATAB;/*需發送數據第二個字節,此數據代表寫入外設寄存器的內容高8位*/ 115 Tx_En = 1; /*啟動發送*/ 116 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 117 @(posedge Tx_Done)/*等待發送完成信號*/ 118 #1000; 119 120 Tx_Data = DATAC;/*需發送數據第三個字節,此數據代表寫入外設寄存器的內容低8位*/ 121 Tx_En = 1; /*啟動發送*/ 122 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 123 @(posedge Tx_Done)/*等待發送完成信號*/ 124 #1000; 125 126 Tx_Data = Tail;/*需發送數據為幀尾*/ 127 Tx_En = 1; /*啟動發送*/ 128 #20 Tx_En = 0; /*一個時鍾周期后,清零發送啟動信號*/ 129 @(posedge Tx_Done)/*等待發送完成信號*/ 130 #1000; 131 #10000; 132 end 133 endtask 134 135 task En_DDS_Run;/*使能DDS生成數據*/ 136 begin 137 Send_CMD(`DDS_En, 8'h00, 8'h01); 138 $display("En DDS Run"); 139 end 140 endtask 141 142 task Stop_DDS_Run;/*停止DDS生成數據*/ 143 begin 144 Send_CMD(`DDS_En, 8'h00, 8'h00); 145 $display("Stop DDS Run"); 146 end 147 endtask 148 149 task En_S_DDS;/*使能采樣DDS數據*/ 150 begin 151 Send_CMD(`DDS_Sample_En, 8'h00, 8'h01); 152 $display("En Sample DDS data"); 153 end 154 endtask 155 156 task Stop_S_DDS;/*停止采樣DDS數據*/ 157 begin 158 Send_CMD(`DDS_Sample_En, 8'h00, 8'h00); 159 $display("Stop Sample DDS data"); 160 end 161 endtask 162 163 task En_UART_Send;/*使能串口發送*/ 164 begin 165 Send_CMD(`UART_En_Tx, 8'h00, 8'h01); 166 $display("En UART Send"); 167 end 168 endtask 169 170 task Stop_UART_Send;/*停止串口發送*/ 171 begin 172 Send_CMD(`UART_En_Tx, 8'h00, 8'h00); 173 $display("Stop UART Send"); 174 end 175 endtask 176 177 task En_S_ADC;/*使能采集ADC數據*/ 178 begin 179 Send_CMD(`ADC_Sample_En, 8'h00, 8'h01); 180 $display("En Sample ADC data"); 181 end 182 endtask 183 184 task Stop_S_ADC;/*停止采集ADC數據*/ 185 begin 186 Send_CMD(`ADC_Sample_En, 8'h00, 8'h00); 187 $display("Stop Sample ADC data"); 188 end 189 endtask 190 191 task Set_ADC_Sample_Speed;/*設置ADC采樣率*/ 192 input[25:0] Fs;/*采樣率實際頻率*/ 193 reg [31:0] S_cnt_top;/*分頻計數器計數最大值*/ 194 begin 195 /*由采樣實際頻率值換算出采樣分頻計數器計數最大值*/ 196 S_cnt_top = 50000000/Fs - 1; 197 /*寫采樣分頻計數器計數最大值低16位*/ 198 Send_CMD(`ADC_S_Cnt_Max_L,S_cnt_top[15:8],S_cnt_top[7:0]); 199 /*寫采樣分頻計數器計數最大值高16位*/ 200 Send_CMD(`ADC_S_Cnt_Max_H,S_cnt_top[31:24],S_cnt_top[23:16]); 201 $display("Set ADC Sample Speed as = %0d" ,Fs); 202 end 203 endtask 204 205 task Set_DDS_Sample_Speed;/*設置DDS數據的采樣率*/ 206 input[25:0] Fs;/*采樣率實際頻率*/ 207 reg [31:0] S_cnt_top;/*分頻計數器計數最大值*/ 208 begin 209 /*由采樣實際頻率值換算出采樣分頻計數器計數最大值*/ 210 S_cnt_top = 50000000/Fs - 1; 211 /*寫采樣分頻計數器計數最大值低16位*/ 212 Send_CMD(`DDS_S_Cnt_Max_L,S_cnt_top[15:8],S_cnt_top[7:0]); 213 /*寫采樣分頻計數器計數最大值高16位*/ 214 Send_CMD(`DDS_S_Cnt_Max_H,S_cnt_top[31:24],S_cnt_top[23:16]); 215 $display("Set DDS Sample Speed as = %0d" ,Fs); 216 end 217 endtask 218 219 task Set_DDS_Fout_Speed;/*設置DDS輸出信號頻率*/ 220 input[25:0] Fs;/*輸出信號實際頻率*/ 221 reg [31:0] r_fword;/*DDS頻率控制字*/ 222 begin 223 /*由實際要求輸出頻率數據換算出頻率控制字*/ 224 r_fword = Fs*65536*65536/50000000; 225 Send_CMD(`DDS_Fword_L,r_fword[15:8],r_fword[7:0]); 226 Send_CMD(`DDS_Fword_H,r_fword[31:24],r_fword[23:16]); 227 $display("Set DDS Fout as = %0d" ,Fs); 228 end 229 endtask 230 231 232 task press_key; 233 input [KEY_WIDTH/2:0]Key; 234 reg [15:0]myrand; 235 begin 236 Key_in = {KEY_WIDTH{1'b1}}; 237 /*按下抖動*/ 238 repeat(20)begin 239 myrand = {$random} % 65536; 240 #myrand Key_in[Key] = ~Key_in[Key]; 241 end 242 Key_in[Key] = 1'b0; 243 244 #22000000;/*穩定期*/ 245 246 /*釋放抖動*/ 247 repeat(20)begin 248 myrand = {$random} % 65536; 249 #myrand Key_in[Key] = ~Key_in[Key]; 250 end 251 Key_in[Key] = 1'b1; 252 #22000000;/*穩定期*/ 253 end 254 endtask 255 256 endmodule
下圖為系統仿真架構圖:
這里,在我們提供的工程中,已經設置好了Nativelink,用戶只需要在Quartus II中點擊tools—run rtl simulation tool—rtl simulation即可自動調用modelsim-altera並執行仿真,因為這里完全模擬真實時序進行仿真,因此運行完整個仿真大約需要5—10分鍾。
仿真完成后,結果如圖所示:
其中,Rx_Byte為串口接收仿真模型接收到的數據,這里以波形的方式展示。ADC_Data為ADC采樣結果,DDS_Data為DDS輸出的數據最下方為按鍵標志和按鍵結果,當按下按鍵1時,數據通道切換為ADC的采樣結果,當按下按鍵2時,數據通道切換為DDS的輸出數據。
(如果用戶在進行仿真的過程中發現仿真無法運行,在modelsim中提示錯誤的話,請刪除simulation—>modelsim文件夾下除wave.do和mydo.do文件外的其他所有文件,然后在quartus 中重新啟動仿真)
6、基於串口獵人的板級驗證
這里,我們使用一款功能非常強大的串口調試軟件——串口獵人來調試我們的設計。串口獵人的安裝這里不做過多的講述。首先,我們將FPGA系統的sof文件配置到fpga中,然后運行串口獵人軟件,串口獵人打開后界面如下所示:
我們點擊圖中的動畫即可讓該動畫消失。
接下來我們載入預先設置好的配置文件,如下圖所示:
我們點擊右下角的“載入”按鈕,在彈出的界面中,定位到我們本實驗的根目錄,選擇“serialhunter.ini”文件,
點擊打開。
切換到高級發碼選項卡,顯示如下所示:
點擊啟動自動發碼。
回到基本功能選項卡,可以看到,窗口中開始連續不斷的接收到數據,如下圖所示:
此時,我們切換到波形顯示選項卡,可看到如下所示的效果:
表明我們已經正確的接收到了波形數據。
切換到碼表選項卡,效果如下圖所示:
然后,我們切換到柱狀顯示選項卡,效果如下所示:
然后,我們回到高級發碼選項卡,將0~3組發碼列表前的勾選取消,勾選上第4組,然后點擊啟動自動發碼。此時,我們就已經將fpga系統的接收和發送波特率速率切換到了115200,如下圖所示:
因為波特率不對,所以接下來接收到的數據就全部是錯誤的了。我們回到基本功能選項卡,將波特率切換為115200bps,如下圖所示:
然后我們再回到波形顯示選項卡,結果如下所示:
這時,我們再回到高級發碼選項卡,取消第4組發碼的勾選,勾選上第5組發碼,然后點擊自動發碼,再回到波形顯示選項卡,結果如下所示:
此時,我們的DDS輸出信號頻率便更改為50Hz了。其他更多指令內容,這里就不一一介紹了,歡迎各位積極探索。
7、總結
當然,這個系統的最終目標是教會大家在fpga中使用串口進行簡單的數據收發,離真正的虛擬示波器還相差甚遠。此串口獵人顯示的波形頻率並不能嚴格的和實際信號的頻率對應上,這一點望各位悉知。也歡迎有上位機開發基礎的同學來根據本系統開發獨立的上位機軟件。另外,在使用中,我們只需要按下按鍵2,就能將數據通道切換到ADC的采樣結果上來,此時,給ADC的輸入上給出不同的電壓,在碼表選項卡上就能明顯的看到數值的變化,可作為電壓表之用。按下按鍵1則切換到內部DDS通道。
另外,文檔中使用的ADC型號為TCL549,我們開發套件后續配備的ADC模塊已經更改為TLV1544,因此,我們在工程中使用了條件編譯的方式,用戶可以根據手頭使用的ADC具體型號,設置編譯條件即可切換ADC型號,如下圖所示:
如果使用TLV1544作為實際采樣器件,只需要在uart_scope.v文件的開頭,將 “`define USE_TLV1544 1”這句話使能,將“`define USE_TLC549 1”這句話注釋掉即可。反之亦然。
由於本系統涉及到的功能模塊和代碼較多,無法一一為各位講解,希望各位能夠仔細閱讀代碼,代碼中小梅哥都做了詳細的注釋,希望大家通過代碼,能進一步學習verilog語法,增強對系統級仿真的意識。
如有更多問題,歡迎加入芯航線 FPGA 技術支持群交流學習:472607506
小梅哥
芯航線電子工作室
關於學習資料,小梅哥系列所有能夠開放的資料和更新(包括視頻教程,程序代碼,教程文檔,工具軟件,開發板資料)都會發布在我的雲分享。(記得訂閱)鏈接:http://yun.baidu.com/share/home?uk=402885837&view=share#category/type=0
贈送芯航線AC6102型開發板配套資料預覽版下載鏈接:鏈接:http://pan.baidu.com/s/1slW2Ojj 密碼:9fn3
贈送SOPC公開課鏈接和FPGA進階視頻教程。鏈接:http://pan.baidu.com/s/1bEzaFW 密碼:rsyh