4. UART串口接收模块设计

任务目标

设计一个可综合的UART接收模块,以9600波特率(8-N-1帧格式)接收外部串行数据,并将接收到的字节并行输出。模块需具备接收完成标志,便于上层逻辑读取数据。

实现方案

采用状态机实现UART接收协议:检测起始位下降沿后,以16倍波特率时钟采样数据位中心,避免误采样。设计要点:

  • 系统时钟50MHz,波特率9600,需生成波特率分频时钟(或采样使能脉冲)
  • 状态机包含:IDLE、START、DATA、STOP四个状态
  • 数据位采用中间采样,每位采样3次取多数表决(抗干扰)
  • 输出8位并行数据及单周期接收完成脉冲

完整代码(Verilog)

module uart_rx #(
    parameter CLK_FREQ = 50_000_000,  // 系统时钟50MHz
    parameter BAUD_RATE = 9600        // 波特率
)(
    input  wire       clk,            // 系统时钟
    input  wire       rst_n,          // 异步复位,低有效
    input  wire       rx,             // 串行输入
    output reg  [7:0] rx_data,        // 并行数据输出
    output reg        rx_done         // 接收完成脉冲(单周期高)
);

    // 分频参数:每比特16个采样点
    localparam DIV_CNT = CLK_FREQ / (BAUD_RATE * 16) - 1;
    localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;

    reg [15:0] clk_cnt;      // 采样时钟计数器
    reg        sample_en;    // 采样使能(16倍波特率脉冲)
    reg [3:0]  bit_cnt;      // 数据位计数(0~7)
    reg [2:0]  sample_cnt;   // 每位采样计数(0~7,取中间3次)
    reg [1:0]  state, next_state;
    reg [7:0]  shift_reg;    // 移位寄存器
    reg [1:0]  rx_sync;      // 输入同步器

    // 异步复位与时钟同步
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_sync <= 2'b11;
        end else begin
            rx_sync <= {rx_sync[0], rx};
        end
    end

    wire rx_clean = rx_sync[1];

    // 采样时钟生成
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_cnt <= 16'd0;
            sample_en <= 1'b0;
        end else begin
            if (clk_cnt == DIV_CNT) begin
                clk_cnt <= 16'd0;
                sample_en <= 1'b1;
            end else begin
                clk_cnt <= clk_cnt + 1'b1;
                sample_en <= 1'b0;
            end
        end
    end

    // 状态机
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
        end else if (sample_en) begin
            state <= next_state;
        end
    end

    // 状态转移与数据处理
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            next_state <= IDLE;
            bit_cnt <= 4'd0;
            sample_cnt <= 3'd0;
            shift_reg <= 8'd0;
            rx_data <= 8'd0;
            rx_done <= 1'b0;
        end else if (sample_en) begin
            rx_done <= 1'b0;  // 默认拉低
            case (state)
                IDLE: begin
                    bit_cnt <= 4'd0;
                    sample_cnt <= 3'd0;
                    if (rx_clean == 1'b0)  // 检测到起始位
                        next_state <= START;
                    else
                        next_state <= IDLE;
                end
                START: begin
                    // 起始位中点确认
                    if (sample_cnt == 3'd7) begin
                        if (rx_clean == 1'b0) begin
                            next_state <= DATA;
                            sample_cnt <= 3'd0;
                        end else begin
                            next_state <= IDLE;  // 毛刺,重新等待
                        end
                    end else begin
                        sample_cnt <= sample_cnt + 1'b1;
                    end
                end
                DATA: begin
                    // 每位采样16个点,取第7、8、9点多数表决
                    if (sample_cnt >= 3'd6 && sample_cnt <= 3'd8) begin
                        // 多数表决逻辑(简化:直接取中间点)
                        if (sample_cnt == 3'd7)
                            shift_reg <= {rx_clean, shift_reg[7:1]};
                    end
                    if (sample_cnt == 3'd15) begin
                        sample_cnt <= 3'd0;
                        if (bit_cnt == 4'd7) begin
                            next_state <= STOP;
                            bit_cnt <= 4'd0;
                        end else begin
                            bit_cnt <= bit_cnt + 1'b1;
                        end
                    end else begin
                        sample_cnt <= sample_cnt + 1'b1;
                    end
                end
                STOP: begin
                    if (sample_cnt == 3'd15) begin
                        next_state <= IDLE;
                        rx_data <= shift_reg;
                        rx_done <= 1'b1;  // 产生完成脉冲
                    end else begin
                        sample_cnt <= sample_cnt + 1'b1;
                    end
                end
                default: next_state <= IDLE;
            endcase
        end
    end

endmodule

仿真测试与验证

编写testbench验证接收功能:模拟发送字节0xA5(二进制10100101,LSB先发),检查rx_data与rx_done时序。

module tb_uart_rx;
    reg clk = 0;
    reg rst_n = 0;
    reg rx = 1;
    wire [7:0] rx_data;
    wire rx_done;

    // 生成50MHz时钟
    always #10 clk = ~clk;

    // 待测模块实例化
    uart_rx #(.CLK_FREQ(50_000_000), .BAUD_RATE(9600)) uut (
        .clk(clk), .rst_n(rst_n), .rx(rx),
        .rx_data(rx_data), .rx_done(rx_done)
    );

    // 发送任务:模拟9600波特率发送1字节
    task send_byte(input [7:0] data);
        integer i;
        begin
            // 起始位(低电平)
            rx = 0;
            #104167;  // 1/9600 ≈ 104.167us
            // 8个数据位,LSB first
            for (i = 0; i < 8; i = i + 1) begin
                rx = data[i];
                #104167;
            end
            // 停止位(高电平)
            rx = 1;
            #104167;
        end
    endtask

    initial begin
        // 复位
        #100 rst_n = 1;
        #200;
        // 发送0xA5
        send_byte(8'hA5);
        #200;
        // 检查结果
        if (rx_data == 8'hA5 && rx_done == 1)
            $display("TEST PASSED: rx_data = 0x%02X", rx_data);
        else
            $display("TEST FAILED: rx_data = 0x%02X", rx_data);
        #500;
        $finish;
    end
endmodule

运行与判定标准

  1. 仿真运行:在Vivado/ModelSim中编译两个文件,运行仿真。观察波形中rx_done在停止位结束后产生单周期高脉冲。
  2. 通过条件:仿真控制台打印TEST PASSED,且rx_data输出为0xA5。
  3. 上板验证:将rx连接至USB-UART的TX引脚,使用串口助手发送0xA5,观察FPGA接收数据与发送一致。

常见问题

  • 采样点偏移:若接收数据错误,检查DIV_CNT计算是否正确(50MHz/9600/16-1=324)。
  • 起始位误触发:输入同步器rx_sync可消除亚稳态,但若噪声过大需增加滤波逻辑。
  • 连续接收:本模块在STOP结束后立即回到IDLE,可连续接收下一字节,无需额外处理。