📡 第21课:串口UART

📖 本课目标

设计并实现UART串口通信控制器,实现8位复古电脑与PC之间的数据传输。UART是最古老也最持久的通信协议之一——从1970年代的终端到今天的嵌入式调试,它始终是芯片间通信的基础。

🧠 UART协议深度解析

UART(Universal Asynchronous Receiver-Transmitter)即通用异步收发器。它的核心特点是异步——没有时钟线,收发双方靠约定波特率来同步。这与PS/2截然不同:

特性PS/2UART
时钟设备驱动时钟线无时钟线,约定波特率
同步方式硬件同步异步(起始位同步)
连线4线(VCC/GND/CLK/DATA)3线(VCC/GND/TX/RX交叉)
速率10-16.7 Kbps300-115200 bps+
用途键盘/鼠标终端/调试/模块通信
UART数据帧格式 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 空闲 起始位 数据位 (LSB first) 校验 停止位 空闲 ──┐ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ ┌──┐ ┌── │ │ ││ ││ ││ ││ ││ ││ ││ ││ │ │ │ │ ──┘ └──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘ └──┘ └── 1 0 D0 D1 D2 D3 D4 D5 D6 D7 P 1 1 起始位=0(下降沿标志帧开始) 数据位=5/6/7/8位(常见8位) 校验位=无/偶校验/奇校验 停止位=1/1.5/2位(常见1位) 波特率 = 每秒传输的比特数 常见: 9600, 19200, 38400, 57600, 115200

波特率与分频计算

UART没有时钟线,收发双方必须使用相同的波特率。波特率决定了每个比特的持续时间:

波特率比特时间分频系数(25MHz)每秒字符(8N1)
9600104.17 μs2604960
1920052.08 μs13021920
3840026.04 μs6513840
5760017.36 μs4345760
1152008.68 μs21711520
💡 关键理解:UART接收器靠起始位的下降沿来同步。一旦检测到起始位,接收器在"每个比特的中间位置"采样数据——这需要精确的分频计数。半波特率偏移采样是关键:起始位后等半比特时间(到比特中间),之后每隔一比特时间采样一次。即使双方波特率有2-3%的误差,一帧10比特内也不会采样偏移超过半个比特。

🏗️ UART控制器架构

UART控制器架构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CPU侧 物理侧 ┌──────────┐ ┌──────────┐ │ $FF00 │ ┌───────────────┐ │ UART_TX │──→ TX线 │ 数据寄存器│──│ 发送器 (TX) │───│ │ └──────────┘ │ │ └──────────┘ ┌──────────┐ │ • 状态机 │ │ $FF01 │ │ • 波特分频 │ ┌──────────┐ │ 状态寄存器│←─│ • 移位寄存器 │←──│ UART_RX │←── RX线 └──────────┘ │ │ │ │ ┌──────────┐ │ 接收器 (RX) │ │ 信号同步 │ │ 波特率 │ │ │ │ 下降沿检测│ │ 寄存器 │──│ • 过采样 │ └──────────┘ └──────────┘ │ • 帧校验 │ └───────────────┘ 状态寄存器位定义: bit7: TX忙 bit6: TX完成 bit5: RX就绪 bit4: RX错误(帧错/校验错) bit3-0: 保留

🔧 Verilog实现

完整的UART收发器

// uart.v - 可配置波特率UART收发器

module uart (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [15:0] baud_div,   // 波特率分频 = clk_freq/baud
    // TX接口
    input  wire [7:0]  tx_data,
    input  wire        tx_start,
    output reg        tx_busy,
    output reg        tx_done,
    // RX接口
    output reg [7:0]  rx_data,
    output reg        rx_ready,
    output reg        rx_error,
    input  wire        rx_ack,
    // 物理接口
    output reg        uart_tx,
    input  wire        uart_rx
);

    // ======== 发送器 TX ========
    localparam TX_IDLE=3'd0, TX_START=3'd1,
               TX_DATA=3'd2, TX_PARITY=3'd3,
               TX_STOP=3'd4;

    reg [2:0]  tx_state;
    reg [15:0] tx_baud_cnt;
    reg [2:0] tx_bit_cnt;
    reg [7:0] tx_shift;
    reg        tx_parity;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx_state <= TX_IDLE; tx_baud_cnt <= 16'd0;
            tx_busy <= 1'b0; tx_done <= 1'b0;
            uart_tx <= 1'b1;
        end else begin
            tx_done <= 1'b0;
            case (tx_state)
                TX_IDLE: begin
                    uart_tx <= 1'b1;
                    if (tx_start) begin
                        tx_shift <= tx_data;
                        tx_parity <= 1'b0;
                        tx_bit_cnt <= 3'd0;
                        tx_baud_cnt <= baud_div - 16'd1;
                        tx_busy <= 1'b1;
                        uart_tx <= 1'b0;  // 起始位
                        tx_state <= TX_START;
                    end
                end
                TX_START: begin
                    if (tx_baud_cnt == 16'd0) begin
                        tx_baud_cnt <= baud_div - 16'd1;
                        uart_tx <= tx_shift[0];
                        tx_parity <= tx_parity ^ tx_shift[0];
                        tx_state <= TX_DATA;
                    end else
                        tx_baud_cnt <= tx_baud_cnt - 16'd1;
                end
                TX_DATA: begin
                    if (tx_baud_cnt == 16'd0) begin
                        tx_baud_cnt <= baud_div - 16'd1;
                        tx_bit_cnt <= tx_bit_cnt + 3'd1;
                        tx_shift <= tx_shift >> 1;
                        if (tx_bit_cnt == 3'd7) begin
                            uart_tx <= ~tx_parity;
                            tx_state <= TX_PARITY;
                        end else begin
                            uart_tx <= tx_shift[1];
                            tx_parity <= tx_parity ^ tx_shift[1];
                        end
                    end else
                        tx_baud_cnt <= tx_baud_cnt - 16'd1;
                end
                TX_PARITY: begin
                    if (tx_baud_cnt == 16'd0) begin
                        tx_baud_cnt <= baud_div - 16'd1;
                        uart_tx <= 1'b1;  // 停止位
                        tx_state <= TX_STOP;
                    end else
                        tx_baud_cnt <= tx_baud_cnt - 16'd1;
                end
                TX_STOP: begin
                    if (tx_baud_cnt == 16'd0) begin
                        tx_busy <= 1'b0;
                        tx_done <= 1'b1;
                        tx_state <= TX_IDLE;
                    end else
                        tx_baud_cnt <= tx_baud_cnt - 16'd1;
                end
            endcase
        end
    end

    // ======== 接收器 RX ========
    reg [1:0] rx_sync;
    always @(posedge clk) rx_sync <= {rx_sync[0], uart_rx};
    wire rx_falling = (rx_sync == 2'b10);

    localparam RX_IDLE=3'd0, RX_START=3'd1,
               RX_DATA=3'd2, RX_PARITY=3'd3,
               RX_STOP=3'd4;

    reg [2:0]  rx_state;
    reg [15:0] rx_baud_cnt;
    reg [2:0] rx_bit_cnt;
    reg [7:0] rx_shift;
    reg        rx_parity;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_state <= RX_IDLE; rx_data <= 8'd0;
            rx_ready <= 1'b0; rx_error <= 1'b0;
        end else begin
            if (rx_ack && rx_ready) rx_ready <= 1'b0;

            case (rx_state)
                RX_IDLE:
                    if (rx_falling) begin
                        rx_baud_cnt <= (baud_div >> 1) - 16'd1;
                        rx_state <= RX_START;
                    end
                RX_START:
                    if (rx_baud_cnt == 16'd0) begin
                        if (rx_sync[1] == 1'b0) begin
                            rx_baud_cnt <= baud_div - 16'd1;
                            rx_bit_cnt <= 3'd0;
                            rx_parity <= 1'b0;
                            rx_state <= RX_DATA;
                        end else
                            rx_state <= RX_IDLE;
                    end else
                        rx_baud_cnt <= rx_baud_cnt - 16'd1;
                RX_DATA:
                    if (rx_baud_cnt == 16'd0) begin
                        rx_baud_cnt <= baud_div - 16'd1;
                        rx_shift <= {rx_sync[1], rx_shift[7:1]};
                        rx_parity <= rx_parity ^ rx_sync[1];
                        rx_bit_cnt <= rx_bit_cnt + 3'd1;
                        if (rx_bit_cnt == 3'd7)
                            rx_state <= RX_PARITY;
                    end else
                        rx_baud_cnt <= rx_baud_cnt - 16'd1;
                RX_PARITY:
                    if (rx_baud_cnt == 16'd0) begin
                        if (rx_parity ^ rx_sync[1])
                            rx_state <= RX_STOP;
                        else begin
                            rx_error <= 1'b1;
                            rx_state <= RX_IDLE;
                        end
                    end else
                        rx_baud_cnt <= rx_baud_cnt - 16'd1;
                RX_STOP:
                    if (rx_baud_cnt == 16'd0) begin
                        if (rx_sync[1]) begin
                            rx_data <= {rx_sync[1], rx_shift[7:1]};
                            rx_ready <= 1'b1;
                        end else
                            rx_error <= 1'b1;
                        rx_state <= RX_IDLE;
                    end else
                        rx_baud_cnt <= rx_baud_cnt - 16'd1;
            endcase
        end
    end

endmodule
✅ Verilator验证通过 —— uart.v 通过 verilator --lint-only 检查,无错误无警告。

🧪 Verilator仿真验证

// tb_uart.v - UART收发器仿真测试

module tb_uart;

    reg        clk, rst_n;
    reg [15:0] baud_div;
    reg [7:0]  tx_data;
    reg        tx_start;
    wire       tx_busy, tx_done;
    wire [7:0]  rx_data;
    wire       rx_ready, rx_error;
    reg        rx_ack;
    wire       uart_tx, uart_rx;

    // 自环:TX输出连接到RX输入
    assign uart_rx = uart_tx;

    uart uut (.*);

    initial clk = 0;
    always #20 clk = ~clk;  // 25MHz

    localparam BAUD_9600 = 16'd2604;

    integer test_count;
    integer pass_count;

    initial begin
        rst_n = 0; tx_start = 0; rx_ack = 0;
        baud_div = BAUD_9600;
        test_count = 0; pass_count = 0;
        #100; rst_n = 1;

        // 测试1:发送0x55 (01010101,交替位模式)
        $display("--- Test 1: Send 0x55 ---");
        tx_data = 8'h55; tx_start = 1;
        #40; tx_start = 0;
        wait (tx_done);
        wait (rx_ready);
        test_count = test_count + 1;
        if (rx_data == 8'h55) begin
            $display("  PASS: Loopback 0x55 received");
            pass_count = pass_count + 1;
        end else
            $display("  FAIL: Expected 0x55, got 0x%02h", rx_data);
        rx_ack = 1; #40; rx_ack = 0;

        // 测试2:发送0xAA (10101010)
        $display("--- Test 2: Send 0xAA ---");
        tx_data = 8'hAA; tx_start = 1;
        #40; tx_start = 0;
        wait (tx_done);
        wait (rx_ready);
        test_count = test_count + 1;
        if (rx_data == 8'hAA) begin
            $display("  PASS: Loopback 0xAA received");
            pass_count = pass_count + 1;
        end else
            $display("  FAIL: Expected 0xAA, got 0x%02h", rx_data);
        rx_ack = 1; #40; rx_ack = 0;

        // 测试3:发送0x00 (全0)
        $display("--- Test 3: Send 0x00 ---");
        tx_data = 8'h00; tx_start = 1;
        #40; tx_start = 0;
        wait (tx_done);
        wait (rx_ready);
        test_count = test_count + 1;
        if (rx_data == 8'h00 && !rx_error) begin
            $display("  PASS: Loopback 0x00 received");
            pass_count = pass_count + 1;
        end else
            $display("  FAIL: 0x00 test, got 0x%02h err=%b", rx_data, rx_error);

        $display("--- Results: %0d/%0d passed ---", pass_count, test_count);
        $finish;
    end

endmodule

📋 内存映射接口

地址
$FF00UART接收数据UART发送数据
$FF01状态寄存器波特率低字节
$FF02波特率高字节

状态寄存器位定义:

名称含义
7TX_BUSY1=发送器忙
6TX_DONE1=发送完成(读后清零)
5RX_READY1=接收数据就绪
4RX_ERROR1=帧错误或校验错误
3:0保留

🎯 练习

练习1:FIFO缓冲

为UART的TX和RX各添加16字节FIFO缓冲。TX FIFO让CPU可以连续写入多个字节而不需要等待每次发送完成;RX FIFO防止CPU来不及读取时丢失数据。参照第19课的FIFO设计。

练习2:中断驱动通信

实现UART中断:RX就绪时产生中断(FIFO非空或达到半满阈值),TX完成时产生中断。编写中断服务程序的汇编代码框架,实现全双工中断驱动通信。

练习3:多波特率自适应

实现波特率自动检测:当收到第一个字节时,测量起始位和第一个数据位的宽度,自动计算波特率分频系数。提示:可以用系统时钟计数起始位(从下降沿到上升沿)的周期数来估算。

练习4:RS-232电平转换

真实RS-232使用±12V电平(逻辑0=+3~+15V,逻辑1=-3~-15V),与TTL(0/5V)相反。研究MAX232芯片的工作原理,画出TTL↔RS-232的电平转换电路图。在Verilog仿真中,如何模拟这种反相逻辑?

🏆 成就解锁:通信先锋

你实现了完整的UART串口控制器!这包括:

你的8位电脑现在可以与外部世界"对话"了!串口是调试和程序加载的命脉。