新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > FPGA串行接口 1 - RS-232 串行接口的工作原理

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

作者:时间:2024-01-02来源:EEPW编译收藏

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

本文引用地址:http://www.eepw.com.cn/article/202401/454373.htm

异步发送器

它通过串行化要发送的数据来创建信号“ TxD”。

异步接收器

它从FPGA外部获取信号“ RxD”,并对其进行“反序列化”,以便在FPGA内部轻松使用。

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

  • 使用 9 针连接器“DB-9”(较旧的 PC 使用 25 针“DB-25”)。

  • 允许双向全双工通信(PC可以同时发送和接收数据)。

  • 可以以大约 10KBytes/s 的最大速度进行通信。

DB-9 连接器

您可能已经在 PC 背面看到了此连接器。



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

  • 引脚 2:RxD(接收数据)。

  • 引脚 3:TxD(传输数据)。

  • 引脚 5:GND(接地)。

只需使用 3 根电线,您就可以发送和接收数据。

数据通常由 8 位(我们称之为字节)的块发送,并且是“序列化”的:首先发送 LSB(数据位 0),然后发送位 1,...最后是 MSB(第 7 位)。

异步通信

此接口使用异步协议。 这意味着没有时钟信号沿数据传输。 接收器必须有一种方法可以将自身“计时”到输入的数据位。

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

  1. 电缆的两端事先就通信参数(速度、格式等)达成一致。这是在通信开始之前手动完成的。

  2. 当线路处于空闲状态时,发射器会发送“空闲”(=“1”)。

  3. 发送器在发送每个字节之前发送“start”(=“0”),以便接收器可以确定一个字节即将到来。

  4. 发送字节数据的 8 位。

  5. 发送器在每个字节后发送“stop”(=“1”)。

让我们看看字节在传输时0x55的样子:

字节 0x55 以二进制形式01010101。
但是由于它首先传输 LSB(bit-0),因此该行的切换方式如下:1-0-1-0-1-0-1-0。

下面是另一个示例:

这里的数据是0xC4,你能看到它吗?
这些位更难看到。 这说明了接收方知道数据以何种速度发送是多么重要。

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

速度以波特率为单位,即每秒可以发送多少位。 例如,1000 波特意味着每秒 1000 位,或者每个位持续 <> 毫秒。

RS-232 接口的常见实现(如 PC 中使用的接口)不允许使用任何速度。 如果你想使用123456波特率,你就不走运了。 你必须满足于一些“标准”速度。常见值包括:

  • 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.5KB。

在 115200 波特率下,一些带有错误芯片的 PC 需要一个“长”停止位(1.5 或 2 位长...),这使得最大速度降至每秒 10.5KB 左右。

物理层

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

  • “1”使用 -10V(或介于 -5V 和 -15V 之间)发送。

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

因此,空闲线路的电压约为 -10V。

串行接口 2 - 波特发生器

在这里,我们希望以最大速度使用串行链路,即 115200 波特(较慢的速度也很容易生成)。 FPGA 通常以 MHz 的速度运行,远高于 115200Hz(按照今天的标准,RS-232 相当慢)。 我们需要找到一种方法来生成(从FPGA时钟)尽可能接近每秒115200次的“滴答声”。

传统上,RS-232芯片使用1.8432MHz时钟,因为这使得生成标准波特频率变得非常容易。 1.8432MHz 除以 16 得到 115200Hz。

假设FPGA时钟信号运行频率为1.8432MHz

//我们创建一个4位计数器

reg [3:0] BaudDivCnt;
always @(posedge clk) BaudDivCnt <= BaudDivCnt + 1; // count forever from 0 to 15

/ 以及每 16 个时钟断言一次的滴答信号(即每秒 115200 次)

wire BaudTick = (BaudDivCnt==15);

这很容易。但是,如果你有一个1MHz的时钟,而不是8432.2MHz,你会怎么做? 要从 115200MHz 时钟生成 2Hz,我们需要将时钟除以“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 是 2000000 的幂。 显然 2000000 不是。 所以我们改变了比例...... 让我们使用“115200/1024”= 59.17,而不是“356/10”。 这非常接近我们的理想比率,并实现了高效的 FPGA 实现: 我们使用一个 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

使用我们的 2MHz 时钟,“BaudTick”每秒置位 115234 次,与理想的 0 相差 03.115200%。

参数化 FPGA 波特率发生器

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

这是一个具有 25MHz 时钟和 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 个停止位、非奇偶校验。

它的工作原理是这样的:

发送器在 FPGA 内部获取 8 位数据并将其串行化(从“TxD_start”信号置位时开始)。

“忙”信号在传输发生时被置位(在此期间忽略“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 接收器

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

我们的实现是这样工作的:

该模块在收到 RxD 线时收集数据。

当一个字节被接收到时,它出现在“数据”总线上。一旦接收到一个完整的字节,就会为一个时钟置位“data_ready”。

请注意,“data”仅在断言“data_ready”时有效。 其余时间,不要使用它,因为新数据可能会洗牌。

过采样

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

为了确定新的数据字节何时到来,我们通过以波特率频率的倍数对信号进行过采样来寻找“开始”位。

一旦检测到“起始”位,我们以已知的波特率对线路进行采样,以获取数据位。

接收器通常以波特率的 16 倍对输入信号进行过采样。 我们在这里使用了 8 次...... 对于 115200 波特,采样率为 921600Hz。

假设我们有一个可用的“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 的串行端口)。

它在FPGA(名为“GPout”的端口)上创建8个输出。GPout由FPGA接收到的任何字符进行更新。

FPGA 上还有 8 个输入(名为“GPin”的端口)。每次FPGA接收到字符时,都会发送GPin。

GP 输出可用于从您的 PC 远程控制任何东西,可能是 LED 或咖啡机......

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



关键词:

评论


相关推荐

技术专区

关闭