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
PASS

Every CHECK line matches its expected value and the run ends with PASS, 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 dout and instead exposes a combinational top output (mem[sp-1] when not empty). The pointer width is sized with $clog2(DEPTH), and the testbench watches top update 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
endmodule
CHECK 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
PASS

The log shows top tracking the newest value (10→20→30→40) and unwinding correctly on pop, ending in PASS.

How the pointer and flags work

The stack pointer sp is the single source of truth. It holds a count of live entries from 0 to DEPTH, so empty is simply sp == 0 and full is sp == DEPTH. Because the count can reach DEPTH (one beyond the highest index DEPTH-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 uses sp for the write slot and sp-1 for 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 when full is dropped, and a pop when empty is dropped. This prevents pointer overflow/underflow, which would otherwise corrupt the whole structure. If your application must never silently lose data, expose an overflow/underflow error strobe instead of quietly ignoring the request.

Synthesis notes

The array mem infers either flip-flops or a small block/distributed RAM depending on DEPTH and your target. For shallow stacks (a handful of entries) FFs are fine; for deep stacks, map mem to 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 clear mem, 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 full count — the full flag then never asserts or wraps. Second, reading mem[sp] instead of mem[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 indices 0…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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD