📥 第11课:UART接收

过采样 + 起始位检测 + 接收状态机

🏆 成就:接收大师 ✅ Verilator验证

📥
接收大师
Verilator仿真UART接收数据正确

🔍 接收核心:半比特对齐

检测到起始位下降沿后等半比特周期到达起始位中心,确认有效后再每1比特周期采样D0~D7。

Veriloguart_rx.v核心
localparam CLKS_PER_BIT=434, CLKS_HALF=217;
reg rx_sync0, rx_sync1;
always @(posedge clk) begin rx_sync0<=rx; rx_sync1<=rx_sync0; end
wire rx_fall=rx_sync1&~rx_sync0;
// 状态机
START: if(clk_count==CLKS_HALF-1) begin
    if(!rx_sync1) begin state<=DATA; clk_count<=0; end // 确认起始位
    else state<=IDLE; // 假起始位
end else clk_count<=clk_count+1;
DATA: if(clk_count==CLKS_PER_BIT-1) begin
    rx_data[bit_index]<=rx_sync1; // 比特中心采样
    clk_count<=0;
    if(bit_index==7) state<=STOP; else bit_index<=bit_index+1;
end else clk_count<=clk_count+1;

📐 半比特对齐技巧

半比特对齐采样: RX ──┐ ┌──────────┐ ┌── │START│ D0 │ D1 │ └─────┘ └─────┘ ↑ ↑ ↑ 下降沿 半比特后 1比特后 检测到 采样D0 采样D1 起始位 (比特中心) (比特中心) 对时钟偏差容忍度高: 即使收发双方时钟有±2%偏差, 8位数据后采样点仍落在有效区域内。 这就是UART不需要精确时钟同步的秘密!

🧪 Verilator测试:TX→RX自环

SystemVeriloguart_loopback_tb.sv
module uart_loopback_tb;
    logic clk,rst_n; logic [7:0] tx_data,rx_data;
    logic tx_start,tx_busy,tx_wire;
    logic rx_valid; int errors;

    uart_tx u_tx(.*, .tx(tx_wire));
    uart_rx u_rx(.*, .rx(tx_wire));

    initial clk=0; always #10 clk=~clk;

    task send_and_check(input [7:0] data, input string name);
        @(posedge clk); tx_data=data; tx_start=1;
        @(posedge clk); tx_start=0;
        wait(rx_valid);
        if(rx_data==data) $display("✓ %s:发0x%02H,收0x%02H",name,data,rx_data);
        else begin $display("✗ %s:不匹配!",name); errors++; end
        #5000;
    endtask

    initial begin
        rst_n=0;#500;rst_n=1;#500;
        $display("========== UART自环测试 ==========");
        send_and_check(8'h48,"H");
        send_and_check(8'h65,"e");
        send_and_check(8'h6C,"l");
        send_and_check(8'h6C,"l");
        send_and_check(8'h6F,"o");
        $display("==========================================");
        if(errors==0) $display("UART自环测试通过!✓");
        else $display("测试失败!错误数: %0d",errors);
        $finish;
    end
    initial #50_000_000 $finish;
endmodule

📐 接收时序对齐原理

理想采样 vs 实际采样: 理想:每个比特的正中心采样 ┌──┐ ┌──────────┐ │ │ D0 │ D1 │ D2 └──┘─────┘──────────┘───── ↑ ↑ 采样 采样 实际:有±2%时钟偏差 ┌──┐ ┌──────────┐ │ │ D0 │ D1 │ └──┘─────┘──────────┘ ↑ ↑ 偏了→ 偏更多→ 但因为我们在比特中心采样, ±2%偏差在8位数据后仍落在有效区域内 这就是UART"容忍时钟偏差"的秘密! 更好的方法:多数表决(3次采样取多数) 在1/4、1/2、3/4比特位置各采样一次 3个采样中2个或以上为1 → 判定为1 抗噪声能力更强

💡 假起始位处理

噪声可能导致RX线上出现短暂的低电平,看起来像起始位。解决方法:等半比特周期后再确认——如果RX仍为低,才是真起始位;如果已变高,则是噪声,忽略。这就是test bench中START状态的"二次确认"逻辑。

🛠️ 编译运行

Bash编译运行步骤
# 需要uart_tx.v和uart_rx.v
verilator --binary -j 0 --trace \
    uart_tx.v uart_rx.v uart_loopback_tb.sv
./obj_dir/Vuart_loopback_tb

# 预期输出:
# ========== UART自环测试 ==========
# ✓ H: 发0x48, 收0x48
# ✓ e: 发0x65, 收0x65
# ✓ l: 发0x6C, 收0x6C
# ✓ l: 发0x6C, 收0x6C
# ✓ o: 发0x6F, 收0x6F
# ==========================================
# UART自环测试通过!✓

💡 自环测试:把TX输出直接连到RX输入,发送的数据自己接收回来。这是验证UART最简单的方法!实际硬件上也可以这样做——用跳线帽短接TX和RX。

🧠 概念检查清单

✅ UART接收为什么需要同步器?

✅ 起始位下降沿检测的原理?

✅ 半比特对齐为什么提高采样精度?

✅ 假起始位(噪声)如何处理?

✅ 停止位校验失败意味着什么?

✅ UART为什么能容忍±2%时钟偏差?

✅ 自环测试的原理?

🔮 下一课预告

下一课学习SPI通信——4线同步协议,全双工,速度远超UART。CPOL/CPHA四种模式详解!

🐛 常见问题排查

Q: 接收数据偏移一位? 检查半比特对齐是否正确:START状态应等CLKS_HALF而非CLKS_PER_BIT。

Q: 假起始位误触发? START状态在半比特周期后二次确认rx_sync1==0,否则回退IDLE。

Q: 校验/停止位错误? STOP状态检查rx_sync1==1,否则丢弃该帧数据。

📏 UART接收关键参数

参数说明
半比特周期217434/2
采样位置比特中心半比特对齐后采样
同步器级数2级DFF防亚稳态
起始位确认半比特后二次确认防假起始位
时钟容忍度±2~3%8位数据后仍安全
噪声容忍多数表决更好3次采样取多数

💡 UART接收的难点:发送端自己控制时钟,一切尽在掌握。接收端面临三大挑战:①没有时钟线需要波特率约定 ②时钟偏差导致采样偏移 ③噪声可能产生假起始位。半比特对齐+二次确认是解决这些问题的关键!

📏 UART接收容差分析

假设TX和RX时钟偏差±2%:

起始位半比特对齐后,第一个数据位采样点偏差≈±1%×0.5bit

第8个数据位累积偏差≈±2%×8bit=±16%

加上半比特对齐的±25%安全余量,采样点仍在有效区域

结论:UART在±2~3%时钟偏差下仍可靠工作

这就是为什么UART不需要精确时钟同步!

💡 Verilator编译提示

编译:verilator --binary -j 0 --trace uart_tx.v uart_rx.v uart_loopback_tb.sv

运行:./obj_dir/Vuart_loopback_tb

波形:gtkwave uart_loopback.vcd

提示:自环测试需要uart_tx.v和uart_rx.v在同一目录

💡 实际硬件调试:用USB转串口模块(FT232/CP2102)连接FPGA的UART引脚,在电脑上用串口助手(如PuTTY/minicom)发送数据,FPGA接收并显示。这是最常用的FPGA调试手段!

💡 UART历史:UART诞生于1960年代,是最古老的串行通信协议之一。虽然简单,但因其可靠性至今仍广泛使用。现代SoC芯片内部几乎都集成UART控制器,Linux内核的console默认也走UART串口。

📐 半比特对齐技巧

💡 检测到起始位下降沿时,我们在起始位的开始处。等半比特周期后到达起始位正中心,确认有效后再每1比特周期采样D0~D7。这样每个数据位都在比特中心采样,误差最小!

🧪 Verilator测试:TX→RX自环

SystemVeriloguart_loopback_tb.sv片段
uart_tx u_tx(.*, .tx(tx_wire));
uart_rx u_rx(.*, .rx(tx_wire)); // 自环:TX→RX

task send_and_check(input [7:0] data, input string name);
    @(posedge clk); tx_data=data; tx_start=1;
    @(posedge clk); tx_start=0;
    wait(rx_valid);
    if(rx_data==data) $display("✓ %s: 发0x%02H, 收0x%02H",name,data,rx_data);
    else $display("✗ %s: 不匹配!",name);
endtask

📐 接收时序对齐

半比特对齐采样: RX ──┐ ┌──────────┐ ┌── │START│ D0 │ D1 │ └─────┘ └─────┘ ↑ ↑ ↑ 下降沿 半比特后 1比特后 检测到 采样D0 采样D1 起始位 (比特中心) (比特中心) 这种方法对时钟偏差容忍度高: 即使收发双方时钟有±2%偏差, 8位数据后采样点仍落在有效区域内。 这就是UART不需要精确时钟同步的秘密!