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(最高位)。
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(大约一半)时采样,这就是中间点。
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
4.5 状态机流程图
为了让你更直观地理解整个流程,我画了一张图:
4.6 关键细节:毛刺过滤与同步
做UART接收器,有几个坑是必踩的。我帮你列出来:
- 输入同步:rx信号进来后,先打两拍。第一拍同步,第二拍用来检测边沿。
- 毛刺过滤:检测到下降沿后,不要立刻跳转。等半个比特时间再确认一次。如果还是低电平,才认为是真正的起始位。
- 停止位检查:停止位必须是'1'。如果收到'0',说明发生了帧错误,应该丢弃这帧数据。
- 溢出处理:如果上一帧数据还没被读走,新数据又来了怎么办?我习惯的做法是覆盖旧数据,同时拉高溢出标志。
4.7 仿真验证要点
写完了代码,怎么验证?我一般会构造这几个测试用例:
- 正常收发:发送0x55、0xAA等交替数据,检查接收是否正确
- 毛刺干扰:在起始位前加一个窄脉冲,看状态机能否正确过滤
- 帧错误:把停止位改成'0',看状态机是否报错
- 连续收发:背靠背发送多帧数据,检查状态机能否正确回到IDLE
说实话,UART接收器的状态机并不复杂。但越是简单的东西,越容易在细节上翻车。你只要把状态转移的条件想清楚,把采样点的时序算准确,基本上就稳了。
好了,这一章就到这里。代码你可以直接拿去用,但建议你亲手敲一遍。自己写过的代码,才是真正属于自己的。
- UART接收器状态机:IDLE → START → DATA → PARITY → STOP
- 采样点设在比特中间,用计数器控制
- 输入信号必须同步,起始位要做毛刺过滤
- 停止位检查是帧错误检测的关键