FPGA入门笔记009——UART串口发送模块设计

1、UART通信原理

​ 如图1为UART通信连接图,其中tx为输入,rx为输出。通过tx连接rx进行数据间的发送和接收。

image-20240319112855748

图1——UART通信连接图

​ UART 通信在使用前需要做多项设置,最常见的设置包括:数据位数、波特率大小、奇偶校验类型和停止位数:

​ (1)数据位(Data bits):该参数定义单个 UART 数据传输在开始到停止期间发送的数据位数。可选择为:5、6、7 或者 8(默认)。

​ (2)波特率(Baud)大小:是指从一设备发到另一设备的波特率,即:每秒钟可以通信的数据比特个数。典型的波特率有 300bps/Hz, 1200bps/Hz, 2400bps/Hz, 9600bps/Hz, 19200bps/Hz, 115200bps/Hz 等。一般通信两端设备都要设为相同的波特率,但有些设备也可设置为自动检测波特率。

​ (3)奇偶校验类型(Parity Type):是用来验证数据的正确性。奇偶校验一般不使用,如果使用,则既可以做奇校验(Odd)也可以做偶校验(Even)。

​ (4)停止位(Stop bits):在每个字节的数据位发送完成之后,发送停止位,来标志着一次数据传输完成,同时用来帮助接受信号方硬件重同步。可选择为:1(默认)、1.5 或者 2 位。

​ 在 RS-232 标准中,最常用的配置是 8N1(即八个数据位、一个停止位),其发送一个字节时序图如图 2 所示。

​ 按照一个完整的字节包括:一位起始位、8 位数据位、一位停止位,总共十位数据来算, 要想完整的实现这十位数据的发送,就需要 11 个波特率时钟脉冲,第 1 个脉冲标记一次传输的起始,第 11 个脉冲标记一次传输的结束,如图 2 所示,其中BPS_CLK为波特率时钟信号:

image-20240319112559538

图2——UART发送一个字节时序图

2、串口发送模块整体设计

​ 基于上述原理,本章要实现的串口发送模块整体框图,如图 3 所示,其接口列表如表 1 所示。

image-20240319114701645

图3——串口发送模块整体框图

image-20240320150511652

表1——串口发送模块接口列表

​ 根据功能需求,串口发送模块可进一步细化为如图 4 所示详细结构图,其中每一子模块的作用如表 2 所示。其中绿色的框代表单一结构的寄存器,来实现数据的稳定输入以及输出。

image-20240319115342076

图4——串口发送模块结构图

image-20240319115447397

表2——串口发送子模块块功能描述

3、接口设置

module uart_byte_tx(
    Clk,
    Rst_n,
    Send_En,
    Data_Byte,
    Baud_Set,

    Rs232_Tx,
    Tx_Done,
    UART_state
);

    input Clk;
    input Rst_n;
    input Send_En;
    input [2:0]Baud_Set;
    input [7:0]Data_Byte;

    output reg Rs232_Tx;
    output reg Tx_Done;
    output reg UART_state;

endmodule

4、波特率时钟生成模块设计

​ 从原理部分可知,波特率是 UART 通信中需要设置的参数之一。在波特率时钟生成模 块中,计数器需要的计数值与波特率之间的关系如表 3 所示,其中系统时钟周期为 clk_period,这里为 20ns。如果接入到该模块的时钟频率为其他值,需要根据具体的 频率值修改该参数。

image-20240320104431683

表3——系统时钟50MHz波特率计算

​ 本模块的设计是为了保证模块的复用性。当需要不同的波特率时,只需设置不同的波特 率时钟计数器的计数值。使用查找表即可实现,下面的设计代码中(DR_LUT)只包含了针对 5 个波特率 的设置,如需要其他波特率可根据实际使用情况具体修改。

//波特率时钟生成模块
    reg [15:0]bps_DR;   //分频计数最大值
    reg [15:0]div_cnt;  //分频计数器
    reg bps_clk;        //波特率时钟
    reg [3:0]bps_cnt;   //波特率时钟计数器

    //查找表(DR_LUT)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        bps_DR <= 16'd5207;
    else begin
        case(Baud_Set)
            0:bps_DR <= 16'd5207;
            1:bps_DR <= 16'd2603;
            2:bps_DR <= 16'd1302;
            3:bps_DR <= 16'd867;
            4:bps_DR <= 16'd433;
            default:bps_DR <= 16'd5207;
        endcase
    end

    //分频计数器(Div_Cnt)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        div_cnt <= 16'd0;
	else if(UART_state)begin
        if(div_cnt == bps_DR)
            div_cnt <= 16'd0;
        else
            div_cnt <= div_cnt + 1'b1;
    end
    else
        div_cnt <= 16'd0;

    //产生bps_clk脉冲信号(Div_Cnt)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        bps_clk <= 1'b0;
    else if(div_cnt == 16'd1)   //当计数器开始工作时,bps_clk就产生一个高脉冲,若计数器计数到bps_DR,则脉冲信号会延后一位。
        bps_clk <= 1'b1;
    else
        bps_clk <= 1'b0;

    //波特率时钟计数器(bps_cnt)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        bps_cnt <= 4'd0;
    else if(Tx_Done)
        bps_cnt <= 4'd0;
    else if(bps_clk)
        bps_cnt <= bps_cnt + 1'b1;
    else
        bps_cnt <= bps_cnt;

    //发送结束信号生成(Tx_Done)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        Tx_Done <= 1'b0;
    else if(bps_cnt == 4'd11)
        Tx_Done <= 1'b1;
    else 
        Tx_Done <= 1'b0;

5、数据输出模块设计

//数据输出模块
    //为了保证输出稳定,设置一个内部寄存器r_data_byte = Data_Byte。这样当Send_En = 1的时候,无论data_byte怎么变化,r_data_byte的值都不变。
    reg [7:0]r_data_byte;

    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        r_data_byte <= 8'd0;
    else if(Send_En)
        r_data_byte <= Data_Byte;

    //10选1多路器
    localparam START_BIT = 1'b0;
    localparam STOP_BIT = 1'b1;

    always@(posedge Clk or negedge Rst_n)   //用'*'号可能会出现毛刺,所有用时序逻辑来编写组合逻辑电路,较稳定
    if(!Rst_n)
        Rs232_Tx <= 1'b1;   //Rs232_Tx默认为1
    else begin
        case(bps_cnt)
            0:Rs232_Tx <= 1'b1;
            1:Rs232_Tx <= START_BIT;
            2:Rs232_Tx <= r_data_byte[0];
            3:Rs232_Tx <= r_data_byte[1];
            4:Rs232_Tx <= r_data_byte[2];
            5:Rs232_Tx <= r_data_byte[3];
            6:Rs232_Tx <= r_data_byte[4];
            7:Rs232_Tx <= r_data_byte[5];
            8:Rs232_Tx <= r_data_byte[6];
            9:Rs232_Tx <= r_data_byte[7];
            10:Rs232_Tx <= STOP_BIT;
            default:Rs232_Tx <= 1'b1;
        endcase
    end

6、发送状态模块

//发送状态模块(UART_state)
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        UART_state <= 1'b0;
    else if(Send_En)
        UART_state <= 1'b1;
    else if(Tx_Done)
        UART_state <= 1'b0;
    else
        UART_state <= uart_state;

7、仿真

`timescale 1ns/1ns
`define clk_period 20

module uart_byte_tx_tb;

    reg Clk;
    reg Rst_n;
    reg Send_En;
    reg [2:0]Baud_Set;
    reg [7:0]Data_Byte;

    wire Rs232_Tx;
    wire Tx_Done;
    wire UART_state;

    uart_byte_tx uart_byte_tx(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .Send_En(Send_En),
        .Data_Byte(Data_Byte),
        .Baud_Set(Baud_Set),

        .Rs232_Tx(Rs232_Tx),
        .Tx_Done(Tx_Done),
        .UART_state(UART_state)
    );

    initial Clk = 1'b1;
    always#(`clk_period/2) Clk = ~Clk;

    initial begin
        Rst_n = 1'b0;
        Data_Byte = 8'b0;
        Send_En = 1'b0;
        Baud_Set = 3'd4;

        #(`clk_period*20 + 1)   //让复位信号不与时钟边沿对其,更好观察时序关系
        Rst_n = 1'b1;

        #(`clk_period*50)
        Data_Byte = 8'haa;
        Send_En = 1'b1;
        #`clk_period
        Send_En = 1'd0;

        @(posedge Tx_Done)  //当Tx_Done信号出现上升沿时,进行接下来的语句

        #(`clk_period*5000)
        Data_Byte = 8'h55;
        Send_En = 1'b1;
        #`clk_period
        Send_En = 1'd0;

        @(posedge Tx_Done)
        #(`clk_period*5000)
        $stop;
    end

endmodule

8、板级调试

​ 为了实现导读中所设定的目标,将以前编写好的按键消抖模块添加到工程当中,并再次 使用 ISSP,其参数配置如图 5 所示,并加入到工程中。

image-20240321111114234

图5——ISSP参数配置

​ 然后,新建一个顶层文件,并将按键消抖串口发送以及 ISSP 例化,并将按键状态与串口发送使能端连接即可。设计如下所示,并将串口发送状态(UART_state)连接到 LED 上,以更好的观察数据发送状态。

module uart_byte_tx_top(
    Clk,
    Rst_n,
    Rs232_Tx,
    key_in0,
    led
);
    input Clk;
    input Rst_n;
    input key_in0;

    output Rs232_Tx;
    output led;

    wire Send_En;
    wire [7:0]Dta_Byte;
    wire key_flag0;
    wire key_state0;

    assign Send_En = key_flag0 & !key_state0;   //对Send_En进行按键消抖

//uart串口发送模块
    uart_byte_tx uart_byte_tx(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .Send_En(Send_En),
        .Data_Byte(Data_Byte),
        .Baud_Set(3'd4),    //波特率为115200

        .Rs232_Tx(Rs232_Tx),
        .Tx_Done(),
        .UART_state(led)
    );

//按键消抖模块
    key_filter key_filter(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .key_in(key_in0),
        .key_flag(key_flag0),
        .key_state(key_state0)
    );

//ISSP激励源
    issp issp(
        .probe(),
        .source(Data_Byte)
    );

endmodule

​ 编译无误后,点击 RTL_viewer 可以看到如图 6 的各模块连接图。

image-20240321111643330

图6——RTL_viewer

​ 到此,程序和模块设计部分的内容完成。

9、串口发送多个字节的数据方案

​ 目标:通过串口发送“HELLO”字符串。

​ 在uart_tx_top文件中,去掉ISSP模块和该行代码:assign Send_En = key_flag0 & !key_state0; ,并编写如下代码:

//字符串"HELLO\0"发送模块
    reg [2:0] cnt;

    localparam
        byte1 = "H",
        byte2 = "E",
        byte3 = "L",
        byte4 = "L",
        byte5 = "O",
        byte6 = "\0";

    //计数器计数
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        cnt <= 3'd0;
    else if(Tx_Done)    //D触发器的特征:当检测到Tx_Done时,cnt不会立即更新,cnt会在下一个时钟周期上升沿到来时才跟新(延迟一拍)。
        cnt <= cnt + 1'b1;
    else if(key_flag0 & !key_state0)    //key_flag0 & !key_state0表示按键按下并状态稳定,没有滞后
        cnt <= 3'd0;

    //通过发送结果控制发送
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        Send_En <= 1'b0;
    else if(key_flag0 & !key_state0)    //第一次按下按键,启动一次发送,没有滞后
        Send_En <= 1'b1;
	else if(Tx_Done &(cnt < 3'd5))    //后面每次发送完成,Tx_Done产生一次高脉冲,启动下一次发送。最后一个字节发送完成的Tx_Done不能触发新一次的发送
        Send_En <= 1'b1;    //Send_En滞后一拍变成0,当第6个字节发送完成,产生Tx_Done脉冲时,此时cnt还没自加1,还是5
    else
        Send_En <= 1'b0;

    //要发送的数据
    always@(*)  //组合逻辑
        case(cnt)
            3'd0:Data_Byte <= byte1;
            3'd1:Data_Byte <= byte2;
            3'd2:Data_Byte <= byte3;
            3'd3:Data_Byte <= byte4;
            3'd4:Data_Byte <= byte5;
            3'd5:Data_Byte <= byte6;
        default:Data_Byte = 0;
        endcase

​ 仿真代码如下所示:

`timescale 1ns/1ns
`define clk_period 20

module uart_byte_tx_top_tb;

    reg Clk;
    reg Rst_n;
    
    wire key;
    wire led;
    wire Rs232_Tx;

    reg press;

    uart_byte_tx_top uart_byte_tx_top(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .Rs232_Tx(Rs232_Tx),
        .key_in0(key),
        .led(led)
    );

    key_model key_model(
        .press(press),
        .key(key)
    );

    initial Clk = 1'b1;
    always#(`clk_period/2) Clk = ~Clk;

    initial begin
        Rst_n = 1'b0;
        press = 1'b0;
        #(`clk_period*20 + 1);
        Rst_n = 1'b1;
        #(`clk_period*20 + 1);
        press = 1'b1;
        #(`clk_period*20 + 1);
        press = 1'b0;

        wait(uart_byte_tx_top.Tx_Done &(uart_byte_tx_top.cnt == 3'd5));	//wait函数调用、层次的引用
        #(`clk_period*200 + 1);
        $stop;
    end

endmodule

​ 在如上所示的代码中,所用到的知识点为:

​ 1、加入一个仿真模块:key_model;

​ 2、wait函数调用

​ 3、层次的引用

posted @ 2024-03-26 09:19  Yamada_Ryo  阅读(735)  评论(0编辑  收藏  举报