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

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
    • Sequence Items and the Driver Handshake
    • Sequence Mechanics: Lifecycle, Sequencer, Responses and Control
      • Sequence Lifecycle and Hooks: What start() Does, Step by Step
      • Inside the Sequencer: Arbitration, Priority and Relevance
      • Responses and Pipelined Drivers
      • Stopping Sequences and Reset
      • Sequences and Objections: Who Keeps the Phase Open
      • The uvm_do Macro Family: What It Expands To
    • Virtual Sequences and Coordination
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Sequence Mechanics: Lifecycle, Sequencer, Responses and Control
  1. Topics
  2. UVM Verification
  3. UVM Sequences & Stimulus
  4. Sequence Mechanics: Lifecycle, Sequencer, Responses and Control
  5. Sequence Lifecycle and Hooks: What start() Does, Step by Step

Part 5 · Sequences · Intermediate

Sequence Lifecycle and Hooks: What start() Does, Step by Step

What seq.start(sqr, parent, priority, call_pre_post) does in order: registration, priority, pre_start, pre_body, the parent's pre_do/mid_do/post_do, body, post_body, post_start, and the sequence states. When pre_body and post_body are skipped.

On this page

    A DMA configuration sequence programs KEY and LEN in its pre_body, then streams data in body. Started from a test, it works. A colleague reuses it inside a virtual sequence with a one-line macro, and the regression stays green, but the coverage report shows every job ran with the same key. The configuration never happened. Nothing printed an error.

    To see why, you need to know exactly what start() does and in which order. This lesson walks through it, line by line against the library source, and runs it so you can see every hook fire.

    The idea: start() is a fixed script with optional scenes

    A sequence is an object, not a component, so it has no phases of its own. What it has is start(): a task that runs a fixed script around your body(). Think of a theatre performance. The doors open (pre_start), the overture plays (pre_body), the play runs (body), the cast takes a bow (post_body), and the doors close (post_start). The doors always open and close. The overture and the bow are optional scenes: the caller can ask for a performance without them.

    That option is the fourth argument of start(), call_pre_post. It defaults to 1, so a sequence you start yourself runs all five hooks. The uvm_do macros start sub-sequences with it set to 0, which is the answer to the opening puzzle.

    The picture: the steps of start()

    Flowchart of seq.start: pre_start with the automatic objection raised, pre_body if call_pre_post is 1, the parent's pre_do and mid_do, body, the parent's post_do, post_body if call_pre_post is 1, post_start and the objection drop, then FINISHED.
    The script start() runs around body(). pre_body and post_body depend on call_pre_post; the parent's do-hooks run only when there is a parent.

    Build it: the four arguments, then the hooks

    What each argument of start() decides

    Argument

    Default

    What it decides

    sequencer

    (required)

    Where the sequence's items go. null is legal for a sequence that sends no items itself, such as the bench's dma_vseq, started with vseq.start(null).

    parent_sequence

    null

    Makes this a child: its name becomes parent.child (the bench's ...apb.sqr@@vseq.isr), it may send while the parent holds a lock, the parent's pre_do / mid_do / post_do run around it, and its default priority is the parent's.

    this_priority

    -1

    -1 means 100 with no parent, or the parent's priority. Below -1 is a SEQPRI fatal. Priority matters only in some arbitration modes (Inside the sequencer).

    call_pre_post

    1

    Whether pre_body and post_body run.

    start() on a sequence that is already running is a SEQ_NOT_DONE fatal ("already started"). The bench's dma_vseq shows the parent argument in action: write_reg starts each apb_rw_seq with s.start(apb_sqr, this), so the register writes appear as children of the virtual sequence and inherit its priority.

    The five lifecycle hooks

    • `pre_start` / `post_start`: always run, whoever starts the sequence and however. Put anything here that must happen on every run, such as a manual objection.

    • `pre_body` / `post_body`: run only when call_pre_post is 1. Use them only for work that a parent may legitimately want to skip.

    • `body`: the stimulus. Configuration the sequence depends on belongs here, at the top, not in pre_body.

    The three do-hooks run on the parent

    pre_do, mid_do and post_do are not called on the item or child; they are called on the sequence that sends it, once per item or child sequence. They are where a sequence can count, log or adjust everything it sends without touching each call site:

    Hook

    For an item, called...

    For a child sequence, called...

    pre_do(bit is_item) (task)

    inside start_item, after the grant, with is_item = 1

    after the child's pre_body, with is_item = 0

    mid_do(item) (function)

    inside finish_item, after your randomize, just before the item is sent

    just before the child's body

    post_do(item) (function)

    inside finish_item, after the driver's item_done

    right after the child's body

    Here is a sequence that logs every hook, with two children, one started normally and one with call_pre_post set to 0:

    design.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
    // snippet: seq_lab_tb.sv, a sequence that reports its own lifecycle
    class lab_hooks_seq extends uvm_sequence #(lab_item);
      `uvm_object_utils(lab_hooks_seq)
      function new(string name = "lab_hooks_seq"); super.new(name); endfunction
      task pre_start();  `uvm_info("HOOK", "top.pre_start",  UVM_LOW) endtask
      task pre_body();   `uvm_info("HOOK", "top.pre_body",   UVM_LOW) endtask
      task pre_do(bit is_item);
        `uvm_info("HOOK", $sformatf("top.pre_do(is_item=%0b)", is_item), UVM_LOW)
      endtask
      function void mid_do(uvm_sequence_item this_item);
        `uvm_info("HOOK", {"top.mid_do(", this_item.get_name(), ")"}, UVM_LOW)
      endfunction
      function void post_do(uvm_sequence_item this_item);
        `uvm_info("HOOK", {"top.post_do(", this_item.get_name(), ")"}, UVM_LOW)
      endfunction
      task body();
        lab_item      it = lab_item::type_id::create("it");
        lab_child_seq a  = lab_child_seq::type_id::create("childA");
        lab_child_seq b  = lab_child_seq::type_id::create("childB");
        `uvm_info("HOOK", $sformatf("top.body state=%s", get_sequence_state().name()), UVM_LOW)
        start_item(it);
        `uvm_info("HOOK", "top: start_item returned (granted)", UVM_LOW)
        if (!it.randomize()) `uvm_fatal("RAND", "randomize failed")
        finish_item(it);
        `uvm_info("HOOK", "top: finish_item returned", UVM_LOW)
        a.start(m_sequencer, this);            // call_pre_post defaults to 1
        b.start(m_sequencer, this, -1, 0);     // call_pre_post = 0
      endtask
      task post_body();  `uvm_info("HOOK", "top.post_body",  UVM_LOW) endtask
      task post_start(); `uvm_info("HOOK", "top.post_start", UVM_LOW) endtask
    endclass

    lab_child_seq logs its own five hooks the same way and sends nothing.

    The states a sequence passes through

    get_sequence_state() returns where the sequence is: UVM_CREATED, then UVM_PRE_START, UVM_PRE_BODY, UVM_BODY, UVM_ENDED (body has returned), UVM_POST_BODY, UVM_POST_START and finally UVM_FINISHED. A killed sequence ends in UVM_STOPPED. The values are one-hot, so wait_for_sequence_state(UVM_FINISHED | UVM_STOPPED) waits for either ending, which is the right way for a parent to wait for a child it forked.

    Watch it run

    The lab run, with the driver's two messages at UVM_MEDIUM (the item is granted at 22 ns, when reset ends and the driver first asks for work):

    Diagram
    UVM_INFO seq_lab_tb.sv(173) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.pre_start
    UVM_INFO seq_lab_tb.sv(174) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.pre_body
    UVM_INFO seq_lab_tb.sv(188) @ 0: uvm_test_top.env.sqr@@top [HOOK] top.body state=UVM_BODY
    UVM_INFO seq_lab_tb.sv(176) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=1)
    UVM_INFO seq_lab_tb.sv(190) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top: start_item returned (granted)
    UVM_INFO seq_lab_tb.sv(179) @ 22000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(it)
    UVM_INFO seq_lab_tb.sv(48) @ 22000: uvm_test_top.env.drv [DRV] got  top WR 30=c71e692e
    UVM_INFO seq_lab_tb.sv(52) @ 35000: uvm_test_top.env.drv [DRV] item_done
    UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(it)
    UVM_INFO seq_lab_tb.sv(193) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top: finish_item returned
    UVM_INFO seq_lab_tb.sv(163) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.pre_start
    UVM_INFO seq_lab_tb.sv(164) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.pre_body
    UVM_INFO seq_lab_tb.sv(176) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=0)
    UVM_INFO seq_lab_tb.sv(179) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(childA)
    UVM_INFO seq_lab_tb.sv(165) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.body
    UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(childA)
    UVM_INFO seq_lab_tb.sv(166) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.post_body
    UVM_INFO seq_lab_tb.sv(167) @ 35000: uvm_test_top.env.sqr@@top.childA [HOOK] childA.post_start
    UVM_INFO seq_lab_tb.sv(163) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.pre_start
    UVM_INFO seq_lab_tb.sv(176) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.pre_do(is_item=0)
    UVM_INFO seq_lab_tb.sv(179) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.mid_do(childB)
    UVM_INFO seq_lab_tb.sv(165) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.body
    UVM_INFO seq_lab_tb.sv(182) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_do(childB)
    UVM_INFO seq_lab_tb.sv(167) @ 35000: uvm_test_top.env.sqr@@top.childB [HOOK] childB.post_start
    UVM_INFO seq_lab_tb.sv(197) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_body
    UVM_INFO seq_lab_tb.sv(198) @ 35000: uvm_test_top.env.sqr@@top [HOOK] top.post_start
    • pre_do(is_item=1) fires at 22 ns, together with the grant, not when start_item was called at time 0. It runs inside start_item, after wait_for_grant.

    • mid_do(it) comes after "start_item returned": your randomize ran between them, so mid_do sees the final values just before the driver does.

    • childA, started with the default call_pre_post = 1, runs pre_body and post_body even though it is a child. childB skips both. Being a child does not skip them; the argument does.

    What breaks: configuration that lived in pre_body

    Back to the opening. The configuration sequence wrote KEY and LEN in pre_body. The virtual sequence started it with uvm_do(cfg_seq), and the macro starts sub-sequences with call_pre_post set to 0. So pre_body never ran, body streamed data with whatever KEY and LEN the previous job left behind, and the DMA did exactly what it was told. The scoreboard predicted from the monitored register writes, saw no new KEY, and agreed with the DUT. Everything matched; nothing was being tested.

    The coverage report caught it, because every job had the same key. The fix was to move the register writes to the top of body, and to adopt a rule: pre_body and post_body are for work a parent may skip on purpose, never for work the sequence depends on. The same trap catches objections raised in pre_body, which is why Sequences and objections uses pre_start or an automatic objection instead.

    Interview

    "What happens when you call seq.start()? When is pre_body not called?"

    A weak answer: "It calls pre_body, body and post_body. Child sequences do not call pre_body." The first sentence leaves out half the script; the second is the common myth.

    A strong answer: "start() registers the sequence with the sequencer, sets its priority (100, or the parent's when the argument is -1), raises the automatic objection if enabled, then runs pre_start, pre_body if call_pre_post is 1, the parent's pre_do and mid_do, body, the parent's post_do, post_body if call_pre_post is 1, post_start, and drops the objection. pre_body and post_body are skipped only when call_pre_post is 0, which is what the uvm_do macros pass for sub-sequences. A child started with start(sqr, this) still runs them. For items, pre_do runs after the grant inside start_item, mid_do after randomization inside finish_item, and post_do after item_done."

    Follow-ups: What is the parent argument for? (hierarchy names, lock inheritance, default priority, the parent's do-hooks). What happens if you start a sequence that is still running? (SEQ_NOT_DONE fatal).

    Check yourself A parent wants to count every item its children send. It overrides post_do and increments a counter. Does it see the children's items?

    No. post_do runs on the sequence that sends an item, so a child's items call the child's post_do. The parent's post_do runs once per child sequence it starts (after that child's body). To count every item, count in each child, or count in the driver or a monitor.

    Check yourself Where should a sequence put a check that must run every time it finishes, whoever starts it?

    In post_start, which always runs, or at the end of body. post_body would be skipped whenever a parent starts the sequence with call_pre_post set to 0, as the uvm_do macros do.

    Check yourself A virtual sequence forks a child with fork child.start(sqr, this); join_none and later needs to wait for it. What do you write?

    child.wait_for_sequence_state(UVM_FINISHED | UVM_STOPPED);. The states are one-hot bits, so the mask returns when the child ends normally or is killed. Waiting only for UVM_FINISHED would hang forever if the child were killed.

    Next: start_item waits for a grant. Inside the sequencer shows who decides the grant when several sequences are waiting, and why priority often does nothing.

    Related topics

    • The driver handshake

    • The uvm_do macro family

    • Configuring sequences

    Previous

    79 / 280

    Next: Inside the Sequencer: Arbitration, Priority and Relevance

    Up next

    Up nextInside the Sequencer: Arbitration, Priority and Relevance

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Responses and Pipelined Drivers
    • Stopping Sequences and Reset
    • Sequences and Objections: Who Keeps the Phase Open