第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
你看,顶层模块其实没做啥复杂逻辑。就是把两个模块的端口连起来。这里有个细节:txd和rxd是物理引脚,在FPGA里要分配到对应的IO口上。
top,结果项目大了以后,根本分不清是哪个模块的顶层。建议用模块名_top的格式。
4.2 回环测试设计:最简单的自检方法
回环测试,就是把发送引脚和接收引脚在芯片内部连起来。这样发出去的数据能自己收回来,验证整个链路是否正常。
为什么要做回环测试?说白了,这是最省事的自检方法。你不需要外接串口线,不需要串口助手,甚至不需要示波器。只要在FPGA内部把txd和rxd短接,然后发一个字节,看看收回来的是不是一样的。
回环测试的顶层设计:
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其实有套路。我一般按这个步骤来:
- 生成时钟和复位——这是最基本的
- 模拟发送过程——给tx_start和tx_data赋值
- 检查接收结果——看rx_done和rx_data对不对
- 做回环验证——把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
4.4 常见问题排查:毛刺与亚稳态
做FPGA设计,毛刺和亚稳态是绕不开的两个坑。我刚开始做UART时,在这上面栽过跟头。
4.4.1 毛刺问题
毛刺,就是信号上那些不该有的窄脉冲。在UART里,毛刺最容易出现在两个地方:
- 接收引脚的边沿检测——如果rxd上有毛刺,会被误判为起始位
- 波特率计数器的比较信号——组合逻辑产生的比较结果可能有毛刺
怎么解决?我常用的方法:
- 加寄存器打拍——用两级寄存器同步,滤掉窄毛刺
- 用边沿检测代替电平检测——检测下降沿时,用两级寄存器的异或结果
- 计数器比较用寄存器输出——不要直接用组合逻辑的比较结果
举个例子,接收模块里的边沿检测,我一般这么写:
// 两级同步,消除毛刺
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)频繁出错。
解决方案其实很简单:
- 异步信号必须用两级以上寄存器同步——这是铁律
- 同步后的信号才能用于逻辑判断——不要直接用原始信号
- 如果跨时钟域传递多位数据,用FIFO或者握手协议
4.5 本章小结
这一章我们做了三件事:把发送和接收模块集成到顶层,设计了回环测试电路,写了Testbench做仿真验证。最后聊了毛刺和亚稳态这两个实际工程中经常遇到的问题。
回环测试是个好东西。我建议你在每个UART项目里都保留这个功能。调试时打开回环模式,可以快速确认硬件链路是否正常。量产时也可以用它做自检。
至于毛刺和亚稳态,嗯,这两个问题不会消失。你只能通过良好的设计习惯来规避它们。记住:异步信号要同步,组合逻辑要寄存,边沿检测要打拍。做到这三点,大部分问题都能避免。