📡 第10课:UART发送

UART协议 + 波特率 + 发送状态机

🏆 成就:电报员 ✅ Verilator验证

📡
电报员
Verilator仿真UART发送Hello

📨 UART 8N1格式

起始位(0)+8数据位(LSB first)+停止位(1)。115200bps在50MHz下=434个时钟/位。

Veriloguart_tx.v核心
localparam CLKS_PER_BIT=434; // 50MHz/115200
typedef enum logic[2:0]{IDLE,START,DATA,STOP,CLEANUP} state_t;
reg [2:0] state; reg [15:0] clk_count; reg [2:0] bit_index;
reg [7:0] tx_shift;
always @(posedge clk) case(state)
    IDLE: if(tx_start) begin tx_shift<=tx_data; state<=START; end
    START: begin tx<=0;
        if(clk_count>=CLKS_PER_BIT-1) begin state<=DATA; clk_count<=0; end
        else clk_count<=clk_count+1; end
    DATA: begin tx<=tx_shift[bit_index]; // LSB first
        if(clk_count>=CLKS_PER_BIT-1) begin
            clk_count<=0;
            if(bit_index==7) state<=STOP; else bit_index<=bit_index+1;
        end else clk_count<=clk_count+1; end
    STOP: begin tx<=1;
        if(clk_count>=CLKS_PER_BIT-1) begin state<=CLEANUP; clk_count<=0; end
        else clk_count<=clk_count+1; end
    CLEANUP: begin tx<=1; state<=IDLE; end
endcase

📐 波特率与比特周期

波特率比特周期50MHz分频用途
9600104.17μs5208低速通信
1920052.08μs2604常用
1152008.68μs434高速常用
9216001.09μs54超高速

📐 状态机时序图

发送'H'=0x48=01001000b: tx_start ──┐ └────────────────────── tx_busy ─────────────────────────┐ └── tx ──┐ 0 0 0 1 0 0 1 0 ┌────── │S D0 D1 D2 D3 D4 D5 D6 D7│STOP └──────────────────────┘ │←─ 1bit ─→│ │← 434个时钟周期 →│ 状态: IDLE→START→DATA(8bit)→STOP→IDLE 注意:D0=0,D1=0,D2=0,D3=1,D4=0,D5=0,D6=1,D7=0 LSB first!线上看到 00010010

🧪 Verilator测试台:发送"Hello"

SystemVeriloguart_tx_tb.sv
module uart_tx_tb;
    logic clk,rst_n; logic [7:0] tx_data;
    logic tx_start,tx_busy,tx;
    uart_tx uut(.*);
    initial clk=0; always #10 clk=~clk;

    reg [7:0] msg[0:4];
    initial begin msg[0]=8'h48; msg[1]=8'h65;
                 msg[2]=8'h6C; msg[3]=8'h6C; msg[4]=8'h6F; end

    integer i;
    initial begin
        rst_n=0;#500;rst_n=1;#500;
        $display("========== UART TX 验证 ==========");
        for(i=0;i<5;i++) begin
            @(posedge clk); tx_data=msg[i]; tx_start=1;
            @(posedge clk); tx_start=0;
            wait(!tx_busy); #1000;
        end
        $display("==========================================");
        $finish;
    end
    initial #50_000_000 $finish;
endmodule

📐 UART协议完整时序图

UART 8N1 格式 (8数据位, 无校验, 1停止位): TX空闲=1 ┌─┐ ┌─┐──── ────┘ │ │ │ │ │ │ │ │ │ │ │ │S│D0│D1│D2│D3│D4│D5│D6│D7│ 停止位 │S│ │ │T│ │ │ │ │ │ │ │ │ 1 │T│ │ │A│ │ │ │ │ │ │ │ │ │A│ │ │R│ │ │ │ │ │ │ │ │ │R│ │ │T│←── LSB first ──→│ │ │ │ │ │ │ │ │ │ └─┘ └───────────────────┘ └─┘ 发送 'H' = 0x48 = 0100_1000b: D0=0 D1=0 D2=0 D3=1 D4=0 D5=0 D6=1 D7=0 线上看到: 0_00010010_1 发送 'e' = 0x65 = 0110_0101b: D0=1 D1=0 D2=1 D3=0 D4=0 D5=1 D6=1 D7=0 线上看到: 0_10100110_1 每个1bit持续 1/115200 ≈ 8.68μs 发送1字节 ≈ 10bit × 8.68μs = 86.8μs

💡 为什么空闲=1?

UART线路空闲时为高电平(1)。起始位的下降沿就是信号——"数据来了!"如果空闲是0,接收方无法区分"空闲"和"数据0"。

🛠️ 编译运行

Bash编译运行步骤
verilator --binary -j 0 --trace uart_tx.v uart_tx_tb.sv
./obj_dir/Vuart_tx_tb

# 预期输出:
# ========== UART TX 验证 ==========
# (等待5个字节发送完成)
# ==========================================

# 提示:用GTKWave查看tx信号,可以看到完整的UART帧
# 放大到一个字节的起始位下降沿处,观察8数据位+停止位

💡 为什么LSB first?

UART规定LSB先发,这是历史约定。好处是接收端只需用位索引移位即可,不需要反转。注意I2C/SPI通常是MSB first!这是串行通信中最容易混淆的一点。

🧠 概念检查清单

✅ UART数据帧的格式(起始位+数据位+停止位)?

✅ 为什么空闲时TX线为1?

✅ 波特率如何计算?115200在50MHz下分频系数?

✅ 发送状态机的5个状态?

✅ tx_busy信号的作用?

✅ 为什么UART是LSB first?

✅ 如何计算发送1字节所需的时间?

🔮 下一课预告

下一课学习UART接收——比发送更难!需要半比特对齐、起始位二次确认、过采样。TX→RX自环验证!

🐛 常见问题排查

Q: 接收端收到乱码? 检查波特率是否一致!TX/RX必须使用相同的CLKS_PER_BIT。

Q: tx_start脉冲太宽? tx_start只需1个时钟周期宽,保持高电平会导致重复启动。

Q: 停止位后没有空闲期? CLEANUP状态确保tx=1后才回IDLE,给接收端恢复时间。

📏 UART关键参数

参数说明
帧格式8N18数据+无校验+1停止
帧长度10位START+8DATA+STOP
空闲电平1(高)TX空闲=1
数据顺序LSB first最低位先发
起始位0(低)下降沿=数据来了
停止位1(高)帧结束标志
115200bps周期8.68μs1/115200
1字节耗时86.8μs10bit×8.68μs

💡 UART的广泛应用:UART虽然古老但无处不在!蓝牙模块(HC-05)、GPS模块、WiFi模块(ESP8266)、调试串口、Arduino通信——几乎所有的嵌入式模块都用UART。掌握UART是嵌入式开发的基本功!

📏 UART通信速率计算

115200bps:每秒115,200位,1位≈8.68μs

发送1字节(10位)≈86.8μs → 每秒约11,520字节

发送"Hello"(5字节)≈434μs

9600bps:每秒9,600位,1位≈104.17μs

发送1字节≈1.04ms → 每秒约960字节

1Mbps:50MHz下分频50,1位=1μs,每秒100KB

💡 Verilator编译提示

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

运行:./obj_dir/Vuart_tx_tb

波形:gtkwave uart_tx.vcd

提示:在波形中放大到一个字节的起始位处,可以清楚看到START+8DATA+STOP的完整帧结构

📐 波特率与比特周期

波特率比特周期50MHz分频
9600104.17μs5208
1920052.08μs2604
1152008.68μs434

🧪 Verilator测试台:发送"Hello"

SystemVeriloguart_tx_tb.sv片段
reg [7:0] msg[0:4];
initial begin msg[0]=8'h48; msg[1]=8'h65; msg[2]=8'h6C; msg[3]=8'h6C; msg[4]=8'h6F; end
integer i;
initial begin
    for(i=0;i<5;i++) begin
        @(posedge clk); tx_data=msg[i]; tx_start=1;
        @(posedge clk); tx_start=0;
        wait(!tx_busy); #1000;
    end
end

💡 为什么LSB first?

UART规定LSB先发,这是历史约定。好处是接收端用位索引移位即可,不需要反转。注意I2C/SPI通常是MSB first!

📐 状态机时序图

发送'H'=0x48=01001000b: tx_start ──┐ └────────────────────── tx_busy ─────────────────────────┐ └── tx ──┐ 0 0 0 1 0 0 1 0 ┌────── │S D0 D1 D2 D3 D4 D5 D6 D7│STOP └──────────────────────┘ │←─ 1bit ─→│ │← 434个时钟周期 →│ 状态: IDLE→START→DATA(8bit)→STOP→IDLE 注意:D0=0,D1=0,D2=0,D3=1,D4=0,D5=0,D6=1,D7=0 LSB first!所以线上看到的是 00010010