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

运行与判定

  1. 在Vivado/ModelSim中创建工程,添加uart_rx.v与testbench文件;
  2. 运行仿真,观察波形中rx_done信号是否产生高脉冲;
  3. 检查rx_data输出值:若发送0xA5后rx_data=0xA5且rx_done出现脉冲,则验证通过;
  4. 可修改send_byte参数测试0x00、0xFF、0x55等多组数据,确认均能正确恢复。

常见问题

  • 采样点偏移:若数据错误,检查DIV_CNT计算是否正确,确保sample_clk频率为波特率的16倍;
  • 亚稳态:rx输入必须经两级同步器后再进入状态机,否则高波特率下可能误判起始位;
  • 停止位校验:若rx_filter在STOP状态采样为低,说明发送端停止位异常或波特率不匹配,模块会丢弃该帧。