Empty: Gray pointers equal in the read domain. Full: Gray pointers match after inverting the two MSBs of the synchronized read pointer in the write domain. That is the Cummings style used in production RTL.
The reader knows the exact read Gray pointer. It only has a delayed copy of the write Gray pointer (rq2_wgray). If they are equal, every write the reader has heard about has already been consumed → empty.
If a write just happened but has not yet come through the 2FF, the reader still sees empty. That is conservative: you wait extra rd_clk cycles, you do not underflow.
assign empty = (rgray == rq2_wgray);
Using rgray_next (combinational empty) is an alternative that goes empty on the cycle you pop the last beat, but it creates a combinational loop through rd_fire. Lesson 4.3 uses registered compare — simpler to synthesize. Then rd_fire = rd_en && !empty still prevents underflow.
Pointers are ADDR_W+1 bits. When the writer has wrapped one more time than the reader, addresses match and the extra wrap bit differs. In Gray code that condition is: synchronized read Gray equals write Gray with the upper two bits inverted (not only the single MSB — because Gray’s MSB pair encodes the wrap).
// PTR_W = ADDR_W + 1. Invert Gray[PTR_W-1] and Gray[PTR_W-2].
assign full = (wgray ==
{~wq2_rgray[PTR_W-1:PTR_W-2], wq2_rgray[PTR_W-3:0]});
Worked DEPTH=4, PTR_W=3. After 4 writes, wgray is Gray(4)=110 (bin 100). rptr still 000 → Gray 000. Invert two MSBs of 000 → 110. 110 == 110 → full. Correct.
After 4 reads, rptr=100, rgray=110. Synced into write domain. Invert two MSBs of 110 = 000. wptr also wrapped or not depending on more writes — check equal-pointer empty on read side separately.
Binary extra-MSB full is {~rptr[MSB], rptr[rest]} (one bit). Gray full inverts two MSBs. Mixing the two formulas is a common whiteboard mistake.
For an (n+1)-bit Gray code, going around the circle once flips the pattern of the top two bits in a way that a single-bit invert does not match the Gray of (binary + 2^n). The SNUG-2002 identity is: full when wgray == {~rgray[n:n-1], rgray[n-2:0]} using the synchronized rgray.
wgray updates on wr_clk. The read domain may not see that update for two of its clocks. Until rq2 matches, empty can stay 1 even though RAM already holds data. That lag is required. It is not a drawing of two clocks on the same period.
Addresses are 3 bits. Pointers are 4-bit Gray. After reset both Grays are 0000. Eight writes take wbin from 0 to 8 (1000). Gray(8)=1100. rgray still 0000. Invert two MSBs of 0000 → 1100. wgray==1100 matches → full. One read: rbin=1, rgray=0001. When that Gray syncs to write domain, invert two MSBs of 0001 = 1101, which is not 1100, so full falls — a slot is free from the writer’s conservative view only after the 2FF, which is later than the actual read.
Empty: after eight reads matching those writes, rgray catches wgray (both Gray(8)=1100 once synced) → empty. If the writer already moved on, rq2 is stale and empty may persist for two rd_clk edges — conservative.
Some listings compute empty = (rgray_next == rq2_wgray) so empty rises on the cycle you pop the last beat. Then rd_fire depends on empty which depends on rd_fire unless you carefully break the loop with a registered empty. This course’s 4.3 RTL uses registered Gray compares: simpler, one extra cycle of flag latency, still safe. Do not copy a next-state flag formula into registered RTL without redrawing the loop.
In the read domain: empty when the registered read Gray equals the 2FF-synchronized write Gray (rgray == rq2_wgray).
In the write domain: full when wgray equals the synchronized read Gray with the two MSBs inverted: wgray == {~wq2_rgray[PTR_W-1:PTR_W-2], wq2_rgray[PTR_W-3:0]}.
Binary extra-MSB full inverts one wrap bit. Gray’s wrap is encoded in the top two bits of the (n+1)-bit code. Inverting only one Gray bit is a common whiteboard fail.
Using next Gray for same-cycle flags can loop combinationally through wr_fire/rd_fire. Registered compares are safer to synthesize; the beat that fills the last slot is still legal.