2026. 4. 10. 20:02ㆍUVM
virtual 키워드 먼저 이해하기
"Virtual Sequence", "Virtual Sequencer"라는 이름에 들어있는 virtual이 무엇인지 모르면 이 글이 어렵다. 먼저 virtual 키워드가 SystemVerilog에서 어떤 의미인지 정리한다.
1) virtual function / virtual task - 동적 디스패치
부모 클래스에서 메서드를 virtual로 선언하면, 부모 타입 핸들에 자식 객체를 담았을 때도 자식의 메서드가 호출된다. 이게 다형성(Polymorphism)의 핵심이다.
class Animal;
virtual function void speak(); // virtual 선언
$display("...");
endfunction
endclass
class Dog extends Animal;
virtual function void speak();
$display("멍멍"); // 자식에서 재정의
endfunction
endclass
// 사용
Animal a = new Dog();
a.speak(); // "멍멍" 출력 — 핸들은 Animal이지만 실제 객체(Dog)의 메서드 호출
virtual이 없으면 핸들의 선언 타입(Animal) 기준으로 호출되고, virtual이 있으면 실제 객체의 타입(Dog) 기준으로 호출된다. UVM의 run_phase(), build_phase() 등이 모두 virtual인 이유가 여기에 있다.
2) virtual class - 추상 클래스
virtual class는 직접 인스턴스화할 수 없는 클래스다. 반드시 자식 클래스가 상속받아 구현해야 한다. pure virtual 메서드를 포함하면 자식 클래스에서 반드시 그 메서드를 구현해야 한다.
virtual class BaseSeq; // 직접 new() 불가
pure virtual task body(); // 자식에서 반드시 구현
endclass
class MySeq extends BaseSeq;
task body(); // 반드시 구현하지 않으면 컴파일 에러
// 실제 시퀀스 로직
endtask
endclass
3) virtual interface - 인터페이스 참조 변수
SystemVerilog에서 인터페이스(interface)는 기본적으로 정적 연결이다. 그런데 클래스 기반의 테스트벤치에서는 인터페이스를 동적으로 참조할 변수가 필요하다. 이때 virtual interface를 사용한다.
interface my_if(input clk);
logic data;
logic valid;
endinterface
class MyDriver;
virtual my_if vif; // 인터페이스를 가리키는 핸들 (변수처럼 다룸)
task drive();
vif.data = 1; // 인터페이스 신호 직접 구동
vif.valid = 1;
endtask
endclass
정리하면: interface는 실제 HW 신호 묶음, virtual interface는 그 인터페이스를 클래스 안에서 참조하기 위한 핸들이다.
"Virtual" Sequence / Sequencer에서 virtual의 의미
UVM의 Virtual Sequence와 Virtual Sequencer에서 "virtual"은 위 세 가지 중 어느 것과도 직접 연관되지 않는다. 여기서 "virtual"은 "실제로 DUT를 직접 드라이브하지 않는다"는 의미다. 실제 DUT 구동은 각 에이전트의 driver가 하고, Virtual Sequencer/Sequence는 그것들을 조율(orchestrate)하는 역할만 한다. 물리적 역할 없이 제어만 하기 때문에 "virtual"이라 부른다.
UVM에서 단일 에이전트 제어는 일반 Sequence로 충분하다. 하지만 AXI + APB처럼 여러 에이전트를 동시에 또는 순서대로 제어해야 할 때는 Virtual Sequence와 Virtual Sequencer가 필요하다. 이 글에서는 Virtual Sequencer의 구조, uvm_declare_p_sequencer 매크로의 동작, 멀티 에이전트 환경에서의 실전 구현 패턴을 예제 코드와 함께 완벽하게 정리한다.
1. Virtual Sequencer란?
Virtual Sequencer는 실제로 DUT를 드라이브하지 않는 특수한 sequencer다. 내부에 여러 에이전트의 실제 sequencer 핸들을 멤버 변수로 보관하는 컨테이너 역할만 한다.
uvm_sequencer를 상속받지만 직접 item을 전달하지 않는다- 여러 하위 sequencer의 참조(핸들)를 멤버로 갖는다
connect_phase에서 실제 agent의 sequencer 핸들을 연결받는다
class my_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils(my_virtual_sequencer)
// 실제 에이전트들의 시퀀서 핸들 보관
my_axi_sequencer axi_sqr;
my_apb_sequencer apb_sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
2. Virtual Sequence란?
Virtual Sequence는 Virtual Sequencer 위에서 실행되는 시퀀스다. body() 안에서 Virtual Sequencer가 보관하는 하위 시퀀서 핸들(p_sequencer.axi_sqr 등)에 접근해 각 에이전트의 시퀀스를 실행한다.
핵심 매크로: `uvm_declare_p_sequencer(TYPE)
이 매크로를 선언하면 body() 내부에서 사용할 수 있는 타입이 지정된 p_sequencer 핸들이 자동으로 생성된다. 매크로 없이 m_sequencer를 직접 캐스팅하는 방식보다 훨씬 안전하고 간결하다.
class my_virtual_seq extends uvm_sequence;
`uvm_object_utils(my_virtual_seq)
`uvm_declare_p_sequencer(my_virtual_sequencer) // p_sequencer 타입 자동 선언
function new(string name = "my_virtual_seq");
super.new(name);
endfunction
task body();
my_axi_write_seq axi_seq;
my_apb_cfg_seq apb_seq;
axi_seq = my_axi_write_seq::type_id::create("axi_seq");
apb_seq = my_apb_cfg_seq::type_id::create("apb_seq");
// p_sequencer를 통해 각 에이전트의 시퀀서에 접근
apb_seq.start(p_sequencer.apb_sqr); // APB 설정 먼저
axi_seq.start(p_sequencer.axi_sqr); // AXI 전송 나중
endtask
endclass
3. 왜 Virtual Sequence가 필요한가?
단일 에이전트만 있다면 test에서 직접 seq.start(env.agent.sqr)를 호출하면 된다. 그런데 에이전트가 여러 개면 상황이 복잡해진다.
Virtual Sequence 없이 (비추천)
// test.sv - 에이전트마다 직접 제어해야 함
task run_phase(uvm_phase phase);
phase.raise_objection(this);
fork
apb_seq.start(env.apb_agent.sqr);
axi_seq.start(env.axi_agent.sqr);
join
phase.drop_objection(this);
endtask
테스트가 많아질수록 test마다 fork/join 코드가 중복된다. 시나리오 변경 시 모든 test를 수정해야 한다.
Virtual Sequence 사용 (권장)
// test.sv - 단 한 줄로 전체 시나리오 실행
task run_phase(uvm_phase phase);
my_virtual_seq vseq;
phase.raise_objection(this);
vseq = my_virtual_seq::type_id::create("vseq");
vseq.start(env.virt_sqr); // virtual sequencer 하나에 start
phase.drop_objection(this);
endtask
시나리오 로직이 virtual sequence 안에 캡슐화되고, test는 깔끔해진다.
4. 전체 구현 - 단계별 완성
4-1. Virtual Sequencer 정의
// my_virtual_sequencer.sv
class my_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils(my_virtual_sequencer)
my_axi_sequencer axi_sqr; // AXI 에이전트 sequencer 핸들
my_apb_sequencer apb_sqr; // APB 에이전트 sequencer 핸들
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// build_phase 불필요 - 핸들만 선언, 생성은 각 agent가 담당
endclass
4-2. Virtual Sequence 정의
// my_virtual_seq.sv
class my_virtual_seq extends uvm_sequence;
`uvm_object_utils(my_virtual_seq)
`uvm_declare_p_sequencer(my_virtual_sequencer)
function new(string name = "my_virtual_seq");
super.new(name);
endfunction
task body();
my_axi_write_seq axi_seq;
my_apb_cfg_seq apb_seq;
axi_seq = my_axi_write_seq::type_id::create("axi_seq");
apb_seq = my_apb_cfg_seq::type_id::create("apb_seq");
// 순차 실행: APB 설정 완료 후 AXI 시작
`uvm_info("VSEQ", "APB 설정 시작", UVM_MEDIUM)
apb_seq.start(p_sequencer.apb_sqr);
`uvm_info("VSEQ", "AXI 전송 시작", UVM_MEDIUM)
axi_seq.start(p_sequencer.axi_sqr);
endtask
endclass
4-3. Environment: connect_phase에서 핸들 연결
Virtual Sequencer가 실제 에이전트 sequencer를 참조하려면, connect_phase에서 핸들을 명시적으로 연결해야 한다.
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_axi_agent axi_agt;
my_apb_agent apb_agt;
my_virtual_sequencer v_sqr;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
axi_agt = my_axi_agent::type_id::create("axi_agt", this);
apb_agt = my_apb_agent::type_id::create("apb_agt", this);
v_sqr = my_virtual_sequencer::type_id::create("v_sqr", this);
endfunction
// connect_phase에서 핸들 연결
function void connect_phase(uvm_phase phase);
v_sqr.axi_sqr = axi_agt.sequencer;
v_sqr.apb_sqr = apb_agt.sequencer;
endfunction
endclass
핵심은 v_sqr.axi_sqr = axi_agt.sequencer와 같이 실제 에이전트의 sequencer 핸들을 virtual sequencer 내부 변수에 대입하는 것이다. 이 연결이 없으면 virtual sequence의 p_sequencer.axi_sqr가 null이 되어 런타임 에러가 발생한다.
4-4. Test: Virtual Sequence 실행 (fork-join 병렬 제어)
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_env env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
my_virtual_seq v_seq;
phase.raise_objection(this);
v_seq = my_virtual_seq::type_id::create("v_seq");
v_seq.start(env.v_sqr); // virtual sequencer 위에서 실행
phase.drop_objection(this);
endtask
endclass
병렬 시나리오가 필요한 경우 virtual sequence의 body() 내부에서 fork-join을 활용한다.
task body();
my_axi_seq axi_seq;
my_apb_seq apb_seq;
axi_seq = my_axi_seq::type_id::create("axi_seq");
apb_seq = my_apb_seq::type_id::create("apb_seq");
// AXI와 APB를 동시에 병렬 실행
fork
axi_seq.start(p_sequencer.axi_sqr);
apb_seq.start(p_sequencer.apb_sqr);
join // 둘 다 끝날 때까지 대기
// 이후 순차 실행
axi_seq.start(p_sequencer.axi_sqr);
endtask
fork...join은 모든 스레드가 끝날 때까지 대기, fork...join_any는 하나라도 끝나면 진행, fork...join_none은 즉시 반환한다. 실제 검증 시나리오에 맞게 선택한다.
5. uvm_declare_p_sequencer 상세 분석
`uvm_declare_p_sequencer(TYPE) 매크로는 virtual sequence 내에서 typed된 p_sequencer 핸들을 자동으로 생성해 준다. 이 매크로가 없으면 m_sequencer를 직접 캐스팅해야 해서 코드가 복잡해진다.
5-1. 매크로 미사용 vs 사용 비교
| 항목 | 매크로 미사용 | uvm_declare_p_sequencer 사용 |
|---|---|---|
| p_sequencer 획득 | $cast(typed_sqr, m_sequencer) 수동 캐스팅 |
매크로가 자동 생성, body() 진입 시 자동 캐스팅 |
| 타입 안전성 | 캐스트 실패 시 런타임 에러 | 컴파일 타임에 타입 체크 가능 |
| 코드 가독성 | boilerplate 코드 필요 | p_sequencer.xxx_sqr 바로 사용 |
| 재사용성 | 낮음 | 높음 |
5-2. 매크로 전개 원리
`uvm_declare_p_sequencer(my_virtual_sequencer)는 내부적으로 아래와 같이 전개된다.
// 매크로 전개 결과 (자동 생성)
my_virtual_sequencer p_sequencer;
task body();
// body() 시작 시 자동으로 캐스팅 수행
if (!$cast(p_sequencer, m_sequencer))
`uvm_fatal("SEQR_CAST", "Virtual sequencer type mismatch")
// 이후 사용자 정의 body() 내용 실행
endtask
즉, p_sequencer는 이미 타입이 확정된 virtual sequencer 핸들이며, 이를 통해 p_sequencer.axi_sqr, p_sequencer.apb_sqr 등으로 개별 에이전트 sequencer에 직접 접근할 수 있다.
6. Virtual Sequence vs 일반 Sequence 비교
| 항목 | 일반 Sequence | Virtual Sequence |
|---|---|---|
| 실행 대상 Sequencer | 특정 에이전트 sequencer 1개 | Virtual Sequencer (여러 에이전트 포함) |
| 제어 범위 | 단일 인터페이스 | 다중 인터페이스 |
| p_sequencer 선언 | 불필요 | `uvm_declare_p_sequencer(TYPE) 필요 |
| 시나리오 복잡도 | 단순 트랜잭션 시퀀스 | 멀티 에이전트 협력 시나리오 |
| 재사용 단위 | 개별 서브 시퀀스 | 시스템 레벨 테스트 시나리오 |
| fork-join 병렬 실행 | 불필요 (단일 채널) | 빈번하게 사용 (멀티 채널 동기화) |
| 대표 사용처 | APB 설정 시퀀스, AXI 읽기 시퀀스 | AXI 전송 + APB 상태 확인 동시 실행 |
7. Grab / Ungrab - Sequencer 독점 사용
특정 시나리오에서 다른 sequence가 끼어들지 못하도록 sequencer를 독점해야 할 때 grab() / ungrab()을 사용한다.
task body();
my_axi_seq axi_seq = my_axi_seq::type_id::create("axi_seq");
// AXI sequencer를 독점 (다른 sequence 진입 차단)
grab(p_sequencer.axi_sqr);
axi_seq.start(p_sequencer.axi_sqr);
// 독점 해제
ungrab(p_sequencer.axi_sqr);
endtask
grab()는 해당 sequencer에서 현재 실행 중인 sequence가 끝날 때까지 기다린 후 독점을 획득한다.lock()은 현재 실행 중인 item까지 완료 후 독점 획득 - grab보다 더 강한 독점이다.- 반드시
ungrab()/unlock()으로 해제해야 한다. 그렇지 않으면 시뮬레이션이 멈춘다.
8. 전체 흐름 요약
UVM Virtual Sequence / Virtual Sequencer의 전체 동작 흐름을 정리하면 다음과 같다.
[ Test ]
└─ v_seq.start(env.v_sqr)
│
▼
[ Virtual Sequencer (v_sqr) ]
├─ axi_sqr ─────────────────┐
└─ apb_sqr ──────────┐ │
│ │
[ Virtual Sequence ] │ │
body() │ │
├─ apb_seq.start(apb_sqr) ◄─┘
└─ axi_seq.start(axi_sqr) ◄──┘
│
▼
[ 개별 Sub-sequence ]
├─ my_apb_seq → APB Driver → APB Interface
└─ my_axi_seq → AXI Driver → AXI Interface
핵심 포인트 정리
- virtual 키워드: SV에서 동적 디스패치(virtual function), 추상 클래스(virtual class), 인터페이스 참조(virtual interface) 세 가지 용도. Virtual Sequence/Sequencer에서는 "직접 DUT를 구동하지 않는다"는 의미.
- Virtual Sequencer: 에이전트 sequencer 핸들들을 멤버로 갖는 컨테이너. 직접 트랜잭션을 생성하지 않는다.
- Virtual Sequence: Virtual Sequencer 위에서 실행되며,
p_sequencer를 통해 여러 에이전트를 제어한다. - `uvm_declare_p_sequencer: 타입이 확정된
p_sequencer핸들을 자동 생성.body()진입 시 자동 캐스팅된다. - connect_phase 연결:
v_sqr.axi_sqr = axi_agt.sequencer형태로 실제 sequencer 핸들을 연결해야 한다. - fork-join: 병렬 인터페이스 제어에 사용.
join / join_any / join_none중 시나리오에 맞는 것을 선택한다. - grab/ungrab: 특정 sequencer 독점이 필요한 시나리오에서 사용. 반드시 해제해야 한다.