FPGA串行接口(RS-232)
串行接口是将FPGA连接到PC的简单方法。 我们只需要一个发射器和接收器模块。
异步发射器
它通过序列化要传输的数据来创建信号“TxD”。
异步接收器
它从 FPGA 外部获取信号“RxD”,并将其“解串化”,以便在 FPGA 内部轻松使用。
串行接口 1 - RS-232 串行接口的工作原理
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”)。
发送器在发送每个字节之前发送“start”(=“0”),以便接收器可以确定一个字节即将到来。
发送字节数据的 8 位。
发送器在每个字节后发送“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。
// 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
这很容易。但是,如果你有一个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
加入微信
获取电子行业最新资讯
搜索微信公众号:EEPW
或用微信扫描左侧二维码