RS232是一种常见的串行通讯接口,一种常见的RS232通信接口是9针的接口(DB-9),如下图所示:
在RS232的针脚上,一般使用-3V-15V(有些文档说明中为-5V-15V)之间的任意电平表示逻辑1,而使用+3V+15V(有些文档说明中为5V15V)之间的任意电平表示逻辑0。图中对管脚的标号做了标注,其中pin 2是RXD,pin 3是TxD,pin 5是GND。只需要这三个端口就能完整的实现RS232的数据传输功能。稍后我们将在Vivado环境进行一个简单实现。
RS232的其余特性包括:
- 允许采用双向全双工的通信方式(PC机可以在发射数据的同时通过该接口接收数据);
- 可以最高以10KByte/s的速度进行数据通信。
在开始之前,首先定义清楚RS232信号线上的时序。在RS232关键的三根信号线当中并没有时钟线,相邻两个字符(一个字,一般假定为1个byte,8bits)之间需要开始和终值位进行标定,所以相邻字符的间隔完全可以是任意的,因而也可以认为这是一种异步通信方式。一般认为单个字符在进行传输的时候首先传输低位,我们进行以下约定。
- 发射模块idle状态的时候TXD ='1';
- 发射模块发送开始位start bit的时候TXD = '0';
- 发射模块发送终止位stop bit的时候TXD = '1';
如果发送0x55的话发送数据线上的时序信号如下(注意发送'1'的时候信号线为低电平):
我们实现的传送速度为115200bauds,bauds(波特率)本身表示传送符号的速率,但是这里是异步通信方式,我们认为1个bit就是一个符号,包含所有数据位和起始终止符号位。传输速率为115200bauds的情况下,每个bits的持续时间为(1/115200=)8.7us。
1.波特率产生
在数字系统当中,时钟与数据速率紧密相关,时钟驱动着时序电路的数据流动,数据传输的速度与数字电路的时钟是紧密相关。在不考虑数据有效位的情况下,时钟速率与数据传输速率(bit/s)应该是相等的。前文提到,RS232的9根线当中并没有定义时钟线,那么115200 bauds的数据应该如何驱动呢。事实上,驱动时钟在数据发送端和接收端。假设接口全速运行,数据传送都是连续的,那么需要的最小时钟就是115.2kHz。如果有25MHz的启动时钟((clk\_freq))的话如何产生115.2kHz的驱动时钟((BaudTick))呢。
在200MHz的时钟驱动下每间隔217.014(25MHz/115.2kHz)个时钟周期就输出一个高电平(可以说是间隔217个数或218个数输出一个高电平,但是大多是间隔217个数),那么就能得到(BaudTick)。如果采用计数规则的话,就必须用到取模运算。为了充分利用二进制运算本身的特性,设计如下代码:
parameter ClkFrequency = 25000000; // 25MHz
parameter Baud = 115200;//115200 bauds,32bits数据精度
parameter BaudGeneratorAccWidth = 16;
parameter BaudGeneratorInc = ((Baud<<(BaudGeneratorAccWidth-4))+(ClkFrequency>>5))/(ClkFrequency>>4);//由于该parameter运算数字位整数,所以其输出一定为整数
reg [BaudGeneratorAccWidth:0] BaudGeneratorAcc;
always @(posedge clk)
BaudGeneratorAcc <= BaudGeneratorAcc[BaudGeneratorAccWidth-1:0] + BaudGeneratorInc;//注意累加的时候不取最高位。
wire BaudTick = BaudGeneratorAcc[BaudGeneratorAccWidth];//最高位输出表示到达累加数字。
2.发射端
RS232发射端框图可以表达为
所以我们要完成的工作实际上就是并串的转换。另外,
- 该模块要能够自动添加串行的起始终止位;
- 该模块要能够响应TxD_start以开始数据传输(数据到来应该晚于Tx_start一个baudtick周期);
- 在传输数据的时候,busy输出逻辑'1';
由于同时要处理数据位和数据起始和终止位,所以可以使用状态机构造该结构。
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;//idle
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;//idle
endcase
数据输出端口表示为:
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
//原始代码为(原始代码start状态有问题)
//assign TxD = (state<4) | (state[3] & muxbit);
assign TxD = (state<=4) | (state[3] & muxbit);
3.接收端
数据接收端的框图如下所示:
该模块要完成的功能是:
- 串并转换,并且能够根据同步起始和终止位进行数据同步;
- 在串并转换完成之后data_ready输出一个时钟周期(baudtick)逻辑'1',表明数据已经准备好;
由于在RS232的标准当中没有时钟线,所以采样的相位基本不可能保持一致。想象,如果采样的边沿时钟在数据跳变的边沿,这就总是会导致采样数据与发送数据不一致。因而,在接收系统当中一般都会采用过采样的机制来保证数据的可靠性。这里我们采用8倍过采样的时钟对数据进行数据采样。
另一点,数据在传输的时候可能会产生一些尖峰,直接进行采样是有一些不妥的。为了保持数据结果的可靠性,在8个过采样周期,连续的4个数据都一致,我们才输出这比较可靠的数据输出。为了不产生数据延时,我们使用两个寄存器来接收数据
reg [1:0] RxD_sync;
always @(posedge clk) if(Baud8Tick) RxD_sync <= {RxD_sync[0], RxD};
再使用一个2位的计数器实现计数功能呢:
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);
最后使用移位寄存机对数据进行暂存,然后定期置位data_ready信号即可
reg [7:0] RxD_data;
always @(posedge clk) if(Baud8Tick && next_bit && state[3]) RxD_data <= {RxD_bit, RxD_data[7:1]};
最后对输入输出进行封装
module serialGPIO(
input clk,
input RxD,
input TxD_data_ready,
output TxD,
output RxD_data_ready,
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(TxD_data_ready), .TxD_data(GPin));
endmodule