異步通信:以一個字符為傳輸單位,通信中兩個字符間的時間間隔是不固定的,然而在同一個字符的兩個相鄰位代碼間的時間間隔是固定的。
通信協議:指通信雙方約定的一些規則。在使用串口通信的時候,規定有:空閑位、起始位、數據位、奇偶校驗位、停止位。
02串口通信時序
這個協議在 FPGA 內部是除 SPI 之外最簡單的接口吧,其實就是發送方與接收方相互認定的協議(暗號),這種接口數據一般是單向傳輸,所以發送方和接收方通信一般需要兩根數據線。
圖1 URAT時序圖
數據線在沒有數據傳輸時保持高電平,當需要傳輸數據時,發送方把數據線拉低一段時間,告訴接收方開始傳輸數據了。之后把數據從低位到高位或者高位到低位(這個根據通信雙方的要求確定)依次發送給對方(數據的位數雙方應該事先確認好,通常5~8位數據)。數據發送完,可能會發送一位奇偶校驗(這部分在下一節構建完整UART協議時細說)。最后就是將數據線拉高一段時間表示數據傳輸結束。
在這之間就會有疑問,每位數據電平持續時間到底是多久?
這就引出波特率,通常就是說每秒能傳輸多少位數據,比如波特率為9600bit/s,就是指1秒傳輸9600位數據(當然這是包含起始位,校驗位,停止位在內的,所以有效數據其實并沒有這么多)。當使用該波特率時,那每個電平持續時間不就是1/9600秒么。
03串口接收模塊設計
首先確定模塊接口信號,肯定有個串口的輸入信號uart_rx吧,然后時鐘信號clk和復位信號rst_n也是不可能少的。接收到數據后肯定要輸出吧,所以在加一個uart_rx,注意該信號位寬應該是可以改變的(因為串口協議的數據位可以改變)。一般還要有一個信號用于指示接收到的數據什么時候是有效的,便于后續模塊使用uart_rx,即uart_tx_vld(為高電平時,表示uart_rx有效)。
信號 | I/O | 位寬 | 含義 |
clk | I | 1 | 系統時鐘,50MHZ |
rst_n | I | 1 | 系統復位,低電平有效 |
uart_rx | I | 1 | UART接口輸入信號 |
rx_data | O | 8 | 數據輸出信號 |
rx_vld | O | 1 | 數據有效指示信號,高電平有效 |
模塊總體思路:有了輸入輸出信號后,模塊內部就是根據輸入信號生成輸出信號而已。通過觀察圖1時序知道,每位數據傳輸需要使用 1/波特率 的時間,每次需要傳輸的 “數據” 包括起始位,數據位,校驗位,結束位。那么以上是不是就對應兩個計數器?所以使用計數器data_num來計數一位數據傳輸需要的時間(需要將1/波特率轉換為系統時鐘個數作為data_num的結束條件),使用計數器cnt來計數目前傳輸的第幾位數據了。整體思路就是如此,大致如下圖,接下來就是細節:
圖2 計數器架構
計數器data_num該從什么時候計數?
當發送方發送起始位時會把數據線拉低,并且在之后一段時間內發送起始位,數據位等數據,那么data_num在此期間都要計數,直到停止位接收完成為止。由此引入一個標志信號flag,該信號為高電平時,計數器data_num就計數,當計數到時鐘頻率/波特率(1/波特率對應的時鐘個數)時清零。
故計數器data_num初始值為0,計數條件add_data_num = flag,結束條件為end_data_num = add_data_num && data_num == BSP_NUM - 1。
flag當然就是檢測到數據線下降沿時拉高,當計數器cnt計數結束時拉低,其余時間保持不變了。
故flag拉高條件:檢測到uart_rx下降沿,拉低條件為end_cnt。
接下來就是計數器cnt了,cnt表示數據線此時傳輸的是第幾位數據了。當計數器data_num計數結束時,表示一位數據傳輸完成了,此時cnt就應該加一了。當計數器計數到 起始位數+數據位數+校驗位數+停止位數 時表示數據傳輸完成了,此時cnt計數結束并清零,其余時間保持不變。
故計數器cnt初始值為0,計數條件add_cnt = end_data_num,計數器清零條件end_data_num = add_cnt && cnt == CNT_W - 1。CNT_W = 起始位數+數據位數+校驗位數+停止位數 。
接下來就是接收數據并產生輸出信號了,一般會在計數器data_num計數的中部將數據線上的數據取下來進行保存,此時的數據是比較穩定的。由于最終需要輸出的只是數據位,本文不考慮校驗位,傳輸第0位是起始位,不需要保存。cnt==1時表示傳輸第1位數據,需要保存到輸出信號上的最低位(這是由于串口調試助手是先發的最低位,實際情況要看發送方先發高位還是低位)。
flag拉高后,計數器data_num進行計數,當計數完一位數據后清零,并且cnt計數器進行計數,當cnt大于等于1,小于等于8時,表示此時接收的是數據位,將接收到的數據保存到rx_data對應位(最好是在data_num為容量的一半時進行保存),當cnt計數器計數完成,表示一組數據接收完成,此時有效指示信號拉高,并且flag信號拉低,結束一組數據的接收;所以當cnt=1 && data_num == BSP_CNT/2-1時(BSP_CNT表示波特率對應的時鐘個數),有rx_data[0] <= uart_rx。
經過對其它位的詳細分析,最終會得到這樣的結果:當cnt >=1 && cnt <= DATA_W && data_num == BSP_CNT/2-1 && add_data_num 時(DATA_W表示每次發送的數據位位數),rx_data[cnt - 1] <= uart_rx;這樣就產生了輸出數據信號。
之后就是產生輸出有效指示信號,該信號當然是接收完數據時產生的,其實可以在計數器cnt計數結束時產生。但數據在接收完數據位后,其實數據就已經接收完成了,此時就可以把輸出有效指示信號拉高了,這樣后續模塊就可以提前使用接收到的數據。所以當cnt == DATA_W && add_data_num && data_num == BSP_NUM/2-1時將rx_data_vld拉高,其余時間拉低。
如果想要保證輸出數據線上數據比較干凈,不出現接收過程中的無效數據,那么可以將rx_data和rx_data_vld在rx_data_vld有效時才進行輸出,其余時間保持不變。
最后還要注意,數據線是其他芯片或者設備輸入的信號,為了減小亞穩態出現的機率,一般需要將數據線上的信號通過寄存器寄存兩個時鐘。由于還需要檢測數據線的下降沿,所以還要把該信號延遲一個時鐘,最終將接收到的信號uart_rx打三拍(前兩拍用于同步處理,最后一拍用于檢測輸入信號的下降沿),然后通過uart_rx_ff1和uart_rx_ff2檢測出下降沿,把標志信號flag拉高。
整體時序圖如下:
圖3 整體時序
時序圖可能在手機上沒法看,所以將上圖各個部分截圖放在下面:
圖4 準備傳輸數據
當計數器data_num=BSP_NUM/2-1的時候,將uart_rx_ff2的數據保存到rx_data的第cnt-1位,下圖為最低位,至于為什么是uart_rx_ff2,而不是uart_rx_ff1,通過下圖可知uart_rx_ff2與計數器data_num是對齊的,所以該信號會更準。由于串口傳輸數據還是比較慢的,使用這兩個信號對結果基本沒有影響。
圖5 接收最低位數據
接收完8位數據,將輸出使能信號拉高,rx_data的x表示不確定,因為圖4~圖6只能確定接收的最高位和最低位數據,其余時序沒有畫,中間的時序類似,所以省略雙波浪線表示中間的數據傳輸省略。
最后接收完停止位后,end_cnt拉高表示接收一次數據傳輸完成,將兩個計數器清零,并且將標志信號flag拉低。
圖6 接收完8位數據
上述將模塊內部信號講完了,如果要實現功能完全夠了,但是在調用模塊時,我們往往不習慣去改模塊內部的參數,這就需要通過parameter和localparam添加一些參數,來自動設置計數器位寬,計數器結束條件等等。其實人為需要設置的就是波特率、數據位位數、校驗位數、停止位數(起始位是必須的,故不考慮設置參數),由于計算波特率對應是時鐘個數時還需要知道系統時鐘頻率,所以增加一個系統時鐘頻率參數。
所以parameter就定義波特率BPS、時鐘頻率FCLK、數據位數DATA_W、校驗位數CHECK_W 、停止位數STOP_W 。而localparam需要通過parameter定義的參數得到波特率對應的 時鐘數BPS_CNT=時鐘頻率FCLK/波特率BPS ,計數器data_num需要計數到BPS_CNT,所以需要通過BPS_CNT計算出計數器data_num的位寬BPS_CNT_W,可以通過以下函數實現。
function integer clogb2(input integer depth);begin if(depth==0) clogb2 = 1; else if(depth!=0) for(clogb2=0;depth>0;clogb2=clogb2+1) depth=depth>>1; end endfunction
接下來就是cnt計數器的結束條件了,可以由localparam定義CNT_NUM=DATA_W + CHECK_W + STOP_W。在利用上面函數計算出該計數器的位寬CNT_NUM_W就行了,內部信號根據這些常量變化即可。
由此設計的模塊在例化時,只需要修改parameter的幾個常量即可,不要對模塊內部代碼做任何處理,這部分操作不會占用額外資源,在綜合工具對齊進行綜合時就會處理,不會消耗FPGA的除法器之類的資源。
根據以上分析,直接得到以下代碼,基本上不需要仿真調試。
04 參考代碼
//--############################################################################################### //--# Designer : 發送一位數據所需系統時鐘數計算方式BPS_CNT = 1000_000_000/(Tclk*比特率), //Tclk是系統時鐘周期,單位ns。 //--############################################################################################### module uart_rx #( parameter FCLK = 50_000_000 ,//系統時鐘頻率,默認50MHZ; parameter BPS = 9600 ,//串口波特率; parameter DATA_W = 8 ,//接收數據位數以及輸出數據位寬; parameter CHECK_W = 0 ,//校驗位,0代表無校驗位; parameter STOP_W = 1 //1位停止位; )( input clk ,//系統工作時鐘50MHZ input rst_n ,//系統復位信號,低電平有效 input uart_rx ,//UART接口輸入信號 output reg [DATA_W-1:0] rx_out ,//數據輸出信號 output reg rx_out_vld //數據有效指示信號 ); localparam BPS_CNT = FCLK/BPS;//波特率為9600bit/s,當波特率為115200bit/s時,DATA_115200==434; localparam BPS_CNT_W = clogb2(BPS_CNT-1);//根據BPS_CNT調用函數自動計算計數器data_num位寬; localparam CNT_NUM = DATA_W + CHECK_W + STOP_W;//計數器計數值; localparam CNT_NUM_W = clogb2(CNT_NUM);//根據計數器cnt的值,利用函數自動計算此計數器的位寬; reg rx_vld ;//表示接收完一組串口發來的數據了; reg uart_rx_ff0 ; reg uart_rx_ff1 ; reg uart_rx_ff2 ; reg flag ; reg [BPS_CNT_W-1:0] data_num ; reg [CNT_NUM_W-1:0] cnt ; reg [DATA_W-1:0] rx_data ; wire add_data_num ; wire end_data_num ; wire add_cnt ; wire end_cnt ; /******************注釋開始****************** 自動計算信號位寬; ******************注釋結束******************/ function integer clogb2(input integer depth);begin if(depth==0) clogb2 = 1; else if(depth!=0) for(clogb2=0;depth>0;clogb2=clogb2+1) depth=depth>>1; end endfunction /******************注釋開始****************** 接收一位數據所用時間計數器data_num,初始值為0,當接收到數據時進行計數, 當一位數據接收完成時清零; ******************注釋結束******************/ always@(posedge clk or negedge rst_n)begin if(!rst_n)begin data_num <= {{BPS_CNT_W}{1'b0}}; end else if(add_data_num)begin if(end_data_num) data_num <= {{BPS_CNT_W}{1'b0}}; else data_num <= data_num + {{{BPS_CNT_W-1}{1'b0}},1'b1}; end end assign add_data_num = flag; assign end_data_num = add_data_num && data_num==BPS_CNT-1; //接受一組數據所用時間; always@(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt <= {{CNT_NUM_W}{1'b0}}; end else if(add_cnt)begin if(end_cnt) cnt <= {{CNT_NUM_W}{1'b0}}; else cnt <= cnt + {{{CNT_NUM_W-1}{1'b0}},1'b1}; end end assign add_cnt = end_data_num; assign end_cnt = add_cnt && cnt== CNT_NUM-1; /******************注釋開始****************** PC端相對應于FPGA為異步接口,為預防亞穩態產生,對接收數據進行打兩拍處理,由于需要采集信號下降沿,故打三拍處理; ******************注釋結束******************/ always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//三個寄存器組成移位寄存器,初始化為0; {uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= 3'd0; end else begin//時鐘上升沿時,將uart_rx信號移入移位寄存器,其余位左移一位; {uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= {uart_rx_ff1,uart_rx_ff0,uart_rx}; end end always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin flag <= 1'b0; end else if(uart_rx_ff2==1 && uart_rx_ff1==0)begin//取UART_RX信號下降沿 flag <= 1'b1; end else if(end_cnt)begin//一組數據接收完畢; flag <= 1'b0; end end //在中間時刻對輸入數據進行采集,并且將數據存入rx_data; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_data <= {{DATA_W}{1'b0}}; end else if(cnt>=1 && cnt<=DATA_W && add_data_num && data_num==BPS_CNT/2-1)begin rx_data[cnt-1] <= uart_rx_ff2; end end //在接收完數據后,指示產生rx_data信號有效; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_vld <= 1'b0; end else begin rx_vld <= (cnt==CNT_NUM-1 && add_data_num && data_num==BPS_CNT/2-1); end end //當接收完一組數據后,將接收到的數據經過一組觸發器暫存后輸出; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin// rx_out <= 0; end else if(rx_vld)begin rx_out <= rx_data; end end //在接收完數據后,拉高一個時鐘,指示產生rx_out信號有效; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_out_vld <= 1'b0; end else begin rx_out_vld <= rx_vld; end end endmodule05 modelism仿真
仿真部分的代碼,通過一個任務task實現串口數據的發送,由于上述設計不支持校驗位,所以這個模塊設置校驗位也是沒有用的。
發送數據只需要調用tx();任務即可,內部直接輸入待發送數據,數據位寬依舊通過DATA_W設置,波特率BPS設置。
參考代碼:
`timescale 1 ns/1 ns module uart_rx_test(); parameter CYCLE = 20;//The unit is ns. The default value is 10ns; parameter RST_TIME = 10;//Reset time: Reset 3 clock widths by default; parameter STOP_TIME = 1000;//Time for simulation running after reset (unit: clock cycle). Simulation stops after 1000 clocks are run by default; // uart_rx Parameters parameter FCLK = 50_000_000;//系統時鐘頻率; parameter BPS = 9600 ;//串口波特率; parameter BPS_CNT = FCLK/BPS ;//波特率對應時鐘數,不用手動修改該參數; parameter DATA_W = 8 ;//接收數據位數以及輸出數據位寬; parameter CHECK_W = 2'b01 ;//校驗位,2'b00代表無校驗位,2'b01表示奇校驗,2'b10表示偶校驗,2'b11無效。 parameter STOP_W = 2'b11 ;//停止位,2'b01表示1位停止位,2'b10表示2位停止位,2'b11表示1.5位停止位; // uart_rx Inputs reg clk ; reg rst_n ; reg uart_tx ; // uart_rx Outputs wire [DATA_W-1:0] rx_out ; wire rx_out_vld ; //例化串口接收模塊; uart_rx #( .FCLK (FCLK ), .BPS (BPS ), .DATA_W (DATA_W ), .CHECK_W (CHECK_W ), .STOP_W (STOP_W )) u_uart_rx ( .clk ( clk ), .rst_n ( rst_n ), .uart_rx ( uart_tx ), .rx_out ( rx_out ), .rx_out_vld ( rx_out_vld ) ); //The local clock is generated at 100 MB; initial begin clk = 0; forever #(CYCLE/2) clk=~clk; end //Generate reset signal; initial begin rst_n = 1; #2; rst_n = 0; #(RST_TIME*CYCLE);//復位完成; rst_n = 1; end //Input signal din assignment method; initial begin #1;uart_tx = 1; //初始化時輸入高電平; #(100*CYCLE); //Start assigning values; tx(8'ha5); //以串口形式發送8'h5a; #(500*CYCLE); //發送完成后延遲500個時鐘; tx(8'h5a); //之后發送數據8'h59; #(500*CYCLE); //發送完成后延遲500個時鐘; $stop; //Stop simulation; end //模擬串口發送函數,1位起始位,1位停止位,無校驗位,8位數據,先發低位; integer i;//用于控制循環次數; task tx( input [DATA_W-1:0] data //串口待發送數據; ); begin @(posedge clk);//延遲一個時鐘后發送起始位; #1; uart_tx = 1'b0; repeat(BPS_CNT) @(posedge clk);//延遲BPS_CNT個時鐘; for(i=0 ; i<8 ; i=i+1)begin #1; uart_tx = data[i]; repeat(BPS_CNT) @(posedge clk);//延遲BPS_CNT個時鐘; end if(CHECK_W == 2'b01)begin #1;uart_tx = ~(^data);//奇校驗時,發送數據; repeat(BPS_CNT) @(posedge clk);//延遲BPS_CNT個時鐘; end else if(CHECK_W == 2'b10)begin #1;uart_tx = (^data);//偶校驗時,發送數據; repeat(BPS_CNT) @(posedge clk);//延遲BPS_CNT個時鐘; end @(posedge clk);//延遲一個時鐘后發送停止位; #1; uart_tx = 1'b1; if(STOP_W == 2'b01)//1位停止位; repeat(BPS_CNT) @(posedge clk);//延遲BPS_CNT個時鐘; else if(STOP_W == 2'b10)//2位停止位; repeat(2*BPS_CNT) @(posedge clk);//延遲2*BPS_CNT個時鐘; else if(STOP_W == 2'b11)//1.5位停止位; repeat(BPS_CNT*3/2) @(posedge clk);//延遲1.5*BPS_CNT個時鐘; end endtask endmodule
仿真運行結果(rx_out先接收到8’ha5,后接收到8’h5a):
圖7仿真結果
查看細節:開始接收數據(起始位)片段:
圖8起始位仿真
接收最低位數據仿真如下:
圖9接收第一位數據
接收最后一位數據,并且產生輸出有效指示信號,下一個時鐘將數據輸出,此時串口傳輸實際上并沒有完成,最后一位數據才傳輸一半(data_num計數器才2603==5208/2-1),但已經接收到完整數據,所以直接輸出,節省時間,但flag信號依舊位高電平,表示該模塊還處于工作狀態。
圖10接收完最后一位數據
計數器data_num計數到5208-1并且計數器cnt計數器到8,表示一次傳輸完成,flag信號拉低,并且兩個計數器清零,表示完成傳輸,仿真如下:
圖11接收完停止位
06 綜合測試
這個工程很久了,之前學的時候使用quartus綜合的,綜合效果如下所示:
圖12quartus綜合工程
對應的RTL模塊視圖(由于時鐘頻率FCLK和波特率BPS參數設置會影響計數器cnt和data_num的位寬,所以不同數據匯總和出不同的電路,下圖為時鐘頻率50MHZ,波特率9600的RTL視圖):
圖13RTL視圖
對系統時鐘頻率進行約束后,最大時鐘頻率為120.86MHZ,遠大于實際的50MHZ,滿足時序要求;
圖14系統最大工作時鐘頻率
signal tap II 測試
將程序下載到FPGA,打開串口調試助手,設置波特率9600,發送數據0XA5,使用signal tap II抓取數據8'hA5。
圖15串口助手發送數據
串口調試助手發送數據0XB3,使用signal tap II抓取數據8'hB3。
圖16signal tap接收串口助手發送數據
串口調試助手發送數據0X5a,使用signal tap II抓取數據8'h5A。
圖17 調試
07總結
其實最主要的就是能夠根據協議找到合適的主架構,然后根據該架構去產生輸出信號。
本文就利用兩個計數器作為主架構,根據計數器的狀態生成輸出信號,切記我們需要的并不是計數器,而是計數器生成的輸出信號,如果使用parameter要考慮模塊內部各種會改變的數據與這些參數的關系。
最好不要留需要手動修改的數據,這種數據如果忘記修改,會對后續設計造成很大影響,浪費調試時間。
來源:本文轉載自數字站公眾號
-
FPGA
+關注
關注
1629文章
21743瀏覽量
603558 -
接收模塊
+關注
關注
1文章
20瀏覽量
10465 -
串口
+關注
關注
14文章
1555瀏覽量
76547 -
uart
+關注
關注
22文章
1237瀏覽量
101419 -
串行通信
+關注
關注
4文章
574瀏覽量
35407
原文標題:基于FPGA的UART串口接收模塊
文章出處:【微信號:FPGA研究院,微信公眾號:FPGA研究院】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論