[UVM] uvm_sequence 심화 | start(), body(), pre/post_body(), 서브 시퀀스, mid_do/post_do

2026. 4. 10. 20:02UVM

반응형

uvm_sequence의 실행 흐름

시퀀스가 실행될 때 내부적으로 다음 순서로 메서드가 호출됩니다. 이 흐름을 정확히 이해해야 복잡한 시나리오를 제대로 제어할 수 있습니다.

// 실행 순서
start()
  -> pre_start()
  -> pre_body()   // is_relevant() 체크 후
  -> body()       // 실제 시나리오 구현 위치
  -> post_body()
  -> post_start()

대부분의 경우 body()만 오버라이드하면 충분합니다. 나머지 훅은 공통 전처리/후처리가 필요한 기반 클래스(base sequence)를 만들 때 활용합니다.


start() — 시퀀스 실행의 진입점

start()는 시퀀서를 연결하고 시퀀스를 실행시키는 메서드입니다. Test나 상위 시퀀스에서 호출합니다.

task start(
  uvm_sequencer_base sequencer,  // 실행할 시퀀서
  uvm_sequence_base  parent_sequence = null,
  int                this_priority = -1,
  bit                call_pre_post = 1  // pre/post_body 호출 여부
);

// 사용 예
task run_phase(uvm_phase phase);
  my_sequence seq;
  seq = my_sequence::type_id::create("seq");
  seq.start(env.agent.sequencer);
endtask

call_pre_post = 0으로 설정하면 pre_body()post_body()가 호출되지 않습니다. 하위 시퀀스(sub-sequence)를 start할 때 주로 사용합니다.


body() — 시나리오 구현

실제 트랜잭션을 생성하고 드라이버로 전송하는 핵심 메서드입니다.

class write_sequence extends uvm_sequence #(my_item);
  `uvm_object_utils(write_sequence)

  int unsigned num_trans = 10;

  task body();
    my_item req;
    repeat (num_trans) begin
      req = my_item::type_id::create("req");
      start_item(req);          // 시퀀서에 아이템 전송 요청
      if (!req.randomize() with { addr inside {[0:255]}; })
        `uvm_fatal("RAND", "randomization failed")
      finish_item(req);         // 드라이버가 item_done() 할 때까지 대기
    end
  endtask
endclass

start_item()finish_item()은 항상 쌍으로 써야 합니다. start_item() 이후 finish_item() 이전에 randomize를 호출해야 합니다.


pre_body() / post_body() — 전후 처리 훅

시퀀스 계층 구조에서 기반 클래스에 공통 로직을 넣을 때 사용합니다. 예를 들어, 모든 시퀀스 실행 전에 초기화 트랜잭션을 보내거나, 실행 후 상태를 확인하는 로직을 여기에 넣습니다.

class base_sequence extends uvm_sequence #(my_item);
  `uvm_object_utils(base_sequence)

  task pre_body();
    // 모든 자식 시퀀스 실행 전 공통 처리
    if (starting_phase != null)
      starting_phase.raise_objection(this);
  endtask

  task post_body();
    // 모든 자식 시퀀스 실행 후 공통 처리
    if (starting_phase != null)
      starting_phase.drop_objection(this);
  endtask
endclass

class my_sequence extends base_sequence;
  `uvm_object_utils(my_sequence)

  task body();
    // pre_body/post_body는 base_sequence에서 처리됨
    // 여기서는 실제 시나리오만 구현
    my_item req;
    req = my_item::type_id::create("req");
    start_item(req);
    void'(req.randomize());
    finish_item(req);
  endtask
endclass

Objection과 시퀀스 — starting_phase

테스트에서 직접 seq.start()를 호출하는 경우는 test의 run_phase에서 objection을 관리합니다. 하지만 시퀀서의 default_sequence로 등록된 경우에는 시퀀스가 직접 objection을 raise/drop해야 합니다.

// default_sequence 방식 (구형, UVM 1.1)
// sequencer에서 자동으로 starting_phase 설정됨

// 권장 방식 — test에서 명시적으로 start + objection 관리
task run_phase(uvm_phase phase);
  my_sequence seq = my_sequence::type_id::create("seq");
  phase.raise_objection(this);
  seq.start(env.agent.sequencer);
  phase.drop_objection(this);
endtask

서브 시퀀스 — 시퀀스 안에서 시퀀스 호출

시퀀스 안에서 다른 시퀀스를 호출할 수 있습니다. 복잡한 시나리오를 작은 단위로 분리하는 핵심 기법입니다.

class config_then_write_seq extends uvm_sequence #(my_item);
  `uvm_object_utils(config_then_write_seq)

  task body();
    config_sequence cfg_seq;
    write_sequence  wr_seq;

    // 1단계: 설정 시퀀스 실행
    cfg_seq = config_sequence::type_id::create("cfg_seq");
    cfg_seq.start(m_sequencer, this, -1, 0); // call_pre_post=0

    // 2단계: 쓰기 시퀀스 실행
    wr_seq = write_sequence::type_id::create("wr_seq");
    wr_seq.num_trans = 20;
    wr_seq.start(m_sequencer, this, -1, 0);
  endtask
endclass

m_sequencer는 현재 시퀀스가 실행 중인 시퀀서의 핸들입니다. 서브 시퀀스 호출 시 동일한 시퀀서를 전달합니다.


mid_do() / post_do() — 아이템 레벨 훅

트랜잭션 하나하나에 개입할 때 씁니다. mid_do()start_item()finish_item() 사이에, post_do()finish_item() 이후에 호출됩니다.

class monitored_sequence extends uvm_sequence #(my_item);
  `uvm_object_utils(monitored_sequence)

  // start_item 직후 (randomize 전)
  function void mid_do(uvm_sequence_item this_item);
    my_item req;
    if ($cast(req, this_item))
      `uvm_info("SEQ", $sformatf("about to send addr=0x%0h", req.addr), UVM_LOW)
  endfunction

  // finish_item 직후 (driver 처리 완료 후)
  function void post_do(uvm_sequence_item this_item);
    my_item req;
    if ($cast(req, this_item))
      `uvm_info("SEQ", $sformatf("sent addr=0x%0h", req.addr), UVM_LOW)
  endfunction

  task body();
    my_item req;
    repeat (5) begin
      req = my_item::type_id::create("req");
      start_item(req);
      void'(req.randomize());
      finish_item(req);
      // post_do가 여기서 자동 호출됨
    end
  endtask
endclass

`uvm_do 매크로 시리즈 — create → randomize → start 자동화

앞서 start_item() / finish_item()을 직접 호출하는 방법을 살펴보았습니다. UVM은 이 과정을 한 줄로 단축하는 `uvm_do 매크로 시리즈를 제공합니다.

매크로설명
`uvm_do(seq_or_item)create → randomize → start (또는 start_item/finish_item) 자동 처리
`uvm_do_with(seq, { constraint })constraint 조건으로 randomize 후 실행
`uvm_do_on(seq, sequencer)특정 시퀀서 위에서 실행 (Virtual Sequence에서 활용)
`uvm_do_on_with(seq, sqr, { ... })시퀀서 지정 + constraint 지정
// 직접 호출 방식
req = my_item::type_id::create("req");
start_item(req);
if (!req.randomize() with { addr < 256; })
  `uvm_fatal("RAND", "randomization failed")
finish_item(req);

// uvm_do_with 매크로로 동일한 동작
`uvm_do_with(req, { addr < 256; })

`uvm_do 계열 매크로는 내부적으로 create()randomize()start() (시퀀스) 또는 start_item()/finish_item() (아이템)을 자동으로 처리합니다. 아이템 전송을 위한 별도 헬퍼 매크로도 있습니다.

매크로설명
`uvm_create(item)아이템 생성만 (randomize, start 없음)
`uvm_rand_send(item)randomize → start_item → finish_item
`uvm_send(item)start_item → finish_item (randomize 없이)
// 수동 제어가 필요할 때: create + 직접 설정 + send
`uvm_create(req)
req.addr = 32'h1000;  // 직접 값 할당
req.data = 32'hDEAD;
`uvm_send(req)       // randomize 없이 그대로 전송

randomize() with {} — 선택적 랜덤화와 변수 공유

시퀀스에서 트랜잭션을 생성할 때, 모든 rand 변수를 한꺼번에 랜덤화하면 내부 constraint가 꼬여서 에러가 날 수 있습니다. randomize() with {} 구문을 사용하면 특정 변수만 조건을 걸어 랜덤화할 수 있습니다.

// 모든 rand 변수 랜덤화
req.randomize();

// addr만 조건 부여, 나머지는 자유 랜덤
req.randomize() with { addr inside {[0:255]}; };

// 특정 필드를 고정하고 나머지만 랜덤
req.randomize() with { mode == 1; id == 5; };

여러 시퀀스에서 동일한 랜덤 변수를 공유하려면 상위 시퀀스에서 randomize()uvm_config_dbset()하고, 하위 시퀀스에서 get()으로 받아 사용합니다.

// 상위 제어 시퀀스
class control_seq extends uvm_sequence;
  rand bit shared_mode;

  task body();
    randomize();
    uvm_config_db#(bit)::set(null, "*", "shared_mode", shared_mode);

    // 하위 시퀀스 실행
    sub_seq_a seq_a = sub_seq_a::type_id::create("seq_a");
    seq_a.start(m_sequencer);
  endtask
endclass

// 하위 시퀀스
class sub_seq_a extends uvm_sequence;
  bit shared_mode;

  task body();
    uvm_config_db#(bit)::get(null, "*", "shared_mode", shared_mode);
    `uvm_do_with(req, { mode == shared_mode; })
  endtask
endclass

📌 관련 글: uvm_config_db 완전 정리 · Virtual Sequence 완전 정리 · UVM Phase 완전 정리


핵심 정리

시퀀스를 올바르게 설계하기 위한 포인트입니다. 첫째, body()는 반드시 task로 선언해야 합니다 — function으로는 start_item()/finish_item() 같은 blocking call을 쓸 수 없습니다. 둘째, start_item()finish_item()은 항상 쌍으로, 그 사이에서 randomize를 해야 합니다. 셋째, 서브 시퀀스 호출 시 call_pre_post=0으로 objection 중복 raise를 방지하세요. 넷째, 복잡한 시나리오는 작은 시퀀스 단위로 쪼개고 상위 시퀀스에서 조합하는 방식이 재사용성과 가독성 모두에서 유리합니다.