Lesson 27

📡 UART全双工

🏆 同时收发不丢数据
✅ Verilator仿真验证通过

📖 实验描述

实现UART全双工收发器,发送器和接收器同时独立工作,互不干扰。

全双工UART = TX + RX 同时工作,这是真实串口的标准工作模式。 关键设计: - TX和RX完全独立的状态机,互不影响 - RX需要输入同步器(外部信号异步) - RX中点采样:在每位中间采样,提高噪声容限 - 帧错误检测:停止位不为1则报告framing error 全双工vs半双工: - 全双工:TX和RX同时工作,需要2根线(TX/RX) - 半双工:同一时刻只能发送或接收,1根线 - UART天生支持全双工,因为TX和RX是独立线路 这是嵌入式系统最常用的调试接口,几乎所有MCU都有至少一个全双工UART。

🧠 核心概念

📐 电路结构

电路与状态图
UART全双工架构:

  ┌─────────────────────────────────────┐
  │           UART Full Duplex          │
  │                                     │
  │  TX路径:           RX路径:          │
  │  tx_data──→TX FSM──→tx_line         │
  │  tx_valid   IDLE     rx_line──→2级  │
  │  tx_ready   START              sync │
  │  tx_busy    DATA    ──→RX FSM──→rx_data│
  │             STOP     IDLE      rx_valid│
  │                      START    rx_ferr │
  │                      DATA             │
  │                      STOP             │
  └─────────────────────────────────────┘

  中点采样:在波特率周期的一半时采样起始位,
  确认有效后在整周期边沿采样数据位。

📝 设计步骤

  1. 1实现UART TX状态机(IDLE→START→DATA→STOP)
  2. 2实现UART RX输入同步器(2级FF)
  3. 3实现RX起始位检测(中点采样确认)
  4. 4实现RX数据位采样(每位中心点采样)
  5. 5实现停止位校验和帧错误检测
  6. 6TX/RX独立运行,验证全双工通信

💻 Verilog实现

uart_full_duplex.svSystemVerilog · Verilator 5.020
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
// UART Full Duplex — UART全双工收发器
module uart_full_duplex #(
    parameter CLK_FREQ = 50_000_000,
    parameter BAUD     = 115200
)(
    input  wire       clk,
    input  wire       rst,
    input  wire [7:0] tx_data,
    input  wire       tx_valid,
    output wire       tx_ready,
    output wire       tx_line,
    input  wire       rx_line,
    output wire [7:0] rx_data,
    output wire       rx_valid,
    output wire       tx_busy,
    output wire       rx_framing_err
);

localparam DIV = CLK_FREQ / BAUD;

// TX
typedef enum logic [1:0] {TX_IDLE, TX_START, TX_DATA, TX_STOP} tx_state_t;
tx_state_t tx_state;
reg [31:0] tx_baud_cnt;
reg [2:0]  tx_bit_idx;
reg [7:0]  tx_shift;
reg        tx_line_r, tx_busy_r, tx_ready_r;
assign tx_line = tx_line_r; assign tx_busy = tx_busy_r; assign tx_ready = tx_ready_r;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        tx_state <= TX_IDLE; tx_line_r <= 1; tx_baud_cnt <= 0;
        tx_bit_idx <= 0; tx_shift <= 0; tx_busy_r <= 0; tx_ready_r <= 1;
    end else begin
        tx_ready_r <= 0;
        case (tx_state)
            TX_IDLE: begin
                tx_line_r <= 1; tx_busy_r <= 0;
                if (tx_valid) begin
                    tx_shift <= tx_data; tx_baud_cnt <= 0;
                    tx_state <= TX_START; tx_line_r <= 0; tx_busy_r <= 1;
                end else tx_ready_r <= 1;
            end
            TX_START: begin
                tx_line_r <= 0;
                if (tx_baud_cnt >= DIV - 1) begin
                    tx_baud_cnt <= 0; tx_bit_idx <= 0; tx_state <= TX_DATA;
                end else tx_baud_cnt <= tx_baud_cnt + 1;
            end
            TX_DATA: begin
                tx_line_r <= tx_shift[0];
                if (tx_baud_cnt >= DIV - 1) begin
                    tx_baud_cnt <= 0; tx_shift <= {1'b0, tx_shift[7:1]};
                    if (tx_bit_idx == 7) tx_state <= TX_STOP;
                    else tx_bit_idx <= tx_bit_idx + 1;
                end else tx_baud_cnt <= tx_baud_cnt + 1;
            end
            TX_STOP: begin
                tx_line_r <= 1;
                if (tx_baud_cnt >= DIV - 1) begin
                    tx_baud_cnt <= 0; tx_busy_r <= 0;
                    tx_ready_r <= 1; tx_state <= TX_IDLE;
                end else tx_baud_cnt <= tx_baud_cnt + 1;
            end
            default: tx_state <= TX_IDLE;
        endcase
    end
end

// RX
typedef enum logic [1:0] {RX_IDLE, RX_START, RX_DATA, RX_STOP} rx_state_t;
rx_state_t rx_state;
reg [31:0] rx_baud_cnt;
reg [2:0]  rx_bit_idx;
reg [7:0]  rx_shift, rx_data_r;
reg        rx_valid_r, rx_framing_err_r;
reg        rx_line_sync1, rx_line_sync2;
assign rx_data = rx_data_r; assign rx_valid = rx_valid_r; assign rx_framing_err = rx_framing_err_r;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        rx_state <= RX_IDLE; rx_baud_cnt <= 0; rx_bit_idx <= 0;
        rx_shift <= 0; rx_data_r <= 0; rx_valid_r <= 0;
        rx_framing_err_r <= 0; rx_line_sync1 <= 1; rx_line_sync2 <= 1;
    end else begin
        rx_valid_r <= 0; rx_framing_err_r <= 0;
        rx_line_sync1 <= rx_line; rx_line_sync2 <= rx_line_sync1;
        case (rx_state)
            RX_IDLE: if (rx_line_sync2 == 0) begin rx_baud_cnt <= 0; rx_state <= RX_START; end
            RX_START: begin
                if (rx_baud_cnt == DIV/2 - 1) begin
                    if (rx_line_sync2 == 0) begin rx_baud_cnt <= 0; rx_bit_idx <= 0; rx_state <= RX_DATA; end
                    else rx_state <= RX_IDLE;
                end else rx_baud_cnt <= rx_baud_cnt + 1;
            end
            RX_DATA: begin
                if (rx_baud_cnt >= DIV - 1) begin
                    rx_baud_cnt <= 0; rx_shift <= {rx_line_sync2, rx_shift[7:1]};
                    if (rx_bit_idx == 7) rx_state <= RX_STOP;
                    else rx_bit_idx <= rx_bit_idx + 1;
                end else rx_baud_cnt <= rx_baud_cnt + 1;
            end
            RX_STOP: begin
                if (rx_baud_cnt >= DIV - 1) begin
                    rx_baud_cnt <= 0;
                    if (rx_line_sync2 == 1) begin rx_data_r <= rx_shift; rx_valid_r <= 1; end
                    else rx_framing_err_r <= 1;
                    rx_state <= RX_IDLE;
                end else rx_baud_cnt <= rx_baud_cnt + 1;
            end
            default: rx_state <= RX_IDLE;
        endcase
    end
end
endmodule

🔬 仿真说明

仿真环境与策略

测试:TX发送0x55和0xAA,验证TX线波形。RX接收8N1帧,验证数据正确和rx_valid脉冲。同时启动TX和RX,验证全双工不丢数据。测试帧错误:发送错误停止位,验证rx_framing_err。

✅ 验证结果

Verilator 5.020 仿真通过

本实验所有Verilog代码已通过Verilator编译验证,功能行为正确。测试用例覆盖核心功能路径,确保设计满足规格要求。

⚠️ 常见错误

🌍 真实世界

工业应用

全双工UART是嵌入式调试的标准接口。STM32的USART支持全双工+DMA,可达4.5Mbps。RS-485半双工是工业通信标准。蓝牙模块(HC-05)通过UART与MCU全双工通信。USB-UART芯片(CH340/CP2102)是开发者最常用的调试工具。

💡 扩展挑战

🚀 自己动手

# 编译验证
verilator --lint-only uart_full_duplex.sv --timing
verilator --cc uart_full_duplex.sv --exe tb.cpp --build -j 0 --timing

🎯 试一试

修改参数

尝试修改代码中的关键参数,观察仿真结果变化:

  • 调整深度/位宽,观察行为变化
  • 修改时钟分频,测试不同速度
  • 改变初始值/种子,观察不同起始条件

💡 Verilator会在位宽不匹配时给出Warning,这是学习的好机会

添加功能

在现有基础上增加新功能:

  • 添加新的控制信号和状态
  • 增加状态或修改状态转移条件
  • 扩展输出,增加更多信息

🔧 增量开发:每次只改一个地方,验证通过后再改下一个

📋 Verilog速查

语法说明示例
reg [7:0]8位寄存器reg [7:0] data;
wire组合逻辑连线wire valid = cnt > 5;
always @(posedge clk)时序逻辑上升沿触发
always @(*)组合逻辑敏感列表自动推导
localparam局部常量localparam DIV = 50000000;
case多分支选择注意default分支
$clog2计算位宽$clog2(16)=4

📊 性能指标

资源估算(FPGA参考)

本设计在典型FPGA上的资源占用估算:LUT约20-150个,FF约30-120个,无BRAM/DSP依赖(特殊模块除外)。时钟频率可达50-100MHz+。Verilator仿真速度约5-10M周期/秒。

← 上一课 📚 目录 下一课 →