57 / 63 · Concept
Single-clock FIFO pointers and boundary policies
Implement the pointers and occupancy of a depth-4 FIFO, and explicitly define when simultaneous push/pop requests are accepted at empty and full.
Lessons are free to read. Enroll to save your learning progress.
Preserve order without moving data
A FIFOFIFO A queue that removes data in the order in which it was inserted. Rules for full, empty and simultaneous reads and writes are part of its behavioral contract. returns data in the order it was inserted. With one clock, reads and writes are decided at the same rising edge. This lesson fixes DEPTH=4 and the data width at 8 bits. rptr points to the next item to remove, and wptr to the next insertion location. Each pointer is 2 bits, so it naturally wraps from 3 to 0. occupancy needs 3 bits to represent the number of stored items from 0 through 4.
rptr=wptr can mean either empty or full. Deriving empty from address equality alone would therefore lose the distinction between no data and a full queue. Here we maintain a separate count , with and . For depth , address width and count width are:
Boundary policies distinguish requests from acceptance
push and pop are requests. Let p and q denote actual push and pop acceptance, respectively. From the state immediately before a non-reset edge:
If both requests arrive when empty, there is no existing item to pop, so only push is accepted. The new data does not immediately appear at the output on that edge. If both requests arrive when full, removing the old head makes room, so both are accepted. full means “four items are currently stored”; under this policy, it does not unconditionally prohibit push.
| State just before the edge | Push only | Pop only | Push and pop |
|---|---|---|---|
| C=0 | Store one item | Rejected | Accept push only, C=1 |
| 0<C<4 | C+1 | C−1 | Accept both, hold C |
| C=4 | Rejected | C=3 | Accept both, C=4 |
Advance the clock to check the boundaries
First, request push and pop together. Since the FIFO is empty, only push should be accepted. Then turn pop off and fill all four slots. Enable both requests again with a new input value. Occupancy should stay at 4, with the old head at the output and the new tail in storage.
Four-entry FIFO. Initially empty; read and write pointers are 0. Accept pop when nonempty. Accept push when space is available or a pop is accepted on the same edge.
The diagram is an educational model showing the state immediately after each rising edgeRising edge The instant at which the clock changes from 0 to 1. Distinguish it from a level, which refers to the entire interval during which CLK=1. Learn more. An accepted pop sets output valid to 1. If no pop is accepted at the next edge, the output registerRegister A circuit that stores multiple bits of state. The synchronous registers in this course store their specified inputs at a clock edge. Learn more retains its value, but valid becomes 0. Stale bits in empty slots are hidden. “Start over” resets the entire exercise; it does not represent an RTL memory resetReset A control that returns state to a specified initial value. Define whether it is synchronous or asynchronous and whether it has priority over other controls. Learn more.
RTL including the registered output
module fifo4 (
input logic clk, rst, push, pop,
input logic [7:0] din,
output logic [7:0] dout,
output logic dout_valid, full, empty,
output logic [2:0] occupancy
);
timeunit 1ns;
timeprecision 1ps;
logic [7:0] mem [0:3];
logic [1:0] rptr, wptr;
logic take_push, take_pop;
assign empty = (occupancy == 3'd0);
assign full = (occupancy == 3'd4);
assign take_pop = !rst && pop && !empty;
assign take_push = !rst && push && (!full || take_pop);
always_ff @(posedge clk) begin
if (rst) begin
rptr <= 2'd0;
wptr <= 2'd0;
occupancy <= 3'd0;
dout_valid <= 1'b0;
end else begin
dout_valid <= take_pop;
if (take_push) begin
mem[wptr] <= din;
wptr <= wptr + 2'd1;
end
if (take_pop) begin
dout <= mem[rptr];
rptr <= rptr + 2'd1;
end
case ({take_push, take_pop})
2'b10: occupancy <= occupancy + 3'd1;
2'b01: occupancy <= occupancy - 3'd1;
default: occupancy <= occupancy;
endcase
end
end
endmoduleoccupancy is not incremented and decremented by two independent if statements. With simultaneous acceptance, two nonblockingNonblocking assignment An assignment written as <= in sequential RTL. It evaluates the right-hand side and schedules the update, allowing registers at the same edge to compute from the previous state. Learn more assignments to the same register would leave only the last assignment in effect. Each pointer advances on its own accepted operation, while the count is decided once for all four combinations. The acceptance conditions also prevent decrementing from 0 or incrementing from 4.
Reading and writing the same address when full
After inserting A, B, C, and D in order, rptr=wptr=0 and occupancy=4. Now set pop=push=1 and din=E. After the edge, dout=A, dout_valid=1, and the logical contents are [B,C,D,E]. Both pointers become 1 and the count stays 4. mem[0] changes to E, but the right-hand side of the nonblocking assignment to dout reads the old A. This read-first collision behavior is essential to the full replacement policy.
Next, push=0 and pop=1 returns B and reduces the count to 3. In contrast, presenting din=Z and both requests when empty only changes the count to 1, with dout_valid=0. A pop request at the following edge is needed to output Z. Immediate forwarding when empty and replacement when full are separate design choices.
rst is an active-high synchronous reset that initializes the pointers, count, and dout_valid. It does not erase memory or dout. Even if old bits remain after reset, empty prevents pop, and only locations recorded by new pushes are subsequently read; normal operation does not require resetting the whole array. Do not consume data when dout_valid=0. A valid output is the response to an accepted pop. There is no separate output ready signal, so the consumer must accept that response.
When DEPTH is not a power of two, simple 2-bit overflow cannot be reused. For example, depth 3 requires an explicit wrap from address 2 to 0. If synthesis maps this example to RAM, also check support for old-data behavior on same-address access. This code is for a single clock; it does not directly apply to pointer synchronization in a FIFO with different clocks.
Try it yourself
Starting just after reset, apply one request per edge: (1) push A and pop together, (2) push B only, (3) push C only, (4) push D only, (5) push E and pop together, (6) pop only. After each step, list the queue contents, occupancy, dout_valid, and any valid output. Explain why the fifth step requires read-first behavior.
Read the explanation
(1) [A], 1, valid=0. (2) [A,B], 2, valid=0. (3) [A,B,C], 3, valid=0. (4) [A,B,C,D], 4, valid=0. (5) [B,C,D,E], 4, valid=1, dout=A. (6) [C,D,E], 3, valid=1, dout=B. Step 1 rejects pop because no old item exists. At step 5, rptr=wptr=0 and a read and write share one slot, so the old head A must be read. Returning E with write-first would corrupt FIFO order and contents. Output data with valid=0 is not graded.