FPGA实现串口UART自收发0推荐
发表于 2014/7/22 9:45:03
阅读(2105)
评论(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") (二) 波特率发生器 我们选择的是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代码。 |
|