第2章:UART协议详解

UART,说白了就是串口通信。我在刚入行那会儿,觉得这玩意儿太简单了,不就是一根线发数据嘛。直到后来做高速通信芯片,才发现UART里藏着不少门道。今天咱们就把它彻底聊透。

2.1 UART帧结构

UART的帧结构,我习惯把它想象成一列火车。每节车厢都有固定位置,少了哪节都不行。

一个标准的UART帧包含以下部分:

  • 起始位(Start Bit):逻辑0,告诉接收方“我要发数据了”
  • 数据位(Data Bits):5~8位,通常是8位
  • 校验位(Parity Bit):可选,奇校验或偶校验
  • 停止位(Stop Bit):逻辑1,表示帧结束

关键点:起始位必须是下降沿。接收方就是靠这个边沿来同步时钟的。我曾经调试过一个板子,起始位毛刺太多,接收方老是误触发,折腾了两天才发现是信号完整性出了问题。

数据位通常LSB在前。举个例子,你要发送0x55(二进制01010101),实际线上先发的是最低位1,然后是0、1、0、1、0、1、0,最后是最高位0。嗯,这个顺序别搞反了。

校验位分三种:

  • 无校验:省掉这个位,帧长最短
  • 奇校验:数据位+校验位中1的个数为奇数
  • 偶校验:数据位+校验位中1的个数为偶数

停止位可以是1位、1.5位或2位。我个人习惯用1位停止位,除非是噪声环境特别恶劣的场景。

2.2 波特率计算

波特率就是每秒传输的符号数。对于UART来说,一个符号就是一个bit,所以波特率就等于比特率。

常见的波特率有:9600、19200、38400、115200。115200是我最常用的,速度够快,稳定性也不错。

波特率怎么算?看这个公式:

波特率 = 系统时钟频率 / (16 × 波特率分频值)

为什么是16?因为UART接收器通常用16倍过采样。也就是说,每个bit采样16次,取中间3次的值做多数判决。这样能有效抗干扰。

举个例子,系统时钟50MHz,目标波特率115200:

分频值 = 50,000,000 / (16 × 115200) ≈ 27.13
取整为27,实际波特率 = 50,000,000 / (16 × 27) ≈ 115,740
误差 = (115,740 - 115,200) / 115,200 ≈ 0.47%

经验之谈:波特率误差控制在±2%以内通常没问题。我曾经遇到过用8MHz晶振跑115200,误差高达3.5%,结果在高温下频繁丢数据。所以选晶振时最好算一下误差。

2.3 硬件流控(RTS/CTS)

硬件流控,说白了就是让接收方告诉发送方“我忙,你先别发”。

两个关键信号:

  • RTS(Request To Send):发送方请求发送数据
  • CTS(Clear To Send):接收方允许发送

工作流程是这样的:

  1. 发送方拉低RTS,表示“我想发数据”
  2. 接收方如果缓冲区有空,就拉低CTS,表示“来吧”
  3. 发送方看到CTS为低,开始发送
  4. 接收方缓冲区快满时,拉高CTS,发送方停止发送

你想想看,如果没有流控,接收方处理不过来怎么办?只能丢数据。我在做物联网网关时,遇到过CPU中断响应不及时,串口数据被覆盖的情况。加了硬件流控后,问题就解决了。

注意:RTS/CTS是负逻辑,低电平有效。很多新手会搞反,以为高电平才能发。另外,有些芯片的RTS/CTS是交叉连接的,接错了也会出问题。

2.4 Verilog实现简化UART发送模块

下面是我写的一个简化版UART发送模块。去掉了校验位,只保留核心功能,方便你理解。

module uart_tx (
    input  wire       clk,        // 系统时钟
    input  wire       rst_n,      // 复位,低有效
    input  wire       tx_start,   // 发送启动信号
    input  wire [7:0] tx_data,    // 待发送数据
    output reg        tx_busy,    // 发送忙标志
    output reg        txd         // 串行输出
);

    // 参数:波特率分频值
    // 假设50MHz时钟,115200波特率
    // 分频值 = 50M / (16 * 115200) ≈ 27
    parameter BAUD_DIV = 27;

    // 状态定义
    localparam IDLE    = 3'd0;
    localparam START   = 3'd1;
    localparam DATA    = 3'd2;
    localparam STOP    = 3'd3;

    reg [2:0] state;
    reg [3:0] bit_cnt;      // 已发送位数
    reg [7:0] data_reg;     // 数据寄存器
    reg [4:0] clk_cnt;      // 时钟计数器

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state   <= IDLE;
            txd     <= 1'b1;    // 空闲时高电平
            tx_busy <= 1'b0;
            bit_cnt <= 4'd0;
            clk_cnt <= 5'd0;
        end else begin
            case (state)
                IDLE: begin
                    txd     <= 1'b1;
                    tx_busy <= 1'b0;
                    if (tx_start) begin
                        data_reg <= tx_data;
                        state    <= START;
                        tx_busy  <= 1'b1;
                        clk_cnt  <= 5'd0;
                    end
                end

                START: begin
                    txd <= 1'b0;    // 发送起始位
                    if (clk_cnt == BAUD_DIV - 1) begin
                        clk_cnt <= 5'd0;
                        state   <= DATA;
                        bit_cnt <= 4'd0;
                    end else begin
                        clk_cnt <= clk_cnt + 1'b1;
                    end
                end

                DATA: begin
                    txd <= data_reg[0];  // LSB先发
                    if (clk_cnt == BAUD_DIV - 1) begin
                        clk_cnt <= 5'd0;
                        data_reg <= {1'b0, data_reg[7:1]};  // 右移
                        if (bit_cnt == 4'd7) begin
                            state <= STOP;
                        end else begin
                            bit_cnt <= bit_cnt + 1'b1;
                        end
                    end else begin
                        clk_cnt <= clk_cnt + 1'b1;
                    end
                end

                STOP: begin
                    txd <= 1'b1;    // 发送停止位
                    if (clk_cnt == BAUD_DIV - 1) begin
                        clk_cnt <= 5'd0;
                        state   <= IDLE;
                        tx_busy <= 1'b0;
                    end else begin
                        clk_cnt <= clk_cnt + 1'b1;
                    end
                end

                default: state <= IDLE;
            endcase
        end
    end

endmodule

代码说明:这个模块用了状态机,四个状态分别是空闲、起始位、数据位、停止位。每个bit的持续时间由BAUD_DIV控制。我故意去掉了校验位,因为实际项目中很多场景用不到,加了反而增加复杂度。

使用示例:

// 实例化发送模块
uart_tx #(
    .BAUD_DIV(27)   // 115200 @ 50MHz
) u_uart_tx (
    .clk      (clk),
    .rst_n    (rst_n),
    .tx_start (tx_start),
    .tx_data  (tx_data),
    .tx_busy  (tx_busy),
    .txd      (txd)
);

// 发送一个字节
assign tx_data  = 8'h55;
assign tx_start = send_enable;

调试建议:写完后用仿真看一下波形。重点检查起始位的下降沿、数据位的顺序、停止位的高电平持续时间。我习惯在仿真时把BAUD_DIV设小一点,比如设成5,这样波形看起来快,调试效率高。

好了,UART的核心内容就这些。下一章咱们聊SPI协议,那个比UART快多了,但引脚也多。到时候见。