Skip to content

SystemVerilog · Module 9

Encapsulation — public, protected, local

Access modifiers, the encapsulation contract, getter/setter patterns, and why local is strictly stronger than protected.

Module 9 · Page 9.5

What Encapsulation Actually Means

Imagine you are handing a transaction object to a junior engineer on your team. You wrote the class. He is writing the test. He sees 15 properties. Without any guidance, he sets pkt.internal_crc = 0 directly, bypassing the calculation logic you wrote. The test passes. Coverage looks fine. Then silicon fails at the customer's lab because CRC was never being checked.

That is the problem encapsulation prevents. It is not about hiding things for the sake of hiding them. It is about defining a contract: here is what you are allowed to touch, here is what you are not, and here is the only correct path through which data changes.

SystemVerilog gives you three access qualifiers to express that contract. If you do not write one, public is assumed.

The Three Access Levels

publicSame class✓Child class✓Outside (testbench)✓ Default — no keyword needed. Anyone can read or write. protectedSame class✓Child class✓Outside (testbench)✗ Visible to the family — this class and anything that extends it. localSame class✓Child class✗Outside (testbench)✗ Completely private — only this exact class can touch it. The syntax is simple — just put the keyword before the data type on the property or method declaration. One keyword, applied once.

Access Qualifiers — Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class MyClass;
 
    // public (default — no keyword needed)
    bit [31:0] addr;
 
    // public — written explicitly (same effect)
    public bit [7:0] data;
 
    // protected — accessible to this class and child classes only
    protected int seq_num;
 
    // local — accessible ONLY inside this class
    local bit [31:0] internal_crc;
 
    // methods follow the same rules
    local function void compute_crc();   // private method
        internal_crc = addr ^ data;
    endfunction
 
    protected function void init_seq();  // child classes can call this
        seq_num = 0;
    endfunction
 
endclass

public — The Default

Everything is public unless you say otherwise. For a simple transaction class with a few data fields that your testbench directly sets and reads, public is fine and you do not need to write anything extra.

public — Direct Access from Anywhere
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class MemTransaction;
    rand bit [31:0] addr;    // public by default
    rand bit [31:0] data;    // public by default
    bit               write;   // public by default
endclass
 
module tb;
    MemTransaction t = new();
 
    initial begin
        // All fine — everything is public
        t.addr  = 32'h5000_0000;
        t.data  = 32'h1234_5678;
        t.write = 1;
        $display("addr=0x%08h", t.addr);
    end
endmodule

local — Completely Private

local is the strongest restriction. Only the methods inside the exact class where the property is declared can access it. Child classes cannot. The testbench cannot. Not even friend classes (SystemVerilog does not have friend anyway).

Use local for implementation details that no one outside the class should ever depend on — internal counters, cached computed values, temporary state that is only meaningful during a method call.

local — Private Properties and Methods
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class EccPacket;
 
    // Public — testbench works with these directly
    rand bit [63:0] payload;
    int               pkt_id;
 
    // local — internal implementation detail
    // no one outside should ever touch the raw ECC bits directly
    local bit [7:0]  ecc_bits;
    local int          encode_count;
 
    function new(int id = 0);
        pkt_id       = id;
        ecc_bits     = 8'h00;
        encode_count = 0;
    endfunction
 
    // local method — called only by this class internally
    local function void compute_ecc();
        ecc_bits = payload[7:0]  ^
                   payload[15:8] ^
                   payload[23:16] ^
                   payload[31:24];   // simplified
        encode_count++;
    endfunction
 
    // Public API — this is the only correct way to finalise the packet
    function void finalise();
        compute_ecc();   // calls the local method — fine from inside
    endfunction
 
    function void display();
        $display("[ECC #%0d] payload=0x%016h  ecc=0x%02h  encodes=%0d",
                  pkt_id, payload, ecc_bits, encode_count);
    endfunction
 
endclass
 
module tb;
    EccPacket p = new(1);
    initial begin
        void'(p.randomize());
        p.finalise();     // correct path — triggers ECC computation
        p.display();
 
        // p.ecc_bits   = 8'hFF;  ← COMPILE ERROR: local
        // p.compute_ecc();       ← COMPILE ERROR: local method
    end
endmodule

The testbench can only call finalise(). It cannot touch ecc_bits or compute_ecc() directly. This means ECC is always computed through the correct logic — it cannot be silently skipped or overwritten.

protected — For the Class Family

protected sits between public and local. The outside world cannot touch it, but child classes can. This is how you share internal state between a base class and its subclasses without exposing it everywhere.

Think of it as writing a base transaction class that all your protocol-specific transactions extend. They all need access to a common sequence counter. You do not want the testbench touching it directly, but every child class needs to read and increment it.

protected — Shared Across the Class Hierarchy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Base class ────────────────────────────────────────────────
class BaseTxn;
 
    // Public — testbench can read the ID
    int txn_id;
 
    // protected — shared with child classes but not with testbench
    protected int retry_count;
    protected bit is_error_injected;
 
    function new(int id = 0);
        txn_id           = id;
        retry_count      = 0;
        is_error_injected = 0;
    endfunction
 
    function void display();
        $display("[BaseTxn #%0d] retries=%0d  err=%0b",
                  txn_id, retry_count, is_error_injected);
    endfunction
 
endclass
 
// ── Child class — can access protected members ────────────────
class ErrorTxn extends BaseTxn;
 
    rand bit [1:0] error_type;  // 0=none 1=crc 2=timeout 3=parity
 
    function new(int id = 0);
        super.new(id);
    endfunction
 
    function void inject_error();
        // protected members are fully accessible inside child class
        is_error_injected = 1;   // ← OK — protected
        retry_count       = 3;   // ← OK — protected
    endfunction
 
    function void display();
        super.display();
        $display("        error_type=%0d", error_type);
    endfunction
 
endclass
 
module tb;
    ErrorTxn e = new(10);
    initial begin
        e.inject_error();      // fine — public method
        e.display();
 
        // e.retry_count = 5;   ← COMPILE ERROR: protected
        // e.is_error_injected; ← COMPILE ERROR: protected
    end
endmodule

Getter and Setter Pattern

Once a property is local or protected, the outside world cannot touch it directly. But sometimes you still want to allow controlled read or write access. The answer is getter and setter methods — public functions that provide a single, validated path to the data.

This is where encapsulation earns its keep. The setter can validate the incoming value, log the change, or trigger a side effect. The getter can return a computed view of the data. Neither is possible when the property is simply public.

Getter and Setter — Controlled, Validated Access
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class RegTransaction;
 
    // local — no direct access from outside
    local bit [31:0] addr;
    local bit [31:0] data;
    local int          write_count;
 
    function new();
        addr        = 32'h0;
        data        = 32'h0;
        write_count = 0;
    endfunction
 
    // ── Setter with validation ────────────────────────────────
    function void set_addr(bit [31:0] a);
        // reject unaligned addresses immediately
        if (a[1:0] !== 2'b00)
            $fatal(1, "[RegTxn] Unaligned address 0x%08h — must be word-aligned", a);
        addr = a;
        write_count++;
        $display("[RegTxn] addr set to 0x%08h (write #%0d)", addr, write_count);
    endfunction
 
    // ── Getter ────────────────────────────────────────────────
    function bit [31:0] get_addr();
        return addr;
    endfunction
 
    // ── Setter with side-effect ───────────────────────────────
    function void set_data(bit [31:0] d);
        data = d;
    endfunction
 
    // ── Read-only computed getter ─────────────────────────────
    function int get_write_count();
        return write_count;   // count is local — nobody can reset it externally
    endfunction
 
    function void display();
        $display("addr=0x%08h  data=0x%08h  writes=%0d",
                  addr, data, write_count);
    endfunction
 
endclass
 
module tb;
    RegTransaction r = new();
    initial begin
        r.set_addr(32'h4000_0004);   // validated — passes
        r.set_data(32'hA0B1_C2D3);
        r.display();
 
        $display("write_count = %0d", r.get_write_count());
 
        r.set_addr(32'h4000_0003);
        // ^^^ FATAL: unaligned address — caught immediately at the setter
    end
endmodule

Notice what the setter bought us: the unaligned address is caught the moment it is set — not three clock cycles later when the DUT sends back an error response that might look like something else entirely. That is the difference between a two-minute debug and a two-hour one.

Full Example — All Three Levels in One Class

Here is a realistic SoC register model transaction that uses all three access levels with a clear rationale for each choice.

Full Example — public, protected, local All Together
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class SocRegTxn;
 
    // ── public: testbench fills these in directly ─────────────
    rand bit [31:0] addr;
    rand bit [31:0] wdata;
    rand bit         wr_en;     // 1=write 0=read
    int               txn_id;
 
    // ── protected: sub-classes (e.g. AhbRegTxn) need these ───
    protected bit [3:0] byte_en;     // protocol-specific byte enables
    protected int          burst_len;   // sub-class may override for bursts
 
    // ── local: pure implementation — nobody else's business ───
    local bit [31:0] shadow_rdata;   // last read-back value (cache)
    local int          error_code;     // internal error state
 
    constraint c_align { addr[1:0] == 2'b00; }
 
    function new(int id = 0);
        txn_id      = id;
        byte_en     = 4'hF;    // full word by default
        burst_len   = 1;
        shadow_rdata = 32'h0;
        error_code  = 0;
    endfunction
 
    // ── local method — internal use only ─────────────────────
    local function void cache_rdata(bit [31:0] rdata);
        shadow_rdata = rdata;
    endfunction
 
    // ── public API ────────────────────────────────────────────
    function void set_read_response(bit [31:0] rdata, int err = 0);
        cache_rdata(rdata);   // local method OK from inside
        error_code = err;
    endfunction
 
    function bit has_error();
        return (error_code != 0);
    endfunction
 
    function void display();
        $display("[SocReg #%0d] %s  addr=0x%08h  data=0x%08h  err=%0d",
                  txn_id,
                  wr_en ? "WR" : "RD",
                  addr, wr_en ? wdata : shadow_rdata,
                  error_code);
    endfunction
 
endclass
 
module tb;
    SocRegTxn t = new(1);
    initial begin
        void'(t.randomize());
 
        // Public — direct access, fine
        t.addr  = 32'h5000_0008;
        t.wr_en = 1;
        t.wdata = 32'hF0F0_A1B2;
 
        // Simulate DUT responding after a read
        t.wr_en = 0;
        t.set_read_response(32'h0000_00FF);  // public method
        t.display();
 
        $display("Has error: %0b", t.has_error());
 
        // t.shadow_rdata = 0;  ← COMPILE ERROR: local
        // t.byte_en = 4'h3;   ← COMPILE ERROR: protected
    end
endmodule

Quick Reference

QualifierSame classChild classOutside (tb)Use for
public (default)Data the testbench needs to set or read directly
protectedInternal state that subclasses legitimately need
localPure implementation detail — computed fields, caches, counters
PatternWhen to use
Plain public propertySimple transaction fields the testbench sets and reads freely
local + setter functionProperty that needs validation or logging on every write
local + getter functionRead-only property — outside world can read but never write
protected propertyInternal counter or flag shared across a class hierarchy
local methodHelper function used only inside this class — hide it completely

Verification Usage — Encapsulation in a Real VIP Stack

A protocol agent in production verification IP is a textbook case for encapsulation. The agent's public surface is small: a handful of methods callers invoke to send transactions, query statistics, change runtime configuration. Everything else — the mailbox holding the pending queue, the per-transaction counters, the bus-state machine, the internal coverage collector — is locked behind local. Subclasses get protected hooks to customise per-protocol behaviour. The discipline pays off across releases: every consumer integrates the agent the same way; internal refactors are invisible.

SystemVerilog — Encapsulated agent with three access tiers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class axi_agent;
    // ── Public API — every caller uses these and nothing else ─────
    function new(virtual axi_if v);
        vif       = v;
        inbox     = new(0);
        sent_ok   = 0;
        sent_fail = 0;
    endfunction
 
    task send(axi_xact tr);
        inbox.put(tr);
    endtask
 
    function int get_success_count();
        return sent_ok;
    endfunction
 
    function int get_failure_count();
        return sent_fail;
    endfunction
 
    task start();
        fork run_loop(); join_none
    endtask
 
    // ── Protected hooks — subclasses can override behaviour ─────
    protected virtual task drive_one(axi_xact tr);
        @(posedge vif.clk);
        vif.awaddr  <= tr.addr;
        vif.awvalid <= 1;
        while (!vif.awready) @(posedge vif.clk);
        vif.awvalid <= 0;
    endtask
 
    protected virtual function bit is_legal_xact(axi_xact tr);
        return (tr.addr[1:0] == 2'b00);   // align check
    endfunction
 
    // ── Local internals — locked, refactor-safe ─────────────────
    local virtual axi_if vif;
    local mailbox #(axi_xact) inbox;
    local int sent_ok;
    local int sent_fail;
 
    local task run_loop();
        forever begin
            axi_xact tr;
            inbox.get(tr);
            if (is_legal_xact(tr)) begin
                drive_one(tr);
                sent_ok++;
            end else begin
                sent_fail++;
            end
        end
    endtask
endclass
 
// Subclass for a specialised AXI variant (e.g., AXI4-Lite without bursts)
class axi4_lite_agent extends axi_agent;
    function new(virtual axi_if v); super.new(v); endfunction
 
    // Override the protected hook to add a per-variant constraint
    protected virtual function bit is_legal_xact(axi_xact tr);
        return super.is_legal_xact(tr) && (tr.burst_len == 1);
    endfunction
endclass

Simulation Behavior — How Access Checks Run at Compile Time

Access modifiers (local, protected, public) are compile-time checks. They are enforced by the compiler when it parses references to class members; once compilation succeeds, the runtime has no concept of "this member is private" — every reference has been verified at compile time. This means access modifiers cost zero simulation cycles and zero memory; they exist purely to constrain what code the compiler will accept.

SystemVerilog — Compile-time access enforcement
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class vault;
    local int secret;
    protected int family;
    int open;          // public by default
 
    function void internal_use();
        secret = 1;     // ✓ access to own local — fine
        family = 2;     // ✓ access to own protected — fine
        open   = 3;     // ✓ access to own public — fine
    endfunction
endclass
 
class child extends vault;
    function void subclass_use();
        secret = 1;     // ✗ COMPILE ERROR: local not visible to subclass
        family = 2;     // ✓ protected visible to subclass
        open   = 3;     // ✓ public visible to subclass
    endfunction
endclass
 
initial begin
    vault v = new;
    v.secret = 1;       // ✗ COMPILE ERROR: local not visible externally
    v.family = 2;       // ✗ COMPILE ERROR: protected not visible externally
    v.open   = 3;       // ✓ public — fine
end

Waveform Analysis — Encapsulation Doesn't Affect the Waveform

Encapsulation is a source-code concept; it has no waveform footprint. Whether a property is local or public doesn't change how the simulator represents it in heap memory, doesn't change whether you can mirror it to a module signal, doesn't change what waveform tools can display. The whole point of access modifiers is to constrain what code you can write; once written, the runtime behaviour is unchanged.

SystemVerilog — Mirroring a local property to the waveform
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class agent;
    local int internal_count;   // local — invisible to external code
 
    function int get_count();     // public accessor
        return internal_count;
    endfunction
 
    task tick();
        internal_count++;
    endtask
endclass
 
module tb;
    int tb_agent_count;            // module signal — visible in Verdi
 
    agent ag = new;
 
    always @(posedge clk)
        tb_agent_count = ag.get_count();   // reads through the public accessor
endmodule

Industry Insights — Encapsulation Practices in Production VIP

Debugging Academy — 5 Real Encapsulation Bugs

Each lab is a real failure mode from production projects. Buggy code, symptom, root cause, fix.

1

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class arbiter;
    int grant_count;          // ← implicitly public (no access modifier)
 
    function void record_grant();
        grant_count++;
    endfunction
endclass
 
// Some test code in a corner of the codebase:
initial begin
    arb.grant_count = 999;    // ← writes the counter directly
end
 
// Scoreboard expects monotonic increasing grant_count for its rate calculation
// and reports impossible rates after this write.
2

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class base;
    local int internal_id;
    protected int family_data;
endclass
 
class child extends base;
    // Subclass tries to expose parent's protected via a public method:
    function int leak_family();
        return family_data;       // ← legal access from subclass
    endfunction
 
    // And tries to expose parent's local (compile error — but author tries anyway):
    function int try_leak_internal();
        return internal_id;       // ✗ compile error — local not visible to subclass
    endfunction
endclass
 
// External code now has full access to family_data through child:
child c = new;
int   x = c.leak_family();        // bypasses base's protection
3

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class history_log;
    local string entries [$];     // local queue
 
    function void record(string msg);
        entries.push_back(msg);
    endfunction
 
    // "Public read access" — returns the queue handle directly
    function string get_entries [$] ();
        return entries;           // ← returns reference, not copy
    endfunction
endclass
 
// External code now has a mutable reference to the internal queue:
history_log hl = new;
string list [$] = hl.get_entries();
list.push_back("INJECTED");       // mutates history_log's internal state
list.delete();                    // wipes the history
4

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// scoreboard.sv:
class scoreboard;
    bit [31:0] match_threshold = 1000;   // public default — used by environment
endclass
 
// reset_test.sv (different team, different file):
initial begin
    scb.match_threshold = 50;            // ← reset for faster testing
end
 
// long_run_test.sv (yet another team):
initial begin
    scb.match_threshold = 10000;         // ← long test wants higher bar
end
 
// When tests run in the same regression, the second-running test wins.
// First test's expectations are silently overwritten.
5

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class base_driver;
    local task drive_one(packet p);   // ← local: subclasses can't override
        @(posedge clk);
        bus_signal <= p.data;
    endtask
endclass
 
class burst_driver extends base_driver;
    // Wants to override drive_one to send a burst — can't!
    // function void drive_burst(packet p);
    //     drive_one(p);              // ✗ compile error — drive_one is local
    //     ...
    // endfunction
endclass

Interview Q&A — 12 Questions on Encapsulation

Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.

local: visible only inside the declaring class — even subclasses can't access it. protected: visible inside the class and any subclass; external code can't access it. public: visible everywhere — the default when no modifier is specified. Use local for implementation details, protected for subclass hooks, public for the documented external API.

Best Practices — Encapsulation Rules to Walk Away With

  1. Default every member to local. Promote with intent; document the reason in a comment.
  2. Always specify the access modifier explicitly. Never rely on the public-by-default behaviour; "no modifier" is a code-review red flag.
  3. Use protected only for subclass-extension hooks. Pair with virtual for methods that subclasses should override.
  4. Expose local properties through getters and setters. Zero runtime cost; full access control.
  5. Never return handles to internal collections. Return a copy or a specific query result; protect the live container.
  6. Document the public API in the class header comment. Every public method gets a one-line description; the comment is the integration spec.
  7. Setters validate; getters format. The accessor is the place to add bounds checks, normalisation, or computed views.
  8. Subclass exposure of parent's protected fields requires base-owner review. The leak is a contract extension; treat it like an API change.
  9. Tests interact only through the public API. If a test needs to inspect internals, add a public observation method — never reach into local state.
  10. Lock down configuration via configure() methods that fatal on re-configuration. Prevents test order from accidentally mutating shared state.