【FPGA协议篇】UART通信及其verilog实现(代码采用传参实现模块通用性,适用于快速开发)

UART通信

  • UART通信简介
  • verilog实现
    • 顶层模块
    • 接收模块
    • 发送模块
  • 仿真波形
  • 实测结果

UART通信简介

​ 即通用异步收发器(Universal Asynchronous Receiver/Transmitter),是一种串行、异步、全双工的通信协议。特点是通信线路简单,适用于远距离通信,但传输速度慢。

数据传输速率:波特率(单位:baud,波特)

常见波特率有:1200、2400、4800、19200、38400、57600等,最常用的是9600和115200。

数据通信格式如下:包含一个起始位、n个数据位(通常为8位,即一个字节)、1个校验位、1个结束位

【FPGA协议篇】UART通信及其verilog实现(代码采用传参实现模块通用性,适用于快速开发)_第1张图片

其中各位的意义如下:

空闲位:

UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平,表示当前线路上没有数据传输。

起始位:

每开始一次通信时发送方先发出一个逻辑”0”的信号(低电平),表示传输字符的开始。因为总线空闲时为高电平所以开始一次通信时先发送一个明显区别于空闲状态的信号即低电平。

数据位:

起始位之后就是我们所要传输的数据,数据位可以是5、6、7、8,9位等,构成一个字符(一般都是8位)。如ASCII码(7位),扩展BCD码(8位)。先发送最低位,最后发送最高位,使用低电平表示‘0’高电平表示‘1’完成数据位的传输。

奇偶校验位:

数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。校验位其实是调整个数,串口校验分几种方式:

  • 1、无校验(no parity)。
  • 2、奇校验(odd parity):如果数据位中“1”的数目是偶数,则校验位为“1”,如果“1”的数目是奇数,校验位为“0”。
  • 3、偶校验(even parity):如果数据为中“1”的数目是偶数,则校验位为“0”,如果为奇数,校验位为“1”。
  • 4、mark parity:校验位始终为1(不常用)。
  • 5、parity:校验位始终为0(不常用)。

停止位:

它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备之间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟的机会。停止位个数越多,数据传输越稳定,但是数据传输速度也越慢。

verilog实现

本次UART工程实现采用环路测试,为了达到通用模块的标准,采用参数传递的形式来实现工程,可修改的参数有:

1.时钟频率。对应不同的开发平台,其芯片的工作频率不同,可直接通过修改参数CLK_FRE来实现频率匹配,参数缺省为50MHz。
2. UART传输数据长度。UART的数据长度可修改,通常采用byte的形式,即8位数据长度,缺省也为8位。
3. 奇偶校验位。通过PARITY_ON的参数传递实现是否需要奇偶校验的功能,缺省为不需要奇偶校验。
4. 奇偶校验类型。可选择为奇校验或偶校验,缺省为奇校验。
5. 波特率。波特率类型可自行调整,一般常用为9600,115200。

亲测可用。既可用于学习,也可用于快速开发。废话完了,开始正文……

顶层模块

顶层模块直接调用uart_rx和uart_tx两个模块,前者实现接收数据,后者实现发送数据,从而形成环路。代码(附注释)如下:

module uart_loop(
    input i_clk_sys,
    input i_rst_n,
    input i_uart_rx,
    output o_uart_tx,
    output o_ld_parity
    );
    
    localparam DATA_WIDTH = 8;
    localparam BAUD_RATE = 9600;
    localparam PARITY_ON = 1;
    localparam PARITY_TYPE = 1;
    
    wire w_rx_done;
    wire[DATA_WIDTH-1 : 0] w_data;
    
    
    uart_rx 
    #(
            .CLK_FRE(50),         //时钟频率,默认时钟频率为50MHz
            .DATA_WIDTH(DATA_WIDTH),       //有效数据位,缺省为8位
            .PARITY_ON(PARITY_ON),        //校验位,1为有校验位,0为无校验位,缺省为0
            .PARITY_TYPE(PARITY_TYPE),      //校验类型,1为奇校验,0为偶校验,缺省为偶校验
            .BAUD_RATE(BAUD_RATE)      //波特率,缺省为9600
    ) u_uart_rx
    (
        .i_clk_sys(i_clk_sys),      //系统时钟
        .i_rst_n(i_rst_n),        //全局异步复位,低电平有效
        .i_uart_rx(i_uart_rx),      //UART输入
        .o_uart_data(w_data),    //UART接收数据
        .o_ld_parity(o_ld_parity),    //校验位检验LED,高电平位为校验正确
        .o_rx_done(w_rx_done)       //UART数据接收完成标志
    );
    
    uart_tx
    #(
        .CLK_FRE(50),         //时钟频率,默认时钟频率为50MHz
        .DATA_WIDTH(DATA_WIDTH),       //有效数据位,缺省为8位
        .PARITY_ON(PARITY_ON),        //校验位,1为有校验位,0为无校验位,缺省为0
        .PARITY_TYPE(PARITY_TYPE),      //校验类型,1为奇校验,0为偶校验,缺省为偶校验
        .BAUD_RATE(BAUD_RATE)      //波特率,缺省为9600
    ) u_uart_tx
    (   .i_clk_sys(i_clk_sys),      //系统时钟
        .i_rst_n(i_rst_n),        //全局异步复位
        .i_data_tx(w_data),      //传输数据输入
        .i_data_valid(w_rx_done),   //传输数据有效
        .o_uart_tx(o_uart_tx)       //UART输出
        );
    
endmodule

接收模块

接收模块采用三段式状态机的形式编写,附带基本注释说明。

module uart_rx
#(
    parameter CLK_FRE = 50,         //时钟频率,默认时钟频率为50MHz
    parameter DATA_WIDTH = 8,       //有效数据位,缺省为8位
    parameter PARITY_ON = 0,        //校验位,1为有校验位,0为无校验位,缺省为0
    parameter PARITY_TYPE = 0,      //校验类型,1为奇校验,0为偶校验,缺省为偶校验
    parameter BAUD_RATE = 9600      //波特率,缺省为9600
)
(
    input                           i_clk_sys,      //系统时钟
    input                           i_rst_n,        //全局异步复位,低电平有效
    input                           i_uart_rx,      //UART输入
    output reg[DATA_WIDTH-1 :0]     o_uart_data,    //UART接收数据
    output reg                      o_ld_parity,    //校验位检验LED,高电平位为校验正确
    output reg                      o_rx_done       //UART数据接收完成标志
    );
    
    /*
    UART输入是异步输入,最好是同步到FPGA内部的时钟域
    可以省略,但在异步电路中,保持时钟域同步是一个良好习惯
    */
    reg sync_uart_rx;
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                sync_uart_rx <= 1'b1;
            else
                sync_uart_rx <= i_uart_rx;
        end
    
    /*
    连续采样五个接收路电平来判断rx是否有信号传来
    用五个采样信号来作为判断标准可以有效排除毛刺噪声带来的误判
    */
    reg [4:0] r_flag_rcv_start;
    wire w_rcv_start;
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                r_flag_rcv_start <= 5'b11111;
            else
                r_flag_rcv_start <= {r_flag_rcv_start[3:0], sync_uart_rx};
        end
    
    
    //状态机定义
    reg [2:0] r_current_state;  //当前状态
    reg [2:0] r_next_state;     //次态
        
    localparam STATE_IDLE = 3'b000;         //空闲状态
    localparam STATE_START = 3'b001;        //开始状态
    localparam STATE_DATA = 3'b011;         //数据接收状态
    localparam STATE_PARITY = 3'b100;       //数据校验状态
    localparam STATE_END = 3'b101;          //结束状态
    
    
    localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;   //波特计数周期
    
    reg baud_valid;                         //波特计数有效位
    reg [15:0] baud_cnt;                    //波特率计数器 
    reg baud_pulse;                         //波特率采样脉冲
    
    reg [3:0]   r_rcv_cnt;      //接收数据位计数
    
    //波特率计数器
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                baud_cnt <= 16'h0000;
            else if(!baud_valid)
                baud_cnt <= 16'h0000;
            else if(baud_cnt == CYCLE - 1)
                baud_cnt <= 16'h0000;
            else
                baud_cnt <= baud_cnt + 1'b1;
        end
        
    //波特采样脉冲
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                baud_pulse <= 1'b0;
            else if(baud_cnt == CYCLE/2-1)
                baud_pulse <= 1'b1;
            else
                baud_pulse <= 1'b0;
        end
    
    //状态机状态变化定义
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                r_current_state <= STATE_IDLE;
            else if(!baud_valid)
                r_current_state <= STATE_IDLE;
            else if(baud_valid && baud_cnt == 16'h0000)
                r_current_state <= r_next_state;
        end
    
    //状态机次态定义
    always@(*)
        begin
            case(r_current_state)
                STATE_IDLE:     r_next_state <= STATE_START;
                STATE_START:    r_next_state <= STATE_DATA;
                STATE_DATA:
                    if(r_rcv_cnt == DATA_WIDTH)
                        begin
                            if(PARITY_ON == 0)
                                r_next_state <= STATE_END;
                            else
                                r_next_state <= STATE_PARITY;       //校验位开启时进入校验状态
                        end
                    else
                        begin
                                r_next_state <= STATE_DATA;
                        end
                STATE_PARITY:   r_next_state <= STATE_END;
                STATE_END:      r_next_state <= STATE_IDLE;
                default:;
            endcase
        end
    
    
    reg[DATA_WIDTH - 1 :0] r_data_rcv;
    reg r_parity_check;
    
    //状态机输出逻辑
    always@(posedge i_clk_sys or negedge i_rst_n)
        begin
            if(!i_rst_n)
                begin
                    baud_valid <= 1'b0;
                    r_data_rcv <= 'd0;
                    r_rcv_cnt <= 4'd0;
                    r_parity_check <= 1'b0;
                    o_uart_data <= 'd0;
                    o_ld_parity <= 1'b0;
                    o_rx_done <= 1'b0;
                end
            else
                case(r_current_state)
                    STATE_IDLE:begin
                            //闲置状态下对寄存器进行复位
                            r_rcv_cnt <= 4'd0;
                            r_data_rcv <= 'd0;
                            r_parity_check <= 1'b0;
                            o_rx_done <= 1'b0;
                            //连续检测到低电平时认为UART传来数据,拉高baud_valid
                            if(r_flag_rcv_start == 5'b00000)
                                baud_valid <= 1'b1;
                        end
                    STATE_START:begin
                            if(baud_pulse && sync_uart_rx)     //波特率采样脉冲到来时再次检测是否为低电平,如果不为低电平,认为前期误检测,重新进入IDLE状态
                                baud_valid <= 1'b0;
                        end
                    STATE_DATA:begin
                            if(baud_pulse)
                                begin
                                    r_data_rcv <= {sync_uart_rx, r_data_rcv[DATA_WIDTH-1 : 1]}; //数据移位存储
                                    r_rcv_cnt <= r_rcv_cnt + 1'b1;                          //数据位计数
                                    r_parity_check <= r_parity_check + sync_uart_rx;        //校验位做加法验证高电平的奇偶
                                end
                        end
                    STATE_PARITY:begin
                            if(baud_pulse)
                                begin
                                //校验检测,正确则o_ld_parity拉高,可输出给led检测,如果闪烁则表示有错误数据发生
                                    if(r_parity_check + sync_uart_rx == PARITY_TYPE)
                                        o_ld_parity <= 1'b1;
                                    else
                                        o_ld_parity <= 1'b0;
                                end
                            else
                                        o_ld_parity <= o_ld_parity;
                        end
                    STATE_END:begin
                            if(baud_pulse)
                                begin
                                //没有校验位或者校验位正确时才输出数据,否则直接丢弃数据
                                    if(PARITY_ON == 0 || o_ld_parity)
                                        begin
                                            o_uart_data <= r_data_rcv;
                                            o_rx_done <= 1'b1;
                                        end
                                end
                            else
                                begin
                                    o_rx_done <= 1'b0;
                                end
                            
                            if(baud_cnt == 16'h0000)
                                    baud_valid <= 1'b0;
                        end
                    default:;
                endcase
        end
    
    
endmodule

发送模块

发送模块和接收模块基本写法一致,只有细微的参数改动实现逆过程。重点区别在于三段状态机的状态机输出逻辑不同。

module uart_tx
#(
   parameter CLK_FRE = 50,         //时钟频率,默认时钟频率为50MHz
   parameter DATA_WIDTH = 8,       //有效数据位,缺省为8位
   parameter PARITY_ON = 0,        //校验位,1为有校验位,0为无校验位,缺省为0
   parameter PARITY_TYPE = 0,      //校验类型,1为奇校验,0为偶校验,缺省为偶校验
   parameter BAUD_RATE = 9600      //波特率,缺省为9600
)
(   input                       i_clk_sys,      //系统时钟
   input                       i_rst_n,        //全局异步复位
   input [DATA_WIDTH-1 : 0]    i_data_tx,      //传输数据输入
   input                       i_data_valid,   //传输数据有效
   output reg                  o_uart_tx       //UART输出
   );
   
       
   //状态机定义
           reg [2:0] r_current_state;  //当前状态
           reg [2:0] r_next_state;     //次态
               
           localparam STATE_IDLE = 3'b000;         //空闲状态
           localparam STATE_START = 3'b001;        //开始状态
           localparam STATE_DATA = 3'b011;         //数据发送状态
           localparam STATE_PARITY = 3'b100;       //数据校验计算和发送
           localparam STATE_END = 3'b101;          //结束状态
           
           
           localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;   //波特计数周期
           
           reg baud_valid;                         //波特计数有效位
           reg [15:0] baud_cnt;                    //波特率计数器 
           reg baud_pulse;                         //波特率采样脉冲
           
           reg [3:0]   r_tx_cnt;      //接收数据位计数
           
           //波特率计数器
           always@(posedge i_clk_sys or negedge i_rst_n)
               begin
                   if(!i_rst_n)
                       baud_cnt <= 16'h0000;
                   else if(!baud_valid)
                       baud_cnt <= 16'h0000;
                   else if(baud_cnt == CYCLE - 1)
                       baud_cnt <= 16'h0000;
                   else
                       baud_cnt <= baud_cnt + 1'b1;
               end
               
           //波特采样脉冲
           always@(posedge i_clk_sys or negedge i_rst_n)
               begin
                   if(!i_rst_n)
                       baud_pulse <= 1'b0;
                   else if(baud_cnt == CYCLE/2-1)
                       baud_pulse <= 1'b1;
                   else
                       baud_pulse <= 1'b0;
               end
           
           //状态机状态变化定义
           always@(posedge i_clk_sys or negedge i_rst_n)
               begin
                   if(!i_rst_n)
                       r_current_state <= STATE_IDLE;
                   else if(!baud_valid)
                       r_current_state <= STATE_IDLE;
                   else if(baud_valid && baud_cnt == 16'h0000)
                       r_current_state <= r_next_state;
               end
           
           //状态机次态定义
           always@(*)
               begin
                   case(r_current_state)
                       STATE_IDLE:     r_next_state <= STATE_START;
                       STATE_START:    r_next_state <= STATE_DATA;
                       STATE_DATA:
                           if(r_tx_cnt == DATA_WIDTH)
                               begin
                                   if(PARITY_ON == 0)
                                       r_next_state <= STATE_END;
                                   else
                                       r_next_state <= STATE_PARITY;       //校验位开启时进入校验状态
                               end
                           else
                               begin
                                       r_next_state <= STATE_DATA;
                               end
                       STATE_PARITY:   r_next_state <= STATE_END;
                       STATE_END:      r_next_state <= STATE_IDLE;
                       default:;
                   endcase
               end
   
   
   reg [DATA_WIDTH-1 : 0]      r_data_tx;
   reg                         r_parity_check;
   //状态机输出逻辑
   always@(posedge i_clk_sys or negedge i_rst_n)
       begin
           if(!i_rst_n)
               begin
                   baud_valid  <= 1'b0;
                   r_data_tx   <= 'd0;
                   o_uart_tx   <= 1'b1;
                   r_tx_cnt    <= 4'd0;
                   r_parity_check <= 1'b0;
               end
           else
               case(r_current_state)
                   STATE_IDLE:begin
                           o_uart_tx   <= 1'b1;
                           r_tx_cnt    <= 4'd0;
                           r_parity_check <= 4'd0;
                           if(i_data_valid)
                               begin
                                   baud_valid <= 1'b1;
                                   r_data_tx <= i_data_tx;
                               end
                       end
                   STATE_START:begin
                           if(baud_pulse)
                               o_uart_tx   <= 1'b0;
                       end
                   STATE_DATA:begin
                           if(baud_pulse)
                               begin
                                   r_tx_cnt <= r_tx_cnt + 1'b1;
                                   o_uart_tx <= r_data_tx[0];
                                   r_parity_check <= r_parity_check + r_data_tx[0];
                                   r_data_tx <= {1'b0 ,r_data_tx[DATA_WIDTH-1:1]};
                               end
                       end
                   STATE_PARITY:begin
                           if(baud_pulse)
                               begin
                                   if(PARITY_TYPE == 1)
                                       o_uart_tx <= r_parity_check;
                                   else
                                       o_uart_tx <= r_parity_check + 1'b1;
                               end
                       end
                   STATE_END:begin
                           if(baud_pulse)
                               begin
                                   o_uart_tx <= 1'b1;
                                   baud_valid <= 1'b0;
                               end
                       end
                   default:;
               endcase
       end

endmodule

以上便是整个工程的完整代码,至于FPGA的xdc约束,不同开发平台不同,这里就不贴出来了。

仿真波形

testbench的编写如下:

module uart_loop_tb(
    );
    
    reg clk_sys;
    reg rst_n;
    reg uart_in;
    wire uart_out;
    wire parity;
    
    uart_loop u_uart_loop(
        .i_clk_sys(clk_sys),
        .i_rst_n(rst_n),
        .i_uart_rx(uart_in),
        .o_uart_tx(uart_out),
        .o_ld_parity(parity)
        );
        
        
    initial begin
        clk_sys = 1'b0;
        rst_n = 1'b0;
        uart_in = 1'b1;
    end
    
    always #10 clk_sys = ~clk_sys;
    
    localparam ELEMENT_TIME = 104160;
    reg [7:0] DATA = 8'hAC;
    
    initial begin
        #100 rst_n = 1'b1;
        #20000;
        
        uart_in = 1'b0;
        #ELEMENT_TIME
        uart_in = DATA[0];
        #ELEMENT_TIME
        uart_in = DATA[1];
        #ELEMENT_TIME
        uart_in = DATA[2];
        #ELEMENT_TIME
        uart_in = DATA[3];
        #ELEMENT_TIME
        uart_in = DATA[4];
        #ELEMENT_TIME
        uart_in = DATA[5];
        #ELEMENT_TIME
        uart_in = DATA[6];
        #ELEMENT_TIME
        uart_in = DATA[7];
        #ELEMENT_TIME
        uart_in = 1'b1;
        #ELEMENT_TIME
        uart_in = 1'b1;
        
    end

endmodule

此处我们只进行一次数据传输过程的测试,读者可自行编写测试程序。
传输的字节内容为8’hAC即8’b10101100。
仿真波形如下:
【FPGA协议篇】UART通信及其verilog实现(代码采用传参实现模块通用性,适用于快速开发)_第2张图片仿真波形中蓝色部分为rx端的主要波形,橙色部分为tx端的主要波形。
有兴趣了解细致波形的可以直接建立一个工程,这样子想看哪个波形就能看哪个波形啦~

实测结果

实测直接通过串口CH340实现TTL电平转换,与PC端的串口调试助手进行测试。调试助手采用友善串口调试助手(老实说这个调试助手界面做的不怎么样哈哈),设置条件可查看左边串口设置:

测试结果如下:
【FPGA协议篇】UART通信及其verilog实现(代码采用传参实现模块通用性,适用于快速开发)_第3张图片可以看到,红色为发送数据,蓝色为接收数据,串口回环实现了将接收数据再次转发给PC端。

以上就是本次分享的内容啦~有机会再更新其他的协议。

你可能感兴趣的:(FPGA,基础知识,verilog,fpga,uart)