基于FPGA的UART串口接收模块设计

描述

 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


    endmodule
05 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要考虑模块内部各种会改变的数据与这些参数的关系。

最好不要留需要手动修改的数据,这种数据如果忘记修改,会对后续设计造成很大影响,浪费调试时间。

来源: 本文转载自数字站公众号

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分