FPGA零基础学习:UART协议驱动设计

2020-12-30 15:43:51 浏览数 (1)

大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

本系列将带来FPGA的系统性学习,从最基本的数字电路基础开始,最详细操作步骤,最直白的言语描述,手把手的“傻瓜式”讲解,让电子、信息、通信类专业学生、初入职场小白及打算进阶提升的职业开发者都可以有系统性学习的机会。

系统性的掌握技术开发以及相关要求,对个人就业以及职业发展都有着潜在的帮助,希望对大家有所帮助。后续会陆续更新 Xilinx 的 Vivado、ISE 及相关操作软件的开发的相关内容,学习FPGA设计方法及设计思想的同时,实操结合各类操作软件,会让你在技术学习道路上无比的顺畅,告别技术学习小BUG卡破脑壳,告别目前忽悠性的培训诱导,真正的去学习去实战应用。话不多说,上货。

UART协议驱动设计

作者:郝旭帅 校对:陆辉

本篇实现基于叁芯智能科技的SANXIN -B01 FPGA开发板,以下为配套的教程,如有入手开发板,可以登录官方淘宝店购买,还有配套的学习视频。

SANXIN-B01 Verilog教程-郝旭帅团队

通用异步收发传输器(Universal Asynchronous Receiver / Transmitter),通常称作UART,是一种异步收发传输器。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART通常被集成于其他通讯接口的连接上。

UART是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。在嵌入式设计中,UART用于主机与辅助设备通信,如汽车音响与外接AP之间的通信,与PC机通信包括与监控调试器和其它器件。

  • 并行通信与串行通信

两个芯片或者设备之间传递信息称为通信。对于信息来说可能会有很多位,例如:传输ASCII码(8bit)。在设计时,我们可以采取在两个通信设备之间设计8根数据线,将8bit数据同时发送过去,对方同时接收8位数据。这种同时发送多位数据的传输方式,称为并行通信。

由于某些原因,设备A和设备B之间不能设计多根数据线,只能设计一根数据线。如果此时还是需要传输ASCII码,那么应该怎么办呢?

设备A可以将ASCII码的8位,按照一定的顺序一位一位的发送到数据线上。设备B按照设备A发送的顺序进行一位一位的接收,然后拼接为一个8位。这种通信方式成为串行通信。

将8位或者多位数据拆分为一位一位的发送出去的过程称为并转串。将一位一位接收的数据合并为8位或者多位数据的过程称为串转并。

对于串行通信设备来说,发送方都是在执行并转串,接收方都是在执行串转并。

UART设备为串行通信设备。

  • 全双工通信、半双工通信和单工通信

全双工通信是指在同一时刻信息可以进行双向传输。例如:打电话,说的同时也能听。

半双工通信是指在同一时刻信息只能单向传输,双方都可以进行发送和接收,但是不能够同时发送和接收。例如:对讲机通信。

单工通信是指在通信过程中,只能够设备A发送,设备B接收。例如:听收音机。

SANXIN – B01的开发板上的UART接口设备可以做到半双工通信。

  • UART的通信电平标准

两个设备之间能够互相通信的基础条件为电平标准相同。UART的接口标准有很多,有RS232、RS485等等。

台式PC上一般会有一个DB9,接口标准为RS232。

此接口在各个工业板上也有很多。随着技术的发展,PC上的DB9的接口逐渐被淘汰,换成了USB接口。

我们的开发板上选择使用USB接口,方便大家学习,便于和PC进行通信。

FPGA芯片是无法(较为复杂)发出对应的电平标准,如:RS485、RS232、USB接口电平等。在大多数板卡设计时,都会在FPGA外围添加电平转换器,将FPGA的电平标准转换为通信的电平标准。

我们的开发板上采用USB <->UART(LVCOMS/LVTTL)的电平转换芯片CP2102。所以开发板上的供电端口不仅仅可以供电,还可以进行通信。

对于开发者来说,不用考虑线路的电平标准,只需要考虑如何发送和接收逻辑即可。

  • UART通信协议

双方进行通信时,并不是每时每刻都在进行通信,大多数的通信都是突发性的。此时发送设备需要在发送有效之前需要提前通知接收设备应该要进行接收信息。有的人就会有疑问,发送方有信息就发送,没有信息就停止;接收设备检测有信息发送过来就接收,没有信息就不接收;这样的话不就可以了吗?为什么需要提前通知有信息过来?

双方既然能够进行通信,中间势必建立了通信线路。发送方有信息时,发送信息;没有信息就停止发送。对于发送方来说,没有任何问题。接收方的难度就比较大,在通信线路中,发送方不发送信息时,接收方也会在线路上接收到信息,由于接收方也不知道是不是发送方发送的信息,此时就会造成接收方无法判断是信息还是噪声。

为了解决上述的问题,我们规定了通信协议。

在UART通信协议中,我们规定:

1. 在不通信时,发送高电平。

2. 发送信息时,应该首先发送起始位(1bit、低电平)。可以理解为告诉接收方,应该接收信息了。

3. 发送数据位,由于是串行通信,规定从低位开始发,最后到高位(协议规定信息位可以为4、5、6、7、8)。

4. 校验位(1bit)。可以采用奇校验、偶校验、直接发1、直接发0、不发等5种情况。

5. 停止位(1bit、1.5bit、2bit。高电平)。

6. 空闲位(1bit,高电平)

1bit的时间宽度为多少呢?

在UART协议中,一般常用的波特率(BAUD)为300、600、1200、2400、4800、9600、19200、38400、43000、56000、57600、115200。1秒钟除以波特率就是1bit的时间宽度。

校验位有什么作用?如何进行校验?

在发送信息时,由于要经过很长的线路,中间极有可能受到干扰,导致某些信息位发生反转,最终导致通信失败。校验位的作用为当接收到数据后,进行检验,如果检验不通过,视为接收数据有误,直接丢弃即可。

在校验时,可以选择奇校验和偶校验。奇校验是要求发送的数据位和校验位中1的个数为奇数个;偶校验是要求发送的数据位和校验位中1的个数为偶数个。

  • 发送器设计原理

发送器中加入缓冲器。即上游模块把想要发送的数据写入到发送器中的FIFO里,发送器的控制逻辑检测到FIFO中有数据时,就读出来进行发送。因为发送器发送的速率比较慢,加入FIFO后,上游模块不用等待上一个数据发送完成就可以直接写入后续的数据。

根据提前约定好的波特率和校验方式,发送器的控制逻辑读出FIFO的数据后,按照UART的协议向外发送即可。

  • 接收器设计原理

接收器中加入缓冲器。即接收器的控制逻辑接收到信息后,发送到缓冲器中。由于有缓冲器的存在,主控制器可以不必时时刻刻检查接收状态,只需要一定的时间检测缓冲器中是否有数据即可。

在接收时,起始位的低电平持续时间要超过半个周期才可以认为是开始,避免线路上干扰,引起错误接收。

在接收数据位、校验位、停止位时,采用倍频(16倍频)采样,使用中间的6、7、8、9、10这五次采样值作为采样依据,当五次全部为同一个电平时,即认为本位为此电平值;当四次相同,一次不同时,即认为本位与四次相同的电平值相同;当出现其他情况时,认为线路干扰太大,不做任何接收。

接收完成后,进行帧检测和校验,满足设计要求时,将其中的数据写入到FIFO中。

  • 架构设计和信号说明

此模块命名为uart_drive,共有四个模块构成。

tx_fifo模块:发送缓冲区256深度、宽度为8,该缓冲区设计一个高电平有效的复位。负责将上游想要发送的数据缓存起来。

tx_ctrl模块:发送逻辑控制部分。负责将tx_fifo中的数据按照UART的协议规定发送出去。

rx_ctrl模块:接收逻辑控制部分。负责将外部数据线上的数据按照UART协议规定解析出来,存储到tx_fifo中。

rx_fifo模块:接收缓冲区256深度、宽度为8,该缓冲区设计一个高电平有效的复位。负责将接收逻辑控制部分解析的数据缓存起来,等待着控制器件的读取。

在上述表格中,所有的为端口但是不分配管脚的信号都是由上游逻辑控制给出。本次下板实验时,也会给出上游控制模块。

  • 调用tx_fifo

调用tx_fifo和7.4节中方法类似,其他有几个步骤不太一样,下面给出具体说明。

对于很多的标志信号在设计中用不到,就不再引出。

引出清除信号(高电平有效),并使清除信号同步于读时钟。

  • 调用rx_fifo

调用rx_fifo和7.4节中方法类似,其他有几个步骤不太一样,下面给出具体说明。

对于很多的标志信号在设计中用不到,就不再引出。

引出清除信号(高电平有效),并使清除信号同步于写时钟。

  • tx_ctrl设计实现

参数PARITY为选择的校验方式,1表示为奇校验,0表示为偶校验。

参数BAUD为选择的波特率。

参数F_clk为参考的时钟频率。

参数T为需要计数多个参考时钟周期才可以到波特率规定的时间。

设计代码为:

代码语言:javascript复制
module tx_ctrl (

  input   wire              clk,
  input   wire              rst_n,
  
  input   wire              tx_fifo_rdempty,
  output  reg               tx_fifo_rden,
  input   wire    [7:0]     tx_fifo_rdata,
  
  output  reg               uart_txd
);

  parameter   PARITY   =    1;
  parameter   BAUD     =    9600;
  parameter   F_clk    =    50_000_000;
  
  localparam  T        =    F_clk/BAUD;
  
  reg                       tx_en;
  reg                       tx_done;
  reg         [25:0]        baud_cnt;
  reg         [3:0]         bit_cnt;
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      baud_cnt <= 26'd0;
    else
      if (tx_en == 1'b1 && baud_cnt < T - 1'b1)
        baud_cnt <= baud_cnt   1'b1;
      else
        baud_cnt <= 26'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      bit_cnt <= 4'd0;
    else
      if (tx_en == 1'b1)
        if (baud_cnt == 26'd1)
          bit_cnt <= bit_cnt   1'b1;
        else
          bit_cnt <= bit_cnt;
      else
        bit_cnt <= 4'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      tx_done <= 1'b0;
    else
      if (bit_cnt == 4'd13 && baud_cnt == 26'd1)
        tx_done <= 1'b1;
      else
        tx_done <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      tx_en <= 1'b0;
    else
      if (tx_en == 1'b0 && tx_fifo_rdempty == 1'b0)
        tx_en <= 1'b1;
      else
        if (tx_done == 1'b1)
          tx_en <= 1'b0;
        else
          tx_en <= tx_en;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      tx_fifo_rden <= 1'b0;
    else
      if (tx_en == 1'b0 && tx_fifo_rdempty == 1'b0)
        tx_fifo_rden <= 1'b1;
      else
        tx_fifo_rden <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      uart_txd <= 1'b1;
    else  
      case (bit_cnt)
        4'd0    :   uart_txd <= 1'b1;  //  no busy
        4'd1    :   uart_txd <= 1'b0;  //  start
        4'd2    :   uart_txd <= tx_fifo_rdata[0];  // bit0
        4'd3    :   uart_txd <= tx_fifo_rdata[1];
        4'd4    :   uart_txd <= tx_fifo_rdata[2];
        4'd5    :   uart_txd <= tx_fifo_rdata[3];
        4'd6    :   uart_txd <= tx_fifo_rdata[4];
        4'd7    :   uart_txd <= tx_fifo_rdata[5];
        4'd8    :   uart_txd <= tx_fifo_rdata[6];
        4'd9    :   uart_txd <= tx_fifo_rdata[7]; // bit7
        4'd10   :   uart_txd <= PARITY ? ~(^tx_fifo_rdata) : ^tx_fifo_rdata; // parity bit
        4'd11   :   uart_txd <= 1'b1; // stop 
        4'd12   :   uart_txd <= 1'b1; // no busy
        4'd13   :   uart_txd <= 1'b1; // no busy
        default :   uart_txd <= 1'b1;
      endcase
  end

endmodule

tx_en为发送标志信号,当发送逻辑处于不发送状态时,并且tx_fifo中不空,就将tx_en拉高,启动发送逻辑。当发送完成后,拉高tx_done,将tx_en拉低。其他时间tx_en保持不变。

tx_fifo_rden为tx_fifo的读使能信号,拉高一拍,读出一个数据,所以每次只能拉高一拍。在tx_en为低器件,且外部tx_fifo中有数据时,拉高tx_fifo_rden。tx_fifo_rden和tx_en拉高的条件相同,故而会同步拉高,下一拍时,tx_en会变为高电平,所以此时tx_fifo_rden只会拉高一拍。

baud_cnt是为了记录每发送1bit时间宽度的计数器。在发送使能tx_en拉高后,baud_cnt就开始不断的计数即可。

bit_cnt为此时应该发送UART协议中哪一位的计数器,此计数器在发送使能拉高后,baud_cnt每次计数到1时,bit_cnt进行加1。由于baud_cnt为循环计数,无论在什么时刻bit_cnt加1,后续加1的时间间隔都是一个bit时间宽度。为了能够使tx_en一旦拉高,发送逻辑能够快速发送起始位,所以本设计中选择1。

tx_done信号为发送完成信号,当bit_cnt等于13(起始位1bit、数据位8bit、校验位1bit,停止位1bit和空闲位1bit,共计12bit。本设计中bit_cnt为1时,发送起始位;bit_cnt为12时,发送空闲位)时,证明所有的bit位都已经发送完成,将tx_done拉高。

在算术运算中,假设data的位宽为3,^data=data[1] ^ data[1] ^ data[0],这种运算规则称为缩减运算符。缩减运算符还有“&”和“|”。如果data中1的个数为奇数个,那么缩减异或之后的记过为1,否则为0。当采用奇校验时,数据位和校验位的1个数为奇数,所以校验位应该是~(^tx_fifo_rdata)。当采用偶校验时,数据位和校验位的1个数为偶数,所以校验位应该是^tx_fifo_rdata。

  • rx_ctrl设计实现

参数PARITY为选择的校验方式,1表示为奇校验,0表示为偶校验。

参数BAUD为选择的波特率。

参数F_clk为参考的时钟频率。

参数T为需要计数多个参考时钟周期才可以到16倍波特率规定的时间。

由于外部uart_rxd的信号为异步信号,首先需要打两拍。

设计代码为:

代码语言:javascript复制
module rx_ctrl (

  input     wire                clk,
  input     wire                rst_n,
  
  input     wire                uart_rxd,
  
  output    reg     [7:0]       rx_fifo_data,
  output    reg                 rx_fifo_wren
);

  parameter   PARITY   =    1;
  parameter   BAUD     =    9600;
  parameter   F_clk    =    50_000_000;

  localparam  T        =    F_clk/(16*BAUD);
  
  reg                           rxd_r;
  reg                           rxd_rr;
  reg                           rx_en;
  reg                           rx_done;
  reg               [25:0]      start_cnt;
  reg               [25:0]      baudx16_cnt;
  reg               [7:0]       cap_cnt;
  reg               [151:0]     cap_buf;
  reg               [8:0]       rx_buf;
  reg               [8:0]       rx_error;
  reg                           rx_done_r;

  initial rxd_r = 1'b1;
  initial rxd_rr = 1'b1;
  
  always @ (posedge clk) rxd_r <= uart_rxd;
  always @ (posedge clk) rxd_rr <= rxd_r;
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      start_cnt <= 26'd0;
    else
      if (rx_en == 1'b0 && rxd_rr == 1'b0)
        start_cnt <= start_cnt   1'b1;
      else
        start_cnt <= 26'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rx_en <= 1'b0;
    else
      if (start_cnt == 8 * T)
        rx_en <= 1'b1;
      else
        if (rx_done == 1'b1)
          rx_en <= 1'b0;
        else
          rx_en <= rx_en;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      baudx16_cnt <= 26'd0;
    else
      if (rx_en == 1'b1 && baudx16_cnt < T - 1'b1)
        baudx16_cnt <= baudx16_cnt   1'b1;
      else
        baudx16_cnt <= 26'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      cap_buf <= 152'd0;
    else
      if (rx_en == 1'b1 && baudx16_cnt == T - 1'b1)
        cap_buf <= {cap_buf[150:0], rxd_rr};
      else
        cap_buf <= cap_buf;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      cap_cnt <= 8'd0;
    else
      if (rx_en == 1'b1)
        if (baudx16_cnt == T - 1'b1)
          cap_cnt <= cap_cnt   1'b1;
        else  
          cap_cnt <= cap_cnt;
      else
        cap_cnt <= 8'd0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rx_done <= 1'b0;
    else
      if (cap_cnt == 8'd152 && baudx16_cnt == 26'd2)
        rx_done <= 1'b1;
      else
        rx_done <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0) begin
      rx_buf <= 9'd0;
      rx_error <= 9'd0;
    end
    else 
      if (rx_done == 1'b1) begin
        {rx_error[8],rx_buf[8]} <= decoder_rxd(cap_buf[10:6]);
        {rx_error[7],rx_buf[7]} <= decoder_rxd(cap_buf[26:22]);
        {rx_error[6],rx_buf[6]} <= decoder_rxd(cap_buf[42:38]);
        {rx_error[5],rx_buf[5]} <= decoder_rxd(cap_buf[58:54]);
        {rx_error[4],rx_buf[4]} <= decoder_rxd(cap_buf[74:70]);
        {rx_error[3],rx_buf[3]} <= decoder_rxd(cap_buf[90:86]);
        {rx_error[2],rx_buf[2]} <= decoder_rxd(cap_buf[106:102]);
        {rx_error[1],rx_buf[1]} <= decoder_rxd(cap_buf[122:118]);
        {rx_error[0],rx_buf[0]} <= decoder_rxd(cap_buf[138:134]);
      end
      else begin
        rx_error <= rx_error;
        rx_buf <= rx_buf;
      end
  end
  
  function [1:0] decoder_rxd;
    
    input   [4:0]   data;
    
    reg     [2:0]   num;
    
    begin
      
      num = data[4]   data[3]   data[2]   data[1]   data[0];
      decoder_rxd[1] = (num < 3'd4)&&(num > 3'd1) ? 1'b1 : 1'b0;
      decoder_rxd[0] = (num > 3'd3) ? 1'b1 : 1'b0;
      
    end
  endfunction
  
  initial rx_done_r = 1'b0;
  
  always @ (posedge clk) rx_done_r <= rx_done;
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rx_fifo_data <= 8'd0;
    else
      if (rx_done_r == 1'b1)
        rx_fifo_data <= rx_buf[7:0];
      else
        rx_fifo_data <= rx_fifo_data;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rx_fifo_wren <= 1'b0;
    else
      if (rx_done_r == 1'b1 && (|rx_error == 1'b0) && ^rx_buf == PARITY)
        rx_fifo_wren <= 1'b1;
      else
        rx_fifo_wren <= 1'b0;
  end
endmodule

start_cnt为记录在没有启动接收时,低电平的持续时间。

当start_cnt的低电平持续时间等于8个T时(16个T为一个bit的时间宽度),认为此起始位有效,拉高rx_en。当接收完成后,rx_en拉低,其他时间,rx_en保持不变。

当rx_en拉高后,baudx16_cnt不断开始计数,最大值为16倍频的宽度值。

当rx_en拉高后,且baudx16_cnt为最大值时,就开始进行移位采样。由于起始位只判断一半,所以半个起始位、8个数据位、1个奇偶校验位,在16倍频采样的情况下,一共会采样152次。

cap_cnt为采样的计数,当采样到152时,且baudx16_cnt等于2时,认为采样结束。利用baudx16_cnt等于2只是为了产生的tx_done为一个脉冲。

采样结束后,利用函数的特性,得出8个数据位、1个校验位,并且得出9个线路是不是出现干扰的标志rx_error。

得到最终结果后,将数据进行输出。

产生rx_fifo_wren时,进行了线路干扰检测判断,停止位判断,以及奇偶校验判断,当都符合预期后,输出为1。其他情况输出为0。

  • 顶层设计

顶层设计只负责将上述四个模块按照架构图的方式进行连接。

设计代码为:

代码语言:javascript复制
module uart_drive (

  input   wire            clk,
  input   wire            rst_n,
  
  input   wire            tx_clk,
  input   wire            tx_en,
  input   wire    [7:0]   tx_data,
  output  wire            tx_full,
  output  wire            uart_txd,
  
  input   wire            uart_rxd,
  input   wire            rx_clk,
  output  wire            rx_empty,
  input   wire            rx_en,
  output  wire    [7:0]   rx_data
);
   
  parameter   PARITY   =    1;
  parameter   BAUD     =    9600;
  parameter   F_clk    =    50_000_000;

  wire                    tx_fifo_rdempty;
  wire                    tx_fifo_rden;
  wire            [7:0]   tx_fifo_rdata;
  wire                    rx_fifo_wren;
  wire            [7:0]   rx_fifo_data;
  
  tx_fifo  tx_fifo_inst (
      .aclr         ( ~rst_n ),
      .data         ( tx_data ),
      .rdclk        ( clk ),
      .rdreq        ( tx_fifo_rden ),
      .wrclk        ( tx_clk ),
      .wrreq        ( tx_en ),
      .q            ( tx_fifo_rdata ),
      .rdempty      ( tx_fifo_rdempty ),
      .wrfull       ( tx_full )
    );
  
  tx_ctrl # (
      .PARITY       (PARITY),
      .BAUD         (BAUD),  
      .F_clk        (F_clk)
    )tx_ctrl_inst(
      .clk          (clk),
      .rst_n        (rst_n),
      
      .tx_fifo_rdempty(tx_fifo_rdempty),
      .tx_fifo_rden (tx_fifo_rden),
      .tx_fifo_rdata(tx_fifo_rdata),
      
      .uart_txd     (uart_txd)
    );
    
  rx_ctrl # (
      .PARITY       (PARITY),
      .BAUD         (BAUD),  
      .F_clk        (F_clk)
    ) rx_ctrl_inst (

      .clk          (clk),
      .rst_n        (rst_n),
      
      .uart_rxd     (uart_rxd),
      
      .rx_fifo_data (rx_fifo_data),
      .rx_fifo_wren (rx_fifo_wren)
    );
  
  rx_fifo  rx_fifo_inst (
      .aclr         ( ~rst_n ),
      .data         ( rx_fifo_data ),
      .rdclk        ( rx_clk ),
      .rdreq        ( rx_en ),
      .wrclk        ( clk ),
      .wrreq        ( rx_fifo_wren ),
      .q            ( rx_data ),
      .rdempty      ( rx_empty )
    );
    
endmodule

parameter所定义的参数,在例化时,可以对它进行重新赋值,方便我们参数化设计。

综合出来的RTL视图如下:

  • RTL仿真

在仿真中,将uart_rxd和uart_txd相连接,实现自发自收。

对于tx_clk和rx_clk都采用clk连接。

仿真代码如下:

代码语言:javascript复制
`timescale 1ns/1ps

module uart_drive_tb;
  
  reg                 clk;
  reg                 rst_n;
  
  reg                 tx_en;
  reg     [7:0]       tx_data;
  wire                tx_full;
  wire                uart_sda;
  
  reg                 rx_en;
  wire                rx_empty;
  wire    [7:0]       rx_data;
  
  uart_drive uart_drive_inst(

      .clk            (clk),
      .rst_n          (rst_n),
      
      .tx_clk         (clk),
      .tx_en          (tx_en),
      .tx_data        (tx_data),
      .tx_full        (tx_full),
      .uart_txd       (uart_sda),
      
      .uart_rxd       (uart_sda),
      .rx_clk         (clk),
      .rx_empty       (rx_empty),
      .rx_en          (rx_en),
      .rx_data        (rx_data)
    );
    
  initial clk = 1'b0;
  always # 10 clk = ~clk;
  
  initial begin
    rst_n = 1'b0;
    tx_data = 8'd0;
    tx_en = 1'b0;
    rx_en = 1'b0;
    # 201
    rst_n = 1'b1;
    # 200;
    repeat (5) begin
      @ (posedge clk);
      # 2;
      tx_data = {$random} % 256;
      tx_en = 1'b1;
    end
    
    @ (posedge clk);
    # 2;
    tx_data = 8'd0;
    tx_en = 1'b0;
    
    repeat (5) begin
      @ (negedge rx_empty);
      # 2;
      rx_en = 1'b1;
      @ (posedge clk);
      # 2;
      rx_en = 1'b0;
      # 100;
    end
    
    # 50000;
    $stop;
  end

endmodule

复位结束后,采用写入随机数的方式,写入了五个数据。从RTL仿真图中可以看到这个五个数据为24、81、09、63、0d。

接收端口时刻监测rx_empty是不是为假值,一旦为假值,就证明有接收到数据,立刻拉高一拍rx_en,进行读出。大概经过十几毫秒后,仿真会自动停止。

从RTL仿真图中可以看到,读出的数据为24、81、09、63、0d这五个数据,和我们写入的相同。

  • 下板验证

由于此设计外设接口众多,并且在使用时,都是由上游控制器进行控制。本小节编写上游控制器,实现回环测试(将接收到的数据,全部在发送出去)。

在测试时,rx_clk和tx_clk都采用系统时钟。

本模块命名为uart_drive_example。

test_ctrl模块负责监控rx_empty是否为假值,一旦有数据接收到就可以读出,发送到发送缓冲区中。

此模块采用状态实现。共分为WAIT_RX(等待UART接收数据),WAIT_RD(等待读数据),SEND(发送数据)。

将rx_en置高后,rx_data需要等待一拍才会有效。

状态转移图如下:

设计代码如下:

代码语言:javascript复制
module test_ctrl (

  input   wire              clk,
  input   wire              rst_n,
  
  input   wire              rx_empty,
  output  reg               rx_en,
  input   wire      [7:0]   rx_data,
  
  output  reg               tx_en,
  output  reg       [7:0]   tx_data
);

  localparam  WAIT_RX     =   3'b001;
  localparam  WAIT_RD     =   3'b010;
  localparam  SEND        =   3'b100;
  
  reg               [2:0]   c_state;
  reg               [2:0]   n_state;
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      c_state <= 3'd0;
    else
      c_state <= n_state;
  end

  always @ * begin
    case (c_state)
      WAIT_RX   :   begin
        if (rx_empty == 1'b1)
          n_state = WAIT_RX;
        else
          n_state = WAIT_RD;
      end
      
      WAIT_RD   :   begin
        n_state = SEND;
      end
      
      SEND      :   begin
        n_state = WAIT_RX;
      end
      
      default   :   n_state = WAIT_RX;
    endcase
  end

  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      rx_en <= 1'b0;
    else
      if (c_state == WAIT_RX && rx_empty == 1'b0)
        rx_en <= 1'b1;
      else
        rx_en <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      tx_en <= 1'b0;
    else
      if (c_state == SEND)
        tx_en <= 1'b1;
      else
        tx_en <= 1'b0;
  end
  
  always @ (posedge clk, negedge rst_n) begin
    if (rst_n == 1'b0)
      tx_data <= 8'd0;
    else
      if (c_state == SEND)
        tx_data <= rx_data;
      else
        tx_data <= 8'd0;
  end
  
endmodule

uart_drive_example负责将test_ctrl和uart_drive联系起来。

设计代码如下:

代码语言:javascript复制
module uart_drive_example (

  input   wire              clk,
  input   wire              rst_n,
  
  input   wire              uart_rxd,
  output  wire              uart_txd
);

  wire                      rx_empty;
  wire                      rx_en;
  wire          [7:0]       rx_data;
  wire                      tx_en;
  wire          [7:0]       tx_data;
  
  test_ctrl test_ctrl_inst(

      .clk          (clk),
      .rst_n        (rst_n),
      
      .rx_empty     (rx_empty),
      .rx_en        (rx_en),
      .rx_data      (rx_data),
      
      .tx_en        (tx_en),
      .tx_data      (tx_data)
    );

  uart_drive uart_drive_inst(

      .clk            (clk),
      .rst_n          (rst_n),
      
      .tx_clk         (clk),
      .tx_en          (tx_en),
      .tx_data        (tx_data),
      .tx_full        (),
      .uart_txd       (uart_txd),
      
      .uart_rxd       (uart_rxd),
      .rx_clk         (clk),
      .rx_empty       (rx_empty),
      .rx_en          (rx_en),
      .rx_data        (rx_data)
    );
    
endmodule

将uart_drive_example设置为顶层。

在file界面,右击uart_drive_example文件,选择set as top level……。

进行综合分析后,分配管脚,形成配置文件。

  • 安装驱动

将开发板与电脑相连接,打开设备管理器。可以看到在其他设备中出现了CP2102 USB to UART Bridge Controller,并且前面有一个黄色的感叹号,标志着此端口还不能使用。

在我们的开发板上,使用的USB <->UART的芯片就是CP2102,所以在此需要安装驱动。

打开04_串口驱动,安装CP210x_windows_drivers。

文件中有两个安装程序。一个是CP210xVCPInstaller_x64,另外一个是CP210xVCPInstaller_x86。此时我们需要查看自己电脑的系统是多少位的,打开控制面板中的系统就可以看自己的电脑是多少位的操作系统。

64位的操作系统,安装CP210xVCPInstaller_x64,32位的操作系统安装CP210xVCPInstaller_x86。

双击对应的安装程序后,点击下一步。

点击“我接受”,点击下一步。

等待一段时间后,选择完成即可。

此时对开发板进行断电再上电的处理,就可以在设备管理的端口(COM和LPT)中看到安装好的程序,并且记住后面的COM口的编号,一会儿需要使用。在此,笔者的PC上的COM口为COM3。

  • 安装串口助手软件

如果电脑上有其他串口助手软件的,也可以使用。

电脑上没有串口助手的软件的,可以安装我们提供的软件。

打开09_工具,安装串口猎人。

双击串口猎人安装程序,点击下一步。

选择安装位置。点击下一步。

点击下一步,开始安装。

等待一段时间后,安装成功。点击完成。

此软件安装好,并不会在桌面上形成快捷方式。可以在程序列表中找到这个软件。

打开后,可以看到串口猎人的界面。

  • 串口助手调试

利用quartus将生成的配置文件下载到开发板中。

将串口助手配置为,端口号:com3(每个人会有不同,请去设备管理器中查找),波特率:9600,校验位:Odd(奇校验)。

点击启动串行端口。

把所有的信息全部清除一下。

清除后,再发码区,随便两个十六进制的数,点击发送。

开发板中配置的是回环测试的代码,所以发送的数据会在收吗区显示出来。

开发板上还有两个LED用来指示发送接收数据。当发送或者接收数据时,对应的LED会点亮。

将发码区和收码区清除一下,然后都改为字符串。然后发送任意一串字符,进行测试。

我们可以设置其他的波特率或者校验方式,进行其他测试。在此就不再叙述。

在应用时,只需要将uart_drive例化使用即可。

- End -

0 人点赞