FPGA設計中RS232串列埠的Verilog實現(TX控制器)
一.硬體電路:
下面是一個典型的計算機與串列埠裝置的連線示意圖。RS232採用DB9或DB25的介面。最簡單的連線方法只需要TXD和RXD兩根訊號線分別傳輸和接收資料。
現在常用的FPGA的IO電平一般都在1.8/2.5/3.3V,不滿足RS232的電平要求,所以我們要通過一個RS232的串列埠電平轉換晶片,將FPGA的IO輸出電平轉換為RS232的電平。這裡我們採用一片MAX3232實現兩路串列埠的電平轉換。UART_*直接與FPGA的IO連線,EXT_*與DB9/DB25的RS232介面連線。
要注意的一點是,RS232的電平。邏輯“0”的電平是+5~+15V,邏輯“1”的電平是-5~-15V,是和TTL電平的定義相反的。
二.TXD(傳送資料)方的Verilog實現
串列埠通訊是以位元組為單位的。每個位元組的傳輸包括一個起始位,8bit資料位,一到兩個的結束位,以及可選的校驗位等。下圖是一次傳輸的邏輯波形的定義。在傳輸中,每一位的寬度相同的,由傳輸的波特率決定。
對於串列埠TXD的方向,控制器要根據所要傳輸的一個位元組的資料,生成上面格式的邏輯波形併發送出去。
每次傳輸除了資料不同外,整個傳輸的基本格式是一樣的。在verilog的實現中,用一個包含有4個狀態的狀態機來實現一個位元組資料的傳輸。四個狀態分別是UART_IDLE,UART_START,UART_DATA和UART_STOP。
當沒有資料傳輸時,串列埠處於IDLE狀態,TXD為高電平。當有一個位元組的資料需要傳輸時,進入UART_START狀態,同時生成一個bit的起始位(低電平),然後進入UART_DATA狀態,將要傳輸的一個位元組的資料按照從bit0到bit7的順序,依次放到TXD上,然後進入結束位(這裡用一個bit的結束位),當該次傳輸結束以後,串列埠重新進入IDLE狀態。如果有多個位元組的資料需要傳輸,那麼當前一個位元組的資料傳輸結束時(UART_STOP結束),直接進入UART_START狀態,開始下一個位元組的傳輸。
下面是狀態機的verilog實現(其中FIFO_EMPTY用於表示是否有資料需要傳輸,UART_DATA_CNT用來選擇當前需要傳輸的資料位)。UART_CLK是串列埠的時鐘,由串列埠波特率決定。
wire FIFO_EMPTY;
reg [2:0] UART_DATA_CNT;
parameter UART_IDLE = 4'b0001,
UART_START = 4'b0010,
UART_DATA = 4'b0100,
UART_STOP = 4'b1000;
reg [3:0] UARTSM, UARTSMNXT;
wire PHASE_IDLE = UARTSM[0];
wire PHASE_START = UARTSM[1];
wire PHASE_DATA = UARTSM[2];
wire PHASE_STOP = UARTSM[3];
always @(posedge UART_CLK or negedge RESET)
begin
if(~RESET)
UARTSM <= UART_IDLE;
else
UARTSM <= UARTSMNXT;
end
always @( UARTSM or FIFO_EMPTY or UART_DATA_CNT) begin
UARTSMNXT = UARTSM;
case (UARTSM)
UART_IDLE:
if(~FIFO_EMPTY)
UARTSMNXT = UART_START;
else
UARTSMNXT = UART_IDLE;
UART_START:
UARTSMNXT = UART_DATA;
UART_DATA:
if(UART_DATA_CNT == 3'b111)
UARTSMNXT = UART_STOP;
else
UARTSMNXT = UART_DATA;
UART_STOP:
if(~FIFO_EMPTY)
UARTSMNXT = UART_START;
else
UARTSMNXT = UART_IDLE;
default:
UARTSMNXT = UART_IDLE;
endcase
end
UART_DATA狀態需要持續8個bit的寬度,在這個狀態下,需要將8bit的資料(TX_DATA)一次放到TXD上。可以通過下面的邏輯來實現:
assign TX_DATA_EN = PHASE_START;
always @(posedge UART_CLK or negedge RESET)
begin
if(~RESET)
UART_DATA_CNT <= 3'b0;
else if(PHASE_DATA)
UART_DATA_CNT <= UART_DATA_CNT + 3'b1;
else
UART_DATA_CNT <= 3'b0;
end
reg UART_DATA_BIT;
always @( UART_DATA_CNT or TX_DATA)
begin
case(UART_DATA_CNT)
3'b000:
UART_DATA_BIT = TX_DATA[0];
3'b001:
UART_DATA_BIT = TX_DATA[1];
3'b010:
UART_DATA_BIT = TX_DATA[2];
3'b011:
UART_DATA_BIT = TX_DATA[3];
3'b100:
UART_DATA_BIT = TX_DATA[4];
3'b101:
UART_DATA_BIT = TX_DATA[5];
3'b110:
UART_DATA_BIT = TX_DATA[6];
3'b111:
UART_DATA_BIT = TX_DATA[7];
default:
UART_DATA_BIT = 1'b0;
endcase
end
wire TXD_PRE;
assign TXD_PRE = PHASE_START ? 1'b0 : (PHASE_DATA & UART_DATA_BIT) | PHASE_STOP | PHASE_IDLE;
為了簡化時序設計,用下面的邏輯將TXD用register out的方式送到FPGA的IO上。
reg TXD;
always @(posedge UART_CLK or negedge RESET)
begin
if(~RESET)
TXD <= 1'b1;
else
TXD <= TXD_PRE;
End
一般情況下,串列埠作為一個通訊模組,都是要為其他功能模組服務的,功能模組產生資料,送給串列埠,串列埠將資料傳送給上位機或者其他裝置。那麼在設計的過程中,就需要注意功能模組與串列埠之間的資料傳輸的問題。一般情況下,功能模組和串列埠的工作時鐘都不會相同,所以兩個模組之間的資料可能要以非同步方式傳輸。這裡我們在串列埠中直接實現一個8bit位寬的非同步FIFO。功能模組可以直接將資料寫入FIFO,當FIFO不為空的情況下,串列埠就直接從FIFO中讀取資料併發送出去。
由於本文主要是介紹串列埠的實現的,對於非同步FIFO的實現,就不做過多的討論,直接採用ALTERA的非同步FIFO的IP核生成。
asyn_fifo uart_tx_fifo (
.aclr ( FIFO_CLR ), //FIFO Clear,可以由RESET生成
.data ( DATA_IN ), //資料輸入,由功能模組生成,8bit位寬
.rdclk ( UART_CLK ), //串列埠時鐘,與波特率相關
.rdreq ( TX_DATA_EN ), //資料輸出使能訊號,從FIFO中輸出資料
.wrclk ( CLK ), //資料輸入時鐘,與功能模組的時鐘相關
.wrreq ( DATA_EN ), //資料輸入使能訊號
.q ( FIFO_DATA_OUT ), //FIFO資料輸出,送串列埠
.rdempty ( FIFO_EMPTY ), //FIFO empty訊號,非空時,串列埠即要啟動
.wrfull ( FIFO_FULL ) //FIFO full訊號,如果出現該種情況,則說明波特率太低,串列埠無法滿足資料傳輸要求。
);
下圖是從示波器上抓取的串列埠單個位元組的傳輸波形。波特率為115200,白色為FPGA IO的3.3V TTL輸出,紅色為MAX232的RS232電平輸出。