1. 程式人生 > >FPGA:實現序列介面 RS232

FPGA:實現序列介面 RS232

序列介面(RS-232)
序列介面是連線FPGA和PC機的一種簡單方式。這個專案向大家展示瞭如果使用FPGA來建立RS-232收發器。

整個專案包括5個部分

3.     傳送模組

4.     接收模組

5.     應用例項

RS-232介面是怎樣工作的
作為標準裝置,大多數的計算機都有1到2個RS-232串列埠。

特性
RS-232有下列特性:

·        使用9針的"DB-9"插頭(舊式計算機使用25針的"DB-25"插頭).

·        允許全雙工的雙向通訊(也就是說計算機可以在接收資料的同時傳送資料).

·        最大可支援的傳輸速率為10KBytes/s.

DB-9插頭
你可能已經在你的計算機背後見到過這種插頭

它一共有9個引腳,但是最重要的3個引腳是:

·        引腳2: RxD (接收資料).

·        引腳3: TxD (傳送資料).

·        引腳5: GND (地).

僅使用3跟電纜,你就可以傳送和接收資料.

序列通訊
資料以每次一位的方式傳輸;每條線用來傳輸一個方向的資料。由於計算機通常至少需要若干位資料,因此資料在傳送之前先“序列化”。通常是以8位資料為1組的。。先發送最低有效位,最後傳送最高有效位。
非同步通訊
RS-232使用非同步通訊協議。也就是說資料的傳輸沒有時鐘訊號。接收端必須有某種方式,使之與接收資料同步。
對於RS-232來說,是這樣處理的:

1.     序列線纜的兩端事先約定好序列傳輸的引數(傳輸速度、傳輸格式等)

2.     當沒有資料傳輸的時候,傳送端向資料線上傳送"1"

3.     每傳輸一個位元組之前,傳送端先發送一個"0"來表示傳輸已經開始。這樣接收端便可以知道有資料到來了。

4.     開始傳輸後,資料以約定的速度和格式傳輸,所以接收端可以與之同步

5.     每次傳輸完成一個位元組之後,都在其後傳送一個停止位("1")

讓我們來看看0x55是如何傳輸的:

0x55的二進位制表示為:01010101。
但是由於先發送的是最低有效位,所以傳送序列是這樣的: 1-0-1-0-1-0-1-0.
下面是另外一個例子 :

傳輸的資料為0xC4,你能看出來嗎?
從圖中很難看出來所傳輸的資料,這也說明了事先知道傳輸的速率對於接收端有多麼重要。

資料傳輸可以多快?

資料的傳輸速度是用波特來描述的,亦即每秒鐘傳輸的資料位,例如1000波特表示每秒鐘傳輸100位元的資料, 或者說每個資料位持續1毫秒。
波特率不是隨意的,必須服從一定的標準,如果希望設計123456波特的RS-232介面,對不起,你很不幸運,這是不行的。常用的序列傳輸速率值包括以下幾種:

·        1200 波特.

·        9600 波特.

·        38400 波特.

·        115200 波特 (通常情況下是你可以使用的最高速度).

在115200 波特傳輸速度下, 每位資料持續 (1/115200) =8.7μs. 如果傳輸8位資料,共持續 8 x 8.7μs = 69μs。但是每個位元組的傳輸又要求額外的“開始位”和“停止位”,所以實際上需要花費10 x 8.7μs = 87μs的時間。最大的有效資料傳輸率只能達到 11.5KBytes每秒。
在115200 波特傳輸速度下,一些使用了不好的晶片的計算機要求一個長的停止位(1.5或2位資料的長度),這使得最大傳輸速度降到大約10.5KBytes每秒

物理層
電纜上的訊號使用正負電壓的機制:

·        "1" 用 -10V 的電壓表示(或者在 -5V 與 -15V之間的電壓).

·        "0" 用 +10V 的電壓表示(或者在 5V 與 15V之間的電壓).

所以沒有資料傳輸的電纜上的電壓應該為-10V或-5到-10之間的某個電壓。

波特率發生器

這裡我們使用序列連線的最大速度115200波特,其他較慢的波特也很容易由此產生。

FPGA通常執行在遠高於115200Hz的時鐘頻率上(對於今天的標準的來說RS-232真是太慢了),這就意味著我們需要用一個較高的時鐘來分頻產生儘量接近於115200Hz的時鐘訊號。
從1.8432MHz的時鐘產生
通常RS-232晶片使用1.8432MHz的時鐘,以為這個時鐘很容易產生標準的波特率,所以我們假設已經擁有了一個這樣的時鐘源。

只需要將 1.8432MHz 16分頻便可得到 115200Hz的時鐘,多方便啊!

reg [3:0] BaudDivCnt;(1843200/16=115200
always @(posedge clk) BaudDivCnt <= BaudDivCnt + 1;
wire BaudTick = (BaudDivCnt==15);

所以"BaudTick" 每16個時鐘週期需要置位一次,從而從1.8432MHz的時鐘得到115200Hz的時鐘。

從任意頻率產生
早期的發生器假設使用1.8432MHz的時鐘。但如果我們使用2MHz的時鐘怎麼辦呢?要從2MHz的時鐘得到 115200Hz,需要將時鐘"17.361111111..." 分頻,並不是一個整數。我的解決辦法是有時候17分頻,有時候18分頻,使得整體的分頻比保持在 "17.361111111"。這是很容易做到的。

下面是實現這個想法的C語言程式碼:
while(1) // 死迴圈
{
acc += 115200;        
if(acc >=2000000) printf("*"); else printf(" ");
acc %= 2000000;
}

這段程式碼會精確的以平均每"17.361111111..." 個時鐘間隔打印出一個"*"。

為了從FPGA得到同樣的效果,考慮到序列介面可以容忍一定的波特率誤差,所以即使我們使用17.3或者17.4這樣的分頻比也是沒有關係的。

FPGA波特率發生器
我們希望2000000是2的整數冪,但很可惜,它不是。所以我們改變分頻比,"2000000/115200" 約等於 "1024/59" = 17.356. 這跟我們要求的分頻比很接近,並且使得在FPGA上實現起來相當有效。

//10 位的累加器 ([9:0]), 1位進位輸出 ([10])
reg [10:0] acc; //一共11位!
always @(posedge clk)
acc <= acc[9:0] + 59; //我們使用上一次結果的低10位,但是保留11位結果
wire BaudTick = acc[10]; //第11位作為進位輸出,這裡的方法用的非常好,可以作為分頻始終來用

使用 2MHz 時鐘, "BaudTick"為 115234 波特, 跟理想的115200波特存在 0.03% 的誤差。

引數化的FPGA波特率發生器
前面的設計我們使用的是10位的累加器,如果時鐘頻率提高的話,需要更多的位數。

下面是一個使用 25MHz 時鐘和 16 位累加器的設計,該設計是引數化的,所以很容易根據具體情況修改。

parameter ClkFrequency = 25000000; // 25MHz  FPGA的工作頻率
parameter Baud = 115200;
parameter BaudGeneratorAccWidth = 16;
parameter BaudGeneratorInc =(Baud<<BaudGeneratorAccWidth)/ClkFrequency;//左移的話意味著乘以2的幾次方
reg [BaudGeneratorAccWidth:0] BaudGeneratorAcc; //留出一位用於分頻進位用的
always @(posedge clk)
BaudGeneratorAcc <= BaudGeneratorAcc[BaudGeneratorAccWidth-1:0] +BaudGeneratorInc;
wire BaudTick = BaudGeneratorAcc[BaudGeneratorAccWidth];//這個式子就是我們分頻的結果,達到了由25MHZ分頻到115200HZ的效果

上面的設計中存在一個錯誤: "BaudGeneratorInc"的計算是錯誤的, 因為 Verilog 使用 32 位的預設結果, 但實際計算過程中的某些資料超過了32位,所以改變一種計算方法。

parameter BaudGeneratorInc = ((Baud<<(BaudGeneratorAccWidth-4))+(ClkFrequency>>5))/(ClkFrequency>>4);

這行程式也使得結果成為整數,從而避免截斷。

這就是整個的設計方法了。
現在我們已經得到了足夠精確的波特率,可以繼續設計序列接收和傳送模組了。

RS-232傳送模組
下面是我們所想要實現的:

它應該能像這樣工作:

·        傳送器接收8位的資料,並將其序列輸出。("TxD_start"置位後開始傳輸).

·        當有數傳輸的時候,使"busy"訊號有效,此時“TxD_start”訊號被忽略.

RS-232模組的引數是固定的: 8位資料, 2個停止位, 無奇偶校驗.

資料序列化
假設我們已經有了一個115200波特的"BaudTick"訊號.

我們需要產生開始位、8位資料以及停止位。
用狀態機來實現看起來比較合適。

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

注意看這個狀態機是怎樣實現當"TxD_start"有效就開始,但只在"BaudTick"有效的時候才轉換狀態的。.

現在,我們只需要產生"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

//將開始位、資料以及停止位結合起來
assign TxD = (state<4) | (state[3] & muxbit);

RS232接收模組
下面是我們想要實現的模組:

我們的設計目的是這樣的:
     1.當RxD線上有資料時,接收模組負責識別RxD線上的資料
     2.當收到一個位元組的資料時,鎖存接收到的資料到"data"匯流排,並使"data_ready"有效一個週期。
注意:只有當"data_ready"有效時,"data"匯流排的資料才有效,其他的時間裡不要使用"data"總線上的資料,因為新的資料可能已經改變了其中的部分資料。

過取樣
非同步接收機必須通過一定的機制與接收到的輸入訊號同步(接收端沒有辦法得到傳送斷的時鐘)。這裡採用如下辦法。
     1.為了確定新資料的到來,即檢測開始位,我們使用幾倍于波特率的取樣時鐘對接收到的訊號進行取樣。
     2.一旦檢測到"開始位",再將取樣時鐘頻率降為已知的傳送端的波特率。
典型的過取樣時鐘頻率為接收到的訊號的波特率的16倍,這裡我們使用8倍的取樣時鐘。當波特率為115200時,取樣時鐘為921600Hz。(115200*8=921600)

假設我們已經有了一個8倍于波特率的時鐘訊號"Baud8Tick",其頻率為 921600Hz。

具體設計
首先,接受到的"RxD"訊號與我們的時鐘沒有任何關係,所以採用兩個D觸發器對其進行過取樣,並且使之我我們的時鐘同步。
reg [1:0] RxD_sync;
always @(posedge clk) if(Baud8Tick) RxD_sync <= {RxD_sync[0], RxD};

首先我們對接收到的資料進行濾波,這樣可以防止毛刺訊號被誤認為是開始訊號。
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

一旦檢測到"開始位",使用如下的狀態機可以檢測出接收到每一位資料。
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"來遍歷所有資料位。
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);

最後我們使用一個移位暫存器來儲存接受到的資料。
reg [7:0] RxD_data;
always @(posedge clk) if(Baud8Tick && next_bit && state[3])RxD_data <= {RxD_bit, RxD_data[7:1]};

怎樣使用傳送和接收模組
這個設計似的我們可以通過計算機的序列口來控制FPGA的幾個引腳。

具體來說,該設計完成以下功能。

   1. 將FPGA的8個引腳作為輸出(稱為“通用輸出”)。 FPGA收到任何資料時都會更新這8個GPout 的值。
   2. 將FPGA的8個引腳作為輸入(稱為“通用輸入”)。FPGA收到任何資料後,都會將GPin上的數值通過序列口傳送出去。

通用輸出可以用來通過計算機遠端控制任何東西,例如FPGA板上的LED,甚至可以再新增一個繼電器來控制咖啡機。
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

記得包含非同步傳送和接收模組的設計檔案,並更新裡面的時鐘頻率。

相關推薦

FPGA實現序列介面 RS232

序列介面(RS-232) 序列介面是連線FPGA和PC機的一種簡單方式。這個專案向大家展示瞭如果使用FPGA來建立RS-232收發器。 整個專案包括5個部分 3.     傳送模組 4.     接收模組 5.     應用例項 RS-232介面是怎樣工作的 作為

多執行緒之建立執行緒的方式之一實現Callable介面(三)

對於多執行緒,大家並不陌生,對於如何建立執行緒也是輕車熟路,對於使用new thread和實現runable介面的方式,不再多說。這篇博文我們介紹第三種:實現Callable介面。 Callable介面 介面定義: @FunctionalInterface

建立執行緒的第三種方式實現Callable介面

以前建立執行緒有兩種方式:一種是繼承Thread類,重寫run()方法;另一種就是實現runnable介面的run()方法。我們先來看看這兩種方式的實現。 1、繼承Thread類 package com.jdk8.Thread; public c

建立執行緒的第二種方法實現Runnable介面

package thread; public class ThreadDemo3 {  /**   * @執行緒示例:   * 需求:簡單的賣票程式。   * 多個視窗賣票。   *   * 建立執行緒的第二種方式:實現Runnable介面   *   * 步驟:  

JAVA併發程式設計(五)建立執行緒的第三種方式實現Callable介面

眾所周知建立執行緒的方式有兩種:1.繼承Thread類。2.實現Runnable介面。從jdk1.5開始,提供了另一種建立執行緒的方式。今天我們就來看看這第三種方式:實現Callable介面 一、Callable與Runnable 我們直接來看一個使用C

Android探索之路實現登入介面的記住密碼功能

目的功能: 點選 記住密碼時,下次登入時,輸入賬號,密碼自動出現 下面是實現的主要過程: 1、註冊時,除了填寫使用者的一些資訊,還需要在資料庫中設一個欄位,用於判斷使用者是否已經註冊  private

基於FPGARS232序列介面實現

3、序列通訊資料以每次一位的方式傳輸;每條線用來傳輸一個方向的資料。由於計算機通常至少需要若干位資料,因此資料在傳送之前先“序列化”。通常是以8位資料為1組的。 。先發送最低有效位,最後傳送最高有效位。4、非同步通訊RS-232使用非同步通訊協議。也就是說資料的傳輸沒有時鐘訊號。接收端必須有某種方式,使之與接

TensorFlow入門(六) 雙端 LSTM 實現序列標註(分詞)

vsm max poc 代碼 單詞 arch 大致 雙端 fun http://blog.csdn.net/Jerr__y/article/details/70471066 歡迎轉載,但請務必註明原文出處及作者信息。 @author: huangyongye @creat_

Mybatis逆向工程的pojo實現序列介面的程式碼

這兩天在學習一個分散式的專案--淘淘商城,使用了Alibaba的dubbo作為通訊工具,zookeeper作為register,由於dubbo是基於socket協議的,所以在進行pojo傳輸的時候報了異常,因為pojo沒有實現序列化介面,就無法進行基於二進位制的序列化傳輸。報錯如下:    但是很麻煩的一

多執行緒(1)繼承Thread類和實現Runnable介面

多執行緒的兩種實現方法: 1.繼承Thread類     繼承Thread類,重寫run()方法。建立多執行緒的時候,需要建立物件例項,然後呼叫start()方法。類物件的屬性屬於執行緒私有,執行緒之間互不影響。 public class ClassExtendT

Effective Java 第三版讀書筆記——條款14考慮實現 Comparable 介面

與本章討論的其他方法不同,compareTo 方法並沒有在 Object 類中宣告。相反,它是 Comparable 介面中的唯一方法。 通過實現 Comparable 介面,一個類表明它的例項有一個自然序( natural ordering )。對實現 Comparable 介面的物件所組成的陣列排序非常簡

實體類實現序列介面(Serializable)的用處

Serializable,之前一直有使用,預設的實體類就會實現Serializable介面,對具體原因一直不是很瞭解,同時如果沒有實現序列化,同樣沒什麼影響,什麼時候應該進行序列化操作呢?今天查了下資料,大致總結一下。 1.首先,什麼是序列化? 其實序列

oracle+.net 資料序列化物件時報錯"物件必須實現 IConvertible"介面解決方法

oracle+.net 資料序列化物件時報錯”物件必須實現 IConvertible”介面解決方法 見圖: 具體錯誤就是這樣 寫過序列化的都知道 資料系列化失敗無非欄位和資料型別有問題 只是

Java中執行緒建立的方式繼承thread類與實現Runnable介面

Java中執行緒的建立有兩種方式: 1.  通過繼承Thread類,重寫Thread的run()方法,將執行緒執行的邏輯放在其中 2.  通過實現Runnable介面,例項化Thread類     在實際應用中,我們經常用到多執行緒,如車站的售票系統,車站的

Jsoup實現java專案的服務端之間的通訊 (需要開放介面

@Override public Map<String, Object> getbarrierInfoCount(Map<String, Object> param, String signType) throws Exception { Co

java 物件實現序列化 Serializable()介面

總結一下Serializable介面的實現原理。 當一個類實現了Seializable介面(該介面僅為標記介面,不包含任何方法定義),表示該類可以序列化,序列化的目的是將一個實現了Serializable介面的物件可以轉換成一個位元組序列,儲存物件的狀態。 把該位元組序列

【修真院java小課堂】什麼是序列化和反序列化,在RMI中是否要實現 SERIALIZABLE 介面, SERIALVERSIONUID的用處是什麼?

8.更多討論 1、serialVersionUID實際作用 假設本地資料庫中儲存了大量的user物件,後來由於需求,要修改User類中的屬性;如果不設定SerialVersionUID,根據屬性方法等自動生成,就會出現程式碼演示中的錯誤,造

.net 4.5版本MVVM模式下ViewModel基類使用CallerMemberName優雅實現INotifyPropertyChanged介面

using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; namespace SSMart_Model {     /// &

.net 4.0及以下版本,MVVM模式ViewModel基類使用StackTrace實現INotifyPropertyChanged介面

using System; using System.ComponentModel; namespace SSMart_Model {     public class VModelBase: INotifyPropertyChanged, IDisposable    

資料結構實現動態順序表的各項介面(初始化,銷燬,尾插尾刪,頭插頭刪,刪除,排序,查詢等)

實現動態順序表 SeqList.h #pragma once ////靜態順序表 //#define N 10 //typedef int SLDateType; //typedef struct SeqList //{ // int _a[N];//陣列 /