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 指针跳转的核心逻辑图

为了让你更直观地理解,我画了一张流程图。这张图展示了写操作和读操作分别如何影响指针,以及状态如何变化。

FIFO 指针跳转与状态变化流程图 初始状态 wptr=0, rptr=0, empty=1 写操作发生 (winc=1, full=0) 写指针 wptr = wptr + 1 写后状态判断 若 wptr == rptr → full=1 (FIFO满) 否则 → 继续可写 读操作发生 (rinc=1, empty=0) 读指针 rptr = rptr + 1 读后状态判断 若 wptr == rptr → empty=1 (FIFO空) 否则 → 继续可读 关键总结 • 写指针递增条件:写使能 && 非满 • 读指针递增条件:读使能 && 非空 • 空满判断:比较 wptr 与 rptr 是否相等

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 的状态跳转,本质就是两个指针的追逐游戏。写指针追读指针,追上了就是满;读指针追写指针,追上了就是空。