串行接口是将FPGA连接到PC机上的一种简单方法。我们只需要一个发射机和接收器模块。
异步发送器
它通过序列化要传输的数据来创建一个信号“TXD”。
异步接收机
它从FPGA外部获取一个信号“RxD”,并将其“反串行化”,以便于在FPGA内使用。
这个项目由五个部分组成。
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”)。
- 发送器在发送每个字节之前发送“start”(=“0”),这样接收器就可以知道一个字节即将到来的是发送字节数据的8位。
- 发射机在每个字节后发送“停止”(=“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个停止位,无奇偶校验。
它的工作原理是:
- 发射机在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接收机
我们正在构建一个“异步接收器”:
我们的实施工作如下:
- 该模块接收来自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的串口)。
- 它在FPGA上创建了8个输出(端口名为“GPout”)。GPout由FPGA接收到的任何字符更新。
- 在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
记得用分析仪抓取异步接收器和异步发送器模块,并观察相关内部寄存器的值。