uart串口接收模块
1、UART(异步串行接口)
串行通信:指利用一条数据线将资料一位位的顺序传输。
异步通信:以一个字符为传输单位,通信中两个字符间的时间间隔是不固定的,然而在同一个字符的两个相邻位代码间的时间间隔是固定的。
通信协议:指通信双方约定的一些规则。在使用串口通信的时候,规定有:空闲位、起始位、数据位、奇偶校验位、停止位。
2、串口通信时序
这个协议在 FPGA 内部是除 SPI 之外最简单的接口吧,其实就是发送方与接收方相互认定的协议(暗号),这种接口数据一般是单向传输,所以发送方和接收方通信一般需要两根数据线。
数据线在没有数据传输时保持高电平,当需要传输数据时,发送方把数据线拉低一段时间,告诉接收方开始传输数据了。之后把数据从低位到高位或者高位到低位(这个根据通信双方的要求确定)依次发送给对方(数据的位数双方应该事先确认好,通常5~8位数据)。数据发送完,可能会发送一位奇偶校验(这部分在下一节构建完整UART协议时细说)。最后就是将数据线拉高一段时间表示数据传输结束。
在这之间就会有疑问,每位数据电平持续时间到底是多久?
这就引出波特率,通常就是说每秒能传输多少位数据,比如波特率为9600bit/s,就是指1秒传输9600位数据(当然这是包含起始位,校验位,停止位在内的,所以有效数据其实并没有这么多)。当使用该波特率时,那每个电平持续时间不就是1/9600秒么。
3、串口接收模块设计
首先确定模块接口信号,肯定有个串口的输入信号uart_rx吧,然后时钟信号clk和复位信号rst_n也是不可能少的。接收到数据后肯定要输出吧,所以在加一个uart_rx,注意该信号位宽应该是可以改变的(因为串口协议的数据位可以改变)。一般还要有一个信号用于指示接收到的数据什么时候是有效的,便于后续模块使用uart_rx,即uart_tx_vld(为高电平时,表示uart_rx有效)。
表1 端口信号
信号 | 输入输出 | 位宽 | 定义 |
---|---|---|---|
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来计数目前传输的第几位数据了。整体思路就是如此,大致如下图,接下来就是细节:
计数器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拉高。
上述将模块内部信号讲完了,如果要实现功能完全够了,但是在调用模块时,我们往往不习惯去改模块内部的参数,这就需要通过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的除法器之类的资源。
根据以上分析,直接得到以下代码,基本上不需要仿真调试。
4、参考代码
//--###############################################################################################
//--# Designer : 发送一位数据所需系统时钟数计算方式BPS_CNT = 1000_000_000/(Tclk*比特率),
//Tclk是系统时钟周期,单位ns。
//--###############################################################################################
module uart_rx #(
parameter FLCK = 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 = FLCK/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
endmodule
5、modelism仿真
仿真部分的代码,通过一个任务task实现串口数据的发送,由于上述设计不支持校验位,所以这个模块设置校验位也是没有用的,将在下篇文章里面加入校验位,以及1.5位停止位之类的数据位。
发送数据只需要调用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 = 0 ;//校验位,0代表无校验位;
parameter STOP_W = 1 ;//1位停止位;
// 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 #(
.FLCK (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;
$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
@(posedge clk);//延迟一个时钟后发送停止位;
#1; uart_tx = 1'b1;
repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;
end
endtask
endmodule
仿真运行结果(rx_out先接收到8’ha5,后接收到8’h5a):
查看细节:开始接收数据(起始位)片段:
接收最低位数据仿真如下:
接收最后一位数据,并且产生输出有效指示信号,下一个时钟将数据输出,此时串口传输实际上并没有完成,最后一位数据才传输一半(data_num计数器才2603==5208/2-1),但已经接收到完整数据,所以直接输出,节省时间,但flag信号依旧位高电平,表示该模块还处于工作状态。
计数器data_num计数到5208-1并且计数器cnt计数器到8,表示一次传输完成,flag信号拉低,并且两个计数器清零,表示完成传输,仿真如下:
6、综合测试
这个工程很久了,之前学的时候使用quartus综合的,综合效果如下所示:
对应的RTL模块视图(由于时钟频率FCLK和波特率BPS参数设置会影响计数器cnt和data_num的位宽,所以不同数据汇总和出不同的电路,下图为时钟频率50MHZ,波特率9600的RTL视图):
对系统时钟频率进行约束后,最大时钟频率为120.86MHZ,远大于实际的50MHZ,满足时序要求;
sigtap II 测试
将程序下载到FPGA,打开串口调试助手,设置波特率9600,发送数据0XA5,使用signal tap II抓取数据8’hA5。
串口调试助手发送数据0XB3,使用signal tap II抓取数据8’hB3。
串口调试助手发送数据0X5a,使用signal tap II抓取数据8’h5A。
7、总结
其实最主要的就是能够根据协议找到合适的主架构,然后根据该架构去产生输出信号。本文就利用两个计数器作为主架构,根据计数器的状态生成输出信号,切记我们需要的并不是计数器,而是计数器生成的输出信号,如果使用parameter要考虑模块内部各种会改变的数据与这些参数的关系,最好不要留需要手动修改的数据,这种数据如果忘记修改,会对后续设计造成很大影响,浪费调试时间。
本模块还需要完善奇偶校验以及停止位,放在下文处理,需要原工程的自取。
工程链接:https://pan.baidu.com/s/1oMLYvioXl496p9KH3rFy4w
提取码:98qn
时序图采用TimeGen所画,如果需要该软件,后台回复即可。