FPGA實現串口UART自收發0推薦
發表于 2014/7/22 9:45:03
閱讀(2105)
評論(0)
串行接口是最簡單的一種通信方式,串口通信有兩種方式,一種是同步串行,如SPI接口;另一種則是異步串行,即我們所說的UART。這個項目向大家展示了如何使用FPGA來模擬UART收發器。 普遍意義上講,UART接口是分為兩種: a)TTL電平接口 b)RS232電平接口 通常我們看不到設備直接裸露出來的TTL電平接口,TTL電平接口一般為芯片直接連接的引腳,一般是供我們調試設備用的端口。實際上我們看得到的都是RS232電平接口,這個在臺式機上很常見,有DB9和DB25兩種規格插頭。 TTL電平UART和電腦相連接,有以下幾種方式: a)TTL電平接口通過PL2303(PL2302)即USB轉TTL電平芯片和電腦相連 b)如果電腦上直接有RS232接口,則可通過MAX232芯片將RS232電平與TTL電平轉換 c)當然,如果FPGA端已經板載了MAX232,你又不想重新自己引出TTL電平UART的話,如果電腦上又沒有RS232接口,則通過一根USB轉串口線(PL2303+MAX232)與FPGA端RS232接口連接 異步串行通訊 RS-232使用異步通訊協議,也就是說數據的傳輸沒有時鐘信號。數據以每次一位的方式傳輸;每條線用來傳輸一個方向的數據。通常是以8位數據為1個字節,先發送最低有效位,最后發送最高有效位。接收端必須有某種方式,使之與接收數據同步。 對于RS-232來說,是這樣處理的:
每次傳輸完成一個字節之后,都在其后發送一個停止位("1") (二) 波特率發生器 我們選擇的是9600的波特率,這個參數可以根據實際需要來調整。實際上,針對固定的時鐘頻率,可以事先做成一個分頻比表格,這樣就可以方便的調節波特率了。 FPGA通常運行在遠高于9600Hz的時鐘頻率上(對于今天的標準的來說RS-232真是太慢了),我們使用板載的50MHz的晶振作為波特率發生器的輸入時鐘,這就意味著我們需要用一個較高的時鐘來分頻產生盡量接近于9600Hz的時鐘信號。 1) 循環波特率發生器 我們希望5000000是2的整數冪,但很可惜,它不是。所以我們改變分頻比,"5000000/9600" 約等于 "2^17/25" = 5242.88. 這跟我們要求的分頻比5208.333很比較近,并且使得在FPGA上實現起來相當有效。 reg [17:0] acc; //一共18位 always @(posedge clk) acc <= acc[16:0] + 25; //我們使用上一次結果的低17位,但是保留18位結果 wire BaudTick = acc[17]; //第18位作為進位輸出 使用 50MHz 時鐘, "BaudTick" 為 9537波特,與理想的9600波特存在 0.65% 的誤差,誤差太高,實際上我們采樣數據的時候都是在波特率周期一半的時間采樣,似乎影響不大,但是考慮到發送數據量大的時候會積累出很大的周期偏移,故我們不采用此種分配方式。 我們的全局時鐘周期為1/50MHz=20ns,而要求的9600波特率的周期為1/9600Hz=104.2us,兩者的倍數關系為5210,即按照上述算出來的波特率周期為104.9us。按照標準波特率產生的數據,進行采樣的時候,為了保證采樣數據的穩定正確,我們在數據的中間點采樣,由于我們產生的波特率偏小,導致每次應該在52.1us采樣的數據推延到52.45us,亦即每1bit會使實際的采樣點延后正常的采樣點3.45us,故發送完15bit之后,數據的采樣點會偏移到下一個字符,數據便會出現紊亂,出現亂碼。 2) 參數化FPGA波特率發生器 由于前面所述的波特率發生器設置方法產生的偏差過大,故我們采用以下波特率產生方式即暴力直接累加法。
//以下波特率分頻計數值可參照需要設計的參數進行更改 `define BPS_PARA 5208 //波特率為9600時的分頻計數值 `define BPS_PARA_2 2604 //波特率為9600時的分頻計數值的一半,用于數據采樣 always @ (posedge clk or negedge rst_n) if(!rst_n) cnt <= 13'd0; else if((cnt == `BPS_PARA) || !bps_start) cnt <= 13'd0; //波特率計數清零 else cnt <= cnt+1'b1; //波特率時鐘計數啟動 這就是整個的設計方法了。 按照此種方法設計的波特率發生器,波特率為50MHz/5208=9600.6,與標準9600波特率誤差為0.00625%,已經相當精確。 (三) TX發送模塊 下面是我們所想要實現的:
接收模塊傳送8位數據到發送模塊,rx_int信號使能tx_en,8位數據被串行輸出。("tx_en"置位后開始傳輸)。 TX發送模塊的參數是固定的: 8位數據, 1個停止位, 無奇偶校驗。 數據串行化 經過上述的波特率發生器,我們已經產生了9600的波特率。 由于我們的程序功能實現的是,將接收來的數據發送回去,程序如下: if(neg_rx_int) begin //接收數據完畢,準備把接收到的數據發回去 bps_start_r <= 1'b1; tx_data <= rx_data; //把接收到的數據存入發送數據寄存器 tx_en <= 1'b1; //進入發送數據狀態中 在always模塊中進行數據發送的判斷, if(tx_en) begin //使能發送的信號 if(clk_bps) begin //波特率時鐘到后開始發送 num <= num+1'b1; case (num) 4'd0: rs232_tx_r <= 1'b0; //發送起始位 4'd1: rs232_tx_r <= tx_data[0]; //發送bit0 4'd2: rs232_tx_r <= tx_data[1]; //發送bit1 4'd3: rs232_tx_r <= tx_data[2]; //發送bit2 4'd4: rs232_tx_r <= tx_data[3]; //發送bit3 4'd5: rs232_tx_r <= tx_data[4]; //發送bit4 4'd6: rs232_tx_r <= tx_data[5]; //發送bit5 4'd7: rs232_tx_r <= tx_data[6]; //發送bit6 4'd8: rs232_tx_r <= tx_data[7]; //發送bit7 4'd9: rs232_tx_r <= 1'b1; //發送結束位 default: rs232_tx_r <= 1'b1; endcase end else if(num==4'd10) num <= 4'd0; //復位 end end 最后將發送的數據送到總線上, assign rs232_tx = rs232_tx_r; (四) RX接收模塊 下面是我們想要實現的模塊:
我們的設計目的是這樣的: 1.當rs232_rx線上有數據時,接收模塊負責識別rs232_rx線上的數據 2.當收到一個字節的數據時,鎖存接收到的數據到"rx_data"總線,并使"rx_int"有效一個周期。 注意:只有 當"rx_int"有效時," rx_data "總線的數據才有效,其他的時間里不允許使用" rx_data "總線上的數據,因為新的數據可能已經改變了其中的部分數據。 數據采樣 異步接收機必須通過一定的機制與接收到的輸入信號同步(接收端沒有辦法得到發送斷的時鐘),這里采用如下辦法: 為了確定新數據的到來,需檢測開始位,我們在波特率時鐘周期的一半處進行數據的采樣。 首先,接收到的" rx_data "信號與我們的時鐘沒有任何關系,所以采用4個D觸發器對其進行采樣,并且使之我我們的時鐘同步,同時也是對接收到的數據進行濾波,這樣可以防止毛刺信號被誤認為是開始信號。 reg rs232_rx0,rs232_rx1,rs232_rx2,rs232_rx3; //接收數據寄存器,濾波用 wire neg_rs232_rx; //表示數據線接收到下降沿 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin rs232_rx0 <= 1'b1; rs232_rx1 <= 1'b1; rs232_rx2 <= 1'b1; rs232_rx3 <= 1'b1; end else begin rs232_rx0 <= rs232_rx; rs232_rx1 <= rs232_rx0; rs232_rx2 <= rs232_rx1; rs232_rx3 <= rs232_rx2; end end //下面的下降沿檢測可以濾掉<20ns-40ns的毛刺(包括高脈沖和低脈沖毛刺), //這里就是用資源換穩定(前提是我們對時間要求不是那么苛刻,因為輸入信號打了好幾拍) //我們的有效低脈沖信號肯定是遠遠大于40ns的,104us。 assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; //接收到下降沿后neg_rs232_rx置高一個時鐘周期 一旦檢測到"開始位",使用如下的代碼可以檢測出接收到每一位數據。 if(rx_int) begin //一旦檢測到開始位,即rs232_rx下降沿,rx_int置位 if(clk_bps) begin //讀取并保存數據,接收數據為一個起始位,8bit數據,1個結束位 num <= num+1'b1; case (num) 4'd1: rx_temp_data[0] <= rs232_rx; //鎖存第0bit 4'd2: rx_temp_data[1] <= rs232_rx; //鎖存第1bit 4'd3: rx_temp_data[2] <= rs232_rx; //鎖存第2bit 4'd4: rx_temp_data[3] <= rs232_rx; //鎖存第3bit 4'd5: rx_temp_data[4] <= rs232_rx; //鎖存第4bit 4'd6: rx_temp_data[5] <= rs232_rx; //鎖存第5bit 4'd7: rx_temp_data[6] <= rs232_rx; //鎖存第6bit 4'd8: rx_temp_data[7] <= rs232_rx; //鎖存第7bit default: ;//起始位和停止位均通過default去除 endcase 使用一個寄存器來存儲接受到的數據, if(num == 4'd10) begin //標準接收模式下只有1+8+1=10bit有效數據 num <= 4'd0; //接收到STOP位后結束,num清零 rx_data_r <= rx_temp_data; //把數據鎖存到數據寄存器rx_data end 利用此寄存器來驅動模塊間接口rx_data, assign rx_data = rx_data_r; RX模塊中,以下兩處num清零及波特率發生信號關閉的信號的輸出,在num==4'd10或者num==4'd9時做出判斷程序的功能正常,我的理解是: 在num==4'd10做出判斷是正常的選擇,因為要接受第10位停止位;而在num==4'd9做出判斷功能正常,原因在于,雖然沒有接收第10位,但是因為第10位是停止位,是高電平,在接收下一個符號的時候我只監測總線上的下降沿,不管高電平的時間長度,故num==4‘d也不影響程序的功能。僅為個人看法,拋磚引玉。 if(num == 4'd10) begin //標準接收模式下只有1+8+1=10bit有效數據 num <= 4'd0; //接收到STOP位后結束,num清零 rx_data_r <= rx_temp_data; //把數據鎖存到數據寄存器rx_data end if(num==4'd10) begin //接收完有用數據信息 bps_start_r <= 1'b0; //數據接收完畢,釋放波特率啟動信號 rx_int <= 1'b0; //接收數據中斷信號關閉 (五) 發送模塊和接收模塊的連接 為了更好的驗證功能,我們設計的UART接口實現如下的功能: 整個UART模塊對外提供了RX、TX接口,實現的是接收與之連接的設備發送來的數據,而后又發送回去。應用在電腦上,就是說RX模塊接收上位機串口調試助手發送給FPGA的數據,然后利用其中的TX模塊將數據又送回到上位機,顯示在電腦的串口調試助手中。 可以看到,RX模塊和TX模塊的端口定義是有一定關系的,兩個模塊中,clk,rst_n為全局時鐘和復位信號,也為外部硬件的輸入信號端口,bps_start為輸出的波特率啟動信號,clk_bps為波特率時鐘信號。
在TX模塊中,rs232_tx為硬件輸出端口,rx_data以及rx_int為輸入信號, module my_uart_tx( clk,rst_n, rx_data,rx_int,rs232_tx, clk_bps,bps_start ); RX模塊,rs232_rx為硬件輸入端口,rx_data以及rx_int為輸出信號, module my_uart_rx( clk,rst_n, rs232_rx,rx_data,rx_int, clk_bps,bps_start ); 可以很清楚的看到,兩個模塊,通過rx_int以及rx_data作為信號交換的接口,來完成輸入數據的轉發。 到這里,UART接口的設計就完成了。 以下為整個工程UART代碼。 |
|