第2章:UART协议详解
UART,说白了就是串口通信。我在刚入行那会儿,觉得这玩意儿太简单了,不就是一根线发数据嘛。直到后来做高速通信芯片,才发现UART里藏着不少门道。今天咱们就把它彻底聊透。
2.1 UART帧结构
UART的帧结构,我习惯把它想象成一列火车。每节车厢都有固定位置,少了哪节都不行。
一个标准的UART帧包含以下部分:
- 起始位(Start Bit):逻辑0,告诉接收方“我要发数据了”
- 数据位(Data Bits):5~8位,通常是8位
- 校验位(Parity Bit):可选,奇校验或偶校验
- 停止位(Stop Bit):逻辑1,表示帧结束
关键点:起始位必须是下降沿。接收方就是靠这个边沿来同步时钟的。我曾经调试过一个板子,起始位毛刺太多,接收方老是误触发,折腾了两天才发现是信号完整性出了问题。
数据位通常LSB在前。举个例子,你要发送0x55(二进制01010101),实际线上先发的是最低位1,然后是0、1、0、1、0、1、0,最后是最高位0。嗯,这个顺序别搞反了。
校验位分三种:
- 无校验:省掉这个位,帧长最短
- 奇校验:数据位+校验位中1的个数为奇数
- 偶校验:数据位+校验位中1的个数为偶数
停止位可以是1位、1.5位或2位。我个人习惯用1位停止位,除非是噪声环境特别恶劣的场景。
2.2 波特率计算
波特率就是每秒传输的符号数。对于UART来说,一个符号就是一个bit,所以波特率就等于比特率。
常见的波特率有:9600、19200、38400、115200。115200是我最常用的,速度够快,稳定性也不错。
波特率怎么算?看这个公式:
波特率 = 系统时钟频率 / (16 × 波特率分频值)
为什么是16?因为UART接收器通常用16倍过采样。也就是说,每个bit采样16次,取中间3次的值做多数判决。这样能有效抗干扰。
举个例子,系统时钟50MHz,目标波特率115200:
分频值 = 50,000,000 / (16 × 115200) ≈ 27.13
取整为27,实际波特率 = 50,000,000 / (16 × 27) ≈ 115,740
误差 = (115,740 - 115,200) / 115,200 ≈ 0.47%
经验之谈:波特率误差控制在±2%以内通常没问题。我曾经遇到过用8MHz晶振跑115200,误差高达3.5%,结果在高温下频繁丢数据。所以选晶振时最好算一下误差。
2.3 硬件流控(RTS/CTS)
硬件流控,说白了就是让接收方告诉发送方“我忙,你先别发”。
两个关键信号:
- RTS(Request To Send):发送方请求发送数据
- CTS(Clear To Send):接收方允许发送
工作流程是这样的:
- 发送方拉低RTS,表示“我想发数据”
- 接收方如果缓冲区有空,就拉低CTS,表示“来吧”
- 发送方看到CTS为低,开始发送
- 接收方缓冲区快满时,拉高CTS,发送方停止发送
你想想看,如果没有流控,接收方处理不过来怎么办?只能丢数据。我在做物联网网关时,遇到过CPU中断响应不及时,串口数据被覆盖的情况。加了硬件流控后,问题就解决了。
注意:RTS/CTS是负逻辑,低电平有效。很多新手会搞反,以为高电平才能发。另外,有些芯片的RTS/CTS是交叉连接的,接错了也会出问题。
2.4 Verilog实现简化UART发送模块
下面是我写的一个简化版UART发送模块。去掉了校验位,只保留核心功能,方便你理解。
module uart_tx (
input wire clk, // 系统时钟
input wire rst_n, // 复位,低有效
input wire tx_start, // 发送启动信号
input wire [7:0] tx_data, // 待发送数据
output reg tx_busy, // 发送忙标志
output reg txd // 串行输出
);
// 参数:波特率分频值
// 假设50MHz时钟,115200波特率
// 分频值 = 50M / (16 * 115200) ≈ 27
parameter BAUD_DIV = 27;
// 状态定义
localparam IDLE = 3'd0;
localparam START = 3'd1;
localparam DATA = 3'd2;
localparam STOP = 3'd3;
reg [2:0] state;
reg [3:0] bit_cnt; // 已发送位数
reg [7:0] data_reg; // 数据寄存器
reg [4:0] clk_cnt; // 时钟计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
txd <= 1'b1; // 空闲时高电平
tx_busy <= 1'b0;
bit_cnt <= 4'd0;
clk_cnt <= 5'd0;
end else begin
case (state)
IDLE: begin
txd <= 1'b1;
tx_busy <= 1'b0;
if (tx_start) begin
data_reg <= tx_data;
state <= START;
tx_busy <= 1'b1;
clk_cnt <= 5'd0;
end
end
START: begin
txd <= 1'b0; // 发送起始位
if (clk_cnt == BAUD_DIV - 1) begin
clk_cnt <= 5'd0;
state <= DATA;
bit_cnt <= 4'd0;
end else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
DATA: begin
txd <= data_reg[0]; // LSB先发
if (clk_cnt == BAUD_DIV - 1) begin
clk_cnt <= 5'd0;
data_reg <= {1'b0, data_reg[7:1]}; // 右移
if (bit_cnt == 4'd7) begin
state <= STOP;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
STOP: begin
txd <= 1'b1; // 发送停止位
if (clk_cnt == BAUD_DIV - 1) begin
clk_cnt <= 5'd0;
state <= IDLE;
tx_busy <= 1'b0;
end else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
代码说明:这个模块用了状态机,四个状态分别是空闲、起始位、数据位、停止位。每个bit的持续时间由BAUD_DIV控制。我故意去掉了校验位,因为实际项目中很多场景用不到,加了反而增加复杂度。
使用示例:
// 实例化发送模块
uart_tx #(
.BAUD_DIV(27) // 115200 @ 50MHz
) u_uart_tx (
.clk (clk),
.rst_n (rst_n),
.tx_start (tx_start),
.tx_data (tx_data),
.tx_busy (tx_busy),
.txd (txd)
);
// 发送一个字节
assign tx_data = 8'h55;
assign tx_start = send_enable;
调试建议:写完后用仿真看一下波形。重点检查起始位的下降沿、数据位的顺序、停止位的高电平持续时间。我习惯在仿真时把BAUD_DIV设小一点,比如设成5,这样波形看起来快,调试效率高。
好了,UART的核心内容就这些。下一章咱们聊SPI协议,那个比UART快多了,但引脚也多。到时候见。