M_sequencer Vs P_sequencer

In short: Both are handles that a sequence uses to reach its sequencer, but they differ in type. m_sequencer is the general handle of type uvm_sequencer_base that every sequence always has. p_sequencer is a typed handle to your specific sequencer class, created by the `uvm_declare_p_sequencer macro. Use m_sequencer for the normal item handshake; use p_sequencer only when a sequence needs to reach custom fields or handles that live inside your sequencer.

When you first read UVM code and see both m_sequencer and p_sequencer, it is easy to assume they are two different objects. They are not. They point to the same sequencer; the difference is only the type of the handle and therefore what you are allowed to touch through it. This guide explains what each handle is, when the p_sequencer handle even exists, how the declare macro creates it, a side-by-side table, two worked examples, and the mistakes that produce null-handle errors. We keep one running example: a sequencer that carries a shared configuration object.

A simple way to picture it

Imagine a building with a general reception desk. Everyone who works there has the reception phone number (m_sequencer): you can call it to do the standard things, like asking for the next delivery slot. But the accounts department has a private direct line with extra features, like reaching the finance files (p_sequencer). That direct line only exists if someone set it up for you, and it only works because it is typed for the accounts department specifically. If you try to use the direct line before it has been set up, the call goes nowhere. Both numbers ring the same office; one just gives you access to more.

What m_sequencer is

m_sequencer is a member that every uvm_sequence already has, with the type uvm_sequencer_base. It is set automatically when a sequence starts, so it is always valid inside body(). Because its type is the generic base class, you can use it for anything the base class offers, most importantly the item handshake through start_item and finish_item. You do not declare it and you do not create it; it is simply there.

class base_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(base_seq)
  function new(string name="base_seq"); super.new(name); endfunction

  task body();
    bus_item req = bus_item::type_id::create("req");
    start_item(req);            // uses m_sequencer under the hood
    assert(req.randomize());
    finish_item(req);
    // m_sequencer is valid here, but typed as the base class
    `uvm_info("SEQ", $sformatf("running on %s", m_sequencer.get_full_name()), UVM_LOW)
  endtask
endclass

What p_sequencer is

p_sequencer is a typed handle to your own sequencer class, for example my_sequencer rather than the generic base. It does not exist by default. You create it by adding the `uvm_declare_p_sequencer macro to your sequence, naming the sequencer class you expect. Once declared, p_sequencer lets you reach members you added to your sequencer, such as a config handle or a shared event, which the generic m_sequencer handle cannot see.

// a custom sequencer that carries a shared config object
class my_sequencer extends uvm_sequencer #(bus_item);
  `uvm_component_utils(my_sequencer)
  bus_config cfg;                 // custom member
  function new(string n, uvm_component p); super.new(n,p); endfunction
endclass

How the declare macro creates p_sequencer

The macro `uvm_declare_p_sequencer(my_sequencer) does two things. It declares a handle named p_sequencer of type my_sequencer, and it adds code that casts m_sequencer to that type when the sequence starts. In effect p_sequencer is just m_sequencer after a checked cast to your specific class.

class cfg_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(cfg_seq)
  `uvm_declare_p_sequencer(my_sequencer)   // creates the typed p_sequencer
  function new(string name="cfg_seq"); super.new(name); endfunction

  task body();
    bus_item req = bus_item::type_id::create("req");
    // reach a member that only exists on my_sequencer
    if (p_sequencer.cfg.enable_errors) begin
      // configure the item using the sequencer config
    end
    start_item(req);
    assert(req.randomize());
    finish_item(req);
  endtask
endclass

The important detail: p_sequencer.cfg works because p_sequencer is typed as my_sequencer, which has a cfg member. Trying the same through m_sequencer.cfg would not compile, because the base class has no cfg. That is the whole reason p_sequencer exists.

m_sequencer versus p_sequencer at a glance

Aspectm_sequencerp_sequencer
Typeuvm_sequencer_base (generic)Your sequencer class (specific)
Exists by default?Yes, alwaysNo, only after the declare macro
How it is setAutomatically when the sequence startsCast from m_sequencer by the macro
Can reach custom sequencer members?NoYes
Used for the item handshake?Yes (start_item, finish_item)Not needed for that
Points toThe same sequencer objectThe same sequencer object

When to use which

The rule is short. Use m_sequencer for everything the standard flow needs, which is the item handshake and generic sequencer queries. Reach for p_sequencer only when a sequence must read or use something you specifically added to your sequencer class, such as a shared configuration, an event pool, or a reference to another component. If your sequencer is a plain uvm_sequencer with no extra members, you never need p_sequencer at all.

A second example: sharing an event through the sequencer

A common real use is coordinating sequences through a shared event that lives on the sequencer. Only p_sequencer can see it.

class my_sequencer extends uvm_sequencer #(bus_item);
  `uvm_component_utils(my_sequencer)
  uvm_event start_traffic = new("start_traffic");
  function new(string n, uvm_component p); super.new(n,p); endfunction
endclass

class gated_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(gated_seq)
  `uvm_declare_p_sequencer(my_sequencer)
  function new(string name="gated_seq"); super.new(name); endfunction
  task body();
    p_sequencer.start_traffic.wait_trigger();   // wait for the shared gate
    bus_item req = bus_item::type_id::create("req");
    start_item(req); assert(req.randomize()); finish_item(req);
  endtask
endclass

Here several sequences can each wait on the same start_traffic event, and one controller triggers it to release them together. That coordination is only possible because p_sequencer exposes the event that the generic handle would hide.

Common mistakes to avoid

  • Using p_sequencer without the `uvm_declare_p_sequencer macro, so the handle does not exist and the code fails to compile.
  • Declaring p_sequencer with the wrong sequencer class, so the internal cast fails and p_sequencer stays null, giving a null-handle error at run time.
  • Starting the sequence on a different sequencer type than the one declared, which also makes the cast fail.
  • Reaching for p_sequencer when m_sequencer would do, adding a tight coupling between the sequence and one sequencer class for no reason.
  • Assuming m_sequencer and p_sequencer are different objects; they point to the same sequencer, only the type differs.

Keep learning

These handles make more sense once the sequencer-driver link is clear. Read UVM sequencer and driver communication to see where m_sequencer is used in the handshake, learn how items get scheduled in UVM sequence arbitration, and see the sequencer inside the full stack in the UVM testbench architecture guide. More topics live on the UVM category page, and this comparison is a favorite interview question.

Frequently asked questions

Are m_sequencer and p_sequencer two different sequencers?

No. They are two handles that point to the same sequencer object. The only difference is the type: m_sequencer is typed as the generic base class, while p_sequencer is typed as your specific sequencer class, which lets it reach members the base class does not have.

Do I always need p_sequencer?

No. You only need it when a sequence has to access something you added to your sequencer class, such as a config handle or a shared event. If your sequencer has no custom members, m_sequencer is all you use and p_sequencer is unnecessary.

What does `uvm_declare_p_sequencer do?

It declares a handle named p_sequencer typed as the sequencer class you name, and it inserts a checked cast from m_sequencer to that type when the sequence starts. So p_sequencer is essentially m_sequencer after being cast to your specific sequencer class.

Why is my p_sequencer null?

The usual cause is that the sequence was started on a sequencer whose type does not match the class named in the declare macro, so the internal cast failed. Make sure the declared type matches the actual sequencer the sequence runs on.

Can I use m_sequencer to reach a custom sequencer field?

No, not directly. Because m_sequencer is typed as the base class, the compiler does not know about your custom fields. You would need to cast it yourself, which is exactly what the declare macro does for you when it creates p_sequencer.

Is using p_sequencer bad practice?

Not inherently, but it couples the sequence to one sequencer class, which reduces reuse. Use it when you genuinely need sequencer-specific data, and prefer passing configuration through the config database when you want sequences to stay reusable across sequencers.

Similar Posts