【主題】:詳細解析基于FPGA的串口通信 【作者】:LinCoding 【時間】:2016.11.28 串口通信,用的實在太廣泛了,原理也很簡單,在串口通信中最重要的是波特率的概念,大家不懂的可以百度,然后就是數(shù)據(jù)的格式,1位起始位,8位數(shù)據(jù)位,1位校驗位(可選),1位停止位。 先看結(jié)果(PC端將數(shù)據(jù)下發(fā)至FPGA,F(xiàn)PGA接收到數(shù)據(jù)后原封不動的將數(shù)據(jù)上發(fā)至PC端):

(源碼出自CrazyBingo,尊重版權(quán)) 下面直接看程序。 1、串口接收模塊
module uart_receiver
(
input clk, //global clock
input rst_n, //global reset
input clk_16bps,
input rxd,
output reg [7:0] rxd_data,
output reg rxd_flag
); 第一部分是輸入輸出定義,有兩點需要注意: 1、對于串口接收模塊,原理上要使用16倍的波特率去采樣,因此輸入clk_16bps,而這個輸入信號來自于——《詳細解析基于FPGA的任意分頻》這篇文章的divide_clken的輸出。 2、對于一個系統(tǒng)而言是由眾多模塊組成的,模塊與模塊之間肯定是要溝通的,那么對于輸入模塊而言,就就少不了“輸入完成標志位信號”,并且,這個標志位信要與輸入的數(shù)據(jù)同步輸出,如本例中的rxd_flag,還有之前按鍵消抖文章中的key_flag,SPI文章中的rxd_flag都是如此,因此,這也要成為一個固定的模式! //-----------------------------------
//synchronize the input signal
reg rxd_sync;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
rxd_sync <= 1'b1;
else
rxd_sync <= rxd;
end 第二部分,由于是接收模塊,接收的信號是來自其他時鐘域的,因此,這里需要打一拍將信號同步到自己的時鐘域,當然,打兩拍也是可以的!
//-----------------------------------
//receive FSM: encode
localparam R_IDLE = 4'd0;
localparam R_START = 4'd1;
localparam R_SAMPLE = 4'd2;
localparam R_STOP = 4'd3;
//-----------------------------------
//receive FSM
localparam SMP_TOP = 4'd15;
localparam SMP_CENTER = 4'd7;
reg [3:0] smp_cnt;
reg [3:0] rxd_cnt;
reg [3:0] rxd_state;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
smp_cnt <= 4'd0;
rxd_cnt <= 4'd0;
rxd_state <= R_IDLE;
end
else
case ( rxd_state )
R_IDLE:
begin
smp_cnt <= 4'd0;
rxd_cnt <= 4'd0;
if ( ! rxd_sync )
rxd_state <= R_START;
else
rxd_state <= R_IDLE;
end
R_START:
if ( clk_16bps )
begin
smp_cnt <= smp_cnt + 1'b1;
if ( smp_cnt == SMP_CENTER && rxd_sync )
begin
rxd_cnt <= 4'd0;
rxd_state <= R_IDLE;
end
else if ( smp_cnt == SMP_TOP )
begin
rxd_cnt <= 4'd1;
rxd_state <= R_SAMPLE;
end
else
begin
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
end
else
begin
smp_cnt <= smp_cnt;
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
R_SAMPLE:
if ( clk_16bps )
begin
smp_cnt <= smp_cnt + 1'b1;
if ( smp_cnt == SMP_TOP )
begin
if ( rxd_cnt < 4'd8 )
begin
rxd_cnt <= rxd_cnt + 1'b1;
rxd_state <= R_SAMPLE;
end
else
begin
rxd_cnt <= 4'd9;
rxd_state <= R_STOP;
end
end
else
begin
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
end
else
begin
smp_cnt <= smp_cnt;
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
R_STOP:
if ( clk_16bps )
begin
smp_cnt <= smp_cnt + 1'b1;
if ( smp_cnt == SMP_TOP )
begin
rxd_cnt <= 4'd0;
rxd_state <= R_IDLE;
end
else
begin
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
end
else
begin
smp_cnt <= smp_cnt;
rxd_cnt <= rxd_cnt;
rxd_state <= rxd_state;
end
default:
begin
smp_cnt <= 4'd0;
rxd_cnt <= 4'd0;
rxd_state <= R_IDLE;
end
endcase
end 第三部分就是長長的狀態(tài)機了,真的是太長了,不過基本都是重復(fù)的內(nèi)容,有以下幾點需要注意:
1、對于串口接收模塊,我們?yōu)榱耸菇邮盏降臄?shù)據(jù)最大程度的穩(wěn)定,使用了16倍波特率去采樣,也就是在每個數(shù)據(jù)位上,有16個采樣點,這樣的話,當然在最中間的采樣點的數(shù)據(jù)原則上是最穩(wěn)定的,事實也確實如此。因此需要一個smp_cnt信號去計數(shù)當前是第幾個采樣點,并且當采樣到16個點后使得rxd_cnt加1以采樣下一位數(shù)據(jù)。 2、使用rxd_cnt信號 來計數(shù)獲得當前采樣的第幾位數(shù)據(jù)。需要注意的是本always塊只涉及rxd_cnt的變遷,而在另一個always中會根據(jù)rxd_cnt的情況來取值各個位上的數(shù)據(jù)。 3、由于串口傳輸?shù)奶匦裕鹗嘉蛔優(yōu)榈碗娖揭暈閭鬏數(shù)拈_始,因此也就意味著,起始位其實是個傳輸開始的使能信號,所以要使用狀態(tài)機的IDLE態(tài)來始終監(jiān)測起始位是否變化。 reg [7:0] rxd_data_r;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
rxd_data_r <= 8'd0;
else if ( rxd_state == R_SAMPLE && clk_16bps && smp_cnt == SMP_CENTER )
case ( rxd_cnt )
4'd1 : begin rxd_data_r[0] <= rxd_sync; end
4'd2 : begin rxd_data_r[1] <= rxd_sync; end
4'd3 : begin rxd_data_r[2] <= rxd_sync; end
4'd4 : begin rxd_data_r[3] <= rxd_sync; end
4'd5 : begin rxd_data_r[4] <= rxd_sync; end
4'd6 : begin rxd_data_r[5] <= rxd_sync; end
4'd7 : begin rxd_data_r[6] <= rxd_sync; end
4'd8 : begin rxd_data_r[7] <= rxd_sync; end
default : begin rxd_data_r <= 8'd0; end
endcase
else if ( rxd_state == R_STOP )
rxd_data_r <= rxd_data_r;
else
rxd_data_r <= rxd_data_r;
end 第四部分就是取值了,根據(jù)rxd_cnt的值來取值不同位上的數(shù)據(jù)。
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
rxd_data <= 8'd0;
rxd_flag <= 1'b0;
end
else if ( rxd_state == R_STOP && clk_16bps && smp_cnt == SMP_TOP )
begin
rxd_data <= rxd_data_r;
rxd_flag <= 1'b1;
end
else
begin
rxd_data <= rxd_data;
rxd_flag <= 1'b0;
end
end 第五部分就是同步輸出rxd_data和rxd_flag信號了。而為了同步輸出,同樣又采用了一級D觸發(fā)器來給rxd_data打了一拍,使其與rxd_flag同步輸出。 2、串口發(fā)送模塊 module uart_transfer
(
input clk, //global clock
input rst_n, //global reset
input clk_16bps,
input txd_en,
input [7:0] txd_data,
output reg txd,
output reg txd_flag
); 第一部分是輸入輸出定義, 既然是發(fā)送模塊,當然需要發(fā)送使能信號了,其次,還需要發(fā)送完成標志位信號。當然了,既然有了發(fā)送使能信號,狀態(tài)機是不可避免的了。clk_16bps與串口接收模塊同理。
//-----------------------------------
//receive FSM: encode
localparam T_IDLE = 4'd0;
localparam T_SEND = 4'd1;
//-----------------------------------
//receive FSM
localparam SMP_TOP = 4'd15;
localparam SMP_CENTER = 4'd7;
reg [3:0] smp_cnt;
reg [3:0] txd_cnt;
reg [3:0] txd_state;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
begin
smp_cnt <= 4'd0;
txd_cnt <= 4'd0;
txd_state <= T_IDLE;
end
else
case ( txd_state )
T_IDLE:
begin
smp_cnt <= 4'd0;
txd_cnt <= 4'd0;
if ( txd_en )
txd_state <= T_SEND;
else
txd_state <= T_IDLE;
end
T_SEND:
if ( clk_16bps )
begin
smp_cnt <= smp_cnt + 1'b1;
if ( smp_cnt == SMP_TOP )
begin
if ( txd_cnt < 4'd9 )
begin
txd_cnt <= txd_cnt + 1'b1;
txd_state <= T_SEND;
end
else
begin
txd_cnt <= 4'd0;
txd_state <= T_IDLE;
end
end
else
begin
txd_cnt <= txd_cnt;
txd_state <= txd_state;
end
end
else
begin
smp_cnt <= smp_cnt;
txd_cnt <= txd_cnt;
txd_state <= txd_state;
end
default:
begin
smp_cnt <= 4'd0;
txd_cnt <= 4'd0;
txd_state <= T_IDLE;
end
endcase
end 第二部分就是長長的發(fā)送狀態(tài)機了,相比串口接收模塊相對簡單,而與之不同的是,由于是發(fā)送,我們只需在數(shù)據(jù)的第一個采樣點將數(shù)據(jù)發(fā)送出去,在采樣點到達16個時,換下一位數(shù)據(jù)進行發(fā)送就可以了。 always @ ( * )
begin
if ( txd_state == T_SEND )
case ( txd_cnt )
4'd0 : begin txd = 1'b0; end
4'd1 : begin txd = txd_data[0]; end
4'd2 : begin txd = txd_data[1]; end
4'd3 : begin txd = txd_data[2]; end
4'd4 : begin txd = txd_data[3]; end
4'd5 : begin txd = txd_data[4]; end
4'd6 : begin txd = txd_data[5]; end
4'd7 : begin txd = txd_data[6]; end
4'd8 : begin txd = txd_data[7]; end
4'd9 : begin txd = 1'b1; end
default : begin txd = 1'b1; end
endcase
else
txd = 1'b1;
end 第三部分就是根據(jù)txd_cnt的計數(shù)來發(fā)送數(shù)據(jù)了,注意的是本模塊為組合邏輯,為的就是使數(shù)據(jù)對齊!

圖1

圖2
圖2是圖1的放大版本,可見,在時鐘邊沿是對齊的。 如果改為時序邏輯,如下程序:
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
txd <= 1'b1;
else if ( txd_state == T_SEND )
case ( txd_cnt )
4'd0 : begin txd = 1'b0; end
4'd1 : begin txd = txd_data[0]; end
4'd2 : begin txd = txd_data[1]; end
4'd3 : begin txd = txd_data[2]; end
4'd4 : begin txd = txd_data[3]; end
4'd5 : begin txd = txd_data[4]; end
4'd6 : begin txd = txd_data[5]; end
4'd7 : begin txd = txd_data[6]; end
4'd8 : begin txd = txd_data[7]; end
4'd9 : begin txd = 1'b1; end
default : begin txd = 1'b1; end
endcase
else
txd = 1'b1;
end 則如下圖所示: 
txd比txd_cnt晚了一個clk,當然晚了一個clk是無所謂的,但是為了時序的完美,還是用組合邏輯吧!
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
txd_flag <= 1'b0;
else if ( txd_state == T_SEND && clk_16bps && smp_cnt == SMP_TOP && txd_cnt == 4'd9 )
txd_flag <= 1'b1;
else
txd_flag <= 1'b0;
end 最后一個部分就是輸出發(fā)送完成標志位信號。 這樣一個基于FPGA的串口通信就完成了,在頂層模塊中我們可以將接收到的數(shù)據(jù)直接交給發(fā)送模塊,通過PC端的串口調(diào)試助手下發(fā)數(shù)據(jù),然后FPGA會原封不動的將數(shù)據(jù)再發(fā)回來完成板級驗證。 總結(jié):
1、對于接收類的模塊,接收完成后需要有接收完成標志位信號,并且要與所接收的數(shù)據(jù)同步輸出。
2、對于發(fā)送類的模塊,要有發(fā)送使能信號和發(fā)送完成標志位信號,并且使用狀態(tài)機的IDLE態(tài)來監(jiān)測發(fā)送使能信號的變化。
|