第4章:UART顶层集成与验证

好,前面几章我们把UART的发送和接收模块都搞定了。现在该把它们连起来,看看整个系统能不能跑通。这一章,我带你做顶层集成、回环测试,还有仿真验证。最后聊聊那些让人头疼的毛刺和亚稳态问题。

4.1 顶层模块设计:把发送和接收串起来

顶层模块说白了就是个壳子。它把tx模块和rx模块实例化,再把数据通路连好。我个人习惯把顶层模块命名为uart_top,这样一看就知道是干啥的。

先看代码:

module uart_top (
    input  wire       clk,          // 系统时钟
    input  wire       rst_n,        // 异步复位,低有效
    input  wire       tx_start,     // 发送启动信号
    input  wire [7:0] tx_data,      // 待发送数据
    output wire       tx_busy,      // 发送忙标志
    output wire       tx_done,      // 发送完成标志
    output wire       txd,          // UART发送引脚
    input  wire       rxd,          // UART接收引脚
    output wire       rx_done,      // 接收完成标志
    output wire [7:0] rx_data       // 接收到的数据
);

    // 实例化发送模块
    uart_tx u_uart_tx (
        .clk      (clk),
        .rst_n    (rst_n),
        .tx_start (tx_start),
        .tx_data  (tx_data),
        .tx_busy  (tx_busy),
        .tx_done  (tx_done),
        .txd      (txd)
    );

    // 实例化接收模块
    uart_rx u_uart_rx (
        .clk      (clk),
        .rst_n    (rst_n),
        .rxd      (rxd),
        .rx_done  (rx_done),
        .rx_data  (rx_data)
    );

endmodule

你看,顶层模块其实没做啥复杂逻辑。就是把两个模块的端口连起来。这里有个细节:txdrxd是物理引脚,在FPGA里要分配到对应的IO口上。

我的经验:顶层模块的命名要规范。我见过有人把顶层叫top,结果项目大了以后,根本分不清是哪个模块的顶层。建议用模块名_top的格式。

4.2 回环测试设计:最简单的自检方法

回环测试,就是把发送引脚和接收引脚在芯片内部连起来。这样发出去的数据能自己收回来,验证整个链路是否正常。

为什么要做回环测试?说白了,这是最省事的自检方法。你不需要外接串口线,不需要串口助手,甚至不需要示波器。只要在FPGA内部把txdrxd短接,然后发一个字节,看看收回来的是不是一样的。

回环测试的顶层设计:

module uart_loopback (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       btn_start,    // 按键触发发送
    input  wire [7:0] dip_sw,       // 拨码开关设置数据
    output wire [7:0] led           // LED显示接收数据
);

    wire tx_start;
    wire [7:0] tx_data;
    wire tx_busy;
    wire tx_done;
    wire txd;
    wire rxd;
    wire rx_done;
    wire [7:0] rx_data;

    // 按键消抖和边沿检测
    // ...(省略具体实现)

    // 回环连接:发送引脚直接连到接收引脚
    assign rxd = txd;

    // 顶层UART实例
    uart_top u_uart_top (
        .clk      (clk),
        .rst_n    (rst_n),
        .tx_start (tx_start),
        .tx_data  (tx_data),
        .tx_busy  (tx_busy),
        .tx_done  (tx_done),
        .txd      (txd),
        .rxd      (rxd),
        .rx_done  (rx_done),
        .rx_data  (rx_data)
    );

    // 接收数据锁存到LED
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            led <= 8'd0;
        else if (rx_done)
            led <= rx_data;
    end

endmodule
关键点:回环测试中,rxd = txd这个赋值必须在顶层完成。不能在UART内部做,否则会破坏模块的独立性。

我曾经在一个项目里犯过这个错误。把回环逻辑写在了uart_tx模块里,结果后面想单独测试接收模块时,发现端口被占用了。嗯,从那以后我坚持:回环逻辑只放在顶层。

4.3 使用Testbench进行功能仿真

仿真这一步,我建议你认真对待。很多新手觉得仿真麻烦,直接上板子调。结果出了问题,抓耳挠腮半天找不到原因。

写Testbench其实有套路。我一般按这个步骤来:

  1. 生成时钟和复位——这是最基本的
  2. 模拟发送过程——给tx_start和tx_data赋值
  3. 检查接收结果——看rx_done和rx_data对不对
  4. 做回环验证——把txd和rxd连起来,发一个收一个

来看一个完整的Testbench:

`timescale 1ns / 1ps

module tb_uart_top;

    reg         clk;
    reg         rst_n;
    reg         tx_start;
    reg  [7:0]  tx_data;
    wire        tx_busy;
    wire        tx_done;
    wire        txd;
    wire        rxd;
    wire        rx_done;
    wire [7:0]  rx_data;

    // 时钟生成:50MHz -> 20ns周期
    always #10 clk = ~clk;

    // 实例化顶层模块
    uart_top u_uart_top (
        .clk      (clk),
        .rst_n    (rst_n),
        .tx_start (tx_start),
        .tx_data  (tx_data),
        .tx_busy  (tx_busy),
        .tx_done  (tx_done),
        .txd      (txd),
        .rxd      (rxd),
        .rx_done  (rx_done),
        .rx_data  (rx_data)
    );

    // 回环连接
    assign rxd = txd;

    // 测试序列
    initial begin
        // 初始化
        clk = 0;
        rst_n = 0;
        tx_start = 0;
        tx_data = 8'd0;

        // 释放复位
        #100 rst_n = 1;

        // 等待系统稳定
        #200;

        // 发送第一个字节:0xA5
        @(posedge clk);
        tx_data = 8'hA5;
        tx_start = 1;
        @(posedge clk);
        tx_start = 0;

        // 等待发送完成
        wait(tx_done);
        $display("发送完成: 0x%h", tx_data);

        // 等待接收完成
        wait(rx_done);
        $display("接收完成: 0x%h", rx_data);

        // 检查回环结果
        if (rx_data == tx_data)
            $display("回环测试通过!");
        else
            $display("回环测试失败!");

        // 发送第二个字节:0x5A
        #500;
        @(posedge clk);
        tx_data = 8'h5A;
        tx_start = 1;
        @(posedge clk);
        tx_start = 0;

        wait(tx_done);
        wait(rx_done);

        if (rx_data == tx_data)
            $display("第二次回环测试通过!");
        else
            $display("第二次回环测试失败!");

        #1000;
        $finish;
    end

endmodule
仿真技巧:我习惯在Testbench里加$display打印信息。这样看仿真日志就能知道结果,不用每次都盯着波形图看。特别是做回归测试时,几百个case跑下来,看日志比看波形高效多了。

4.4 常见问题排查:毛刺与亚稳态

做FPGA设计,毛刺和亚稳态是绕不开的两个坑。我刚开始做UART时,在这上面栽过跟头。

4.4.1 毛刺问题

毛刺,就是信号上那些不该有的窄脉冲。在UART里,毛刺最容易出现在两个地方:

  • 接收引脚的边沿检测——如果rxd上有毛刺,会被误判为起始位
  • 波特率计数器的比较信号——组合逻辑产生的比较结果可能有毛刺

怎么解决?我常用的方法:

  1. 加寄存器打拍——用两级寄存器同步,滤掉窄毛刺
  2. 用边沿检测代替电平检测——检测下降沿时,用两级寄存器的异或结果
  3. 计数器比较用寄存器输出——不要直接用组合逻辑的比较结果

举个例子,接收模块里的边沿检测,我一般这么写:

// 两级同步,消除毛刺
reg rxd_sync1, rxd_sync2;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rxd_sync1 <= 1'b1;
        rxd_sync2 <= 1'b1;
    end else begin
        rxd_sync1 <= rxd;
        rxd_sync2 <= rxd_sync1;
    end
end

// 下降沿检测
wire rxd_falling = rxd_sync2 & ~rxd_sync1;
注意:两级同步只能滤掉宽度小于一个时钟周期的毛刺。如果毛刺很宽,需要增加同步级数或者用数字滤波器。

4.4.2 亚稳态问题

亚稳态,说白了就是信号在时钟沿附近变化,导致寄存器采样到不确定的值。这在跨时钟域或者异步信号处理时特别常见。

UART里最容易出现亚稳态的地方:

  • rxd信号进入系统时钟域——rxd是异步的,必须同步
  • 波特率时钟和系统时钟的交互——如果用了多时钟域,要注意同步

我曾经在一个项目里,接收模块偶尔会收到错误数据。查了两天才发现,是rxd的同步没做好。当时用的是单级寄存器,结果在高速率下(比如115200bps)频繁出错。

解决方案其实很简单:

  1. 异步信号必须用两级以上寄存器同步——这是铁律
  2. 同步后的信号才能用于逻辑判断——不要直接用原始信号
  3. 如果跨时钟域传递多位数据,用FIFO或者握手协议
记住:亚稳态无法完全消除,但可以通过同步器把概率降到极低。两级同步器的MTBF(平均无故障时间)通常能达到几百年甚至更久,对大多数应用足够了。

4.5 本章小结

这一章我们做了三件事:把发送和接收模块集成到顶层,设计了回环测试电路,写了Testbench做仿真验证。最后聊了毛刺和亚稳态这两个实际工程中经常遇到的问题。

回环测试是个好东西。我建议你在每个UART项目里都保留这个功能。调试时打开回环模式,可以快速确认硬件链路是否正常。量产时也可以用它做自检。

至于毛刺和亚稳态,嗯,这两个问题不会消失。你只能通过良好的设计习惯来规避它们。记住:异步信号要同步,组合逻辑要寄存,边沿检测要打拍。做到这三点,大部分问题都能避免。

我的建议:如果你在调试UART时遇到奇怪的问题,先检查同步器有没有做好。我遇到过不下五次,最后发现都是同步器的问题。别问我怎么知道的...

专注资料整理