UART协议 + 波特率 + 发送状态机
🏆 成就:电报员 ✅ Verilator验证
起始位(0)+8数据位(LSB first)+停止位(1)。115200bps在50MHz下=434个时钟/位。
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分频 | 用途 |
|---|---|---|---|
| 9600 | 104.17μs | 5208 | 低速通信 |
| 19200 | 52.08μs | 2604 | 常用 |
| 115200 | 8.68μs | 434 | 高速常用 |
| 921600 | 1.09μs | 54 | 超高速 |
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💡 为什么空闲=1?
UART线路空闲时为高电平(1)。起始位的下降沿就是信号——"数据来了!"如果空闲是0,接收方无法区分"空闲"和"数据0"。
verilator --binary -j 0 --trace uart_tx.v uart_tx_tb.sv ./obj_dir/Vuart_tx_tb # 预期输出: # ========== UART TX 验证 ========== # (等待5个字节发送完成) # ========================================== # 提示:用GTKWave查看tx信号,可以看到完整的UART帧 # 放大到一个字节的起始位下降沿处,观察8数据位+停止位
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,给接收端恢复时间。
| 参数 | 值 | 说明 |
|---|---|---|
| 帧格式 | 8N1 | 8数据+无校验+1停止 |
| 帧长度 | 10位 | START+8DATA+STOP |
| 空闲电平 | 1(高) | TX空闲=1 |
| 数据顺序 | LSB first | 最低位先发 |
| 起始位 | 0(低) | 下降沿=数据来了 |
| 停止位 | 1(高) | 帧结束标志 |
| 115200bps周期 | 8.68μs | 1/115200 |
| 1字节耗时 | 86.8μs | 10bit×8.68μs |
💡 UART的广泛应用:UART虽然古老但无处不在!蓝牙模块(HC-05)、GPS模块、WiFi模块(ESP8266)、调试串口、Arduino通信——几乎所有的嵌入式模块都用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 --binary -j 0 --trace uart_tx.v uart_tx_tb.sv
运行:./obj_dir/Vuart_tx_tb
波形:gtkwave uart_tx.vcd
提示:在波形中放大到一个字节的起始位处,可以清楚看到START+8DATA+STOP的完整帧结构
| 波特率 | 比特周期 | 50MHz分频 |
|---|---|---|
| 9600 | 104.17μs | 5208 |
| 19200 | 52.08μs | 2604 |
| 115200 | 8.68μs | 434 |
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
endUART规定LSB先发,这是历史约定。好处是接收端用位索引移位即可,不需要反转。注意I2C/SPI通常是MSB first!