第1章:流水线级数划分——串口接收拆分为3级流水线
各位同学,今天我们来聊聊串口接收模块的流水线改造。说实话,串口(UART)接收这个模块,很多初学者都能写出来,但写出来的东西能不能稳定跑在高速时钟下,那就是另一回事了。
我个人习惯,在设计任何通信接口模块时,第一件事就是考虑流水线怎么划分。为什么?因为串口接收天然就是一个多步骤的过程——你要采样、要判断边沿、要组装数据。把这些步骤塞到一个时钟周期里完成,说白了就是给自己挖坑。
3.1 为什么是3级流水线?
你想想看,串口接收的核心任务是什么?就三件事:
- 采样:在正确的时刻把RX线上的电平抓下来
- 边沿检测:判断起始位什么时候来,什么时候该收下一个bit
- 数据组装:把收到的bit拼成一个完整的字节
这三件事,每一步都有各自的时序要求。我在项目中遇到过这样的情况:把采样和边沿检测放在同一个always块里,结果因为组合逻辑路径太长,采样时刻总是偏掉,导致误码率飙升。后来我痛定思痛,决定把它们拆成三级流水线。
核心思想:每一级流水线只做一件事,把组合逻辑路径缩短,让时序更容易收敛。
3.2 三级流水线的划分方案
好,我们直接上干货。三级流水线怎么分?我给出一个经过实战检验的方案:
| 流水线级 | 名称 | 功能描述 | 时钟周期 |
|---|---|---|---|
| 第1级 | 采样级 | 用系统时钟对RX线进行过采样,得到稳定的采样值 | 第1个时钟 |
| 第2级 | 边沿检测级 | 检测起始位下降沿,生成采样使能信号 | 第2个时钟 |
| 第3级 | 数据组装级 | 根据使能信号,将串行bit组装成并行字节 | 第3个时钟 |
嗯,这里要注意:每一级之间用寄存器打一拍。这样做的目的是把组合逻辑的传播路径截断,避免出现长路径时序违例。
3.3 每一级的具体设计
第1级:采样级
采样级说白了就是做两件事:同步和过采样。因为RX线来自外部,是异步信号,必须先同步到系统时钟域。我一般用两级触发器做同步,然后再用系统时钟的N倍频(通常是16倍)去采样。
// 采样级:同步+过采样
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_sync1 <= 1'b1;
rx_sync2 <= 1'b1;
rx_sample <= 1'b1;
end else begin
rx_sync1 <= rx; // 第一级同步
rx_sync2 <= rx_sync1; // 第二级同步
rx_sample <= rx_sync2; // 采样值输出到下一级
end
end
小技巧:我习惯把同步器和采样放在同一个always块里,这样寄存器更紧凑,布局布线时路径更短。
第2级:边沿检测级
边沿检测这一级,核心是检测起始位的下降沿。怎么检测?把当前采样值和上一拍的采样值做异或。如果上一拍是高电平、当前是低电平,那就是下降沿。
// 边沿检测级:检测下降沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_delay <= 1'b1;
neg_edge <= 1'b0;
end else begin
rx_delay <= rx_sample; // 打一拍,用于边沿比较
neg_edge <= rx_delay && !rx_sample; // 下降沿检测
end
end
我曾经踩过一个坑:只检测边沿,没考虑毛刺。结果RX线上一个毛刺就触发了起始位检测,整个数据帧全乱了。后来我加了连续三次采样都为低才确认是下降沿,这才稳定下来。
避坑指南:我曾经因为边沿检测没做滤波,导致误码率高达10%。记住,串口接收一定要做3次连续采样确认,这是血的教训。
第3级:数据组装级
数据组装级,就是把收到的bit按顺序拼起来。这里需要一个移位寄存器和一个bit计数器。每收到一个有效的bit,就把它移入寄存器,同时计数器加1。当计数器达到8时,说明一个字节收完了。
// 数据组装级:移位寄存器+bit计数
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shift_reg <= 8'b0;
bit_cnt <= 3'b0;
rx_data <= 8'b0;
rx_done <= 1'b0;
end else if (sample_en && !stop_bit) begin
shift_reg <= {rx_sample, shift_reg[7:1]}; // 低位先收
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'b0;
rx_data <= {rx_sample, shift_reg[7:1]};
rx_done <= 1'b1;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
rx_done <= 1'b0;
end
end
3.4 流水线时序图
为了让大家更直观地理解三级流水线的工作过程,我画了一张时序图。你看,每一级都在自己的时钟周期里完成工作,互不干扰。
从时序图上你可以看到:在T1时刻,第1级采样RX;T2时刻,第1级继续采样,同时第2级开始处理T1的采样结果;T3时刻,第1、2、3级同时工作,各自处理不同时刻的数据。这就是流水线的精髓——每一级都在干活,没有空闲等待。
3.5 为什么这样划分最合理?
你可能会问:为什么不多分几级?比如分成5级、8级?
嗯,这个问题我思考过。串口接收的波特率通常不高(115200bps算快的了),系统时钟往往是MHz级别。三级流水线已经能把组合逻辑路径压到很短。分太多级反而会增加寄存器延迟和面积开销,得不偿失。
我在一个项目中试过把串口接收分成5级流水线,结果时序确实更好看了,但面积增加了30%,而且因为级间握手逻辑变复杂,反而引入了新的bug。后来我改回3级,一切就稳了。
经验之谈:流水线不是越多越好。对于串口接收这种低速接口,3级是黄金分割点——既保证了时序收敛,又不会过度消耗资源。
3.6 本章小结
好了,这一章我们讲了串口接收模块的三级流水线划分:
- 采样级:负责同步和过采样,把异步信号变成同步信号
- 边沿检测级:检测起始位下降沿,生成采样使能
- 数据组装级:移位寄存器+bit计数器,拼出完整字节
每一级之间用寄存器打一拍,组合逻辑路径被截断,时序自然就好收敛了。下一章我们会深入每一级的细节,看看Verilog代码怎么写才能既高效又稳定。
课后思考:如果系统时钟是50MHz,串口波特率是115200,过采样倍数设为16倍,那么采样时钟应该是多少?这个计算会影响到你的采样级设计。