4. UART接收模块
任务目标
实现一个标准UART接收模块,完成起始位检测、波特率时钟采样、8位数据恢复与停止位校验,并通过仿真验证接收数据的正确性。
实现方案
采用过采样+中间点采样策略:系统时钟经分频产生16倍波特率的采样时钟,在检测到起始位下降沿后,于每个数据位的中间时刻采样,以规避信号边沿抖动。状态机包含IDLE、START、DATA、STOP四个状态。
关键完整代码
module uart_rx #(
parameter CLK_FREQ = 50_000_000, // 系统时钟50MHz
parameter BAUD_RATE = 9600 // 波特率9600
)(
input wire clk,
input wire rst_n,
input wire rx, // UART接收线
output reg [7:0] rx_data, // 接收到的8位数据
output reg rx_done // 接收完成脉冲
);
// 分频参数:16倍过采样
localparam DIV_CNT = CLK_FREQ / (BAUD_RATE * 16) - 1;
localparam BIT_CNT = 16; // 每个数据位的采样时钟数
reg [15:0] clk_cnt;
reg sample_clk; // 16倍波特率采样时钟
reg [3:0] sample_cnt; // 采样计数器(0~15)
reg [2:0] bit_cnt; // 数据位计数(0~7)
reg [1:0] state;
reg rx_sync1, rx_sync2; // 两级同步消除亚稳态
reg rx_filter; // 滤波后的rx信号
localparam IDLE = 2'd0;
localparam START = 2'd1;
localparam DATA = 2'd2;
localparam STOP = 2'd3;
// 时钟分频产生采样时钟
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_cnt <= 0;
sample_clk <= 1'b0;
end else if (clk_cnt == DIV_CNT) begin
clk_cnt <= 0;
sample_clk <= 1'b1; // 单周期脉冲
end else begin
clk_cnt <= clk_cnt + 1'b1;
sample_clk <= 1'b0;
end
end
// 输入同步与毛刺滤波
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_sync1 <= 1'b1;
rx_sync2 <= 1'b1;
rx_filter <= 1'b1;
end else begin
rx_sync1 <= rx;
rx_sync2 <= rx_sync1;
// 简单滤波:连续两个采样为低才认为低电平
if (rx_sync1 == 1'b0 && rx_sync2 == 1'b0)
rx_filter <= 1'b0;
else if (rx_sync1 == 1'b1 && rx_sync2 == 1'b1)
rx_filter <= 1'b1;
end
end
// 接收状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
sample_cnt <= 4'd0;
bit_cnt <= 3'd0;
rx_data <= 8'd0;
rx_done <= 1'b0;
end else begin
rx_done <= 1'b0; // 默认无完成脉冲
case (state)
IDLE: begin
if (rx_filter == 1'b0) begin // 检测到起始位下降沿
state <= START;
sample_cnt <= 4'd0;
end
end
START: begin
if (sample_clk) begin
if (sample_cnt == 4'd7) begin // 起始位中点
// 确认仍是低电平,否则视为毛刺
if (rx_filter == 1'b0) begin
state <= DATA;
bit_cnt <= 3'd0;
sample_cnt <= 4'd0;
end else begin
state <= IDLE;
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
end
DATA: begin
if (sample_clk) begin
if (sample_cnt == 4'd15) begin // 到达数据位中点
rx_data[bit_cnt] <= rx_filter;
if (bit_cnt == 3'd7) begin
state <= STOP;
sample_cnt <= 4'd0;
end else begin
bit_cnt <= bit_cnt + 1'b1;
sample_cnt <= 4'd0;
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
end
STOP: begin
if (sample_clk) begin
if (sample_cnt == 4'd15) begin
// 停止位应为高电平
if (rx_filter == 1'b1) begin
rx_done <= 1'b1; // 接收成功
end
state <= IDLE;
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
end
endcase
end
end
endmodule
仿真验证
编写testbench模拟发送数据0xA5(二进制1010_0101),验证接收结果。关键验证点:rx_done产生高脉冲时,rx_data应等于0xA5。
// testbench 关键片段
reg clk = 0;
reg rx = 1;
wire [7:0] data_out;
wire done;
uart_rx #(.CLK_FREQ(50_000_000), .BAUD_RATE(9600)) uut (
.clk(clk), .rst_n(rst_n), .rx(rx),
.rx_data(data_out), .rx_done(done)
);
// 发送1字节:起始位(0) + 8数据位(LSB first) + 停止位(1)
task send_byte(input [7:0] byte_data);
integer i;
begin
rx = 0; // 起始位
#104167; // 1位时间 ≈ 104.167us @9600
for (i = 0; i < 8; i = i + 1) begin
rx = byte_data[i]; // LSB first
#104167;
end
rx = 1; // 停止位
#104167;
end
endtask
initial begin
rst_n = 0; #100 rst_n = 1;
#200;
send_byte(8'hA5);
#200;
if (done && data_out == 8'hA5)
$display("TEST PASS: rx_data = %02h", data_out);
else
$display("TEST FAIL: rx_data = %02h, done = %b", data_out, done);
#500 $finish;
end
运行与判定
- 在Vivado/ModelSim中创建工程,添加uart_rx.v与testbench文件;
- 运行仿真,观察波形中rx_done信号是否产生高脉冲;
- 检查rx_data输出值:若发送0xA5后rx_data=0xA5且rx_done出现脉冲,则验证通过;
- 可修改send_byte参数测试0x00、0xFF、0x55等多组数据,确认均能正确恢复。
常见问题
- 采样点偏移:若数据错误,检查DIV_CNT计算是否正确,确保sample_clk频率为波特率的16倍;
- 亚稳态:rx输入必须经两级同步器后再进入状态机,否则高波特率下可能误判起始位;
- 停止位校验:若rx_filter在STOP状态采样为低,说明发送端停止位异常或波特率不匹配,模块会丢弃该帧。