FPGA实现串口UART自收发
0赞
串行接口是最简单的一种通信方式,串口通信有两种方式,一种是同步串行,如SPI接口;另一种则是异步串行,即我们所说的UART。这个项目向大家展示了如何使用FPGA来模拟UART收发器。
普遍意义上讲,UART接口是分为两种:
a)TTL电平接口
b)RS232电平接口
通常我们看不到设备直接裸露出来的TTL电平接口,TTL电平接口一般为芯片直接连接的引脚,一般是供我们调试设备用的端口。实际上我们看得到的都是RS232电平接口,这个在台式机上很常见,有DB9和DB25两种规格插头。
TTL电平UART和电脑相连接,有以下几种方式:
a)TTL电平接口通过PL2303(PL2302)即USB转TTL电平芯片和电脑相连
b)如果电脑上直接有RS232接口,则可通过MAX232芯片将RS232电平与TTL电平转换
c)当然,如果FPGA端已经板载了MAX232,你又不想重新自己引出TTL电平UART的话,如果电脑上又没有RS232接口,则通过一根USB转串口线(PL2303+MAX232)与FPGA端RS232接口连接
异步串行通讯
RS-232使用异步通讯协议,也就是说数据的传输没有时钟信号。数据以每次一位的方式传输;每条线用来传输一个方向的数据。通常是以8位数据为1个字节,先发送最低有效位,最后发送最高有效位。接收端必须有某种方式,使之与接收数据同步。
对于RS-232来说,是这样处理的:
- 串行线缆的两端事先约定好串行传输的参数:传输速度(波特率)、传输格式(几位数据,有无校验位,有几位停止位)等
- 当没有数据传输的时候,发送端向数据线上发送"1" ,也即空闲状态,总线默认拉高。
- 每传输一个数据之前,发送端先发送一个"0"来表示传输已经开始。接收端检测到下降沿后便开始接收数据。
- 开始传输后,数据以约定的速度和格式传输,所以接收端可以与之同步
每次传输完成一个字节之后,都在其后发送一个停止位("1")
(二) 波特率发生器
我们选择的是9600的波特率,这个参数可以根据实际需要来调整。实际上,针对固定的时钟频率,可以事先做成一个分频比表格,这样就可以方便的调节波特率了。
FPGA通常运行在远高于9600Hz的时钟频率上(对于今天的标准的来说RS-232真是太慢了),我们使用板载的50MHz的晶振作为波特率发生器的输入时钟,这就意味着我们需要用一个较高的时钟来分频产生尽量接近于9600Hz的时钟信号。
1) 循环波特率发生器
我们希望5000000是2的整数幂,但很可惜,它不是。所以我们改变分频比,"5000000/9600" 约等于 "2^17/25" = 5242.88. 这跟我们要求的分频比5208.333很比较近,并且使得在FPGA上实现起来相当有效。
reg [17:0] acc; //一共18位
always @(posedge clk)
acc <= acc[16:0] + 25; //我们使用上一次结果的低17位,但是保留18位结果
wire BaudTick = acc[17]; //第18位作为进位输出
使用 50MHz 时钟, "BaudTick" 为 9537波特,与理想的9600波特存在 0.65% 的误差,误差太高,实际上我们采样数据的时候都是在波特率周期一半的时间采样,似乎影响不大,但是考虑到发送数据量大的时候会积累出很大的周期偏移,故我们不采用此种分配方式。
我们的全局时钟周期为1/50MHz=20ns,而要求的9600波特率的周期为1/9600Hz=104.2us,两者的倍数关系为5210,即按照上述算出来的波特率周期为104.9us。按照标准波特率产生的数据,进行采样的时候,为了保证采样数据的稳定正确,我们在数据的中间点采样,由于我们产生的波特率偏小,导致每次应该在52.1us采样的数据推延到52.45us,亦即每1bit会使实际的采样点延后正常的采样点3.45us,故发送完15bit之后,数据的采样点会偏移到下一个字符,数据便会出现紊乱,出现乱码。
2) 参数化FPGA波特率发生器
由于前面所述的波特率发生器设置方法产生的偏差过大,故我们采用以下波特率产生方式即暴力直接累加法。
//以下波特率分频计数值可参照需要设计的参数进行更改
`define BPS_PARA 5208 //波特率为9600时的分频计数值
`define BPS_PARA_2 2604 //波特率为9600时的分频计数值的一半,用于数据采样
always @ (posedge clk or negedge rst_n)
if(!rst_n) cnt <= 13'd0;
else if((cnt == `BPS_PARA) || !bps_start) cnt <= 13'd0; //波特率计数清零
else cnt <= cnt+1'b1; //波特率时钟计数启动
这就是整个的设计方法了。
按照此种方法设计的波特率发生器,波特率为50MHz/5208=9600.6,与标准9600波特率误差为0.00625%,已经相当精确。
(三) TX发送模块
下面是我们所想要实现的:
接收模块传送8位数据到发送模块,rx_int信号使能tx_en,8位数据被串行输出。("tx_en"置位后开始传输)。
TX发送模块的参数是固定的: 8位数据, 1个停止位, 无奇偶校验。
数据串行化
经过上述的波特率发生器,我们已经产生了9600的波特率。
由于我们的程序功能实现的是,将接收来的数据发送回去,程序如下:
if(neg_rx_int) begin //接收数据完毕,准备把接收到的数据发回去
bps_start_r <= 1'b1;
tx_data <= rx_data; //把接收到的数据存入发送数据寄存器
tx_en <= 1'b1; //进入发送数据状态中
在always模块中进行数据发送的判断,
if(tx_en) begin //使能发送的信号
if(clk_bps) begin //波特率时钟到后开始发送
num <= num+1'b1;
case (num)
4'd0: rs232_tx_r <= 1'b0; //发送起始位
4'd1: rs232_tx_r <= tx_data[0]; //发送bit0
4'd2: rs232_tx_r <= tx_data[1]; //发送bit1
4'd3: rs232_tx_r <= tx_data[2]; //发送bit2
4'd4: rs232_tx_r <= tx_data[3]; //发送bit3
4'd5: rs232_tx_r <= tx_data[4]; //发送bit4
4'd6: rs232_tx_r <= tx_data[5]; //发送bit5
4'd7: rs232_tx_r <= tx_data[6]; //发送bit6
4'd8: rs232_tx_r <= tx_data[7]; //发送bit7
4'd9: rs232_tx_r <= 1'b1; //发送结束位
default: rs232_tx_r <= 1'b1;
endcase
end
else if(num==4'd10) num <= 4'd0; //复位
end
end
最后将发送的数据送到总线上,
assign rs232_tx = rs232_tx_r;
(四) RX接收模块
下面是我们想要实现的模块:
我们的设计目的是这样的:
1.当rs232_rx线上有数据时,接收模块负责识别rs232_rx线上的数据
2.当收到一个字节的数据时,锁存接收到的数据到"rx_data"总线,并使"rx_int"有效一个周期。
注意:只有 当"rx_int"有效时," rx_data "总线的数据才有效,其他的时间里不允许使用" rx_data "总线上的数据,因为新的数据可能已经改变了其中的部分数据。
数据采样
异步接收机必须通过一定的机制与接收到的输入信号同步(接收端没有办法得到发送断的时钟),这里采用如下办法:
为了确定新数据的到来,需检测开始位,我们在波特率时钟周期的一半处进行数据的采样。
首先,接收到的" rx_data "信号与我们的时钟没有任何关系,所以采用4个D触发器对其进行采样,并且使之我我们的时钟同步,同时也是对接收到的数据进行滤波,这样可以防止毛刺信号被误认为是开始信号。
reg rs232_rx0,rs232_rx1,rs232_rx2,rs232_rx3; //接收数据寄存器,滤波用
wire neg_rs232_rx; //表示数据线接收到下降沿
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rs232_rx0 <= 1'b1;
rs232_rx1 <= 1'b1;
rs232_rx2 <= 1'b1;
rs232_rx3 <= 1'b1;
end
else begin
rs232_rx0 <= rs232_rx;
rs232_rx1 <= rs232_rx0;
rs232_rx2 <= rs232_rx1;
rs232_rx3 <= rs232_rx2;
end
end
//下面的下降沿检测可以滤掉<20ns-40ns的毛刺(包括高脉冲和低脉冲毛刺),
//这里就是用资源换稳定(前提是我们对时间要求不是那么苛刻,因为输入信号打了好几拍)
//我们的有效低脉冲信号肯定是远远大于40ns的,104us。
assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; //接收到下降沿后neg_rs232_rx置高一个时钟周期
一旦检测到"开始位",使用如下的代码可以检测出接收到每一位数据。
if(rx_int) begin //一旦检测到开始位,即rs232_rx下降沿,rx_int置位
if(clk_bps) begin //读取并保存数据,接收数据为一个起始位,8bit数据,1个结束位
num <= num+1'b1;
case (num)
4'd1: rx_temp_data[0] <= rs232_rx; //锁存第0bit
4'd2: rx_temp_data[1] <= rs232_rx; //锁存第1bit
4'd3: rx_temp_data[2] <= rs232_rx; //锁存第2bit
4'd4: rx_temp_data[3] <= rs232_rx; //锁存第3bit
4'd5: rx_temp_data[4] <= rs232_rx; //锁存第4bit
4'd6: rx_temp_data[5] <= rs232_rx; //锁存第5bit
4'd7: rx_temp_data[6] <= rs232_rx; //锁存第6bit
4'd8: rx_temp_data[7] <= rs232_rx; //锁存第7bit
default: ;//起始位和停止位均通过default去除
endcase
使用一个寄存器来存储接受到的数据,
if(num == 4'd10) begin //标准接收模式下只有1+8+1=10bit有效数据
num <= 4'd0; //接收到STOP位后结束,num清零
rx_data_r <= rx_temp_data; //把数据锁存到数据寄存器rx_data
end
利用此寄存器来驱动模块间接口rx_data,
assign rx_data = rx_data_r;
RX模块中,以下两处num清零及波特率发生信号关闭的信号的输出,在num==4'd10或者num==4'd9时做出判断程序的功能正常,我的理解是:
在num==4'd10做出判断是正常的选择,因为要接受第10位停止位;而在num==4'd9做出判断功能正常,原因在于,虽然没有接收第10位,但是因为第10位是停止位,是高电平,在接收下一个符号的时候我只监测总线上的下降沿,不管高电平的时间长度,故num==4‘d也不影响程序的功能。仅为个人看法,抛砖引玉。
if(num == 4'd10) begin //标准接收模式下只有1+8+1=10bit有效数据
num <= 4'd0; //接收到STOP位后结束,num清零
rx_data_r <= rx_temp_data; //把数据锁存到数据寄存器rx_data
end
if(num==4'd10) begin //接收完有用数据信息 bps_start_r <= 1'b0; //数据接收完毕,释放波特率启动信号 rx_int <= 1'b0; //接收数据中断信号关闭
(五) 发送模块和接收模块的连接
为了更好的验证功能,我们设计的UART接口实现如下的功能:
整个UART模块对外提供了RX、TX接口,实现的是接收与之连接的设备发送来的数据,而后又发送回去。应用在电脑上,就是说RX模块接收上位机串口调试助手发送给FPGA的数据,然后利用其中的TX模块将数据又送回到上位机,显示在电脑的串口调试助手中。
可以看到,RX模块和TX模块的端口定义是有一定关系的,两个模块中,clk,rst_n为全局时钟和复位信号,也为外部硬件的输入信号端口,bps_start为输出的波特率启动信号,clk_bps为波特率时钟信号。
在TX模块中,rs232_tx为硬件输出端口,rx_data以及rx_int为输入信号,
module my_uart_tx(
clk,rst_n,
rx_data,rx_int,rs232_tx,
clk_bps,bps_start
);
RX模块,rs232_rx为硬件输入端口,rx_data以及rx_int为输出信号,
module my_uart_rx(
clk,rst_n,
rs232_rx,rx_data,rx_int,
clk_bps,bps_start
);
可以很清楚的看到,两个模块,通过rx_int以及rx_data作为信号交换的接口,来完成输入数据的转发。
到这里,UART接口的设计就完成了。
以下为整个工程UART代码。
