3. UART串口发送模块设计

任务目标

设计一个可综合的UART发送模块,支持1个起始位、8个数据位、1个停止位(8N1格式),通过FPGA引脚实现数据的串行输出。模块接收并行字节数据后,按设定的波特率逐位发送,并输出发送完成标志。

实现方案

采用状态机方式实现UART发送协议。系统时钟50MHz,目标波特率9600bps,因此需要分频计数器产生位周期。模块接口包含时钟、复位、发送使能、并行数据输入、串行输出及忙状态指示。核心逻辑分为波特率生成、状态转移和移位输出三部分。

完整代码

module uart_tx (
    input       clk,        // 50MHz系统时钟
    input       rst_n,      // 异步复位,低有效
    input       tx_start,   // 发送启动脉冲(高有效一个时钟周期)
    input [7:0] tx_data,    // 待发送的并行数据
    output reg  txd,        // 串行数据输出
    output reg  tx_busy     // 忙标志:1=正在发送,0=空闲
);

// 波特率参数:50MHz / 9600 = 5208
localparam BAUD_CNT_MAX = 5208 - 1;
localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;

reg [1:0]  state;
reg [12:0] baud_cnt;    // 波特率分频计数器
reg [2:0]  bit_cnt;     // 数据位计数(0~7)
reg [7:0]  shift_reg;   // 发送数据移位寄存器

// 波特率时钟生成
wire baud_tick = (baud_cnt == BAUD_CNT_MAX);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        baud_cnt <= 13'd0;
    end else if (state == IDLE) begin
        baud_cnt <= 13'd0;
    end else if (baud_tick) begin
        baud_cnt <= 13'd0;
    end else begin
        baud_cnt <= baud_cnt + 1'b1;
    end
end

// 主状态机
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state     <= IDLE;
        txd       <= 1'b1;      // 空闲时输出高电平
        tx_busy   <= 1'b0;
        shift_reg <= 8'd0;
        bit_cnt   <= 3'd0;
    end else begin
        case (state)
            IDLE: begin
                txd <= 1'b1;
                tx_busy <= 1'b0;
                if (tx_start) begin
                    state     <= START;
                    shift_reg <= tx_data;
                    tx_busy   <= 1'b1;
                end
            end
            START: begin
                txd <= 1'b0;    // 起始位
                if (baud_tick) begin
                    state   <= DATA;
                    bit_cnt <= 3'd0;
                end
            end
            DATA: begin
                txd <= shift_reg[0];  // LSB先发送
                if (baud_tick) begin
                    if (bit_cnt == 3'd7) begin
                        state <= STOP;
                    end else begin
                        shift_reg <= shift_reg >> 1;
                        bit_cnt   <= bit_cnt + 1'b1;
                    end
                end
            end
            STOP: begin
                txd <= 1'b1;    // 停止位
                if (baud_tick) begin
                    state   <= IDLE;
                    tx_busy <= 1'b0;
                end
            end
            default: state <= IDLE;
        endcase
    end
end

endmodule

仿真测试代码

module uart_tx_tb;
reg clk = 0, rst_n = 0, tx_start = 0;
reg [7:0] tx_data = 8'h00;
wire txd, tx_busy;

uart_tx dut (
    .clk(clk), .rst_n(rst_n), .tx_start(tx_start),
    .tx_data(tx_data), .txd(txd), .tx_busy(tx_busy)
);

always #10 clk = ~clk;  // 50MHz

initial begin
    #20 rst_n = 1;
    #20 tx_data = 8'hA5;  // 1010_0101
    @(posedge clk) tx_start = 1;
    @(posedge clk) tx_start = 0;
    // 等待发送完成
    wait (!tx_busy);
    #100 $finish;
end

// 监控串行输出
initial begin
    $monitor("time=%0t txd=%b tx_busy=%b", $time, txd, tx_busy);
end
endmodule

运行与验证

仿真验证:在Vivado或ModelSim中运行上述测试平台,观察波形。发送数据0xA5时,预期输出序列为:空闲高电平→起始位0→数据位LSB优先(1,0,1,0,0,1,0,1)→停止位1。每个位持续5208个时钟周期(约104.16μs @50MHz)。

通过条件:

  1. tx_busy在tx_start有效后拉高,发送完成后自动拉低;
  2. txd在起始位输出0,停止位输出1,空闲保持高电平;
  3. 数据位顺序与tx_data的LSB-first一致,且每位宽度正确。

板级验证:将txd连接至FPGA的UART_TX引脚,使用USB转串口工具连接PC,发送单字节数据(如0xA5),PC端串口助手应收到对应十六进制数据。注意PC端需配置为9600-8-N-1。

常见问题

  • 接收数据错误:检查波特率分频值是否精确(50MHz/9600=5208.33,取5208引入约0.006%误差,可接受);确认PC端波特率设置一致。
  • tx_start毛刺:发送启动信号需为单时钟周期脉冲,若外部信号持续拉高,模块会在发送完成后立即重新发送,需在外部逻辑中处理。
  • 时序约束:若系统时钟非50MHz,需重新计算BAUD_CNT_MAX值。