4. UART串口发送模块

任务目标

实现一个可配置波特率的UART发送模块,将8位并行数据按标准UART帧格式(1起始位 + 8数据位 + 1停止位,无校验)串行发出,并通过Testbench仿真验证发送波形与时序正确性。

实现方案

采用状态机方式实现发送逻辑:IDLE → START → DATA[7:0] → STOP。波特率通过参数 CLK_FREQ 与 BAUD_RATE 计算分频计数阈值,发送时钟基于系统时钟分频产生。核心代码包含发送模块与对应Testbench,可直接在Vivado/ModelSim中运行仿真。

关键完整代码

// uart_tx.v
module uart_tx #(
    parameter CLK_FREQ = 50_000_000,  // 系统时钟频率(Hz)
    parameter BAUD_RATE = 115200      // 目标波特率
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] tx_data,        // 待发送8位数据
    input  wire       tx_start,       // 发送启动脉冲(高有效1周期)
    output reg        tx,             // 串行输出
    output reg        tx_busy         // 忙标志(1=发送中)
);

    // 波特率分频计数阈值
    localparam BAUD_CNT_MAX = CLK_FREQ / BAUD_RATE - 1;
    localparam BIT_CNT_MAX  = 9;       // 起始位+8数据位+停止位 = 10位,索引0~9

    reg [15:0] baud_cnt;
    reg [3:0]  bit_cnt;
    reg        baud_pulse;            // 波特率时钟脉冲
    reg [7:0]  shift_reg;

    // 状态编码
    localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;
    reg [1:0] state, next_state;

    // 波特率脉冲生成
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            baud_cnt <= 0;
            baud_pulse <= 0;
        end else if (state == IDLE) begin
            baud_cnt <= 0;
            baud_pulse <= 0;
        end else begin
            if (baud_cnt == BAUD_CNT_MAX) begin
                baud_cnt <= 0;
                baud_pulse <= 1;
            end else begin
                baud_cnt <= baud_cnt + 1;
                baud_pulse <= 0;
            end
        end
    end

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

    // 次态逻辑
    always @(*) begin
        next_state = state;
        case (state)
            IDLE:  if (tx_start) next_state = START;
            START: if (baud_pulse) next_state = DATA;
            DATA:  if (baud_pulse && (bit_cnt == 4'd8)) next_state = STOP;
            STOP:  if (baud_pulse) next_state = IDLE;
        endcase
    end

    // 发送过程控制
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx <= 1'b1;
            tx_busy <= 1'b0;
            bit_cnt <= 0;
            shift_reg <= 0;
        end else begin
            case (state)
                IDLE: begin
                    tx <= 1'b1;
                    tx_busy <= 1'b0;
                    bit_cnt <= 0;
                    if (tx_start) begin
                        shift_reg <= tx_data;
                        tx_busy <= 1'b1;
                    end
                end
                START: begin
                    tx <= 1'b0;  // 起始位
                    if (baud_pulse) bit_cnt <= 0;
                end
                DATA: begin
                    if (baud_pulse) begin
                        tx <= shift_reg[0];          // LSB先发
                        shift_reg <= {1'b0, shift_reg[7:1]};
                        bit_cnt <= bit_cnt + 1;
                    end
                end
                STOP: begin
                    tx <= 1'b1;  // 停止位
                    if (baud_pulse) tx_busy <= 1'b0;
                end
            endcase
        end
    end
endmodule

仿真验证Testbench

// tb_uart_tx.v
`timescale 1ns/1ps
module tb_uart_tx;

    reg clk = 0;
    reg rst_n = 0;
    reg [7:0] tx_data;
    reg tx_start = 0;
    wire tx, tx_busy;

    // 实例化待测模块:50MHz时钟,波特率115200
    uart_tx #(
        .CLK_FREQ(50_000_000),
        .BAUD_RATE(115200)
    ) dut (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_data), .tx_start(tx_start),
        .tx(tx), .tx_busy(tx_busy)
    );

    // 10ns周期时钟(50MHz)
    always #10 clk = ~clk;

    // 测试序列
    initial begin
        // 复位
        rst_n = 0;
        tx_data = 8'hA5;  // 发送数据0xA5 = 1010_0101
        repeat(5) @(posedge clk);
        rst_n = 1;

        // 发送第一个字节
        @(posedge clk);
        tx_start = 1;
        @(posedge clk);
        tx_start = 0;

        // 等待发送完成
        wait (!tx_busy);
        #100;

        // 发送第二个字节 0x3C
        tx_data = 8'h3C;
        @(posedge clk);
        tx_start = 1;
        @(posedge clk);
        tx_start = 0;

        wait (!tx_busy);
        #200;
        $finish;
    end

    // 波形转储
    initial begin
        $dumpfile("uart_tx.vcd");
        $dumpvars(0, tb_uart_tx);
    end

endmodule

运行与验证方法

  1. 仿真环境:Vivado/ModelSim/Verilator均可,需支持Verilog-2001。
  2. 编译运行:将 uart_tx.v 与 tb_uart_tx.v 加入工程,运行行为仿真。
  3. 观察波形:查看 tx、tx_busy、state 信号。
  4. 通过判定标准:
    • 复位释放后,tx 保持高电平(空闲态);
    • 拉高 tx_start 后,tx_busy 变为高电平,tx 先输出1位低电平起始位;
    • 随后依次输出8位数据(LSB在前):发送0xA5时,数据位应为 1,0,1,0,0,1,0,1(对应A5的bit0~bit7);
    • 最后输出1位高电平停止位,tx_busy 拉低,完成一帧发送;
    • 每比特持续时间约为 1/115200 ≈ 8.68μs(50MHz时钟下约434个时钟周期)。

常见问题

  • 起始位/停止位不完整:检查状态机中 baud_pulse 是否在进入新状态时被正确清零,避免额外跳变。
  • 数据位顺序错误:确认发送顺序为LSB先行,即先发送 tx_data[0]。
  • 波特率偏差:若系统时钟不是整数倍分频,实际波特率会有微小误差,但通常低于2%即可正常通信。