UART 串口收发模块设计及Verilog实现

文章目录

  • 一、UART协议介绍
    • 1.1 UART协议层
      • 1.2.1 UART的帧格式
      • 1.2.2 UART的波特率
    • 1.2 UART物理层
      • 1.3.1 物理连接
      • 1.3.2 接口标准
      • 1.3.3 硬件设计
        • 1. USB转串口电路
        • 2. RS232转串口电路
  • 二、UART设计及Verilog实现
    • 2.1 UART设计概述
    • 2.2 UART详细设计
      • 2.2.1 UART发送模块设计
        • 1. 波特率时钟分频模块
        • 2. 发送模块
        • 3. testbench
        • 4. 结果
      • 2.2.2 UART接收模块设计
        • 1. 波特率时钟分频模块
        • 2. 接收模块
        • 3. testbench
        • 4. 结果

一、UART协议介绍

UART的全称是通用异步收发器(Universal Asynchronous Receiver/Transmitter)。

Universal 通用性: 体现在UART使用范围广上,作为一个通用的接口协议,UART广泛的应用在各类MCU和SOC产品上。
Asynchronous 异步性: 体现在”不需要额外的时钟线进行数据的同步传输”即只要信号拉低,即可开始传送数据,而另一些通讯协议,需要引入时钟信号来进行操作,如AMBA,需要在时钟的上升沿发送数据。
Receiver/Transmitter收发器:则更好理解,即一个数据的发送方和一个数据的接收方,也意味着在数字IC设计中需要分别设计Receiver和Transmitter。

发送端的UART将来自控制设备(如CPU)的并行数据转换为串行数据,以串行方式将其发送到接收端的UART,然后由接收端的UART将串行数据转换为并行数据以用于接收设备的正常处理。

这里只需要两条线RX/TX即可在两个UART之间传输数据。具体如下图所示;

1.1 UART协议层

1.2.1 UART的帧格式

UART的一帧由1位起始位 、5~9个数据位 、一个可选的奇偶校验位 和停止位 组成。数据逐位传输,如下图所示。

  1. 为什么UART的传输需要起始位?

    因为UART没有控制线,要让接收方知道什么时候开始接收数据,需要一些手段。

    在发送数据之前,先发一位逻辑“0”作为数据发送的起始标志,接收方在空闲时,当检测到有一个低电平,则开始以波特率的频率接逐位接收数据。

    在具体设计UART的过程中,如何检测低电平,使用到了电平检测电路。很显然这里使用的是下降沿检测电路

  2. UART基本的数据形式

    • 默认无传输数据时,为高电平。

    • 当信号拉低,传输线上的电平拉低,意味着开始进行数据传输

    • 紧接着起始位的是数据位,它可以是5、6、7或8位

    • UART的“校验位”紧挨着“数据位”,采用奇偶校验​ 方式,根据设置,校验位可以存在也可以不存在。

    • UART将停止位作为停止标志,是在数据位(没有校验位)和校验位(有校验位)之后发送1~2位的逻辑“1”高电平。当发送完停止位之后,UART总线进入空闲。

  3. 为什么UART的数据位可变?

    数据位包含正在传输的实际数据。如果使用奇偶校验位,则可以是5位,最多8位。如果不使用奇偶校验位,则数据帧的长度可以为9位。

    因为UART是一种低速总线,每多发一位都占用不少的时间(由传输波特率决定),所以可以根据传输数据的特点,采用不同位宽以节约数据传输的时间。

1.2.2 UART的波特率

如果从更高的level审视UART传输协议,如嵌入式开发者的角度,我们会发现,在使用具体的UART协议前,我们需要对发送端和接收端进行波特率的同步,以此来确保发射端的数据可以在接收端得到正确的采集。常用的波特率可以是300,1200,2400,9600,19200,38400,115200,这些数意味着什么呢?别着急,我们接下来要讨论这个内容。

  1. 什么是波特率

    波特率等于每秒钟传输的数据位数,即Bit/s。 假如波特率设置为9600,那么意味着每秒该UART传输协议可以传输9600bits的数据,换句话说传输1比特需时间约为:10^9(ns)/9600=104166(ns)。

    下表是各个波特率下数据位时间宽度。

  2. 如何换算波特率

    时钟频率假如为100MHz,这意味着我们的时钟周期为10ns,因此10416个时钟周期我们就可以传输1bit数据,换言之我们需要一个大小为10416的分频电路来对100MHz时钟进行处理,因此在设计UART的过程中,我们需要使用分频电路依据波特率处理全局时钟,依据分频后的时钟节奏来发送数据和接收数据。

    同样的,参考作者之前的文章,我们可以获知分频电路的设计方法

  3. 波特率和采样频率是一样的吗?

    按照前文所说,好像波特率和采样频率是一个意思,即9600波特率对应接收端1s进行9600次采样,也对应发射端1s进行9600bit的发射,那么请读者思考,真的是这样吗?
    答案其实是否定的
    这是因为:在数据的传输中,信号可能受到一些干扰而产生一些抖动(比如说电磁兼容性设计中的近端串扰),如果接收端只对这些信号进行一次采样,那么它有可能采样到的是不准确的数据,所以接收端在采样时,通常都要
    采样多次
    ,然后通过处理获得准确的数据,比如说,我们可以用多数表决的方法来在接收端进行多次采样,得到准确值。

1.2 UART物理层

1.3.1 物理连接

1.3.2 接口标准

UART、RS232、RS485在串口通信中,主要区别是电平的不同,其中UART通常使用TTL电平,下面介绍这几个存在的差异;

  • TTL

    TTL全名是晶体管-晶体管逻辑集成电路(Transistor-Transistor Logic)

    输入高电平最小2V,输出高电平最小2.4V,典型值3.4V

    输入低电平最大0.8V,输出低电平最大0.4V,典型值0.2V

  • RS232

    RS232 逻辑1电平(MARK)=-3V~-15V逻辑0电平(SPACE)=+3~+15V

    同样的,对于传输数据0x55,即二进制的01010101,RS232和TTL的区别如下;(LSB:最低位,MSB:最高位)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WP8Evw5R-1682070990015)(assets/image-20230417144644-ke2ksdg.png)]

  • RS485

    RS485是差分信号进行串行传输;

    逻辑1以两线间的电压差为+(2 ~ 6)V表示 逻辑 “0”以两线间的电压差为-(2 ~ 6)V表示
    在工业通信中,使用RS485比较多,因为RS485是差分信号,可以抑制共模干扰,因此在恶劣的环境中拥有很好的抗干扰性,比较稳定

1.3.3 硬件设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NvE0gMS7-1682070990015)(assets/image-20230417163754-syblf26.png)]

FPGA采用TTL电平:3.3V(1)、0V(0)。而PC采用的是负逻辑电平:-15~-5为逻辑1,+5 ~ +15为逻辑0。

因此,PC与FPGA之间通信时,需要进行电平转换。

老早以前的台式机和笔记本可都是有串口的,虽然现在很多台式机串口没有了,笔记本也没有串口了,但是串口从来没有被丢弃,只是以另外一种形式存在,这就是 USB 转串口芯片。串口只需要 2 根线就可以实现一

收一发,使用简单,可靠方便,在低速场合大量使用。

1. USB转串口电路
  1. CH340G

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSm2Oh1A-1682070990016)(assets/image-20230417171151-b44eajn.png)]

  1. CP2104

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ly88fsAo-1682070990017)(assets/cp2104-20230417171240-6htj5gk.png)]

2. RS232转串口电路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8qYaRhaf-1682070990017)(assets/image-20230417171130-yq2a170.png)]

二、UART设计及Verilog实现

2.1 UART设计概述

本实例设计的UART特性:

  1. 并没有支持奇偶检验,故没有奇偶校验位。
  2. 并不支持数据位宽可配,数据位位宽固定为8bit。
  3. 支持输入时钟与波特率可配。

原理图如下,分两个大模块,一个==数据接收控制模块(Receive_Control)==,一个数据发送控制模块(Send_Control)

模块启动后,

  • 接收模块一直在接收数据uart_rx。每当有新数据发送过来时,将新的8位数据放置于rx_data中,接收完成信号rx_data会置高。
  • 发送模块发送数据需要发送使能tx_en有效后,才能将要发送的数据tx_data发送到uart_tx端口,发送完成后tx_finish会置高,之后可以使能tx_en发送新的数据。

图片

各模块详细设计图示

端口说明:

图片

接收控制模块与发送控制模块内部都有一个波特率时钟产生模块(BuadRate_set),用于将电路输入时钟(clk)进行分频产生波特率时钟,用于接收和发送数据控制。

图片

可以看到,BuadRate_set模块有一个enable控制信号,只有当enable信号为高时,BuadRate_set模块才工作。 在发送控制模块里面,只有发送数据的时候才拉高其相应的enable;在接收控制模块里面,只有检测到有数据发送进来的时候才拉高其相应的enable。这是为了降低功耗

2.2 UART详细设计

2.2.1 UART发送模块设计

数据发送模块的时序如下所示。

  • 当发送使能tx_en有效时,启动波特率时钟分频模块开始计数。状态由IDLE转为EN_TX。
  • 波特率时钟分频模块用于生成发送数据的时钟,并控制数据移位。
  • 当波特率时钟到来时将此刻要发送的数据送出。
1. 波特率时钟分频模块
`timescale 1ns / 1ps
module BuadRate_set #(
  parameter CLK_Period=50000000,//the unit is Hz
  parameter Buad_Rate=9600 //the unit is bits/s
)(
   input      clk,         //原始系统时钟
   input      rst_n,       //复位
   input      enable,      //模块使能信号 
   output     Buad_clk     //输出的分频
);

localparam DIV_PEREM=CLK_Period/Buad_Rate/2;    //计数器最大值
reg[15:0] cnt;

always @( posedge clk )
  if( !rst_n )
    cnt <= 16'b0000;
  else if( enable ) begin
    if( cnt != DIV_PEREM )
      cnt <= cnt+1'b1;	  
    else	  
      cnt <= 16'h0000;
  end
  else
    cnt <= 16'h0000;

reg DIV_clk;

always @( posedge clk )
  if( !rst_n || !enable )    //当复位或者模块不使能的时候,分频器停止。
    DIV_clk <= 1'b1;
  else if(cnt==DIV_PEREM)    //计数器满,输出时钟翻转
    DIV_clk <= ~DIV_clk;

assign Buad_clk = DIV_clk;
    
endmodule
2. 发送模块
`timescale 1ns / 1ps
module Send_Control #(
  parameter CLK_Period=50000000,//the unit is Hz
  parameter Buad_Rate=9600 //the unit is bits/s
)(
  input        clk,
  input        rst_n,
  input        tx_en,
  input [7:0]  tx_data,
  output       tx_finish,
  output       uart_tx
);
localparam IDLE=2'b00;    //0
localparam EN_TX=2'b10;   //2
localparam END_BIT=2'b11; //3
wire Buad_clk;
wire Buad_en;
BuadRate_set #(
  .CLK_Period(CLK_Period),//the unit is Hz
  .Buad_Rate (Buad_Rate) //the unit is bits/s
) BuadRate_set_inst(
   .clk     (clk     ),
   .rst_n   (rst_n   ),
   .enable  (Buad_en ),
   .Buad_clk(Buad_clk)
);

reg [9:0] data_to_send;    //发送10个数

reg [1:0] state;
reg [3:0] cnt;
//发送模块状态机
always @( posedge clk )
  if( !rst_n )
    state <= IDLE;
  else
    case(state)
    IDLE:if( tx_en )    //tx_en发送有效的下一个时钟上升沿,开始发送数据
         state <= EN_TX;
    EN_TX: if( cnt=='d10 )
         state <= IDLE;
    END_BIT: if( cnt=='d0 )
         state <= IDLE;
    default: state <= IDLE;
    endcase
//波特率时钟发生启动信号
assign Buad_en = (state == EN_TX || state == END_BIT);    
//发送计数0~9
always @( posedge Buad_clk or negedge rst_n)
  if( !rst_n )
    cnt <= 4'b000;
  else begin
    if( cnt<4'd10)
      cnt <= cnt + 1'b1;
    else 
      cnt <= 4'b0000;
  end	 
//发送数据
always @( posedge Buad_clk or negedge rst_n )
  if( !rst_n )
    data_to_send <= 10'h3ff;
  else if( cnt == 'd0 )    //发送第一个数
    data_to_send <= {1'b1,tx_data,1'b0}; 
  else    //之后,数据右移位
    data_to_send <= {1'b1,data_to_send[8:1]};

assign uart_tx = (Buad_en)?data_to_send[0]:1'b1;	
assign tx_finish = (state == END_BIT)? 1'b1:1'b0;

endmodule
3. testbench
`timescale 1ns / 1ps
module tb_send();
  reg        clk;
  reg        rst_n;
  reg        tx_en;
  reg [7:0]  tx_data;
  wire       tx_finish;
  wire       uart_tx;
  // 50MHz,9600bps 
  Send_Control my_Send_Control(clk, rst_n, tx_en, tx_data, tx_finish, uart_tx);	
  initial begin
    clk=0;rst_n=0; tx_en=0;  //复位信号不能太短。
    #100 rst_n=1;  tx_en=1;    tx_data=8'b11001100;
    # 100000000 $stop;
  end
  
  //50Mhz的时钟
  always #10 clk=~clk;
endmodule
4. 结果

2.2.2 UART接收模块设计

数据接收模块的时序如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZLx4c0r-1682071409104)(assets/image-20230421174441-y6ba21m.png)]

  • 当检测到输入数据从高位跳转到低位时,启动波特率时钟分频模块开始计数。状态由IDLE转为GET_DATA。
  • 波特率时钟分频模块用于生成接收数据的使能信号,并控制数据移位。
  • 当波特率时钟到来时将此刻接口上的数据保存到接收数据数组rx_data_tmp中。
  • 该设计中每bit的数据都会检测8次。取平均值作为最终的检测结果。
1. 波特率时钟分频模块
// 公众号: 小鱼FPGA
// Engineer: littlefish

module BuadRate_set_multi #(
  parameter CLK_Period=50000000,//the unit is Hz
  parameter Buad_Rate=9600 //the unit is bits/s
)(
   input      clk,         //原始系统时钟
   input      rst_n,       //复位
   input      enable,      //模块使能信号 
   output   reg  Buad_en,     //输出的分频
   output   reg  Cap_en
);

localparam DIV_PEREM=CLK_Period/Buad_Rate;    //计数器最大值
localparam Cap_DIV_PEREM=DIV_PEREM/8;    //计数器最大值
reg[15:0] cnt;


//波特率分频
always @( posedge clk )
  if( !rst_n )
    cnt <= 16'b0000;
  else if( enable ) begin
    if( cnt != DIV_PEREM )
      cnt <= cnt+1'b1;	  
    else	  
      cnt <= 16'h0000;
  end
  else
    cnt <= 16'h0000;



always @( posedge clk )
  if( !rst_n || !enable )    //当复位或者模块不使能的时候,分频器停止。
    Buad_en <= 1'b0;
  else if(cnt==DIV_PEREM)    //计数器满,输出时钟翻转
    Buad_en <= 1;
  else 
    Buad_en<=0;

//采集分频
reg[15:0] cap_cnt;

always @( posedge clk )
  if( !rst_n )
    cap_cnt <= 16'b0000;
  else if( enable ) begin
    if( cap_cnt < Cap_DIV_PEREM )
      cap_cnt <= cap_cnt+1'b1;	  
    else	  
      cap_cnt <= 16'h0000;
  end
  else
      cap_cnt <= 16'h0000;

always @( posedge clk )
  if( !rst_n || !enable )    //当复位或者模块不使能的时候,分频器停止。
    Cap_en <= 1'b0;
  else if(cap_cnt==Cap_DIV_PEREM)    //计数器满,输出时钟翻转
    Cap_en <= 1;
  else 
    Cap_en<=0;
endmodule
2. 接收模块
`timescale 1ns / 1ps
module receive_control_opt #(
  parameter CLK_Period=50000000,//the unit is Hz
  parameter Buad_Rate=9600 //the unit is bits/s
)(
  input            clk,	
  input            rst_n,

  input            uart_rx,
  output wire [7:0] rx_data,
  output            rx_finish,
  output           o_valid  
);

    localparam IDLE=2'b00;
    localparam GET_DATA=2'b01;
    localparam END=2'b11;
    
    
    reg[5:0] get_start_bit;
    reg start;
    wire rx_start;
    wire Buad_en;
    wire Cap_en;
    reg [8:0] rx_data_tmp;   //8位数据位+1位停止位
    //波特率采集时钟生成
    BuadRate_set_multi #(
      .CLK_Period(CLK_Period),//the unit is Hz
      .Buad_Rate (Buad_Rate) //the unit is bits/s
    ) BuadRate_set_multi_inst(
       .clk     (clk     ),
       .rst_n   (rst_n   ),
       .enable  (rx_start ),
       .Buad_en(Buad_en),
       .Cap_en(Cap_en)
    );
    
    //启动低电平信号检测!
    always @( posedge clk )
      if( !rst_n )
        get_start_bit <= 6'b111111;
      else
        get_start_bit <= {get_start_bit[4:0],uart_rx};
    always @( posedge clk )
      if( !rst_n )
        start <= 1'b0;
      else if(state !=IDLE)                //至到状态state转为非IDLE状态时,start才转为0。
        start <= 1'b0;
      else if( get_start_bit==6'b111000 )    //有三个联系低电平0出现是,start==1。
        start <= 1'b1;
    //波特率产生模块启动信号:当未检测到起始位并且状态是IDLE空闲时,置无效。其他时刻为有效。
    assign rx_start =( ~(start==1'b0 && state==IDLE) )? 1'b1:1'b0;   
    
    reg [1:0] state;
    reg [3:0] cnt;
    //接收模块状态机
    always @( posedge clk or negedge rst_n )    //特定波特率采集时钟下
      if( !rst_n )
        state <= IDLE;
      else 
          case( state )
            IDLE: begin
                if( start )    //检测到起始低电平信号,状态转为开始接收,并开始生成检测波特率时钟
                  state <= GET_DATA;
              end
            GET_DATA: begin     //开始接收9个数据:1个起始位+8个数据位
                    if( cnt=='d9 )
                  state <= END;
            end
            END: begin     //接收停止位
                  state <= IDLE;
            end
            default: begin 
                state <= IDLE;
            end
          endcase
    reg [4:0] cap_tmp;
    reg [3:0] cap_cnt;
    //每bit数据采集7次,取7次的平均值作为该bit的数值
    always @( posedge clk or negedge rst_n ) begin
      if( !rst_n || Buad_en==1)
        cap_tmp <= 5'd15;
      else if( state == GET_DATA && Cap_en==1 ) begin
        if(uart_rx==1)
            cap_tmp<=cap_tmp+1;
        else if(uart_rx==0)
            cap_tmp<=cap_tmp-1;
      end
    end
    always @( posedge clk or negedge rst_n ) begin
      if( !rst_n )
        cap_cnt <=0;
      else if( state == GET_DATA && Cap_en==1 ) begin
        if(cap_cnt<7)
            cap_cnt<=cap_cnt+1;
        else 
            cap_cnt<=0;
      end
    end
    
    //接收9位有效数据:1个起始位+8个数据位    存放于rx_data_tmp
    always @( posedge clk or negedge rst_n ) begin
      if( !rst_n )
        rx_data_tmp <= 9'b0;
      else if( state == GET_DATA &&Buad_en==1) begin
        if(cap_tmp>'d15)
            rx_data_tmp <= {'b1,rx_data_tmp[8:1]};
        else
            rx_data_tmp <= {'b0,rx_data_tmp[8:1]};
      end  
    end
    //接收计数,接收9位有效数据:1个起始位+8个数据位,取其中的8位数据位即可。
    always @( posedge clk or negedge rst_n)  begin
      if( !rst_n || state==IDLE ||state==END )
        cnt <= 'd0;
      else if(state==GET_DATA&&Buad_en==1 )
        cnt <= cnt+1'b1;
    end
    
    assign rx_finish=(state==END);
    reg finish_d;
    always @( negedge clk or negedge rst_n )
      if( !rst_n )
        finish_d<=1'b0;
      else 
        finish_d<=rx_finish;
    
    assign o_valid = rx_finish&(~finish_d);   
    
    assign rx_data=(state==GET_DATA)?8'b0:rx_data_tmp[8:1];
        
endmodule
3. testbench
module tb_receive_opt();
  reg            clk;	
  reg            rst_n;	
  reg            uart_rx;
  	
  wire [7:0]     rx_data;	
  wire           rx_finish;	
  wire           o_valid;
  // 50MHz,9600bps 
  receive_control_opt my_Receive_Control(clk, rst_n, uart_rx, rx_data, rx_finish, o_valid);	
  initial begin
    clk=0;rst_n=0;          //复位信号不能太短。
    #15 rst_n=1;uart_rx=1;
    //1位起始位
    #104166 uart_rx=0;
    //8位数据:先发低位
    #104166 uart_rx=1;
    #104166 uart_rx=0;
    #104166 uart_rx=1;
    #104166 uart_rx=0;
    #104166 uart_rx=1;
    #104166 uart_rx=0;
    #104166 uart_rx=1;
    #104166 uart_rx=0;
    //1位停止位
    #104166 uart_rx=1; 
    #104166 uart_rx=1;
    #104166 uart_rx=1;
    #104166 uart_rx=1;
    #104166 uart_rx=1;
  end
  
  //50Mhz的时钟
  always #10 clk=~clk;
endmodule
4. 结果

版权声明:本文为博主作者:qq_24287711原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/qq_24287711/article/details/130294198

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2024年1月11日
下一篇 2024年1月11日

相关推荐