4. 状态机设计第二步:数据接收状态(DATA)的设计,逐位采样与移位寄存

好,咱们接着往下走。上一步我们把起始位逮住了,现在状态机就该进入真正的数据接收环节了。这一步的核心任务就两个:逐位采样和移位寄存。说白了,就是把串行线上一个个比特,按照约定的波特率,稳稳当当地收进来,再拼成一个完整的字节。

我个人习惯把DATA状态称为“苦力状态”。为什么?因为这里要处理最繁琐的时序细节。你想想看,一个字节8个位,每个位要在最佳时刻采样,采完还得存好。任何一个环节出问题,收到的数据就是错的。我在项目中遇到过好几次,板子调通了,但收上来的数据总是错位,最后查出来都是采样时刻没算准。

4.1 逐位采样的核心:找到“眼图中心”

串口通信里,每个bit的持续时间是固定的。比如波特率9600,一个bit就是约104微秒。但问题来了:我们什么时候去读这根数据线才是对的?

答案是:在bit的中间时刻采样。为什么?因为信号在跳变沿附近最不稳定,有毛刺、有抖动。而bit中间是最平稳的,就像眼图睁开最大的那个位置。嗯,这里要注意,如果你在边沿采样,很可能采到错误的电平。

所以,我们需要一个计数器,来精确地数出半个bit、一个bit的时间。我通常的做法是:

  • 用一个baud_cnt计数器,从0数到波特率分频值的一半,就表示到了bit中间
  • 到了中间时刻,锁存当前rxd的电平值
  • 然后继续计数,直到一个bit结束,再准备采下一个

关键点:采样时刻 = 起始位确认后的第 (baud_rate_div/2) 个时钟周期。这个“一半”的位置,就是黄金采样点。

4.2 移位寄存器的设计:左移还是右移?

采到bit之后,怎么存?当然是用移位寄存器。这里有个小细节:先收到的bit是LSB还是MSB?标准UART协议是LSB first,也就是先发最低位。所以我们的移位寄存器应该设计成右移(低位在前)。

我见过一些新手把移位方向搞反了,结果收上来的字节高低位颠倒,调试半天才发现。我曾经也犯过这个错,后来养成了一个习惯:画个时序图,把每个bit标出来,再写代码。

移位寄存器的典型写法如下:

// 数据接收状态下的移位寄存
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        shift_reg <= 8'b0;
    end else if (state == DATA && bit_center) begin
        // 右移一位,新采到的bit放到最高位
        shift_reg <= {rxd_sync, shift_reg[7:1]};
    end
end

注意看,这里我把新采到的rxd_sync放到了最高位,然后整体右移。这样,当8个bit收完时,shift_reg[7:0]里就是正确的字节顺序:shift_reg[0]是第一个收到的bit(LSB),shift_reg[7]是最后一个(MSB)。

小技巧:如果你习惯左移,也可以。只要保证最后输出时把高低位swap一下就行。但我个人推荐右移,更符合UART协议的自然顺序,不容易出错。

4.3 状态机中的bit计数器

光有移位寄存器还不够,我们还得知道什么时候8个bit收完了。所以需要一个bit计数器,每收一个bit就加1。当计到8时,说明一个字节收完了,状态机就该跳转到停止位检测状态了。

这个计数器怎么设计?我一般这样写:

// bit计数器,在DATA状态下工作
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bit_cnt <= 3'b0;
    end else if (state == DATA && bit_done) begin
        // 每个bit结束时加1
        bit_cnt <= bit_cnt + 1'b1;
    end else if (state != DATA) begin
        bit_cnt <= 3'b0;  // 离开DATA状态就清零
    end
end

这里bit_done信号表示一个bit的采样周期结束了。当bit_cnt == 3'd7且bit_done有效时,就说明第8个bit收完了。这时候,我们可以把shift_reg里的数据锁存到输出寄存器,同时状态机跳转到STOP状态。

避坑指南:我曾经在项目中遇到一个问题:bit_cnt清零的时机没处理好。状态机跳走之后,bit_cnt还残留着旧值,导致下一次进入DATA状态时计数起点不对。记住:离开DATA状态时,一定要把bit_cnt清零。最好在状态机跳转的条件里一并处理。

4.4 完整的DATA状态流程图

说了这么多,咱们用一张图把整个流程串起来。下面这张SVG图展示了从进入DATA状态到收完8个bit的完整路径:

DATA状态逐位采样与移位寄存流程图 进入DATA状态 到达bit中心? (baud_cnt == half) 否 是 采样rxd电平 移位寄存:shift_reg <= {rxd, shift_reg[7:1]} bit结束? (baud_cnt == full) 否 是 bit_cnt +1 收完8bit? (bit_cnt == 7) 否,继续采下一个bit 是 锁存数据到rx_data 跳转到STOP状态

4.5 代码实现要点总结

最后,我把DATA状态的核心代码片段整理一下。你写的时候,这几个信号一定要理清楚:

信号名 作用 关键说明
baud_cnt 波特率分频计数器 从0数到分频值-1,用于产生bit采样时钟
bit_center bit中间时刻标志 baud_cnt == (分频值/2) 时拉高,用于采样
bit_done bit结束标志 baud_cnt == 分频值-1 时拉高,用于bit计数
shift_reg 8位移位寄存器 右移方式,新bit放最高位
bit_cnt 已接收bit数量 0~7,到7时表示字节收完
rx_data 最终输出的接收数据 bit_cnt==7且bit_done时锁存shift_reg

核心原则:采样要准(bit中间),移位要对(LSB first),计数要清(离开状态就清零)。这三条做到了,DATA状态就稳了。

嗯,DATA状态的设计就讲到这里。说白了,就是用一个计数器卡住时间点,用一个移位寄存器攒数据,再用一个bit计数器知道什么时候收完。逻辑不复杂,但细节很多。你写代码的时候,建议先在纸上把时序波形画一遍,把每个时钟沿上信号的变化理清楚,再动手写。这样能省掉很多调试时间。

可可精品资料库