过采样 + 起始位检测 + 接收状态机
🏆 成就:接收大师 ✅ Verilator验证
检测到起始位下降沿后等半比特周期到达起始位中心,确认有效后再每1比特周期采样D0~D7。
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;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💡 假起始位处理
噪声可能导致RX线上出现短暂的低电平,看起来像起始位。解决方法:等半比特周期后再确认——如果RX仍为低,才是真起始位;如果已变高,则是噪声,忽略。这就是test bench中START状态的"二次确认"逻辑。
# 需要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,否则丢弃该帧数据。
| 参数 | 值 | 说明 |
|---|---|---|
| 半比特周期 | 217 | 434/2 |
| 采样位置 | 比特中心 | 半比特对齐后采样 |
| 同步器级数 | 2级DFF | 防亚稳态 |
| 起始位确认 | 半比特后二次确认 | 防假起始位 |
| 时钟容忍度 | ±2~3% | 8位数据后仍安全 |
| 噪声容忍 | 多数表决更好 | 3次采样取多数 |
💡 UART接收的难点:发送端自己控制时钟,一切尽在掌握。接收端面临三大挑战:①没有时钟线需要波特率约定 ②时钟偏差导致采样偏移 ③噪声可能产生假起始位。半比特对齐+二次确认是解决这些问题的关键!
假设TX和RX时钟偏差±2%:
起始位半比特对齐后,第一个数据位采样点偏差≈±1%×0.5bit
第8个数据位累积偏差≈±2%×8bit=±16%
加上半比特对齐的±25%安全余量,采样点仍在有效区域
结论:UART在±2~3%时钟偏差下仍可靠工作
这就是为什么UART不需要精确时钟同步!
编译: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。这样每个数据位都在比特中心采样,误差最小!
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