4. 状态跳转基础:读写操作对状态的影响
好,咱们今天聊点实在的。FIFO 的状态跳转,说白了就是两个指针在跳舞——写指针往前走,读指针也往前走。谁走得快,谁走得慢,决定了 FIFO 是满、是空、还是半满。
我刚开始做 FIFO 设计时,总觉得这玩意儿简单。直到有一次调试一个跨时钟域的数据传输,发现数据莫名其妙丢了。查了两天,最后发现是状态判断的逻辑出了问题。嗯,从那以后,我对这两个指针的跳转逻辑就格外小心了。
4.1 写操作:写指针递增
写操作发生时,写指针会加 1。这个动作看似简单,但背后有几个关键点要注意。
核心规则:每次写入一个数据,写指针就递增一次。当写指针到达 FIFO 深度时,它会回绕到 0。
举个例子。假设 FIFO 深度是 8,地址从 0 到 7。初始时写指针指向 0。写入第一个数据后,写指针变成 1;写入第二个数据后,变成 2……写到第 8 个数据时,写指针从 7 变成 0。这就是回绕。
我在项目中遇到过一个问题:有人把写指针的递增逻辑写成了组合逻辑,结果写使能信号有毛刺时,指针会多跳一次。这会导致什么?数据覆盖!所以,写指针一定要用时序逻辑,只在时钟上升沿更新。
// 写指针递增的典型写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wptr <= 0;
else if (winc && !full) // 写使能且 FIFO 未满
wptr <= wptr + 1;
end
你看,这里有个关键条件:winc && !full。意思是只有写使能有效,并且 FIFO 没满,才能写。如果 FIFO 已经满了,写使能再高也没用,指针不能动。
个人经验:我习惯把写指针的位宽设计成比地址位宽多 1 位。比如深度 8 的 FIFO,地址需要 3 位,但写指针我用 4 位。多出来的最高位用来判断回绕次数,这样判断空满状态时会更方便。后面讲空满判断时我会细说。
4.2 读操作:读指针递增
读操作和写操作是对称的。每次读出一个数据,读指针就加 1。同样,读指针也会回绕。
你想想看,读指针的递增逻辑和写指针几乎一模一样,只是条件变成了读使能且 FIFO 非空。
// 读指针递增的典型写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rptr <= 0;
else if (rinc && !empty) // 读使能且 FIFO 非空
rptr <= rptr + 1;
end
这里有个坑,我曾经踩过。读使能信号如果来自外部,可能会有毛刺。毛刺导致读指针误跳,读出来的数据就乱了。我的建议是:读使能一定要做同步处理,特别是跨时钟域的场景。
注意:读指针和写指针是独立的。写操作只影响写指针,读操作只影响读指针。它们之间没有直接的因果关系。FIFO 的状态(空、满、半满)是通过比较这两个指针的差值来确定的。
4.3 指针跳转的核心逻辑图
为了让你更直观地理解,我画了一张流程图。这张图展示了写操作和读操作分别如何影响指针,以及状态如何变化。
4.4 指针跳转的时序细节
写指针和读指针的递增,都是在时钟上升沿完成的。这意味着什么?意味着在一个时钟周期内,指针的值是稳定的。你可以在时钟上升沿采样到正确的指针值,然后用来判断状态。
我曾经犯过一个错误:在同一个时钟周期内,既写数据又读数据,然后立即判断空满状态。结果呢?判断结果滞后了一个周期。因为写指针和读指针的更新是在时钟上升沿发生的,而判断逻辑如果用了组合逻辑,就会读到旧值。
避坑指南:判断空满状态时,一定要用寄存器版本的指针值。也就是说,在时钟上升沿之后,用更新后的指针值去做比较。如果你用组合逻辑直接连到指针的输出,可能会读到不确定的值。
4.5 一个简单的例子
咱们用深度 4 的 FIFO 来走一遍流程。地址范围 0~3。
| 时钟周期 | 操作 | 写指针 (wptr) | 读指针 (rptr) | FIFO 状态 |
|---|---|---|---|---|
| 0 | 复位 | 0 | 0 | 空 |
| 1 | 写数据 A | 1 | 0 | 非空非满 |
| 2 | 写数据 B | 2 | 0 | 非空非满 |
| 3 | 读数据 A | 2 | 1 | 非空非满 |
| 4 | 写数据 C | 3 | 1 | 非空非满 |
| 5 | 写数据 D | 0 (回绕) | 1 | 满 (wptr == rptr) |
你看,在第 5 个周期,写指针回绕到了 0,此时 wptr 和 rptr 都是 0,FIFO 就满了。这时候不能再写,但可以读。读一个数据后,rptr 变成 1,wptr 还是 0,状态就变成非满。
嗯,这里要注意:回绕后的 wptr 等于 rptr,并不一定代表 FIFO 是空的。因为写指针可能已经绕了一圈又追上读指针了。这就是为什么我前面说要用多 1 位的指针——用最高位来区分是「空」还是「满」。
好了,这一节的内容就这些。写指针和读指针的递增逻辑,是 FIFO 状态跳转的基础。你只要记住:写操作让写指针加 1,读操作让读指针加 1,状态由两个指针的比较结果决定。后面几节我们会深入讲空满判断的具体实现,以及跨时钟域的处理。
一句话总结:FIFO 的状态跳转,本质就是两个指针的追逐游戏。写指针追读指针,追上了就是满;读指针追写指针,追上了就是空。