01UART(异步串行接口)
串行通信:指利用一条数据线将资料一位位的顺序传输。
异步通信:以一个字符为传输单位,通信中两个字符间的时间间隔是不固定的,然而在同一个字符的两个相邻位代码间的时间间隔是固定的。
通信协议:指通信双方约定的一些规则。在使用串口通信的时候,规定有:空闲位、起始位、数据位、奇偶校验位、停止位。
02串口通信时序
这个协议在 FPGA 内部是除 SPI 之外最简单的接口吧,其实就是发送方与接收方相互认定的协议(暗号),这种接口数据一般是单向传输,所以发送方和接收方通信一般需要两根数据线。
图1 URAT时序图
数据线在没有数据传输时保持高电平,当需要传输数据时,发送方把数据线拉低一段时间,告诉接收方开始传输数据了。之后把数据从低位到高位或者高位到低位(这个根据通信双方的要求确定)依次发送给对方(数据的位数双方应该事先确认好,通常5~8位数据)。数据发送完,可能会发送一位奇偶校验(这部分在下一节构建完整UART协议时细说)。最后就是将数据线拉高一段时间表示数据传输结束。
在这之间就会有疑问,每位数据电平持续时间到底是多久?
这就引出波特率,通常就是说每秒能传输多少位数据,比如波特率为9600bit/s,就是指1秒传输9600位数据(当然这是包含起始位,校验位,停止位在内的,所以有效数据其实并没有这么多)。当使用该波特率时,那每个电平持续时间不就是1/9600秒么。
03串口接收模块设计
首先确定模块接口信号,肯定有个串口的输入信号uart_rx吧,然后时钟信号clk和复位信号rst_n也是不可能少的。接收到数据后肯定要输出吧,所以在加一个uart_rx,注意该信号位宽应该是可以改变的(因为串口协议的数据位可以改变)。一般还要有一个信号用于指示接收到的数据什么时候是有效的,便于后续模块使用uart_rx,即uart_tx_vld(为高电平时,表示uart_rx有效)。
信号 | I/O | 位宽 | 含义 |
clk | I | 1 | 系统时钟,50MHZ |
rst_n | I | 1 | 系统复位,低电平有效 |
uart_rx | I | 1 | UART接口输入信号 |
rx_data | O | 8 | 数据输出信号 |
rx_vld | O | 1 | 数据有效指示信号,高电平有效 |
模块总体思路:有了输入输出信号后,模块内部就是根据输入信号生成输出信号而已。通过观察图1时序知道,每位数据传输需要使用 1/波特率 的时间,每次需要传输的 “数据” 包括起始位,数据位,校验位,结束位。那么以上是不是就对应两个计数器?所以使用计数器data_num来计数一位数据传输需要的时间(需要将1/波特率转换为系统时钟个数作为data_num的结束条件),使用计数器cnt来计数目前传输的第几位数据了。整体思路就是如此,大致如下图,接下来就是细节:
图2 计数器架构
计数器data_num该从什么时候计数?
当发送方发送起始位时会把数据线拉低,并且在之后一段时间内发送起始位,数据位等数据,那么data_num在此期间都要计数,直到停止位接收完成为止。由此引入一个标志信号flag,该信号为高电平时,计数器data_num就计数,当计数到时钟频率/波特率(1/波特率对应的时钟个数)时清零。
故计数器data_num初始值为0,计数条件add_data_num = flag,结束条件为end_data_num = add_data_num && data_num == BSP_NUM - 1。
flag当然就是检测到数据线下降沿时拉高,当计数器cnt计数结束时拉低,其余时间保持不变了。
故flag拉高条件:检测到uart_rx下降沿,拉低条件为end_cnt。
接下来就是计数器cnt了,cnt表示数据线此时传输的是第几位数据了。当计数器data_num计数结束时,表示一位数据传输完成了,此时cnt就应该加一了。当计数器计数到 起始位数+数据位数+校验位数+停止位数 时表示数据传输完成了,此时cnt计数结束并清零,其余时间保持不变。
故计数器cnt初始值为0,计数条件add_cnt = end_data_num,计数器清零条件end_data_num = add_cnt && cnt == CNT_W - 1。CNT_W = 起始位数+数据位数+校验位数+停止位数 。
接下来就是接收数据并产生输出信号了,一般会在计数器data_num计数的中部将数据线上的数据取下来进行保存,此时的数据是比较稳定的。由于最终需要输出的只是数据位,本文不考虑校验位,传输第0位是起始位,不需要保存。cnt==1时表示传输第1位数据,需要保存到输出信号上的最低位(这是由于串口调试助手是先发的最低位,实际情况要看发送方先发高位还是低位)。
flag拉高后,计数器data_num进行计数,当计数完一位数据后清零,并且cnt计数器进行计数,当cnt大于等于1,小于等于8时,表示此时接收的是数据位,将接收到的数据保存到rx_data对应位(最好是在data_num为容量的一半时进行保存),当cnt计数器计数完成,表示一组数据接收完成,此时有效指示信号拉高,并且flag信号拉低,结束一组数据的接收;所以当cnt=1 && data_num == BSP_CNT/2-1时(BSP_CNT表示波特率对应的时钟个数),有rx_data[0] <= uart_rx。
经过对其它位的详细分析,最终会得到这样的结果:当cnt >=1 && cnt <= DATA_W && data_num == BSP_CNT/2-1 && add_data_num 时(DATA_W表示每次发送的数据位位数),rx_data[cnt - 1] <= uart_rx;这样就产生了输出数据信号。
之后就是产生输出有效指示信号,该信号当然是接收完数据时产生的,其实可以在计数器cnt计数结束时产生。但数据在接收完数据位后,其实数据就已经接收完成了,此时就可以把输出有效指示信号拉高了,这样后续模块就可以提前使用接收到的数据。所以当cnt == DATA_W && add_data_num && data_num == BSP_NUM/2-1时将rx_data_vld拉高,其余时间拉低。
如果想要保证输出数据线上数据比较干净,不出现接收过程中的无效数据,那么可以将rx_data和rx_data_vld在rx_data_vld有效时才进行输出,其余时间保持不变。
最后还要注意,数据线是其他芯片或者设备输入的信号,为了减小亚稳态出现的机率,一般需要将数据线上的信号通过寄存器寄存两个时钟。由于还需要检测数据线的下降沿,所以还要把该信号延迟一个时钟,最终将接收到的信号uart_rx打三拍(前两拍用于同步处理,最后一拍用于检测输入信号的下降沿),然后通过uart_rx_ff1和uart_rx_ff2检测出下降沿,把标志信号flag拉高。
整体时序图如下:
图3 整体时序
时序图可能在手机上没法看,所以将上图各个部分截图放在下面:
图4 准备传输数据
当计数器data_num=BSP_NUM/2-1的时候,将uart_rx_ff2的数据保存到rx_data的第cnt-1位,下图为最低位,至于为什么是uart_rx_ff2,而不是uart_rx_ff1,通过下图可知uart_rx_ff2与计数器data_num是对齐的,所以该信号会更准。由于串口传输数据还是比较慢的,使用这两个信号对结果基本没有影响。
图5 接收最低位数据
接收完8位数据,将输出使能信号拉高,rx_data的x表示不确定,因为图4~图6只能确定接收的最高位和最低位数据,其余时序没有画,中间的时序类似,所以省略双波浪线表示中间的数据传输省略。
最后接收完停止位后,end_cnt拉高表示接收一次数据传输完成,将两个计数器清零,并且将标志信号flag拉低。
图6 接收完8位数据
上述将模块内部信号讲完了,如果要实现功能完全够了,但是在调用模块时,我们往往不习惯去改模块内部的参数,这就需要通过parameter和localparam添加一些参数,来自动设置计数器位宽,计数器结束条件等等。其实人为需要设置的就是波特率、数据位位数、校验位数、停止位数(起始位是必须的,故不考虑设置参数),由于计算波特率对应是时钟个数时还需要知道系统时钟频率,所以增加一个系统时钟频率参数。
所以parameter就定义波特率BPS、时钟频率FCLK、数据位数DATA_W、校验位数CHECK_W 、停止位数STOP_W 。而localparam需要通过parameter定义的参数得到波特率对应的 时钟数BPS_CNT=时钟频率FCLK/波特率BPS ,计数器data_num需要计数到BPS_CNT,所以需要通过BPS_CNT计算出计数器data_num的位宽BPS_CNT_W,可以通过以下函数实现。
function integer clogb2(input integer depth);begin if(depth==0) clogb2 = 1; else if(depth!=0) for(clogb2=0;depth>0;clogb2=clogb2+1) depth=depth>>1; end endfunction
接下来就是cnt计数器的结束条件了,可以由localparam定义CNT_NUM=DATA_W + CHECK_W + STOP_W。在利用上面函数计算出该计数器的位宽CNT_NUM_W就行了,内部信号根据这些常量变化即可。
由此设计的模块在例化时,只需要修改parameter的几个常量即可,不要对模块内部代码做任何处理,这部分操作不会占用额外资源,在综合工具对齐进行综合时就会处理,不会消耗FPGA的除法器之类的资源。
根据以上分析,直接得到以下代码,基本上不需要仿真调试。
04 参考代码
//--############################################################################################### //--# Designer : 发送一位数据所需系统时钟数计算方式BPS_CNT = 1000_000_000/(Tclk*比特率), //Tclk是系统时钟周期,单位ns。 //--############################################################################################### module uart_rx #( parameter FCLK = 50_000_000 ,//系统时钟频率,默认50MHZ; parameter BPS = 9600 ,//串口波特率; parameter DATA_W = 8 ,//接收数据位数以及输出数据位宽; parameter CHECK_W = 0 ,//校验位,0代表无校验位; parameter STOP_W = 1 //1位停止位; )( input clk ,//系统工作时钟50MHZ input rst_n ,//系统复位信号,低电平有效 input uart_rx ,//UART接口输入信号 output reg [DATA_W-1:0] rx_out ,//数据输出信号 output reg rx_out_vld //数据有效指示信号 ); localparam BPS_CNT = FCLK/BPS;//波特率为9600bit/s,当波特率为115200bit/s时,DATA_115200==434; localparam BPS_CNT_W = clogb2(BPS_CNT-1);//根据BPS_CNT调用函数自动计算计数器data_num位宽; localparam CNT_NUM = DATA_W + CHECK_W + STOP_W;//计数器计数值; localparam CNT_NUM_W = clogb2(CNT_NUM);//根据计数器cnt的值,利用函数自动计算此计数器的位宽; reg rx_vld ;//表示接收完一组串口发来的数据了; reg uart_rx_ff0 ; reg uart_rx_ff1 ; reg uart_rx_ff2 ; reg flag ; reg [BPS_CNT_W-1:0] data_num ; reg [CNT_NUM_W-1:0] cnt ; reg [DATA_W-1:0] rx_data ; wire add_data_num ; wire end_data_num ; wire add_cnt ; wire end_cnt ; /******************注释开始****************** 自动计算信号位宽; ******************注释结束******************/ function integer clogb2(input integer depth);begin if(depth==0) clogb2 = 1; else if(depth!=0) for(clogb2=0;depth>0;clogb2=clogb2+1) depth=depth>>1; end endfunction /******************注释开始****************** 接收一位数据所用时间计数器data_num,初始值为0,当接收到数据时进行计数, 当一位数据接收完成时清零; ******************注释结束******************/ always@(posedge clk or negedge rst_n)begin if(!rst_n)begin data_num <= {{BPS_CNT_W}{1'b0}}; end else if(add_data_num)begin if(end_data_num) data_num <= {{BPS_CNT_W}{1'b0}}; else data_num <= data_num + {{{BPS_CNT_W-1}{1'b0}},1'b1}; end end assign add_data_num = flag; assign end_data_num = add_data_num && data_num==BPS_CNT-1; //接受一组数据所用时间; always@(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt <= {{CNT_NUM_W}{1'b0}}; end else if(add_cnt)begin if(end_cnt) cnt <= {{CNT_NUM_W}{1'b0}}; else cnt <= cnt + {{{CNT_NUM_W-1}{1'b0}},1'b1}; end end assign add_cnt = end_data_num; assign end_cnt = add_cnt && cnt== CNT_NUM-1; /******************注释开始****************** PC端相对应于FPGA为异步接口,为预防亚稳态产生,对接收数据进行打两拍处理,由于需要采集信号下降沿,故打三拍处理; ******************注释结束******************/ always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin//三个寄存器组成移位寄存器,初始化为0; {uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= 3'd0; end else begin//时钟上升沿时,将uart_rx信号移入移位寄存器,其余位左移一位; {uart_rx_ff2,uart_rx_ff1,uart_rx_ff0} <= {uart_rx_ff1,uart_rx_ff0,uart_rx}; end end always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin flag <= 1'b0; end else if(uart_rx_ff2==1 && uart_rx_ff1==0)begin//取UART_RX信号下降沿 flag <= 1'b1; end else if(end_cnt)begin//一组数据接收完毕; flag <= 1'b0; end end //在中间时刻对输入数据进行采集,并且将数据存入rx_data; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_data <= {{DATA_W}{1'b0}}; end else if(cnt>=1 && cnt<=DATA_W && add_data_num && data_num==BPS_CNT/2-1)begin rx_data[cnt-1] <= uart_rx_ff2; end end //在接收完数据后,指示产生rx_data信号有效; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_vld <= 1'b0; end else begin rx_vld <= (cnt==CNT_NUM-1 && add_data_num && data_num==BPS_CNT/2-1); end end //当接收完一组数据后,将接收到的数据经过一组触发器暂存后输出; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin// rx_out <= 0; end else if(rx_vld)begin rx_out <= rx_data; end end //在接收完数据后,拉高一个时钟,指示产生rx_out信号有效; always@(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin rx_out_vld <= 1'b0; end else begin rx_out_vld <= rx_vld; end end endmodule05 modelism仿真
仿真部分的代码,通过一个任务task实现串口数据的发送,由于上述设计不支持校验位,所以这个模块设置校验位也是没有用的。
发送数据只需要调用tx();任务即可,内部直接输入待发送数据,数据位宽依旧通过DATA_W设置,波特率BPS设置。
参考代码:
`timescale 1 ns/1 ns module uart_rx_test(); parameter CYCLE = 20;//The unit is ns. The default value is 10ns; parameter RST_TIME = 10;//Reset time: Reset 3 clock widths by default; parameter STOP_TIME = 1000;//Time for simulation running after reset (unit: clock cycle). Simulation stops after 1000 clocks are run by default; // uart_rx Parameters parameter FCLK = 50_000_000;//系统时钟频率; parameter BPS = 9600 ;//串口波特率; parameter BPS_CNT = FCLK/BPS ;//波特率对应时钟数,不用手动修改该参数; parameter DATA_W = 8 ;//接收数据位数以及输出数据位宽; parameter CHECK_W = 2'b01 ;//校验位,2'b00代表无校验位,2'b01表示奇校验,2'b10表示偶校验,2'b11无效。 parameter STOP_W = 2'b11 ;//停止位,2'b01表示1位停止位,2'b10表示2位停止位,2'b11表示1.5位停止位; // uart_rx Inputs reg clk ; reg rst_n ; reg uart_tx ; // uart_rx Outputs wire [DATA_W-1:0] rx_out ; wire rx_out_vld ; //例化串口接收模块; uart_rx #( .FCLK (FCLK ), .BPS (BPS ), .DATA_W (DATA_W ), .CHECK_W (CHECK_W ), .STOP_W (STOP_W )) u_uart_rx ( .clk ( clk ), .rst_n ( rst_n ), .uart_rx ( uart_tx ), .rx_out ( rx_out ), .rx_out_vld ( rx_out_vld ) ); //The local clock is generated at 100 MB; initial begin clk = 0; forever #(CYCLE/2) clk=~clk; end //Generate reset signal; initial begin rst_n = 1; #2; rst_n = 0; #(RST_TIME*CYCLE);//复位完成; rst_n = 1; end //Input signal din assignment method; initial begin #1;uart_tx = 1; //初始化时输入高电平; #(100*CYCLE); //Start assigning values; tx(8'ha5); //以串口形式发送8'h5a; #(500*CYCLE); //发送完成后延迟500个时钟; tx(8'h5a); //之后发送数据8'h59; #(500*CYCLE); //发送完成后延迟500个时钟; $stop; //Stop simulation; end //模拟串口发送函数,1位起始位,1位停止位,无校验位,8位数据,先发低位; integer i;//用于控制循环次数; task tx( input [DATA_W-1:0] data //串口待发送数据; ); begin @(posedge clk);//延迟一个时钟后发送起始位; #1; uart_tx = 1'b0; repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟; for(i=0 ; i<8 ; i=i+1)begin #1; uart_tx = data[i]; repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟; end if(CHECK_W == 2'b01)begin #1;uart_tx = ~(^data);//奇校验时,发送数据; repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟; end else if(CHECK_W == 2'b10)begin #1;uart_tx = (^data);//偶校验时,发送数据; repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟; end @(posedge clk);//延迟一个时钟后发送停止位; #1; uart_tx = 1'b1; if(STOP_W == 2'b01)//1位停止位; repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟; else if(STOP_W == 2'b10)//2位停止位; repeat(2*BPS_CNT) @(posedge clk);//延迟2*BPS_CNT个时钟; else if(STOP_W == 2'b11)//1.5位停止位; repeat(BPS_CNT*3/2) @(posedge clk);//延迟1.5*BPS_CNT个时钟; end endtask endmodule
仿真运行结果(rx_out先接收到8’ha5,后接收到8’h5a):
图7 仿真结果
查看细节:开始接收数据(起始位)片段:
图8 起始位仿真
接收最低位数据仿真如下:
图9 接收第一位数据
接收最后一位数据,并且产生输出有效指示信号,下一个时钟将数据输出,此时串口传输实际上并没有完成,最后一位数据才传输一半(data_num计数器才2603==5208/2-1),但已经接收到完整数据,所以直接输出,节省时间,但flag信号依旧位高电平,表示该模块还处于工作状态。
图10 接收完最后一位数据
计数器data_num计数到5208-1并且计数器cnt计数器到8,表示一次传输完成,flag信号拉低,并且两个计数器清零,表示完成传输,仿真如下:
图11 接收完停止位
06 综合测试
这个工程很久了,之前学的时候使用quartus综合的,综合效果如下所示:
图12 quartus综合工程
对应的RTL模块视图(由于时钟频率FCLK和波特率BPS参数设置会影响计数器cnt和data_num的位宽,所以不同数据汇总和出不同的电路,下图为时钟频率50MHZ,波特率9600的RTL视图):
图13 RTL视图
对系统时钟频率进行约束后,最大时钟频率为120.86MHZ,远大于实际的50MHZ,满足时序要求;
图14 系统最大工作时钟频率
signal tap II 测试
将程序下载到FPGA,打开串口调试助手,设置波特率9600,发送数据0XA5,使用signal tap II抓取数据8'hA5。
图15 串口助手发送数据
串口调试助手发送数据0XB3,使用signal tap II抓取数据8'hB3。
图16 signal tap接收串口助手发送数据
串口调试助手发送数据0X5a,使用signal tap II抓取数据8'h5A。
图17 调试
07 总结
其实最主要的就是能够根据协议找到合适的主架构,然后根据该架构去产生输出信号。
本文就利用两个计数器作为主架构,根据计数器的状态生成输出信号,切记我们需要的并不是计数器,而是计数器生成的输出信号,如果使用parameter要考虑模块内部各种会改变的数据与这些参数的关系。
最好不要留需要手动修改的数据,这种数据如果忘记修改,会对后续设计造成很大影响,浪费调试时间。
来源: 本文转载自数字站公众号
全部0条评论
快来发表一下你的评论吧 !