Stack (LIFO) in Verilog: Hardware Push/Pop with Full and Empty Flags
A stack is a last-in, first-out (LIFO) memory: the last value you push is the first one you pop. In hardware it appears inside CPUs for return addresses, in parsers, in expression evaluators, and anywhere you need nested, reversible state. This page delivers clean, synthesizable stack verilog code together with a self-checking testbench so you can see exactly how push, pop, and the full/empty flags behave.
The core idea is a small register array mem[0:DEPTH-1] plus a stack pointer that tracks how many elements are live. Push writes at the pointer and increments it; pop decrements and reads the element below. Both operations are guarded so a push on a full stack or a pop on an empty stack is harmlessly ignored — essential for safe hardware.
Parameterized LIFO stack with registered pop output
Here sp counts valid entries (0…DEPTH). On push we store din at mem[sp] and increment; on pop we drive dout with the current top mem[sp-1] and decrement. The testbench pushes three bytes, pops them, and asserts strict LIFO order (C3, B2, A1), then over-fills to prove the full guard works.
// Hardware stack (LIFO) with push/pop and full/empty flags
module stack #(parameter W=8, DEPTH=8)(
input clk, rst_n,
input push, pop,
input [W-1:0] din,
output reg [W-1:0] dout,
output full, empty
);
// storage and stack pointer (counts valid entries, 0..DEPTH)
reg [W-1:0] mem [0:DEPTH-1];
integer sp; // number of elements currently on stack
assign empty = (sp == 0);
assign full = (sp == DEPTH);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sp <= 0;
dout <= {W{1'b0}};
end else begin
// push has priority; ignored when full
if (push && !full) begin
mem[sp] <= din; // write at top
sp <= sp + 1;
end else if (pop && !empty) begin
dout <= mem[sp-1]; // output current top
sp <= sp - 1;
end
end
end
endmodule
module tb;
localparam W=8, DEPTH=8;
reg clk, rst_n, push, pop;
reg [W-1:0] din;
wire [W-1:0] dout;
wire full, empty;
integer errors;
stack #(.W(W), .DEPTH(DEPTH)) dut (
.clk(clk), .rst_n(rst_n), .push(push), .pop(pop),
.din(din), .dout(dout), .full(full), .empty(empty)
);
// clock generator
initial clk = 0;
always #5 clk = ~clk;
// one push transaction on a clock edge
task do_push(input [W-1:0] v);
begin
@(negedge clk); push=1; pop=0; din=v;
@(negedge clk); push=0; din=0;
end
endtask
task do_pop;
begin
@(negedge clk); push=0; pop=1;
@(negedge clk); pop=0;
end
endtask
initial begin
errors=0; push=0; pop=0; din=0;
rst_n=0;
@(negedge clk); @(negedge clk);
rst_n=1;
// after reset must be empty, not full
if (empty!==1'b1 || full!==1'b0) begin errors=errors+1; $display("FAIL reset flags"); end
else $display("CHECK reset: empty=%b full=%b OK", empty, full);
// push 3 values
do_push(8'hA1); do_push(8'hB2); do_push(8'hC3);
$display("CHECK after 3 pushes: empty=%b full=%b", empty, full);
if (empty!==1'b0) begin errors=errors+1; $display("FAIL should not be empty"); end
// pop them: LIFO -> C3, B2, A1
do_pop; $display("CHECK pop1 dout=%02h exp=c3", dout);
if (dout!==8'hC3) begin errors=errors+1; $display("FAIL pop1"); end
do_pop; $display("CHECK pop2 dout=%02h exp=b2", dout);
if (dout!==8'hB2) begin errors=errors+1; $display("FAIL pop2"); end
do_pop; $display("CHECK pop3 dout=%02h exp=a1", dout);
if (dout!==8'hA1) begin errors=errors+1; $display("FAIL pop3"); end
// must be empty now
if (empty!==1'b1) begin errors=errors+1; $display("FAIL not empty after draining"); end
else $display("CHECK empty after drain OK");
// pop on empty is ignored (dout holds last value a1)
do_pop;
if (dout!==8'hA1) begin errors=errors+1; $display("FAIL pop-on-empty changed dout"); end
else $display("CHECK pop-on-empty ignored OK");
// fill to full (DEPTH pushes) then over-push
begin : fill
integer i;
for (i=0; i<DEPTH; i=i+1) do_push(i[W-1:0]);
end
$display("CHECK after %0d pushes: full=%b", DEPTH, full);
if (full!==1'b1) begin errors=errors+1; $display("FAIL should be full"); end
// over-push must be ignored: top stays DEPTH-1 value
do_push(8'hFF);
do_pop;
$display("CHECK top after over-push dout=%02h exp=%02h", dout, (DEPTH-1));
if (dout!==(DEPTH-1)) begin errors=errors+1; $display("FAIL over-push wrote data"); end
else $display("CHECK over-push ignored OK");
if (errors==0) $display("PASS");
else $display("FAILED with %0d errors", errors);
$finish;
end
endmodule
CHECK reset: empty=1 full=0 OK CHECK after 3 pushes: empty=0 full=0 CHECK pop1 dout=c3 exp=c3 CHECK pop2 dout=b2 exp=b2 CHECK pop3 dout=a1 exp=a1 CHECK empty after drain OK CHECK pop-on-empty ignored OK CHECK after 8 pushes: full=1 CHECK top after over-push dout=07 exp=00000007 CHECK over-push ignored OK PASSEvery
CHECKline matches its expected value and the run ends withPASS, confirming LIFO ordering, the ignored pop-on-empty, and the ignored over-push.Variant: continuously exposed top-of-stack
Some designs want the top element visible on every cycle rather than only after a pop. This variant drops the registered
doutand instead exposes a combinationaltopoutput (mem[sp-1]when not empty). The pointer width is sized with$clog2(DEPTH), and the testbench watchestopupdate immediately as the stack grows and shrinks.// Variant: stack whose dout continuously shows top-of-stack (combinational read) module stack_top #(parameter W=8, DEPTH=4)( input clk, rst_n, input push, pop, input [W-1:0] din, output [W-1:0] top, output full, empty ); reg [W-1:0] mem [0:DEPTH-1]; reg [$clog2(DEPTH):0] sp; // 0..DEPTH assign empty = (sp == 0); assign full = (sp == DEPTH); // combinational top read: valid only when !empty assign top = empty ? {W{1'b0}} : mem[sp-1]; always @(posedge clk or negedge rst_n) begin if (!rst_n) sp <= 0; else begin if (push && !full) begin mem[sp] <= din; sp <= sp + 1'b1; end else if (pop && !empty) begin sp <= sp - 1'b1; end end end endmodule module tb; localparam W=8, DEPTH=4; reg clk, rst_n, push, pop; reg [W-1:0] din; wire [W-1:0] top; wire full, empty; integer errors; stack_top #(.W(W), .DEPTH(DEPTH)) dut ( .clk(clk), .rst_n(rst_n), .push(push), .pop(pop), .din(din), .top(top), .full(full), .empty(empty)); initial clk=0; always #5 clk=~clk; task push_v(input [W-1:0] v); begin @(negedge clk); push=1; pop=0; din=v; @(negedge clk); push=0; end endtask task pop_v; begin @(negedge clk); push=0; pop=1; @(negedge clk); pop=0; end endtask initial begin errors=0; push=0; pop=0; din=0; rst_n=0; @(negedge clk); rst_n=1; @(negedge clk); if (empty!==1'b1) begin errors=errors+1; $display("FAIL empty after rst"); end else $display("CHECK empty-after-reset OK"); push_v(8'd10); $display("CHECK top=%0d exp=10", top); if (top!==8'd10) begin errors=errors+1; $display("FAIL top=10"); end push_v(8'd20); $display("CHECK top=%0d exp=20", top); if (top!==8'd20) begin errors=errors+1; $display("FAIL top=20"); end push_v(8'd30); if (top!==8'd30) begin errors=errors+1; $display("FAIL top=30"); end push_v(8'd40); $display("CHECK full=%b exp=1", full); if (full!==1'b1) begin errors=errors+1; $display("FAIL full"); end // over-push ignored push_v(8'd99); if (top!==8'd40) begin errors=errors+1; $display("FAIL over-push changed top"); end else $display("CHECK over-push ignored, top=%0d OK", top); // pop reveals previous tops: 40->30->20->10 pop_v; if (top!==8'd30) begin errors=errors+1; $display("FAIL after pop expect 30 got %0d", top); end else $display("CHECK after pop top=30 OK"); pop_v; if (top!==8'd20) begin errors=errors+1; $display("FAIL expect 20"); end pop_v; if (top!==8'd10) begin errors=errors+1; $display("FAIL expect 10"); end pop_v; if (empty!==1'b1) begin errors=errors+1; $display("FAIL expect empty"); end else $display("CHECK empty after draining OK"); if (errors==0) $display("PASS"); else $display("FAILED %0d", errors); $finish; end endmoduleCHECK empty-after-reset OK
CHECK top=10 exp=10
CHECK top=20 exp=20
CHECK full=1 exp=1
CHECK over-push ignored, top=40 OK
CHECK after pop top=30 OK
CHECK empty after draining OK
PASSThe log shows
toptracking the newest value (10→20→30→40) and unwinding correctly on pop, ending inPASS.How the pointer and flags work
The stack pointer
spis the single source of truth. It holds a count of live entries from 0 toDEPTH, soemptyis simplysp == 0andfullissp == DEPTH. Because the count can reachDEPTH(one beyond the highest indexDEPTH-1), the pointer needs one extra bit of range — which is why the second example sizes it as[$clog2(DEPTH):0]rather than[$clog2(DEPTH)-1:0]. Addressing the array always usesspfor the write slot andsp-1for the top element.Push/pop priority and guards
Both designs give push priority over pop in the same cycle via
if (push && !full) … else if (pop && !empty). More importantly, each branch is guarded by a flag: a push whenfullis dropped, and a pop whenemptyis dropped. This prevents pointer overflow/underflow, which would otherwise corrupt the whole structure. If your application must never silently lose data, expose anoverflow/underflowerror strobe instead of quietly ignoring the request.Synthesis notes
The array
meminfers either flip-flops or a small block/distributed RAM depending onDEPTHand your target. For shallow stacks (a handful of entries) FFs are fine; for deep stacks, mapmemto a single-port RAM and register the read, which changes pop to a two-cycle (request then data) protocol. The asynchronous, active-low reset (negedge rst_n) only clears the pointer — it does not clearmem, because real memories power up with unknown contents and clearing them costs area for no functional benefit.Common pitfalls
Three mistakes dominate. First, under-sizing the pointer so it cannot represent the
fullcount — thefullflag then never asserts or wraps. Second, readingmem[sp]instead ofmem[sp-1]on pop, which returns a stale/empty slot. Third, forgetting the full/empty guards, so a stray push or pop silently desynchronizes the pointer. Always drive a self-checking testbench that over-pushes and over-pops, exactly as shown here, to catch these.Interview angle
Expect questions like “How does a hardware stack differ from a FIFO?” (LIFO vs FIFO ordering; a stack needs only one pointer, a FIFO needs read and write pointers). “Why one extra bit on the pointer?” (to represent the count
DEPTH, not just indices0…DEPTH-1). And “What happens on simultaneous push and pop?” — state your priority convention explicitly, since both a net-zero (pop-then-push) and a priority scheme are defensible as long as the behavior is documented.Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres