天天看点

用Verilog代码实现一个简易的串行接口(RS-232)

串行接口是将FPGA连接到PC机上的一种简单方法。我们只需要一个发射机和接收器模块。

异步发送器

它通过序列化要传输的数据来创建一个信号“TXD”。

用Verilog代码实现一个简易的串行接口(RS-232)

异步接收机

它从FPGA外部获取一个信号“RxD”,并将其“反串行化”,以便于在FPGA内使用。

用Verilog代码实现一个简易的串行接口(RS-232)

这个项目由五个部分组成。

    1. RS-232串行接口的工作原理

    2. 波特率发生器

    3. 发送器

    4. 接收机

串行接口1-RS-232串行接口的工作方式

RS-232接口具有以下特点:

  • 使用9引脚连接器“DB-9”(较旧的PC使用25个引脚“DB-25”)。
  • 允许双向全双工通信(PC机可以同时发送和接收数据)。
  • 可以以大约10 KBytes/s的最高速度进行通信。

DB-9连接器

你可能已经在你的电脑背面看到了这个连接器。

它有9个引脚,但最重要的三个是:

  • 引脚2:RxD(接收数据)。
  • PIN 3:TXD(传输数据)。
  • 引脚5:GND(地面)。

只要3根电线,你就可以发送和接收数据。

数据通常由8位块(我们称之为字节)发送,并被“序列化”:首先发送LSB(数据位0),然

后发送比特1,.最后一个是MSB(第7位)。

异步通信

此接口使用异步协议。这意味着没有数据发送的时钟信号。接收方必须有一种方法来“计

时”到输入的数据位。

在RS-232的情况下,这是这样做的:

  1. 线缆的双方事先就通信参数(速度、格式.)达成一致。这是在通信开始之前手动完成的。
  2. 当线路空闲时,发射机发送“空闲”(=“1”)。
  3. 发送器在发送每个字节之前发送“start”(=“0”),这样接收器就可以知道一个字节即将到来的是发送字节数据的8位。
  4. 发射机在每个字节后发送“停止”(=“1”)。

让我们看看字节0x55是如何传输的:

字节0x55以二进制形式表示为01010101。

但是由于它是首先传输的lsb(位-0),线就像这样切换:1-0-1-0-1-0-1-0。

下面是另一个例子:

这里的数据是0xC4,你能看到吗?

细节很难看出来。这说明了接收方知道发送数据的速度是多么重要。

我们能以多快的速度发送数据?

速度以波特为单位,即每秒钟可以发送多少位。例如,1000比特意味着每秒钟1000比

特,或者每个比特持续1毫秒.

RS-232接口的常见实现(就像PC中使用的那样)不允许使用任何速度。如果你想要用

123456波特率,那你就倒霉了。你得适应一些“标准”的速度。共同的标准是:

  • 1200包
  • 9600包
  • 38400包。
  • 115200包(通常是最快的)。

在115200字节时,每比特持续时间为(1/115200)=8.7us。如果传输8位数据,则持续

8x8.7us=69us。但每个字节需要额外的开始和停止位,因此实际上需要10x8.7us=87us。这意味着每秒最高速度为11.5KBytes。

在115200小包时,一些装有小车芯片的个人电脑需要一个“长”停止位(1.5或2位长.)使

最大速度降到每秒10.5KBytes左右。

物理层

电线上的信号采用正/负电压方案。

  • “1”使用-10V(或-5V至-15V之间)发送。
  • “0”使用+10V(或在5V至15v之间)发送。

所以一条空闲线路的容量大约是-10V。

串行接口2-波特发生器

在这里,我们希望以最大速度使用串行链路,即115200 Bauds(较慢的速度也很容易生成)。FPGA通常以兆赫的速度运行,远远超过115200赫兹(以今天的标准来看,RS-232相当慢)。我们需要找到一种方法来产生(从FPGA时钟)一个“滴答”尽可能接近115200次每秒。

传统上,RS-232芯片使用1.8432MHz时钟,因为这使得产生标准波特率非常容

易.1.8432MHz除以16,得到115200 Hz。

// let's assume the FPGA clock signal runs at 1.8432MHz

// we create a 4-bit counter

reg [3:0] BaudDivCnt;

always @(posedge clk) BaudDivCnt <= BaudDivCnt + 1; // count forever from 0 to 15



// and a tick signal that is asserted once every 16 clocks (so 115200 times a second)

wire BaudTick = (BaudDivCnt==15);
           

那很容易。但是,如果你有一个2兆赫的时钟,而不是1.8432MHz,你会做什么呢?要从2 MHz时钟产生115200 Hz,我们需要将时钟除以“17.361111111.”不完全是整数。解决办法是,有时除以17,有时除以18,确保比率保持“17.361111111”。这其实很容易做。

查看以下“C”代码:

while(1) // repeat forever
{
acc += 115200;
  if(acc>=2000000) printf("*"); else printf(" ");
acc %= 2000000;
}
           

以准确的比例打印“*”,每“17.361111111.”一次.平均循环。

为了在FPGA中有效地获得相同的信息,我们依赖于串行接口能够容忍波特率产生器中误

差的几%。

最好2000000是2的幂。显然2000000不是。所以我们改变比率..。让我们用“1024/59”=17.356代替“2000000/115200”。这非常接近我们的理想比率,并使一个高效的FPGA实现:我们使用一个10位累加器增量为59,每次累加器溢出时都有一个滴答标记。

// let's assume the FPGA clock signal runs at 2.0000MHz

// we use a 10-bit accumulator plus an extra bit for the accumulator carry-out

reg [10:0] acc; // 11 bits total!


// add 59 to the accumulator at each clock

always @(posedge clk)

acc <= acc[9:0] + 59; // use 10 bits from the previous accumulator result, but save the full 11 bits result


wire BaudTick = acc[10]; // so that the 11th bit is the accumulator carry-out
           

使用我们的2 MHz时钟,“BaudTick”每秒钟断言115234次,与理想的115200相比有

0.03%的误差。

参数化FPGA波特率发生器

以前的设计是使用10位累加器,但是随着时钟频率的增加,需要更多的比特。

这是一个25 MHz时钟和16位累加器的设计。设计是参数化的,所以很容易定制。

parameter ClkFrequency = 25000000; // 25MHz
parameter Baud = 115200;
parameter BaudGeneratorAccWidth = 16;
parameter BaudGeneratorInc = (Baud<<BaudGeneratorAccWidth)/ClkFrequency;

reg [BaudGeneratorAccWidth:0] BaudGeneratorAcc;
always @(posedge clk)
BaudGeneratorAcc <= BaudGeneratorAcc[BaudGeneratorAccWidth-1:0] + BaudGeneratorInc;

wire BaudTick = BaudGeneratorAcc[BaudGeneratorAccWidth];
           

最后一个实现问题:“BaudGeneratorInc”计算是错误的,因为Verilog使用32位中间

结果,而且计算超过了这一点。更改行,如下所示,以找到解决办法。

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

这一行还具有舍入结果而不是截断结果的优势。

现在我们有了一个足够精确的波特率发生器,我们可以继续使用RS-232发射机和接收模

块。

串行接口3-RS-232发射机

我们正在构建一个具有固定参数的异步发送器:8个数据位,2个停止位,无奇偶校验。

用Verilog代码实现一个简易的串行接口(RS-232)

它的工作原理是:

  • 发射机在FPGA内部接收8位数据并对其进行串化(从“TXD_START”信号断言开始)。
  • “busy”信号在传输发生时有效(数据发送期间的“TXD_START”信号被忽略)。

数据串化

要通过开始位、8位数据位和停止位,利用状态机实现似乎是合适的。

reg [3:0] state;

// the state machine starts when "TxD_start" is asserted, but advances when "BaudTick" is asserted (115200 times a second)
always @(posedge clk)
case(state)
4'b0000: if(TxD_start) state <= 4'b0100;
4'b0100: if(BaudTick) state <= 4'b1000; // start
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; // stop1
4'b0010: if(BaudTick) state <= 4'b0000; // stop2
default: if(BaudTick) state <= 4'b0000;
endcase
           

现在,我们只需要生成“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

// combine start, data, and stop bits together
assign TxD = (state<4) | (state[3] & muxbit);
           

串行接口4-RS-232接收机

我们正在构建一个“异步接收器”:

用Verilog代码实现一个简易的串行接口(RS-232)

我们的实施工作如下:

  • 该模块接收来自RxD线的数据。
  • 当接收到一个字节时,它会出现在“数据”总线上。一旦接收到一个完整的字节,就会为一个时钟产生“data_ready”。

注意,“data”只有在产生“data_ready”时才有效。其余的时间,不要使用它,因为新的数据可能会来冲洗。

过采样

异步接收器必须以某种方式与传入信号保持同步(它通常不能访问发射机使用的时钟)。

  • 为了确定新的数据字节何时到来,我们通过采用波特率倍数的时钟频率对信号进行过采样来寻找“开始”位。
  • 一旦检测到“开始”位,我们将对已知波特率对线路进行采样,以获取数据位。

接收机通常以波特率16倍的速度对输入信号进行过采样。我们在这里用了8次..。对于115200波,这就给出了921600赫兹的采样率。

假设我们有一个“Baud8Tick”信号,每秒921600次。

设计

首先,传入的“RxD”信号与我们的时钟无关。

我们使用两个D触发器对其进行过采样,并将其同步到我们的时钟域.

reg [1:0] RxD_sync;
always @(posedge clk) if(Baud8Tick) RxD_sync <= {RxD_sync[0], RxD};
           

我们过滤数据,这样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]};
           

完整的代码可以找到这里.

它有一些改进;注意代码中的注释。

串行接口5-如何使用rs-232发射机和接收器

这个设计允许从你的PC控制几个FPGA引脚(通过你的PC的串口)。

  1. 它在FPGA上创建了8个输出(端口名为“GPout”)。GPout由FPGA接收到的任何字符更新。
  2. 在FPGA上还有8个输入(端口名为“GPin”)。每次FPGA接收到一个字符时,都会发送GPin。

该GP输出可用于从您的个人电脑控制任何远程终端,例如发光二极管.

module serialGPIO(
    input clk,
    input RxD,
    output TxD,

output reg [7:0] GPout,  // general purpose outputs
    input [7:0] GPin  // general purpose inputs
);

wire RxD_data_ready;
wire [7:0] RxD_data;
async_receiver RX(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data));
always @(posedge clk) if(RxD_data_ready) GPout <= RxD_data;

async_transmitter TX(.clk(clk), .TxD(TxD), .TxD_start(RxD_data_ready), .TxD_data(GPin));
endmodule
           

记得用分析仪抓取异步接收器和异步发送器模块,并观察相关内部寄存器的值。

继续阅读