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次取多数。

接收状态机一般这样设计:

  1. 空闲态:等待起始位(检测到下降沿)
  2. 起始位确认:延迟半个位时间,再采样一次。如果还是低电平,确认是起始位
  3. 数据位采样:每个位时间中间采样,从LSB开始存
  4. 校验位处理:如果有校验,计算收到的数据中1的个数,和校验位对比
  5. 停止位检查:停止位必须是高电平,否则报帧错误

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. 空闲态:输出高电平,等待发送请求
  2. 发送起始位:拉低电平,持续1个位时间
  3. 发送数据位:从LSB开始,逐位输出
  4. 发送校验位:如果有,计算并输出
  5. 发送停止位:拉高电平,持续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协议解析的核心逻辑串起来了:

UART协议解析知识体系 帧格式定义 波特率发生器 收发器实现 起始位/数据位/校验位/停止位 分频系数计算 状态机设计 LSB先发原则 16倍过采样 接收采样/发送移位 奇偶校验计算 误差分析 帧错误检测 三者协同工作:帧格式定义协议规范,波特率发生器提供时序,收发器完成具体实现

4.6 实战中的那些坑

最后分享几个我踩过的坑:

  • 波特率误差累积:发送器和接收器的时钟源不同时,误差会累积。比如发100个字节,每个字节有0.16%误差,累积到最后一个字节可能就错位了。解决办法是用更精确的晶振,或者加同步机制。
  • 起始位误触发:线路上的噪声可能被误判为起始位。我建议在检测到下降沿后,延迟半个位时间再确认一次。如果还是低电平,才认为是真的起始位。
  • 停止位丢失:如果接收方处理速度慢,可能来不及检测停止位。这时候要加FIFO缓冲,或者提高处理速度。
  • 校验位计算错误:奇偶校验要算上数据位和校验位本身。我见过有人只算了数据位,结果校验永远对不上。

个人经验:调试UART时,先用示波器看波形。起始位、数据位、停止位一目了然。如果波形对但数据不对,八成是波特率或校验位设置错了。我曾经花了两小时查代码,最后发现是电脑端的串口助手设成了偶校验,而CPLD这边是无校验。

好了,UART协议解析实战就讲到这里。帧格式、波特率发生器、收发器实现,这三块搞明白了,串口通信就没什么秘密了。下一章咱们聊点更复杂的协议,到时候见。


公众号:蓝海资料掘金营,微信deep3321