4、UART协议解析实战:从帧格式到Verilog实现
UART,说白了就是串口通信。这玩意儿在嵌入式领域太常见了,几乎每个工程师都绕不开它。我刚开始做CPLD项目时,第一个实战任务就是写UART收发器。那时候觉得挺简单,结果调试起来踩了不少坑。今天咱们就把UART协议彻底讲透,从帧格式到Verilog实现,一步到位。
4.1 UART帧格式:一个字节的旅行
UART通信是异步的,没有时钟线。那收发双方怎么同步?靠的是约定好的波特率和帧格式。一个完整的UART帧长这样:
| 起始位 | 数据位(5~8位) | 校验位(可选) | 停止位(1/1.5/2位) |
|---|---|---|---|
| 0(低电平) | LSB先发 | 奇/偶/无 | 1(高电平) |
起始位:线路空闲时是高电平。突然拉低,告诉接收方「我要发数据了」。我见过有人把起始位理解成「开始信号」,其实它就是一位固定的0。
数据位:通常是8位,从最低位(LSB)开始发。为什么从低位开始?历史原因,早期电传打字机就这么干的。你想想看,如果从高位开始,接收方得先知道数据长度才能正确解析,多麻烦。
校验位:可选。奇校验保证数据+校验位里1的个数是奇数;偶校验就是偶数。我在项目中遇到过一个问题:两个设备明明波特率一样,但一个设了奇校验,一个设了无校验,结果数据全乱套。嗯,这种低级错误我也犯过。
停止位:至少1位的高电平。给接收方一点时间处理刚收到的字节。停止位结束后,线路恢复空闲高电平,等待下一个起始位。
关键点:UART帧的最小单位是1位时间,这个时间由波特率决定。比如9600波特率,1位时间就是1/9600 ≈ 104微秒。
4.2 波特率发生器:CPLD里的节拍器
波特率发生器说白了就是一个分频器。系统时钟比如50MHz,要得到9600波特率,需要分频多少?
分频系数 = 系统时钟频率 / (波特率 × 采样倍数)
采样倍数通常是16。为什么是16?因为接收器需要在每个数据位中间采样,16倍采样可以找到位的中心点,抗干扰能力强。
拿50MHz时钟算一下:
分频系数 = 50_000_000 / (9600 × 16) ≈ 325.52
取整为325,实际波特率 = 50_000_000 / (325 × 16) ≈ 9615
误差 = (9615 - 9600) / 9600 ≈ 0.16%
这个误差完全在允许范围内(通常要求<2%)。我个人习惯用整数分频,然后算一下误差,确保不超过1%。
避坑指南:我曾经用过一个奇葩的晶振,频率是11.0592MHz。为什么选这个?因为它能精确分频出9600、19200等常用波特率。如果你用普通晶振,记得算误差,别想当然。
Verilog实现波特率发生器很简单:
module baud_gen (
input clk,
input rst_n,
output reg baud_tick
);
parameter DIV = 325; // 50MHz -> 9600*16
reg [8:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
baud_tick <= 0;
end else if (cnt == DIV-1) begin
cnt <= 0;
baud_tick <= 1;
end else begin
cnt <= cnt + 1;
baud_tick <= 0;
end
end
endmodule
这个baud_tick信号每16个位时间产生一次,正好对应一个数据位。接收器和发送器都靠它来同步。
4.3 UART接收器:从噪声中提取数据
接收器的核心是采样。线路上的信号可能有毛刺,不能一看到电平变化就信了。标准做法是:检测到起始位后,在位的中间位置采样,连续采3次取多数。
接收状态机一般这样设计:
- 空闲态:等待起始位(检测到下降沿)
- 起始位确认:延迟半个位时间,再采样一次。如果还是低电平,确认是起始位
- 数据位采样:每个位时间中间采样,从LSB开始存
- 校验位处理:如果有校验,计算收到的数据中1的个数,和校验位对比
- 停止位检查:停止位必须是高电平,否则报帧错误
Verilog实现片段:
module uart_rx (
input clk, rst_n,
input rx,
input baud_tick,
output reg [7:0] data,
output reg data_valid
);
reg [3:0] state;
reg [3:0] bit_cnt;
reg [2:0] sample_cnt;
reg [7:0] shift_reg;
// 采样逻辑:在baud_tick的中间位置采样
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= 0;
data_valid <= 0;
end else begin
case (state)
0: // 空闲,检测起始位
if (!rx) state <= 1;
1: // 确认起始位
if (baud_tick) begin
if (!rx) begin
state <= 2;
bit_cnt <= 0;
end else state <= 0;
end
2: // 接收数据位
if (baud_tick) begin
shift_reg[bit_cnt] <= rx;
if (bit_cnt == 7) state <= 3;
else bit_cnt <= bit_cnt + 1;
end
3: // 停止位检查
if (baud_tick) begin
if (rx) begin
data <= shift_reg;
data_valid <= 1;
end
state <= 0;
end
endcase
end
end
endmodule
注意:实际项目中,采样要用3次多数判决,上面代码为了简洁只采了一次。你想想看,如果线路有毛刺,单次采样很容易出错。我建议至少采3次取多数,或者用16倍过采样。
4.4 UART发送器:把数据变成波形
发送器比接收器简单。它不需要采样,只需要按顺序把数据位发出去就行。核心也是一个状态机:
- 空闲态:输出高电平,等待发送请求
- 发送起始位:拉低电平,持续1个位时间
- 发送数据位:从LSB开始,逐位输出
- 发送校验位:如果有,计算并输出
- 发送停止位:拉高电平,持续1个位时间
Verilog实现:
module uart_tx (
input clk, rst_n,
input [7:0] data,
input send,
output reg tx,
output reg busy
);
reg [3:0] state;
reg [3:0] bit_cnt;
reg [7:0] tx_data;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx <= 1;
busy <= 0;
state <= 0;
end else begin
case (state)
0: // 空闲
if (send) begin
tx_data <= data;
tx <= 0; // 起始位
busy <= 1;
state <= 1;
bit_cnt <= 0;
end
1: // 发送数据位
if (baud_tick) begin
tx <= tx_data[bit_cnt];
if (bit_cnt == 7) state <= 2;
else bit_cnt <= bit_cnt + 1;
end
2: // 停止位
if (baud_tick) begin
tx <= 1;
busy <= 0;
state <= 0;
end
endcase
end
end
endmodule
这里有个细节:发送器在发送期间,busy信号要拉高,告诉外部模块「我现在忙着,别发新数据」。我见过有人忘了这个信号,结果连续发送时数据全乱了。
4.5 知识体系总览
下面这张图把UART协议解析的核心逻辑串起来了:
4.6 实战中的那些坑
最后分享几个我踩过的坑:
- 波特率误差累积:发送器和接收器的时钟源不同时,误差会累积。比如发100个字节,每个字节有0.16%误差,累积到最后一个字节可能就错位了。解决办法是用更精确的晶振,或者加同步机制。
- 起始位误触发:线路上的噪声可能被误判为起始位。我建议在检测到下降沿后,延迟半个位时间再确认一次。如果还是低电平,才认为是真的起始位。
- 停止位丢失:如果接收方处理速度慢,可能来不及检测停止位。这时候要加FIFO缓冲,或者提高处理速度。
- 校验位计算错误:奇偶校验要算上数据位和校验位本身。我见过有人只算了数据位,结果校验永远对不上。
个人经验:调试UART时,先用示波器看波形。起始位、数据位、停止位一目了然。如果波形对但数据不对,八成是波特率或校验位设置错了。我曾经花了两小时查代码,最后发现是电脑端的串口助手设成了偶校验,而CPLD这边是无校验。
好了,UART协议解析实战就讲到这里。帧格式、波特率发生器、收发器实现,这三块搞明白了,串口通信就没什么秘密了。下一章咱们聊点更复杂的协议,到时候见。
公众号:蓝海资料掘金营,微信deep3321