4. 状态机实战:UART接收器状态机设计(从Idle到Stop的完整流程)

好,咱们今天来啃一块硬骨头——UART接收器的状态机设计。

UART,说白了就是串口通信。你想想看,两个芯片之间要传数据,最简单的方式就是一根线,一个比特一个比特地送。但怎么保证收得准、收得全?这就得靠状态机来管了。

我个人习惯把UART接收器分成两个部分来看:波特率时钟生成和接收状态机。今天重点讲后者,因为这才是真正体现状态机设计功底的地方。

4.1 UART协议回顾:一帧数据长什么样?

先快速过一下UART的帧格式。标准UART一帧数据包含:

  • 起始位:1位,逻辑'0'
  • 数据位:5~8位,通常8位,LSB先发
  • 校验位:可选,奇校验或偶校验
  • 停止位:1位或2位,逻辑'1'

举个例子,你要发送0x55(二进制01010101),在线上看到的就是:

起始位(0) + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 停止位(1)

注意顺序:LSB先发,所以0x55的第一个数据位是1(最低位),最后一个是0(最高位)。

我的小经验:刚开始做UART时,我总把数据位顺序搞反。后来养成习惯,先在纸上画一遍波形,再写代码。你也不妨试试。

4.2 接收状态机的状态定义

UART接收器状态机,我一般设计成5个状态:

状态名 含义 说明
IDLE 空闲状态 等待起始位,检测到下降沿跳转到START
START 起始位接收 确认起始位有效,准备接收数据
DATA 数据位接收 逐位采样,共8位
PARITY 校验位接收 可选,根据配置决定是否使用
STOP 停止位接收 检查停止位是否为'1',完成后回到IDLE

嗯,这里要注意:有些设计把START和DATA合并,但我建议分开。为什么?因为起始位需要做毛刺过滤,而数据位不需要。分开后逻辑更清晰。

4.3 核心设计:采样点的选择

UART接收最关键的技巧是——在比特中间采样。

你想想看,发送端和接收端的时钟不可能完全同步。如果在比特边界采样,很容易采到跳变沿,导致误码。所以业界惯例是在每个比特的中间位置采样。

怎么实现?用波特率时钟的计数器。假设系统时钟是50MHz,波特率是115200,那么每个比特持续的时间是:

50,000,000 / 115,200 ≈ 434 个时钟周期

我们在计数到217(大约一半)时采样,这就是中间点。

避坑指南:我曾经在一个项目中,计数器从0开始计数到433,采样点设在216。结果发现偶尔会采到错误数据。排查了半天,发现是复位后第一个比特的计数起点没对齐。后来我改成从1开始计数,采样点设在217,问题就解决了。细节决定成败啊。

4.4 状态机代码实现

好了,理论讲完,上代码。这是我个人比较喜欢的一种写法,三段式状态机:

// 状态编码
localparam IDLE   = 3'b000;
localparam START  = 3'b001;
localparam DATA   = 3'b010;
localparam PARITY = 3'b011;
localparam STOP   = 3'b100;

// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    state <= IDLE;
  else
    state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
  next_state = state;
  case (state)
    IDLE: begin
      if (rx_d_sync && !rx_d)  // 检测下降沿
        next_state = START;
    end
    START: begin
      if (bit_mid)              // 起始位中间点
        next_state = DATA;
    end
    DATA: begin
      if (bit_done && bit_cnt == 3'd7)  // 8位收完
        next_state = parity_en ? PARITY : STOP;
    end
    PARITY: begin
      if (bit_done)
        next_state = STOP;
    end
    STOP: begin
      if (bit_done)
        next_state = IDLE;
    end
  endcase
end

// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rx_data <= 8'd0;
    rx_valid <= 1'b0;
  end else begin
    rx_valid <= 1'b0;
    case (state)
      DATA: begin
        if (bit_mid) begin
          rx_data <= {rx_d, rx_data[7:1]};  // 移位寄存器
        end
      end
      STOP: begin
        if (bit_done && rx_d) begin
          rx_valid <= 1'b1;  // 停止位正确,数据有效
        end
      end
    endcase
  end
end
注意:上面的代码中,我用了rx_d_sync和rx_d。这是对输入信号做了两级同步处理后的结果。异步信号进状态机之前,必须同步!否则亚稳态会让你怀疑人生。

4.5 状态机流程图

为了让你更直观地理解整个流程,我画了一张图:

IDLE START DATA PARITY STOP 检测到下降沿 起始位采样完成 8位收完且校验使能 8位收完且无校验 校验位采样完成 停止位采样完成 UART接收器状态机流程图

4.6 关键细节:毛刺过滤与同步

做UART接收器,有几个坑是必踩的。我帮你列出来:

  1. 输入同步:rx信号进来后,先打两拍。第一拍同步,第二拍用来检测边沿。
  2. 毛刺过滤:检测到下降沿后,不要立刻跳转。等半个比特时间再确认一次。如果还是低电平,才认为是真正的起始位。
  3. 停止位检查:停止位必须是'1'。如果收到'0',说明发生了帧错误,应该丢弃这帧数据。
  4. 溢出处理:如果上一帧数据还没被读走,新数据又来了怎么办?我习惯的做法是覆盖旧数据,同时拉高溢出标志。
我的一个教训:曾经有个项目,UART在高温下偶尔丢数据。查了三天,发现是毛刺过滤的计数器位宽不够,导致过滤时间偏短。后来把计数器从8位改成10位,问题就消失了。所以啊,计数器位宽一定要留够余量。

4.7 仿真验证要点

写完了代码,怎么验证?我一般会构造这几个测试用例:

  • 正常收发:发送0x55、0xAA等交替数据,检查接收是否正确
  • 毛刺干扰:在起始位前加一个窄脉冲,看状态机能否正确过滤
  • 帧错误:把停止位改成'0',看状态机是否报错
  • 连续收发:背靠背发送多帧数据,检查状态机能否正确回到IDLE

说实话,UART接收器的状态机并不复杂。但越是简单的东西,越容易在细节上翻车。你只要把状态转移的条件想清楚,把采样点的时序算准确,基本上就稳了。

好了,这一章就到这里。代码你可以直接拿去用,但建议你亲手敲一遍。自己写过的代码,才是真正属于自己的。

核心要点回顾:
  • UART接收器状态机:IDLE → START → DATA → PARITY → STOP
  • 采样点设在比特中间,用计数器控制
  • 输入信号必须同步,起始位要做毛刺过滤
  • 停止位检查是帧错误检测的关键

可可精品资料库