UART串口通信的Verilog实现及验证

前言

UART(Universal Asynchronous Receiver/Transmitter)是最常用的低速通信协议之一,可以进行全双工通信。具有结构简单,硬件成本低廉的特点。无论是在嵌入式开发还是数字IC设计,都需要对UART有一定的了解。本文旨在实现UART中的RS232协议。

1.基本结构

RS232规范是UART中比较常见的一种,因其成本较低所以被广泛应用,适用于较短距离的通信。RS232没有时钟线,只有两根数据线,分别是rx和tx,这两根线都是1bit位宽的。其中rx是接收数据的线,tx是发送数据的线。
在这里插入图片描述
接下来我将一步步介绍数字模块的设计。

2.串口接收模块

串口数据的发送与接收是基于帧结构的,即一帧一帧的发送与接收数据。当两个设备需要进行串口通信时,需要双方同时约定好通信的规则,这个规则即波特率

波特率:在信息传输通道中,携带数据信息的信号单元叫码元(因为串口是1bit进行传输的,所以其码元就是代表一个二进制数),每秒钟通过信号传输的码元数称为码元的传输速率,简称波特率,常用符号“Baud”表示,其单位为“波特每秒(Bps)”。串口常见的波特率有4800、9600、115200等,我们选用9600的波特率进行串口章节的讲解。

串口发送或者接收1bit数据的时间为一个波特,即1/9600秒,如果用50MHz(周期为20ns)的系统时钟来计数,需要计数的个数为cnt = (1s * 10^9)ns / 9600bit)ns / 20ns ≈ 5208个系统时钟周期,即每个bit数据之间的间隔要在50MHz的时钟频率下计数5208次。
串口数据的发送与接收是基于帧结构的,即一帧一帧的发送与接收数据。每一帧除了中间包含8bit有效数据外,还在每一帧的开头都必须有一个起始位,且固定为0;在每一帧的结束时也必须有一个停止位,且固定为1,即最基本的帧结构(不包括校验等)有10bit。在不发送或者不接收数据的情况下,rx和tx处于空闲状 态,此时rx和tx线都保持高电平,如果有数据帧传输时,首先会有一个起始位,然后是8bit的数据位,接着有1bit的停止位,然后rx和tx继续进入空闲状态,然后等待下一次的数据传输。如图所示为一个最基本的RS232帧结构。
在这里插入图片描述
整体设计思路:
在这里插入图片描述

具体实现代码如下:
宏定义文件

`define baud_rate 9600				//设置波特率
`define clk_frequent 50_000_000		//设置系统时钟频率

接收模块

`include "defines.v"
module uart_rx (
    input  wire        clk		,	//全局时钟
    input  wire        rst_n	,	//全局复位
    input  wire        rx		,   //串口接收到的串行数据

    output reg[7:0]    po_data	,	//串行转并行后的数据
    output reg         po_flag		//转换完成的标志
);

parameter baud_cnt_max = `clk_frequent/`baud_rate; //波特率计数最大值
 //涉及跨时钟域信号,打三拍产生稳定信号
reg rx_reg1;                   
reg rx_reg2;
reg rx_reg3;
reg start_flag;                 //传输开始标志位
reg work_en;                    //数据传输使能信号
reg [15:0] baud_cnt;        	//波特率计数器
reg [3:0]bit_cnt;               //bit计数器
reg bit_flag;                   //bit信号有效位
reg [7:0]rx_data;               //串行信号转并行
reg rx_flag;                    //接收完毕标志位

/* 先打三拍进行信号同步 */
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rx_reg1 <= 1'b1;
    else
        rx_reg1 <= rx; 
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rx_reg2 <= 1'b1;
    else
        rx_reg2 <= rx_reg1;
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rx_reg3 <= 1'b1;
    else
        rx_reg3 <= rx_reg2;
end
//检测到输入信号从高电平向低电平越变,说明传输开始
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        start_flag <= 1'b0;
    else if((rx_reg2 == 1'b0) && (rx_reg3 == 1'b1) && (work_en == 1'b0))//下降沿检测
        start_flag <= 1'b1;
    else
        start_flag <= 1'b0;
end
//开始标志位有效时,数据传输使能信号有效
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        work_en <= 1'b0;
    else if(start_flag)
        work_en <= 1'b1;
    else if((bit_cnt == 4'd8) && bit_flag)	//当bit_cnt计满,说明传输完一帧数据,则让数据传输使能信号无效
        work_en <= 1'b0;
    else
        work_en <= work_en;
end
//波特率计数器,用于产生波特率时钟,确定每位数据的传输时刻
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        baud_cnt <= 16'd0;
    else if((baud_cnt == baud_cnt_max-1) || (work_en == 1'b0)) //baud_cnt计满清零
        baud_cnt <= 16'd0;
    else
        baud_cnt <= baud_cnt + 1'b1;
end

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        bit_flag <= 1'b0;
    else if(baud_cnt == baud_cnt_max / 2 - 1) //波特率计数器计数值到最大值的一半时,bit标志位有效
        bit_flag <= 1'b1; 
    else 
        bit_flag <= 1'b0; 
end
//bit计数器计数值对应某一bit的值
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        bit_cnt <= 4'd0;
    else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))//bit计数器计满说明数据传输完毕,此时计数器清零
        bit_cnt <= 4'd0;
    else if(bit_flag == 1'b1)
        bit_cnt <= bit_cnt + 1'b1;
    else 
        bit_cnt <= bit_cnt;
end
//串行数据转并行数据
always @(posedge clk ) begin
    if(!rst_n)
        rx_data <= 8'b0000_0000;
    else if((bit_flag == 1'b1) && (bit_cnt >= 4'd1) && (bit_cnt <= 4'd8))//bit标志位有效且bit计数值在8bit数据范围内,写入数据
        rx_data <= {rx_reg3,rx_data[7:1]};
end
//一帧数据接收完毕的标志位
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rx_flag <= 1'b0;
    else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))//bit计数器计满说明数据接收完成
        rx_flag <= 1'b1;
    else
        rx_flag <= 1'b0;
end
//rx_data打一拍输出
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        po_data <= 8'b0000_0000; 
    else if(rx_flag == 1'b1)
        po_data <= rx_data;
end
//rx_flag打一拍输出
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        po_flag <= 1'b0;
    else 
        po_flag <= rx_flag;
end

endmodule

3.串口发送模块

发送数据时,不需要时钟线,只需要一根数据线。在空闲时为高电平,只需要把信号拉低,即可开始传输数据。与接收模块相反,发送模块会将并行数据转换为串行数据发出。
整体设计思路:
在这里插入图片描述

代码如下:

`include "defines.v"
module uart_tx (
    input wire          clk         ,	//全局时钟
    input wire          rst_n       ,	//全局复位
    input wire[7:0]     pi_data     ,	//需要发送的8bit数据
    input wire          pi_flag     ,	//开始发送的信号

    output reg          tx				//串口发送的串行数据
);

parameter baud_max = `clk_frequent/`baud_rate;//波特率计数最大值

reg         work_en;//数据传输使能信号
reg[15:0]   baud_cnt;//波特率计数器
reg         bit_flag;//bit信号有效位
reg[3:0]    bit_cnt;//bit计数器
//开始标志位有效时,数据传输使能信号有效
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        work_en <= 1'b0;
    else if((bit_cnt == 4'd9) && (bit_flag == 1'b1))//当bit_cnt计满,说明传输完一帧数据,则让数据传输使能信号无效
        work_en <= 1'b0;
    else if(pi_flag == 1'b1)
        work_en <= 1'b1;
end
//波特率计数器,用于产生波特率时钟,确定每位数据的传输时刻
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        baud_cnt <= 16d0;
    else if(baud_cnt == (baud_max - 1) || (work_en == 1'b0))//波特率计数器计满或数据传输使能信号无效时,计数器清零
        baud_cnt <= 16'd0;
    else if(work_en == 1'b1)
        baud_cnt <= baud_cnt + 1'b1;
end

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        bit_flag <= 1'b0;
    else if(baud_cnt == 16'd1)//波特率计数器计数值到1时,bit标志位有效
        bit_flag <= 1'b1;
    else
        bit_flag <= 1'b0;
end
//bit计数器计数值对应某一bit的值
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        bit_cnt <= 1'b0;
    else if((bit_cnt == 4'd9) && (bit_flag == 1'b1))//bit计数器计满说明数据接收完成
        bit_cnt <= 1'b0;
    else if(bit_flag == 1'b1)
        bit_cnt <= bit_cnt + 1'b1;
end
//每当bit标志位有效时,传输1位信号
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        tx <= 1'b1;
    else if(bit_flag == 1'b1)begin
        case (bit_cnt)
            0: tx <= 1'b0;			//从高电平变为低电平,表示传输开始
            1: tx <= pi_data[0];	//传输8bit数据位
            2: tx <= pi_data[1];
            3: tx <= pi_data[2];
            4: tx <= pi_data[3];
            5: tx <= pi_data[4];
            6: tx <= pi_data[5];
            7: tx <= pi_data[6];
            8: tx <= pi_data[7];
            9: tx <= 1'b1;			//传输完成,恢复空闲状态,为高电平
      default: tx <= 1'b1;
        endcase
    end
end

endmodule

4.顶层模块

`include "defines.v"
//rs232模块可以将接收到的数据转发出去
module rs232 (
    input  wire      clk    ,	//全局时钟
    input  wire      rst_n  ,	//全局复位
    input  wire      rx     ,	//接收到的bit数据

    output reg       tx			//发送的bit数据
);

wire[7:0] po_data;
wire po_flag;
//例化接收模块
uart_rx uart_rx_inst (
    .clk     (clk),
    .rst_n   (rst_n),
    .rx      (rx),    

    .po_data (po_data),
    .po_flag (po_flag)
);
//例化发送模块
uart_tx uart_tx_inst(
    .clk         (clk),
    .rst_n       (rst_n),
    .pi_data     (po_data),
    .pi_flag     (po_flag),

    .tx          (tx)
);

endmodule

5.仿真代码

module rs232_tb();

reg clk;
reg rst_n;
reg rx;

wire tx;

initial begin
    clk <= 1'b0;
    rst_n <= 1'b0;
    rx <= 1'b1;	//空闲状态为高电平
    #50
    rst_n = 1'b1;
end

always #10 clk <= ~clk;

initial begin
    #200
    rx_byte();

    #(5208*2000);
    $stop;
end
/*定义了两个task,一个设置传输数据,另一个设置bit传输规则*/
//依次传输数据1~8
task rx_byte();
    integer i;
    for(i=1;i<9;i=i+1)
        rx_bit(i);//rx_bit需要传入参数
endtask

task rx_bit(
    input [7:0]data
);
integer j;
    for (j=0;j<10;j=j+1) begin
        case(j)
            0: rx <= 1'b0;
            1: rx <= data[0];
            2: rx <= data[1];
            3: rx <= data[2];
            4: rx <= data[3];
            5: rx <= data[4];
            6: rx <= data[5];
            7: rx <= data[6];
            8: rx <= data[7];
            9: rx <= 1'b1;
        endcase
    #(5208*20);//延迟1个bit计数周期
    end
endtask

rs232 rs232_inst(
    .clk    (clk),
    .rst_n  (rst_n),
    .rx     (rx),

    .tx     (tx)
);
//生成fsdb文件用于Verdi仿真
`ifdef FSDB
initial begin
	$fsdbDumpfile("rs232.fsdb");
	$fsdbDumpvars;
end
`endif

endmodule

6.仿真波形

接收模块

先看接收模块的波形,起始状态复位信号rst_n为低电平,模块复位,经过验证确认功能正确。50ns时rst_n拉高,此后一直保持高电平,模块开始工作。
在这里插入图片描述
在200ns时,rx从高电平变为低电平,开始进行数据传输。由于数据传输会跨越时钟域,为了避免数据产生不稳定,需要先打三拍进行时钟同步。同时,当检测到rx_reg3的下降沿时,start_flag拉高一个时钟周期,标志着数据传输的开始。
在这里插入图片描述
数据传输的开始时,数据传输使能信号有效,波特率计数器开始计数(计数最大值为5208)。计满或者数据传输停止时,波特率计数器清零。
在这里插入图片描述
当波特率计数器计数到最大值的一半时,此时收到的该位数据最为稳定,故在这个时刻采集数据。即在时刻将bit_flag拉高一个时钟周期,此时bit_cnt为0,说明传输的是起始位,rx_data保持不变。随后bit_cnt变为1,准备开始传输第0个数据位。
在这里插入图片描述
在下一次bit_flag拉高时,bit计数器变为2,说明开始传输第0个数据位,此时接受rx_reg3的数据作为rx_data的最高位。
在这里插入图片描述bit计数器变为3,说明开始传输第0个数据位,此时再次接受rx_reg3的数据作为rx_data的最高位,后续将会继续接受共计8个数据位。
在这里插入图片描述
在bit计数器为8且bit_flag拉高时,bit计数器将会清零。此时把第7位数据传输给rx_data,传输结束。
在这里插入图片描述
接着rx信号拉高,说明总线进入空闲状态。此时的rx_data为1,即为第一帧传输的串行转并行后的数据。
在这里插入图片描述
最后po_data和po_flag分别对rx_data和rx_flag打一拍进行输出,总体仿真波形如下
在这里插入图片描述

发送模块

再看发送模块的波形,起始状态复位信号rst_n为低电平,模块复位,经过验证确认功能正确。50ns时rst_n拉高,此后一直保持高电平,模块开始工作。
在这里插入图片描述
在顶层模块中,接收模块的输出信号和发送模块的输入信号直接相连,故pi_data为经过接收模块串行转并行的数据,pi_flag为帧数据发送完毕的标志信号。
当pi_flag拉高时,work_en随之拉高,数据发送开始。
当work_en信号有效时,baud_cnt开始计数,直到计满或者work_en信号无效时清零。
由于是直接转发接收模块的数据所以在这里的数据比较稳定可以不用在baud_cnt计数最大值的一半时采集信号,可以直接在baud_cnt为1时采集信号,即在baud_cnt为1时将bit_flag拉高一个时钟周期。
当bit_flag为高电平时,bit_cnt由0变为1,传输起始位,则将tx拉低。
在这里插入图片描述
当下一此bit_flag为高电平时,bit_cnt由1变为2,传输数据的第0位。
在这里插入图片描述
当bit_cnt计满时,说明数据位发送完毕,此时work_en置零,baud_cnt随之清零,tx拉高,总线进入空闲状态。
在这里插入图片描述
总体波形如图所示:
在这里插入图片描述

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值