There are ways to group a set of statements together that are syntactically equivalent to a single statement and are known as block statements. There are two kinds of block statements: sequential (begin-end) and parallel (fork-join). Block statements enable structured control flow, scoped variable declarations, and concurrent execution modeling.

What You'll Learn

  • Master sequential blocks (begin-end) for ordered statement execution with cumulative delays
  • Understand parallel blocks (fork-join) for concurrent execution with independent timing
  • Apply named blocks for hierarchical references, disabling, and local variable scoping
  • Differentiate delay semantics: relative delays in sequential vs. absolute delays in parallel blocks

Sequential Blocks

Statements are wrapped using begin and end keywords and will be executed sequentially in the given order, one after the other. Delay values are treated relative to the time of execution of the previous statement (cumulative delays). After all the statements within the block are executed, control may be passed elsewhere.

Syntax


	// Basic begin-end block
	begin
		statement1;     // Executes first
		statement2;     // Executes after statement1 completes
		statement3;     // Executes after statement2 completes
	end

	// Named begin-end block (for hierarchical access and disable)
	begin : block_name
		[local variable declarations]  // Optional: variables scoped to this block
		[statements]                   // Executed sequentially
	end

Example: Sequential Execution with Relative Delays

initial-begin-end

module design0;
	reg [31:0] data;            // 4-state variable

	// "initial" block starts at time 0
	initial begin
		// Statement 1: Executes at time 0 + 10 = 10 time units
		#10 data = 8'hfe;         // Wait 10 time units, then assign 0xfe
		$display ("[Time=%0t] data=0x%0h", $time, data);

		// Statement 2: Executes at time 10 + 20 = 30 time units (relative delay)
		#20 data = 8'h11;         // Wait 20 MORE time units (cumulative: 30 total)
		$display ("[Time=%0t] data=0x%0h", $time, data);
	end
endmodule

In the example above, the first statement in the begin-end block will be executed at 10 time units (relative to block start at time 0), and the second statement at 30 time units (10 + 20) because delays are cumulative. The #20 delay is relative to the completion of the previous statement, not to time 0.

Output
ncsim> run
[Time=10] data=0xfe
[Time=30] data=0x11
ncsim: *W,RNQUIE: Simulation is complete.

Parallel Blocks

A parallel block can execute statements concurrently and delay control can be used to provide time-ordering of the assignments. Statements are launched in parallel by wrapping them within the fork and join keywords. Delays in fork-join are absolute (measured from the fork launch time), not cumulative like begin-end blocks.

Syntax


	// Basic fork-join block (waits for ALL threads to complete)
	fork
		statement1;     // Thread 1: executes concurrently with other threads
		statement2;     // Thread 2: executes in parallel with thread 1
		statement3;     // Thread 3: executes in parallel with threads 1 and 2
	join                // Blocks until ALL threads finish

	// Named fork-join block (for hierarchical access and disable)
	fork : block_name
		[statements]    // All statements launch simultaneously at fork time
	join                // Resumes after all threads complete

SystemVerilog adds fork-join_any (resumes when ANY thread completes) and fork-join_none (resumes immediately without waiting); plain Verilog only has fork-join. See SystemVerilog fork-join for details.

Example: Parallel Execution with Absolute Delays


	initial begin
		#10 data = 8'hfe;         // Executes at time 10 (sequential statement before fork)

		fork                      // Fork launches at time 10
		  #20 data = 8'h11;       // Executes at time 10+20 = 30 (absolute from fork)
		  #10 data = 8'h00;       // Executes at time 10+10 = 20 (absolute from fork)
		join                      // Resumes at time 30 (when longest thread completes)
	end
fork-join-verilog

In the example above, the fork-join block will be launched after executing the statement at 10 time units. Statements within this block will be executed in parallel, and the first statement to execute will be data = 8'h00 at time 20 (fork time + 10). Then data = 8'h11 executes at time 30 (fork time + 20). The join blocks until time 30 when all threads complete.

Real-World Application: Testbench Stimulus Generation

Verification testbenches use begin-end blocks for sequential test stimulus and fork-join for concurrent protocol monitoring:


// AXI4 transaction testbench (uses the SystemVerilog join_any and $error)
initial begin
  // Sequential test phases using begin-end
  begin : RESET_PHASE
    reset = 1'b1;           // Assert reset
    #100;                   // Hold for 100ns (cumulative delay)
    reset = 1'b0;           // Deassert reset
    #50;                    // Wait for reset recovery (cumulative: 150ns total)
  end

  // Parallel monitoring and stimulus using fork-join
  fork
    // Thread 1: Write transaction stimulus
    begin
      #200 axi_write(32'h1000, 32'hDEADBEEF);  // First write at 200ns
      #300 axi_write(32'h2000, 32'hCAFEBABE);  // Second write at 500ns (cumulative)
    end

    // Thread 2: Watchdog timeout (concurrent with Thread 1)
    begin
      #1000;                                   // Wait 1000ns from fork
      if (!test_done)
        $error("Test timeout!");
    end

    // Thread 3: Continuous response monitor (concurrent)
    begin
      forever @(posedge bvalid)                // Monitor write responses
        $display("Write response: %h", bresp);
    end
  join_any                  // Resume when ANY thread completes (watchdog or test)

  $finish;
end

This pattern combines sequential test phases (reset, stimulus) with parallel monitoring (responses, timeout), demonstrating how block types model real-world concurrent hardware behavior.

Example: Nested Blocks (Sequential inside Parallel)


	initial begin
		#10 data = 8'hfe;         // Time 0+10 = 10 (sequential statement before fork)

		fork                      // Fork launches at time 10
			// Thread 1: Single assignment with delay
			#10 data = 8'h11;       // Time 10+10 = 20 (absolute delay from fork)

			// Thread 2: Sequential begin-end block (launched in parallel with Thread 1)
			begin
				#20 data = 8'h00;     // Time 10+20 = 30 (delay relative to begin block start)
				#30 data = 8'haa;     // Time 30+30 = 60 (cumulative delay within begin-end)
			end
		join                      // Resumes at time 60 (when longest thread completes)
	end

There is a begin-end block nested inside the fork-join in the example above. All statements within the begin-end block will be executed sequentially with cumulative delays, but the begin-end block itself will be launched in parallel along with Thread 1. Execution timeline:

  • Time 20: data = 8'h11 (Thread 1 completes)
  • Time 30: data = 8'h00 (first statement in Thread 2's begin-end)
  • Time 60: data = 8'haa (second statement in Thread 2's begin-end, cumulative #30 delay)

Naming of blocks

Both sequential and parallel blocks can be named by adding : name_of_block after the keywords begin and fork. Named blocks provide three key benefits:

  • Hierarchical references: Access signals within the block using module.block_name.signal
  • Disable capability: Terminate the block prematurely using disable block_name; (useful for timeouts/watchdogs)
  • Local variable scoping: Declare variables local to the block (the block must be named in Verilog)

	// Named sequential block
	begin : name_seq
		integer local_var;        // Local to this named block
		[statements]              // Can be referenced as module.name_seq.local_var
	end

	// Named parallel block
	fork : name_fork
		[statements]              // Can disable with: disable name_fork;
	join
Aspectbegin-end (Sequential)fork-join (Parallel)
Execution ModelStatements execute in order, one at a timeAll statements launch simultaneously (concurrent threads)
Delay SemanticsRelative (cumulative): #10 #20 -> execute at 10, 30Absolute (from fork): #10 #20 -> execute at 10, 20
Completion TimeSum of all delaysMaximum delay among all threads
Use CasesSequential test stimulus, tasks with ordered stepsConcurrent monitors, parallel transactions, watchdog timers
Synthesizable? Yes (in always blocks for sequential logic)? No (simulation-only construct)
Examplebegin #10; #20; end -> 0, 10, 30fork #10; #20; join -> 0, 10, 20

Real-World Application: Named Blocks for Debugging

Named blocks enable hierarchical signal access for waveform debugging and assertions:


module cpu_core;
  reg [31:0] pc, instr;

  initial begin : FETCH_STAGE          // Named block for instruction fetch
    integer fetch_count;               // Local variable scoped to this block
    forever begin
      @(posedge clk);
      instr = imem[pc];                // Fetch instruction
      pc = pc + 4;                     // Increment program counter
      fetch_count = fetch_count + 1;   // Track fetches
    end
  end

  // External access from testbench:
  // - Waveform dump: $dumpvars(0, cpu_core.FETCH_STAGE.fetch_count);
  // - Check: if (cpu_core.pc >= 32'h1000) $display("pc out of range");
  // - Disable: disable cpu_core.FETCH_STAGE; (abort fetch on exception)
endmodule

Named blocks improve testability by exposing internal state without modifying the design interface.

Common Beginner Mistakes

Mistake #1: Confusing relative delays (begin-end) with absolute delays (fork-join)


// MISUNDERSTANDING: Thinking fork-join delays are cumulative like begin-end
fork
  #10 data = 8'hAA;   // Student expects: time 10
  #20 data = 8'hBB;   // Student expects: time 30 (WRONG!)
join
// ACTUAL: Both execute at time 10 and 20 (absolute from fork), not 10 and 30

Why it's problematic: Delays in fork-join are measured from the fork launch point, not from the previous statement. #10 and #20 mean "10 time units after fork" and "20 time units after fork," not cumulative delays like in begin-end. This causes race conditions where later statements execute before earlier ones if delays are misordered.

Best Practice: Remember: begin-end = cumulative (0->10->30), fork-join = absolute (0->10, 0->20).

Mistake #2: Using fork-join in synthesizable RTL code


always @(posedge clk) begin
  fork                        // ERROR: fork-join not synthesizable
    out1 <= data1;
    out2 <= data2;
  join
end

Why it's problematic: Fork-join is a simulation-only construct for modeling concurrent behavior in testbenches. Synthesis tools cannot convert fork-join to hardware because all statements in an always block execute in zero time (scheduling regions handle timing). This causes synthesis errors: "fork-join not supported in synthesizable code."

Best Practice: Use fork-join only in testbenches (initial blocks). For synthesizable RTL, use multiple always blocks or non-blocking assignments for concurrent behavior.

Mistake #3: Forgetting that join blocks until ALL threads complete


initial begin
  fork
    #10  $display("Thread 1 done");
    #100 $display("Thread 2 done");   // Longest thread
  join
  $display("After join");             // Blocked until time 100!
end

Why it's problematic: join blocks execution until all threads complete, so the block finishes at time 100 (longest delay), not time 10. If one thread has a very long delay or runs forever, the join never completes, hanging the simulation. This is a common cause of simulation hangs when watchdog threads use forever loops.

Best Practice: Use join_any (resume when first thread completes) or join_none (resume immediately) for non-blocking forks. Reserve join for cases where you truly need to wait for all threads.

Mistake #4: Declaring variables inside unnamed blocks (Verilog-2001)


begin
  integer i;                  // ERROR in Verilog-2001: unnamed block cannot have declarations
  for (i = 0; i < 10; i = i + 1)
    $display("i=%0d", i);
end

Why it's problematic: Verilog-2001 requires blocks to be named if they contain local variable declarations. Unnamed blocks can only contain executable statements. SystemVerilog relaxed this restriction, but Verilog-2001 simulators will reject this code with "illegal declaration in unnamed block."

Best Practice: Either name the block (begin : LOOP) or declare variables outside the block in the parent scope (module/task/function level).

Summary

In this tutorial, you learned:

  • Sequential blocks: begin-end execute statements in order with cumulative (relative) delays, used for ordered stimulus and synthesizable RTL
  • Parallel blocks: fork-join launch concurrent threads with absolute delays from fork time, used for testbench parallelism and monitoring
  • Delay semantics: begin-end delays are cumulative (sum of all #delays), fork-join delays are absolute (from fork launch point)
  • Named blocks: Enable hierarchical signal access, disable control flow, and local variable scoping (must be named for declarations in Verilog-2001)
  • Nested blocks: begin-end inside fork-join executes sequentially within a parallel thread, combining both delay semantics
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5