首先了解UART通用异步收发传输器(Universal Asynchronous Receiver/ Transmitter), UART是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。它包括了RS232,RS499、RS423和RS485等接口标准规范和总线标准。
了解以下两张图即可:
串口主要表现在以下几个方面:
设计并实现基于串口RS232的数据收发模块。使用收发模块,完成串口数据回环实验。
我们先设计串口接收模块,该模块的功能是接收通过PC机上的串口调试助手发送的固定波特率的数据,串口接收模块按照串口的协议准确接收串行数据,解析提取有用数据后需将其转化为并行数据,因为并行数据在FPGA内部传输的效率更高,转化为并行数据后同时产生一个数据有效信号伴随着并行的有效数据一同输出。
注:为什么还需要输出一个伴随并行数据有效的标志信号,这是因为后级模块或系统在使用该并行数据的时候可能无法知道该时刻采样的数据是不是稳定有效的。而数据有效标志信号的到来就说明数据在该时刻是稳定有效的,起到一个指示作用。当数据有效标志信号为高时,该并行数据就可以被后级模块或系统使用了。
模块框图
我们将串口接收模块取名为uart_rx ,根据功能简介我们对整个设计要求有了大致的了解,其中设计的关键点是如何将串行数据转化为并行数据,也就是如何正确接收串行数据的问题。PC机通过串口调试助手发过来的信号没有时钟,所以FPGA在接收数据的时候要约定好一个固定的波特率,一个比特一个比特地接收数据,我们选择的波特率为9600bps。
整个模块肯定要用到时序逻辑,所以先设计好时钟sys_clk 和复位 sys_rst_n 两个输入信号,其次是相对于FPGA的rx端接收PC机通过串口调试助手发送过来的1bit输入信号。输出信号一个是FPGA的rx端接收到的数据转换成的8bit并行数据po_data,另一个是8bit并行数据有效的标志信号 po_data_flag
根据上面的分析设计出的visio框图,如下图所示
波形设计
如果所示,我们先把实现uart_rx 功能整体的波形图列出,然后再详细介绍下面的波形是如何一步一步设计实现的。
波形设计思路详细解析
第一部分:首先画出三个输入信号,必不可少的两个输入信号是时钟和复位,另一个是串行输入数据rx,如下图所示,我们发现rx串行数据一开始直接打了两拍,就是经过了两级寄存器,理论上我们应该按照串口接收数据的时序要求找到rx的下降沿,然后开始接收起始位的数据,但为什么先将数据打了两拍呢?那就要从跨时钟域会导致“亚稳态”的问题上说起。
一个小的现象来理解这个问题,当你使用示波器把一个矩形脉冲的上升沿或下降沿放大后会发现其上升沿和下降沿并不是瞬间被拉高或拉低的,而是有一个倾斜变化的过程,这在运放中被称为“压摆率”,如果FPGA的系统时钟刚好次啊寄到rx信号上升沿或下降沿的中间位置附近(按照概率来讲,如果数据传输量足够大或传输速度足够快时一定会产生这种情况),即FPGA在接收rx数据时不满足内部寄存器的建立时间Tsu(指触发器的时钟信号上升沿到来以前,数据稳定不变的最小时间)和保持时间Th(指触发器的时钟信号上升沿到来以后,数据稳定不变的最小时间),此时FPGA的第一级寄存器的输出端在时钟沿到来之后比较长的一段时间内都处于不确定的状态,在0和1之间处于振荡状态,而不是等于串口输入的确定的rx值。
如下图所示产生的亚稳态的波形示意图,rx信号经过FPGA中的第一级寄存器后输出的rx_reg1信号在时钟上升沿Tco事件后会有Tmet(决断时间)的振荡时段,当第一个寄存器发生亚稳态后,经过Tmet的振荡稳定后,第二级寄存器就能采集到一个相对稳定的值。但由于振荡时间Tmet是受到很多因素影响的,所以Tmet时间有长有短。如图所示,当Tmet1时间长到大于一个采样周期,那第二级寄存器就会采集到亚稳态,但是从第二级寄存器输出的信号就是相对稳定的了。当然会有人问到第二级寄存器Tmet2的持续时间会不会继续延长到大于一个采样周期?这种情况虽然会存在,但是概率非常小,寄存器本身就有减小Tmet时间让数据快速稳定的作用!
由于PC机中波特率和rx信号是同步的,而rx信号和FPGA的系统时钟sys_clk 是异步的关系,我们此时要做的是将慢速时钟域(PC机中的波特率)系统中的rx信号同步到快速时钟域(FPGA中的sys_clk)系统中,所使用的方法叫电平同步,俗称“打两拍”。所以rx信号进入FPGA后会首先经过一级寄存器,如上图所示的亚稳态现象,导致rx_reg1 信号的状态不确定是0还是1,就会收到影响使其他相关信号做出不同的判断,有的判断0,有的判断1,有的也进入了亚稳态产生连锁反应,导致后级相关逻辑电路混乱。为了避免这种情况,rx信号进来后首先进行打一拍的处理,打一拍后产生rx_reg1信号。但rx_reg1 可能还存在低概率的亚稳态现象,为了进一步降低出现亚稳态的概率,我们将从rx_reg1 信号再打一拍后产生rx_reg2 信号,使之能够较大概率保证rx_reg2 信号是0或者1 中的一种确定情况,这样rx_reg2 所影响的后级电路就都是相对稳定的了。但是大家一定要注意:打两拍后虽然能让信号稳定到0或者1中确定的值,但究竟是0还是1却是随机的。与打拍之前输入信号的值没有必然的关系。
注:单比特信号从慢速时钟域同步到快速时钟域需要使用打两拍的方式消除亚稳态。第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为70%~80%左右,第二级寄存器可以稳定输出的概率为99%左右,后面再多加寄存器的级数改善效果就不明显了,所以数据进来后一般选择打两拍即可。
另外单比特信号从快速时钟域同步到慢速时钟域还仅仅使用打两拍的方式会漏采数据,所以往往使用脉冲同步法或握手信号法:而多比特信号跨时钟域需要进行格雷编码(多比特顺序数才可以)后才能进行打两拍的处理,或者通过使用FIFO、RAM来处理数据与时钟同步的问题。
第二部分:由上面的分析,我们知道了为什么rx信号进入到FPGA后需要先打两拍的原因,打两拍后的rx_reg2 信号就是我们可以在后级逻辑电路中使用的相对稳定的信号,只比rx信号延后两拍。下一步我们就可以根据串口接收数据的时序要求找到串口帧起始开始的标志下降沿。然后按顺序接收数据,在触摸按键章节我们分析如何产生上升沿和下降沿标志,这里我们可以直接使用。由第一部分的分析得 rx_reg1 信号可能是不稳定的,而rx_reg2 信号是相对稳定的,所以不能直接用rx_reg1信号和 rx_reg2信号来产生下降沿标志信号,因为rx_reg1 信号的不稳定性可能会导致由它产生得下降沿标志信号也不稳定。所以如图所示,我们将rx_reg2 信号再打一拍,得到rx reg3信号,用rx reg2 信号和rx reg3 信号产生state nedge作为下降沿标志信号。
第三部分:我们检测到了第一个下降沿,后面的信号将以下降沿标志信号 start nedge 为条件开始接收一帧10bit的数据,但新的问题又出现了,我们的rx信号本身就是1bit的,如果在判断第一个下降沿后,后面帧中的数据还可能会有下降沿出现,那我们会又产生一个start nedge 标志信号,这样就出现了误判断,那我们该如何避免这种情况呢?这是一个值得考虑的问题,在不知道答案之前我们可以发挥自己的想象并尝试使用各种方法来解决这个问题。我们知道在verilog代码中标志信号 flag 和 使能信号 en 都是非常有用的,标志信号只有一拍,非常适合我们产生下降沿标志这种信号,而使能信号就特别适合在此处使用,即对一段时间区域进行控制锁定。如下图所示,当下降沿标志信号 start nedge 为高电平时拉高工作使能信号 work en (什么时候拉低在后面讲解),在work en信号为高的时间区域内虽然也会有下降沿 start nedge 标志信号产生,但是我们可以根据work en 信号就可以判断出此时出现的start nedge 标志信号并不是我们想要的串口帧起始下降沿,从而将其过滤除掉。
解决了这个问题之后,我们正式开始接收一帧数据。我们使用的是9600bps的波特率和PC机及进行串口通信,PC机的串口调试助手要将发送数据波特率调整为 9600 bps。而FPGA内部使用的系统时钟是50MHz,前面也进行过计算,得出1bit需要的时间约为 5208个系统时钟周期,那么我们就需要产生一个能计5208个数的计数器来依次接收10个比特的数据,计数器每计5208个数就接收一个新比特的数据。如下图所示,计数器名为 baud_cnt ,当work en 信号为高电平的时候就让计数器计数,当计数器计5208个数或 work en 信号为低电平时计数器清零。
第四部分:现在我们可以根据波特率计数器一个一个接收数据了,我们发现baud cnt计数器在计数值为0-5207之间都是数据有效的时刻,那我们该什么时候取数据呢?理论上讲,在数据变化的地方取数时不稳定的,所以我们选择当baud cnt计数器计数到2063,即中间的位置时取数最稳定(其实只要baud cnt计数器在计数值不是在0和5207这两个最不稳定的时候取数都可以,更为准确的是多次取值取概率最大的情况)。所以如图所示,在baud cnt计数器技术到中点时产生一个时钟周期的 bit flag的取数标志信号,用于指示该时刻的数据可以被取走。
讲到这里我们不要忘记第三部分遗留的问题,那就是work en 信号何时拉低。如图所示,我们当bit cnt计数器技术到8且1处 bit flag 取数标志信号何时为高,说明我们已经接收到了所有的8bit 有用数据,这两个条件必须同时满足时才能让work en信号拉低。如果仅仅把bit cnt 计数器的计数值技术到8作为work en信号拉低的条件,er1 处的bit flag取数标志信号为高这个条件,就会使work en在绿线虚线位置处拉低,导致最后1bit数据丢失,致使后面接收的帧出错甚至接收不到数据。
第五部分:我们接收到的rx信号使串行的,后面的系统要使用的使完整的8bit并行数据。也就是说我们还需要将1bit串行数据转换为8bit 并行数据的串并转换的工作,这也是我们在接口设计中常遇到的一种操作。串并转换就需要做移位,我们要考虑清楚什么时候开始移位,不能提前也不能推后,否则会将无用的数据也移位进来,所以我们需要卡准时间。如下图所示PC机的串口调试助手发送的数据使先发送的低位后发送的高位,所以我们接受的rx信号也是先接受的低位后接收的高位,我们采用边接收边移位的操作。移位操作的方法我们已经在前面中讲过。接下来我们需要确定移位开始和结束的时间。如下图所示,当bit cnt 计数器的计数值为1时说明第一个有用数据已经接收到了,刚好剔除了起始位,就可以进行移位了。注意移位的条件,要在bit cnt 计数器的计数值为1 到8 区间内且bit flag取数标志信号同时为高时才能移位,也就是移动7次即可,接收最后1bit有用数据时就不需要在进行移位了。当移位7次后1bit的串行数据已经变为8bit的并行数据了,此时产生一个移位完成标志信号rx flag。
第六部分:rx data 信号是参与移位的数据,在移位的过程中数据是变动的,不可以被后级模块所使用,而可以肯定的是在移位完成标志信号 rx flag 为高时,rx data 信号一定是移位完成的稳定的8bit 有用数据。如下图所示,此时我们当移位完成标志信号 rx flag为高时让rx data信号赋值给专门用于输出稳定8bit 有用数据的 po data 信号就可以了,但rx flag 信号又不能作为 po data信号有效的标志信号,所以需要将rx flag信号再打一拍。最后输出的有用8bit数据为po data 信号和伴随 po data 信号有效的标志信号 po flag 信号。到此为止我们 uart rx 模块的波形就全部设计好了。
// uart_rx.v
///
// Company:
//
// File: uart_rx.v
// File history:
// : :
// : :
// : :
//
// Description:
//
// uart rx
//
// Targeted device:
// Author: chen shuda
//
///
//`timescale /
module uart_rx
#(
parameter UART_BPS = 'd9600, // 串口波特率
parameter CLK_FREQ = 'd50_000_000 // 时钟频率
)
(
input wire sys_clk, // 系统时钟50Mhz
input wire sys_rst_n, // 全局复位
input wire rx, // 串口接收数据
output reg [7:0] po_data, // 串转并后的8bit数据
output reg po_flag // 串转并后的数据有效标志信号
);
// parameter and Internal signal
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS;
// reg define
reg rx_reg1;
reg rx_reg2;
reg rx_reg3;
reg start_nedge;
reg work_en;
reg [12:0] baud_cnt;
reg bit_flag;
reg [3:0] bit_cnt;
reg [7:0] rx_data;
reg rx_flag;
// main code
// 插入两级寄存器进行数据同步,用来消除亚稳态
// rx_reg1:第一级寄存器,寄存器空闲状态复位为1
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
rx_reg1 <= 1'b1;
else begin
rx_reg1 <= rx;
end
end
// rx_reg2: 第二级寄存器,寄存器空闲状态复位为1
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
rx_reg2 <= 1'b1;
else begin
rx_reg2 <= rx_reg1;
end
end
// rx_reg3: 第三级寄存器和第二级寄存器共同构成下降沿检测
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
rx_reg3 <= 1'b1;
else begin
rx_reg3 <= rx_reg2;
end
end
// start_nedge: 检测到下降沿时 start nedge 产生一个时钟的高电平
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
start_nedge <= 1'b0;
else if((~rx_reg2) && (rx_reg3))
start_nedge