FPGA:實(shí)現(xiàn)串行接口 RS232
串行接口(RS-232)
串行接口是連接FPGA和PC機(jī)的一種簡單方式。這個(gè)項(xiàng)目向大家展示了如果使用FPGA來創(chuàng)建RS-232收發(fā)器。
整個(gè)項(xiàng)目包括5個(gè)部分
RS-232接口是怎樣工作的
作為標(biāo)準(zhǔn)設(shè)備,大多數(shù)的計(jì)算機(jī)都有1到2個(gè)RS-232串口。
特性
RS-232有下列特性:
· 使用9針的"DB-9"插頭(舊式計(jì)算機(jī)使用25針的"DB-25"插頭).
· 允許全雙工的雙向通訊(也就是說計(jì)算機(jī)可以在接收數(shù)據(jù)的同時(shí)發(fā)送數(shù)據(jù)).
· 最大可支持的傳輸速率為10KBytes/s.
DB-9插頭
你可能已經(jīng)在你的計(jì)算機(jī)背后見到過這種插頭
它一共有9個(gè)引腳,但是最重要的3個(gè)引腳是:
· 引腳2: RxD (接收數(shù)據(jù)).
· 引腳3: TxD (發(fā)送數(shù)據(jù)).
· 引腳5: GND (地).
僅使用3跟電纜,你就可以發(fā)送和接收數(shù)據(jù).
串行通訊
數(shù)據(jù)以每次一位的方式傳輸;每條線用來傳輸一個(gè)方向的數(shù)據(jù)。由于計(jì)算機(jī)通常至少需要若干位數(shù)據(jù),因此數(shù)據(jù)在發(fā)送之前先“串行化”。通常是以8位數(shù)據(jù)為1組的。。先發(fā)送最低有效位,最后發(fā)送最高有效位。
異步通訊
RS-232使用異步通訊協(xié)議。也就是說數(shù)據(jù)的傳輸沒有時(shí)鐘信號(hào)。接收端必須有某種方式,使之與接收數(shù)據(jù)同步。
對于RS-232來說,是這樣處理的:
1. 串行線纜的兩端事先約定好串行傳輸?shù)膮?shù)(傳輸速度、傳輸格式等)
2. 當(dāng)沒有數(shù)據(jù)傳輸?shù)臅r(shí)候,發(fā)送端向數(shù)據(jù)線上發(fā)送"1"
3. 每傳輸一個(gè)字節(jié)之前,發(fā)送端先發(fā)送一個(gè)"0"來表示傳輸已經(jīng)開始。這樣接收端便可以知道有數(shù)據(jù)到來了。
4. 開始傳輸后,數(shù)據(jù)以約定的速度和格式傳輸,所以接收端可以與之同步
5. 每次傳輸完成一個(gè)字節(jié)之后,都在其后發(fā)送一個(gè)停止位("1")
讓我們來看看0x55是如何傳輸?shù)?/span>:01010101
0x55的二進(jìn)制表示為:01010101。
但是由于先發(fā)送的是最低有效位,所以發(fā)送序列是這樣的: 1-0-1-0-1-0-1-0.
下面是另外一個(gè)例子 :
傳輸?shù)臄?shù)據(jù)為0xC4,你能看出來嗎?11000100
從圖中很難看出來所傳輸?shù)臄?shù)據(jù),這也說明了事先知道傳輸?shù)乃俾蕦τ诮邮斩擞卸嗝粗匾?。?/span>串口發(fā)送的開始位為0,結(jié)束位為1,空閑line是為高)
數(shù)據(jù)傳輸可以多快?
數(shù)據(jù)的傳輸速度是用波特來描述的,亦即每秒鐘傳輸?shù)臄?shù)據(jù)位,例如1000波特表示每秒鐘傳輸100比特的數(shù)據(jù),或者說每個(gè)數(shù)據(jù)位持續(xù)1毫秒(1/1000) = 1ms。
波特率不是隨意的,必須服從一定的標(biāo)準(zhǔn),如果希望設(shè)計(jì)123456波特的RS-232接口,對不起,你很不幸運(yùn),這是不行的。常用的串行傳輸速率值包括以下幾種:
· 1200 波特.
· 9600 波特.
· 38400 波特.
· 115200 波特 (通常情況下是你可以使用的最高速度).
在115200波特傳輸速度下,每位數(shù)據(jù)持續(xù) (1/115200) = 8.7μs.如果傳輸8位數(shù)據(jù),共持續(xù) 8 x 8.7μs = 69μs。但是每個(gè)字節(jié)的傳輸又要求額外的“開始位”和“停止位”,所以實(shí)際上需要花費(fèi)10 x 8.7μs = 87μs的時(shí)間。最大的有效數(shù)據(jù)傳輸率只能達(dá)到 11.5KBytes每秒。
在115200波特傳輸速度下,一些使用了不好的芯片的計(jì)算機(jī)要求一個(gè)長的停止位(1.5或2位數(shù)據(jù)的長度),這使得最大傳輸速度降到大約10.5KBytes每秒
物理層
電纜上的信號(hào)使用正負(fù)電壓的機(jī)制:
· "1" 用 -10V的電壓表示(或者在 -5V與 -15V之間的電壓).
· "0" 用 +10V的電壓表示(或者在 5V與 15V之間的電壓).
所以沒有數(shù)據(jù)傳輸?shù)碾娎|上的電壓應(yīng)該為-10V或-5到-10之間的某個(gè)電壓。
波特率發(fā)生器
這里我們使用串行連接的最大速度115200波特,其他較慢的波特也很容易由此產(chǎn)生。
FPGA通常運(yùn)行在遠(yuǎn)高于115200Hz的時(shí)鐘頻率上(對于今天的標(biāo)準(zhǔn)的來說RS-232真是太慢了),這就意味著我們需要用一個(gè)較高的時(shí)鐘來分頻產(chǎn)生盡量接近于115200Hz的時(shí)鐘信號(hào)。
從1.8432MHz的時(shí)鐘產(chǎn)生
通常RS-232芯片使用1.8432MHz的時(shí)鐘,以為這個(gè)時(shí)鐘很容易產(chǎn)生標(biāo)準(zhǔn)的波特率,所以我們假設(shè)已經(jīng)擁有了一個(gè)這樣的時(shí)鐘源。
只需要將 1.8432MHz 16分頻便可得到 115200Hz的時(shí)鐘,多方便啊!
reg [3:0] BaudDivCnt;(1843200/16=115200
always @(posedge clk) BaudDivCnt <= BaudDivCnt + 1;
wire BaudTick = (BaudDivCnt==15);
所以 "BaudTick"每16個(gè)時(shí)鐘周期需要置位一次,從而從1.8432MHz的時(shí)鐘得到115200Hz的時(shí)鐘。
從任意頻率產(chǎn)生
早期的發(fā)生器假設(shè)使用1.8432MHz的時(shí)鐘。但如果我們使用2MHz的時(shí)鐘怎么辦呢?要從2MHz的時(shí)鐘得到 115200Hz,需要將時(shí)鐘 "17.361111111..."分頻,并不是一個(gè)整數(shù)。我的解決辦法是有時(shí)候17分頻,有時(shí)候18分頻,使得整體的分頻比保持在 "17.361111111"。這是很容易做到的。
下面是實(shí)現(xiàn)這個(gè)想法的C語言代碼:
while(1) //死循環(huán)
{
acc += 115200;
if(acc >=2000000) printf("*"); else printf(" ");
acc %= 2000000;
}
這段代碼會(huì)精確的以平均每 "17.361111111..."個(gè)時(shí)鐘間隔打印出一個(gè)"*"。
為了從FPGA得到同樣的效果,考慮到串行接口可以容忍一定的波特率誤差,所以即使我們使用17.3或者17.4這樣的分頻比也是沒有關(guān)系的。
FPGA波特率發(fā)生器
我們希望2000000是2的整數(shù)冪,但很可惜,它不是。所以我們改變分頻比,"2000000/115200"約等于 "1024/59" = 17.356.這跟我們要求的分頻比很接近,并且使得在FPGA上實(shí)現(xiàn)起來相當(dāng)有效。
//10位的累加器 ([9:0]), 1位進(jìn)位輸出 ([10])
reg [10:0] acc; //一共11位!
always @(posedge clk)
acc <= acc[9:0] + 59; //我們使用上一次結(jié)果的低10位,但是保留11位結(jié)果
wire BaudTick = acc[10]; //第11位作為進(jìn)位輸出,這里的方法用的非常好,可以作為分頻始終來用
使用 2MHz時(shí)鐘, "BaudTick"為 115234波特,跟理想的115200波特存在 0.03%的誤差。
參數(shù)化的FPGA波特率發(fā)生器
前面的設(shè)計(jì)我們使用的是10位的累加器,如果時(shí)鐘頻率提高的話,需要更多的位數(shù)。
下面是一個(gè)使用 25MHz時(shí)鐘和 16位累加器的設(shè)計(jì),該設(shè)計(jì)是參數(shù)化的,所以很容易根據(jù)具體情況修改。
parameter ClkFrequency = 25000000; // 25MHz FPGA的工作頻率
parameter Baud = 115200;
parameter BaudGeneratorAccWidth = 16;
parameter BaudGeneratorInc = (Baud<<BaudGeneratorAccWidth)/ClkFrequency;//左移的話意味著乘以2的幾次方
reg [BaudGeneratorAccWidth:0] BaudGeneratorAcc; //留出一位用于分頻進(jìn)位用的
always @(posedge clk)
BaudGeneratorAcc <= BaudGeneratorAcc[BaudGeneratorAccWidth-1:0] + BaudGeneratorInc;
wire BaudTick = BaudGeneratorAcc[BaudGeneratorAccWidth];//這個(gè)式子就是我們分頻的結(jié)果,達(dá)到了由25MHZ分頻到115200HZ的效果
上面的設(shè)計(jì)中存在一個(gè)錯(cuò)誤: "BaudGeneratorInc"的計(jì)算是錯(cuò)誤的,因?yàn)?/span> Verilog使用 32位的默認(rèn)結(jié)果,但實(shí)際計(jì)算過程中的某些數(shù)據(jù)超過了32位,所以改變一種計(jì)算方法。
parameter BaudGeneratorInc = ((Baud<<(BaudGeneratorAccWidth-4))+(ClkFrequency>>5))/(ClkFrequency>>4);
這行程序也使得結(jié)果成為整數(shù),從而避免截?cái)唷?/span>
這就是整個(gè)的設(shè)計(jì)方法了。
現(xiàn)在我們已經(jīng)得到了足夠精確的波特率,可以繼續(xù)設(shè)計(jì)串行接收和發(fā)送模塊了。
RS-232發(fā)送模塊
下面是我們所想要實(shí)現(xiàn)的:
它應(yīng)該能像這樣工作:
· 發(fā)送器接收8位的數(shù)據(jù),并將其串行輸出。("TxD_start"置位后開始傳輸).
· 當(dāng)有數(shù)傳輸?shù)臅r(shí)候,使"busy"信號(hào)有效,此時(shí)“TxD_start”信號(hào)被忽略.
RS-232模塊的參數(shù)是固定的: 8位數(shù)據(jù), 2個(gè)停止位,無奇偶校驗(yàn).
數(shù)據(jù)串行化
假設(shè)我們已經(jīng)有了一個(gè)115200波特的"BaudTick"信號(hào).
我們需要產(chǎn)生開始位、8位數(shù)據(jù)以及停止位。
用狀態(tài)機(jī)來實(shí)現(xiàn)看起來比較合適。
reg [3:0] state;
always @(posedge clk)
case(state)
4'b0000: if(TxD_start) state <= 4'b0100;
4'b0100: if(BaudTick) state <= 4'b1000; //開始位
4'b1000: if(BaudTick) state <= 4'b1001; // bit 0
4'b1001: if(BaudTick) state <= 4'b1010; // bit 1
4'b1010: if(BaudTick) state <= 4'b1011; // bit 2
4'b1011: if(BaudTick) state <= 4'b1100; // bit 3
4'b1100: if(BaudTick) state <= 4'b1101; // bit 4
4'b1101: if(BaudTick) state <= 4'b1110; // bit 5
4'b1110: if(BaudTick) state <= 4'b1111; // bit 6
4'b1111: if(BaudTick) state <= 4'b0001; // bit 7
4'b0001: if(BaudTick) state <= 4'b0010; //停止位1
4'b0010: if(BaudTick) state <= 4'b0000; //停止位2
default: if(BaudTick) state <= 4'b0000;
endcase
注意看這個(gè)狀態(tài)機(jī)是怎樣實(shí)現(xiàn)當(dāng)"TxD_start"有效就開始,但只在"BaudTick"有效的時(shí)候才轉(zhuǎn)換狀態(tài)的。.
現(xiàn)在,我們只需要產(chǎn)生"TxD"輸出即可.
reg muxbit;
always @(state[2:0])
case(state[2:0])
0: muxbit <= TxD_data[0];
1: muxbit <= TxD_data[1];
2: muxbit <= TxD_data[2];
3: muxbit <= TxD_data[3];
4: muxbit <= TxD_data[4];
5: muxbit <= TxD_data[5];
6: muxbit <= TxD_data[6];
7: muxbit <= TxD_data[7];
endcase
//將開始位、數(shù)據(jù)以及停止位結(jié)合起來
assign TxD = (state<4) | (state[3] & muxbit);
RS232接收模塊
下面是我們想要實(shí)現(xiàn)的模塊:
我們的設(shè)計(jì)目的是這樣的:
1.當(dāng)RxD線上有數(shù)據(jù)時(shí),接收模塊負(fù)責(zé)識(shí)別RxD線上的數(shù)據(jù)
2.當(dāng)收到一個(gè)字節(jié)的數(shù)據(jù)時(shí),鎖存接收到的數(shù)據(jù)到"data"總線,并使"data_ready"有效一個(gè)周期。
注意:只有當(dāng)"data_ready"有效時(shí),"data"總線的數(shù)據(jù)才有效,其他的時(shí)間里不要使用"data"總線上的數(shù)據(jù),因?yàn)樾碌臄?shù)據(jù)可能已經(jīng)改變了其中的部分?jǐn)?shù)據(jù)。
過采樣
異步接收機(jī)必須通過一定的機(jī)制與接收到的輸入信號(hào)同步(接收端沒有辦法得到發(fā)送斷的時(shí)鐘)。這里采用如下辦法。
1.為了確定新數(shù)據(jù)的到來,即檢測開始位,我們使用幾倍于波特率的采樣時(shí)鐘對接收到的信號(hào)進(jìn)行采樣。
2.一旦檢測到"開始位",再將采樣時(shí)鐘頻率降為已知的發(fā)送端的波特率。
典型的過采樣時(shí)鐘頻率為接收到的信號(hào)的波特率的16倍,這里我們使用8倍的采樣時(shí)鐘。當(dāng)波特率為115200時(shí),采樣時(shí)鐘為921600Hz。(115200*8=921600)
假設(shè)我們已經(jīng)有了一個(gè)8倍于波特率的時(shí)鐘信號(hào) "Baud8Tick",其頻率為 921600Hz。
具體設(shè)計(jì)
首先,接受到的"RxD"信號(hào)與我們的時(shí)鐘沒有任何關(guān)系,所以采用兩個(gè)D觸發(fā)器對其進(jìn)行過采樣,并且使之我我們的時(shí)鐘同步。
reg [1:0] RxD_sync;
always @(posedge clk) if(Baud8Tick) RxD_sync <= {RxD_sync[0], RxD};
首先我們對接收到的數(shù)據(jù)進(jìn)行濾波,這樣可以防止毛刺信號(hào)被誤認(rèn)為是開始信號(hào)。
reg [1:0] RxD_cnt;
reg RxD_bit;
always @(posedge clk)
if(Baud8Tick)
begin
if(RxD_sync[1] && RxD_cnt!=2'b11) RxD_cnt <= RxD_cnt + 1;
else
if(~RxD_sync[1] && RxD_cnt!=2'b00) RxD_cnt <= RxD_cnt - 1;
if(RxD_cnt==2'b00) RxD_bit <= 0;
else
if(RxD_cnt==2'b11) RxD_bit <= 1;
end
一旦檢測到"開始位",使用如下的狀態(tài)機(jī)可以檢測出接收到每一位數(shù)據(jù)。
reg [3:0] state;
always @(posedge clk)
if(Baud8Tick)
case(state)
4'b0000: if(~RxD_bit) state <= 4'b1000; // start bit found?
4'b1000: if(next_bit) state <= 4'b1001; // bit 0
4'b1001: if(next_bit) state <= 4'b1010; // bit 1
4'b1010: if(next_bit) state <= 4'b1011; // bit 2
4'b1011: if(next_bit) state <= 4'b1100; // bit 3
4'b1100: if(next_bit) state <= 4'b1101; // bit 4
4'b1101: if(next_bit) state <= 4'b1110; // bit 5
4'b1110: if(next_bit) state <= 4'b1111; // bit 6
4'b1111: if(next_bit) state <= 4'b0001; // bit 7
4'b0001: if(next_bit) state <= 4'b0000; // stop bit
default: state <= 4'b0000;
endcase
注意,我們使用了"next_bit"來遍歷所有數(shù)據(jù)位。
reg [2:0] bit_spacing;
always @(posedge clk)
if(state==0)
bit_spacing <= 0;
else
if(Baud8Tick)
bit_spacing <= bit_spacing + 1;
wire next_bit = (bit_spacing==7);
最后我們使用一個(gè)移位寄存器來存儲(chǔ)接受到的數(shù)據(jù)。
reg [7:0] RxD_data;
always @(posedge clk) if(Baud8Tick && next_bit && state[3]) RxD_data <= {RxD_bit, RxD_data[7:1]};
怎樣使用發(fā)送和接收模塊
這個(gè)設(shè)計(jì)似的我們可以通過計(jì)算機(jī)的串行口來控制FPGA的幾個(gè)引腳。
具體來說,該設(shè)計(jì)完成以下功能。
1.將FPGA的8個(gè)引腳作為輸出(稱為“通用輸出”)。 FPGA收到任何數(shù)據(jù)時(shí)都會(huì)更新這8個(gè)GPout的值。
2.將FPGA的8個(gè)引腳作為輸入(稱為“通用輸入”)。FPGA收到任何數(shù)據(jù)后,都會(huì)將GPin上的數(shù)值通過串行口發(fā)送出去。
通用輸出可以用來通過計(jì)算機(jī)遠(yuǎn)程控制任何東西,例如FPGA板上的LED,甚至可以再添加一個(gè)繼電器來控制咖啡機(jī)。
module serialfun(clk, RxD, TxD, GPout, GPin);
input clk;
input RxD;
output TxD;
output [7:0] GPout;
input [7:0] GPin;
///////////////////////////////////////////////////
wire RxD_data_ready;
wire [7:0] RxD_data;
async_receiver deserializer(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data));
reg [7:0] GPout;
always @(posedge clk) if(RxD_data_ready) GPout <= RxD_data;
///////////////////////////////////////////////////
async_transmitter serializer(.clk(clk), .TxD(TxD), .TxD_start(RxD_data_ready), .TxD_data(GPin));
endmodule
記得包含異步發(fā)送和接收模塊的設(shè)計(jì)文件,并更新里面的時(shí)鐘頻率。