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.
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.
// 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
endfunctionTake 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.
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 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.
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 |
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.
The hidden dependency makes reuse and review harder and can enlarge the inferred combinational cone unexpectedly. Explicit inputs expose the hardware dependency.
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.
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).