Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
    • Parameterization and Generate Hub
    • Top-Level Integration Hub
    • Synthesis Intent Hub
    • Style and Static Checks Hub
    • Assertions in RTL Hub
    • Formal Verification Basics Hub
    • Reusable SystemVerilog for RTL
      • Packages and Compile Order
      • Packed Structs and Unions
      • Parameter Types and Guards
      • Functions and Tasks
      • Interfaces, Modports, and Portability
      • Compile-Order and Package Errors, Decoded
      • Mini Project: a Reusable FIFO IP
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Reusable SystemVerilog for RTL
  1. Topics
  2. RTL Design
  3. Advanced RTL
  4. Reusable SystemVerilog for RTL
  5. Functions and Tasks

Part 8 · Advanced · Advanced

Functions and Tasks

Factor pure combinational transforms into functions and use tasks carefully for repeated procedural behavior.

On this page

    Why this matters

    A helper function that reads a module variable hides a dependency, and the combinational cone grows without anyone seeing it. Integer division in a width, such as W/8 at W=4, also gives [-1:0].

    After this lesson you can: write pure helper functions for Gray conversion and byte merging, trace a Gray encode and decode by hand, and guard the widths they assume.

    Function or task?

    A function returns a value in zero time, so in RTL it describes combinational logic. A task can have several outputs and, in a testbench, can wait for clocks; in synthesizable code it must not wait at all.

    Construct

    RTL use

    Rule

    Automatic function

    Parity, encode/decode, pointer transform, saturation

    No timing controls; return combinational result

    Task in synthesizable process

    Repeated assignments or transaction-independent sequencing

    No delays/events; tool support reviewed

    Testbench task

    Drive/check cycles

    May contain timing controls; not synthesizable

    Rule of thumb for RTL: if it computes one value from its inputs, write a function. Keep tasks for testbenches.

    One function, walked through: binary to Gray

    Gray code changes exactly one bit between neighbouring values, which is why pointers that cross clock domains use it. The conversion is one line: each Gray bit is the XOR of a binary bit and the bit above it.

    design.sv
    1
    2
    3
    4
    // snippet: the encoder from gray_merge below
    function automatic logic [W-1:0] bin2gray(input logic [W-1:0] value);
      return (value >> 1) ^ value;     // g[i] = b[i+1] ^ b[i]; the top bit passes through
    endfunction

    Take value = 0110. Shifting right gives 0011; XOR with 0110 gives 0101. Decoding runs the other way, from the top bit down, because each binary bit needs the one above it.

    Gray encode of 0110 to 0101 with three XOR gates, and the decode back to 0110 as a ripple from bit 3 down

    Read each panel top to bottom; bit 3 is on the left.

    • Encode is parallel: every Gray bit needs only two binary bits, so the logic depth is one XOR whatever the width.

    • Decode is a ripple: bit 2 needs decoded bit 3, bit 1 needs bit 2, and so on, so its depth grows with the width.

    • Bit 3 is the same in both codes, because nothing sits above it.

    The 3-bit table shows the one-bit-change property directly, including the wrap from 7 back to 0.

    Binary

    Gray

    Bit that changed from the row above

    000

    000

    (start)

    001

    001

    bit 0

    010

    011

    bit 1

    011

    010

    bit 0

    100

    110

    bit 2

    101

    111

    bit 0

    110

    101

    bit 1

    111

    100

    bit 0; and 100 to 000 on wrap is bit 2

    The complete module: Gray plus byte merge

    The module below holds two pure functions, the Gray encoder and a byte merge that copies the bytes selected by a strobe from a new word into an old one. Every input is passed as an argument, so the inferred logic is visible at the call. The functions are declared inside the module that owns W, so a copy at another width needs no edit to them.

    gray_merge.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    module gray_merge #(
      parameter int W = 32
    ) (
      input  logic [W-1:0]   bin_i,
      input  logic [W-1:0]   old_i,
      input  logic [W-1:0]   new_i,
      input  logic [W/8-1:0] strobe_i,
      output logic [W-1:0]   gray_o,
      output logic [W-1:0]   merged_o
    );
      // Contract: W must be positive and a multiple of 8. Checked while the design is built,
      // so W = 4 or W = 12 stops the build instead of silently truncating W/8.
      if (W < 8 || (W % 8) != 0) begin : g_w_check
        $error("gray_merge: W must be a positive multiple of 8");
      end
    
      // W is in scope because the functions live inside the module that declares it.
      function automatic logic [W-1:0] bin2gray(input logic [W-1:0] value);
        return (value >> 1) ^ value;
      endfunction
    
      function automatic logic [W-1:0] byte_merge(
        input logic [W-1:0]   old_value,
        input logic [W-1:0]   new_value,
        input logic [W/8-1:0] strobe
      );
        logic [W-1:0] result;
        result = old_value;
        for (int b = 0; b < W/8; b++)
          if (strobe[b]) result[b*8 +: 8] = new_value[b*8 +: 8];
        return result;
      endfunction
    
      assign gray_o   = bin2gray(bin_i);
      assign merged_o = byte_merge(old_i, new_i, strobe_i);
    endmodule
    

    The byte merge for a 32-bit word, old = AABBCCDD and new = 11223344. Strobe bit b selects byte b, and byte 0 is bits 7 to 0:

    Strobe

    Merged

    Strobe

    Merged

    0000

    AABBCCDD

    1000

    11BBCCDD

    0001

    AABBCC44

    1001

    11BBCC44

    0010

    AABB33DD

    1010

    11BB33DD

    0011

    AABB3344

    1011

    11BB3344

    0100

    AA22CCDD

    1100

    1122CCDD

    0101

    AA22CC44

    1101

    1122CC44

    0110

    AA2233DD

    1110

    112233DD

    0111

    AA223344

    1111

    11223344

    Pitfalls

    • Hidden inputs. A function that reads a module variable it was not passed hides a dependency: the logic cone grows and a reviewer cannot see why. Pass everything as an argument, so simulation, synthesis and formal all see the same function.

    • Truncating widths. W/8 is integer division. At W = 4 the strobe range [W/8-1:0] becomes [-1:0], a two-bit range, not zero bits; at W = 12 it is 1 and four data bits are never merged. The module's guard stops both builds. A package cannot hold that check, because elaboration system tasks are not allowed there.

    • Waiting in RTL. A task with @ or # inside a synthesizable process is not hardware. Keep timing controls in testbench tasks.

    Warning

    Limits: the bench enumerates all inputs at W = 16 and all four strobes for one old/new pair. Adjacency alone would also accept a complemented Gray code, so it also decodes each value by cumulative XOR; the wrap check covers the last-to-first step. CDC metastability, bus skew and physical timing are separate obligations; see the CDC lessons and parameterization and generate for dimension guards.

    Check yourself Why should an RTL helper function avoid reading a module variable not passed as an argument?

    The hidden dependency makes reuse and review harder and can enlarge the inferred combinational cone unexpectedly. Explicit inputs expose the hardware dependency.

    Check yourself Encode 1011 to Gray, then decode your answer.

    1011 >> 1 = 0101, and 1011 ^ 0101 = 1110. Decode: b3 = 1, b2 = 1 ^ 1 = 0, b1 = 0 ^ 1 = 1, b0 = 1 ^ 0 = 1, giving 1011 again.

    Remember

    • In RTL, a function computes one value from its inputs; keep waiting tasks in testbenches.

    • Gray encode is one XOR level, g = (b >> 1) ^ b; decode ripples from the top bit down.

    • W/8 truncates: W=4 gives [-1:0] and W=12 leaves four bits unmerged, so keep the legal-width guard.

    Rule of thumb: outputs depend only on inputs, so simulation, synthesis and formal agree.

    How this was checked

    Testbench run with Icarus Verilog 13.0 (2026-10-16): prints PASS. It enumerates all inputs at W=16 and all four strobes for one old and new data pair; cumulative-XOR decoding checks the Gray mapping independently of adjacency, and the wrap check covers the final-to-first transition. It establishes no CDC metastability, bus-skew or physical timing guarantee.

    The 3-bit Gray table and the 16-row byte-merge table were printed by the page's gray_merge module at W=32 under Icarus Verilog 13.0 (2026-10-17).

    Keep going

    • Code lab: Pure function

    • Interview: run a mock interview on this topic

    Related topics

    • Interfaces, modports, and portability

    Previous

    270 / 350

    Next: Interfaces, Modports, and Portability

    Up next

    Up nextInterfaces, Modports, and Portability

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Packages and Compile Order
    • Packed Structs and Unions
    • Compile-Order and Package Errors, Decoded