第三章 UART接口IP设计

UART,说白了就是串口通信。这玩意儿在嵌入式系统里太常见了。我刚开始做芯片设计那会儿,第一个独立完成的IP就是UART。记得当时调试了好久,最后发现是波特率没算对——嗯,这个坑我后面会细说。

3.1 UART协议详解

UART全称是Universal Asynchronous Receiver/Transmitter。异步,意味着没有时钟线。发送方和接收方各用自己的时钟,靠约定好的波特率来同步。

一个标准的UART帧长这样:

起始位(1bit) + 数据位(5~8bit) + 校验位(0/1bit) + 停止位(1/1.5/2bit)

我习惯用8位数据、无校验、1位停止位,也就是常说的8N1。为什么?因为大部分MCU和传感器都支持这个配置,兼容性最好。

关键时序点:

  • 空闲时,TX线保持高电平
  • 起始位:拉低1个波特率周期
  • 数据位:LSB先发,MSB最后
  • 停止位:拉高至少1个周期

你想想看,如果接收方采样到了起始位的下降沿,它就开始数时钟周期。每过一个波特率周期,采样一次数据位。这就是异步通信的精髓——靠边沿触发,靠周期采样。

3.2 波特率发生器设计

波特率发生器,本质上就是一个分频器。系统时钟除以你想要的波特率,得到分频系数。

公式很简单:

baud_div = sys_clk_freq / (target_baud_rate * oversample_factor)

这里有个坑——过采样。我个人习惯用16倍过采样。为什么是16?因为这样可以在一个bit周期内采16个点,取中间3个点做多数判决,抗干扰能力强。

实战经验:

我曾经在一个项目中,系统时钟是50MHz,目标波特率115200。算出来分频系数是27.12。取整27,实际波特率变成了115740,误差0.47%。这在UART协议里是允许的(一般要求误差<2%)。但如果你的系统时钟是33MHz,算115200就尴尬了——分频系数17.9,取整18,误差4.3%,通信就会丢包。

Verilog代码实现:

module baud_gen (
    input  clk,
    input  rst_n,
    input  [15:0] baud_div,
    output reg baud_pulse
);

reg [15:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 16'd0;
        baud_pulse <= 1'b0;
    end else begin
        if (cnt == baud_div - 1) begin
            cnt <= 16'd0;
            baud_pulse <= 1'b1;
        end else begin
            cnt <= cnt + 1'b1;
            baud_pulse <= 1'b0;
        end
    end
end

endmodule

注意,这里输出的是脉冲信号,不是方波。每个波特率周期只拉高一个时钟周期,用来驱动FSM的状态跳转。

3.3 发送器FSM设计

发送器的状态机,我一般设计成4个状态:

IDLE -> START -> DATA -> STOP

每个状态做什么?

  • IDLE:TX线拉高,等待发送使能信号
  • START:TX拉低,持续1个波特率周期
  • DATA:逐位发送数据,LSB first,持续8个周期
  • STOP:TX拉高,持续1个周期,然后回到IDLE

注意:

我曾经犯过一个低级错误——在DATA状态里忘了加bit计数器。结果数据位发完了还在发,接收方那边全乱了。后来我加了一个3位的bit_cnt,每发完一位加1,到8就跳转到STOP。

发送器FSM的Verilog核心代码:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
        tx_reg <= 1'b1;
        bit_cnt <= 3'd0;
    end else begin
        case (state)
            IDLE: begin
                tx_reg <= 1'b1;
                if (tx_start) begin
                    state <= START;
                    data_reg <= tx_data;  // 锁存待发送数据
                end
            end
            
            START: begin
                tx_reg <= 1'b0;
                if (baud_pulse) state <= DATA;
            end
            
            DATA: begin
                tx_reg <= data_reg[bit_cnt];  // LSB first
                if (baud_pulse) begin
                    if (bit_cnt == 3'd7) begin
                        bit_cnt <= 3'd0;
                        state <= STOP;
                    end else begin
                        bit_cnt <= bit_cnt + 1'b1;
                    end
                end
            end
            
            STOP: begin
                tx_reg <= 1'b1;
                if (baud_pulse) state <= IDLE;
            end
        endcase
    end
end

3.4 接收器FSM设计

接收器比发送器麻烦一点。因为你要在噪声中准确找到起始位,还要在正确的时刻采样数据。

我设计的接收器状态机:

IDLE -> START_DETECT -> SAMPLE -> DATA -> STOP_CHECK

这里有个技巧——起始位检测。在IDLE状态下,如果检测到TX线从高变低,不要立刻认为就是起始位。我习惯先等半个波特率周期,再采一次。如果还是低,才确认是真正的起始位。这样可以滤掉毛刺。

避坑指南:

我曾经在一个电磁干扰很强的项目里,UART接收老是误触发。后来加了起始位确认机制——检测到下降沿后,等8个过采样时钟(也就是半个bit周期)再采一次。如果还是低,才进入SAMPLE状态。从此再也没误触发过。

接收器的采样点选择也很关键。16倍过采样下,我一般取第7、8、9个采样点做多数判决。为什么是这三个?因为它们在bit周期的正中间,抖动最小。

// 多数判决逻辑
always @(*) begin
    case ({sample_7, sample_8, sample_9})
        3'b000: rx_bit = 1'b0;
        3'b111: rx_bit = 1'b1;
        default: rx_bit = sample_8;  // 中间值优先
    endcase
end

接收器还有一个关键点——停止位检查。如果停止位不是高电平,说明这帧数据有问题,应该置位帧错误标志。我习惯在STOP_CHECK状态里做这个检查。

3.5 验证环境搭建

验证UART IP,我一般搭三个层次的测试:

  1. 单元测试:单独测波特率发生器、发送FSM、接收FSM
  2. 环路测试:把发送器和接收器连起来,自发自收
  3. 系统测试:用BFM模拟外部设备,测各种异常情况

环路测试的testbench结构:

// 顶层测试模块
module uart_tb;

reg clk, rst_n;
reg [7:0] tx_data;
reg tx_start;
wire tx_line;
wire [7:0] rx_data;
wire rx_done;

// 实例化DUT
uart_top u_dut (
    .clk(clk),
    .rst_n(rst_n),
    .tx_data(tx_data),
    .tx_start(tx_start),
    .tx(tx_line),
    .rx(tx_line),  // 环路连接
    .rx_data(rx_data),
    .rx_done(rx_done)
);

// 测试序列
initial begin
    // 初始化
    rst_n = 0;
    #100 rst_n = 1;
    
    // 发送0x55 (01010101)
    tx_data = 8'h55;
    tx_start = 1;
    #20 tx_start = 0;
    
    // 等待接收完成
    @(posedge rx_done);
    
    // 检查结果
    if (rx_data == 8'h55)
        $display("Test PASS: 0x55 loopback OK");
    else
        $display("Test FAIL: expected 0x55, got 0x%h", rx_data);
    
    // 更多测试...
    #1000 $finish;
end

endmodule

验证要点:

  • 边界值测试:全0、全1、交替模式(0x55、0xAA)
  • 异常测试:波特率偏差、毛刺干扰、帧错误
  • 压力测试:连续发送10000帧,检查有无丢帧

我个人习惯在验证环境里加一个错误注入模块。它可以随机在TX线上插入毛刺、拉长或缩短bit周期。这样能充分验证接收器的鲁棒性。你想想看,如果连这种恶劣环境都能通过,那实际项目里基本不会出问题。

嗯,UART接口IP设计就讲到这里。下一章我们会讲SPI接口,那个比UART稍微复杂一点,但原理是相通的。