分享

FPGA实现串口UART自收发

 quasiceo 2016-01-07

FPGA实现串口UART自收发

0推荐
发表于 2014/7/22 9:45:03 阅读(2105) 评论(0)

       串行接口是最简单的一种通信方式,串口通信有两种方式,一种是同步串行,如SPI接口;另一种则是异步串行,即我们所说的UART。这个项目向大家展示了如何使用FPGA来模拟UART收发器。

(一) 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. 串行线缆的两端事先约定好串行传输的参数:传输速度(波特率)、传输格式(几位数据,有无校验位,有几位停止位)等
  2. 当没有数据传输的时候,发送端向数据线上发送"1" ,也即空闲状态,总线默认拉高。
  3. 每传输一个数据之前,发送端先发送一个"0"来表示传输已经开始。接收端检测到下降沿后便开始接收数据。
  4. 开始传输后,数据以约定的速度和格式传输,所以接收端可以与之同步

每次传输完成一个字节之后,都在其后发送一个停止位("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代码

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多