Part 5 · Sequences · Intermediate

Fork/Join Patterns: join, join_any, Layered vseqs

Parallel independent traffic, join_any with disable fork, IRQ wait patterns, and layered virtual sequence reuse.

Parallel independent traffic — fork/join

Many chip scenarios run independent traffic streams concurrently — AXI stress while APB background config continues. Inside a virtual sequence, fork/join starts multiple sub-sequences in parallel and waits for all to complete.

systemverilog
task body();
  axi_wr_seq axi_stress = axi_wr_seq::type_id::create("axi_stress");
  apb_bg_seq apb_bg     = apb_bg_seq::type_id::create("apb_bg");

  fork
    begin
      axi_stress.num_bursts = 100;
      axi_stress.start(p_sequencer.axi_sqr);
    end
    begin
      apb_bg.start(p_sequencer.apb_sqr);
    end
  join  // [VSEQ] wait for BOTH to complete before continuing

  `uvm_info("VSEQ", "parallel stress complete", UVM_MEDIUM)
endtask
diagram
[VSEQ] fork/join — parallel sub-sequences

  TIME ─────────────────────────────────────────────────────────────►

  axi_stress ON axi_sqr:  [burst][burst][burst]...[burst]──done
  apb_bg ON apb_sqr:      [wr][wr][wr]...[wr]──────────────done
  vseq body:              fork──────────────────────────join──►
                                                    ▲
                                          continues here after BOTH done

fork/join_any — first completion wins

Use join_any when the scenario proceeds as soon as one branch completes — classic pattern: DMA transfer OR interrupt, whichever comes first. Follow with disable fork to kill the losing branch and prevent stray sequences.

systemverilog
task body();
  irq_wait_seq  irq = irq_wait_seq::type_id::create("irq");
  dma_run_seq   dma = dma_run_seq::type_id::create("dma");

  fork
    irq.start(p_sequencer.apb_sqr);   // polls STATUS or waits for event
    dma.start(p_sequencer.dma_sqr);
  join_any
  disable fork;  // [VSEQ] kill the other branch — critical

  `uvm_info("VSEQ", "DMA or IRQ completed first", UVM_MEDIUM)
endtask
diagram
[STIM] join_any + disable fork — IRQ race

  Branch A (irq_wait_seq):  poll STATUS register every 100ns
  Branch B (dma_run_seq):   start DMA, wait for engine idle

  If IRQ fires at 500ns, Branch A completes  join_any exits
  disable fork kills Branch B — prevents duplicate status checks

  WITHOUT disable fork: Branch B keeps running  objection leak or double action

Layered virtual sequences — reuse building blocks

Top-level chip virtual sequences compose smaller scenario vseqs. Each sub-vseq is independently reusable and starts on the same virtual sequencer — inheriting the same p_sequencer handles.

systemverilog
class chip_stress_vseq extends uvm_sequence;
  `uvm_declare_p_sequencer(soc_virtual_sequencer)
  `uvm_object_utils(chip_stress_vseq)

  task body();
    dma_xfer_vseq  dma  = dma_xfer_vseq::type_id::create("dma");
    pcie_boot_vseq boot = pcie_boot_vseq::type_id::create("boot");

    // [VSEQ] Sub-vseqs start on same virtual sequencer
    dma.start(p_sequencer);
    boot.start(p_sequencer);
  endtask
endclass
diagram
[VSEQ] layered virtual sequence hierarchy

  chip_stress_vseq
    │
    ├── dma_xfer_vseq.start(p_sequencer)
    │     ├── prog_dma_seq.start(p_sequencer.apb_sqr)
    │     └── axi_wr/rd.start(p_sequencer.axi_sqr)
    │
    └── pcie_boot_vseq.start(p_sequencer)
          ├── pcie_link_seq.start(p_sequencer.pcie_sqr)
          └── pcie_cfg_seq.start(p_sequencer.pcie_sqr)

  Each layer reusable independently in other top-level vseqs

Pattern selection guide

When to use which

  • Sequential sub-sequences — ordered steps, no fork (program then start).

  • fork/join — parallel independent streams, wait for all (stress + bg).

  • fork/join_any + disable fork — race, first event wins (IRQ vs timeout).

  • Layered vseqs — compose scenarios from reusable dma_xfer_vseq, boot_vseq blocks.

systemverilog
// Sequential then parallel — common DMA pattern
task body();
  prog.start(p_sequencer.apb_sqr);     // must complete first

  fork
    dma_engine.start(p_sequencer.dma_sqr);
    axi_mon.start(p_sequencer.axi_sqr);
  join

  check.start(p_sequencer.axi_sqr);    // after parallel phase
endtask

Key takeaways

  • fork/join — parallel sub-sequences, wait for all branches.

  • fork/join_any + disable fork — first completion wins; kill stragglers.

  • Layered vseqs compose reusable scenario blocks on same p_sequencer.

Common pitfalls

  • join_any without disable fork — losing branch keeps running.

  • Nested fork without clear objection ownership — premature test end.

  • Parallel sub-sequences on same sequencer — arbitration interleaves (see arbitration topic).