===================================================================================
[하드웨어 영역 - 물리적 보드] [소프트웨어(UVM) 영역 - 동적 객체]
===================================================================================
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ tb_top.sv (최상위 모듈) │ │ adder_test.sv (지휘관 상자) │
│ │ │ - 전체 테스트 시작 (run_test) │
│ ┌────────────────────────────┐ │ │ - adder_seq(대본) 실행 명령 │
│ │ adder_if.sv (인터페이스) │◀─┼─(vif)──┼─┐ │
│ │ - clk, rst, a, b, y │ │ │ │ ┌────────────────────────────┐ │
│ └──────┬───────────▲─────────┘ │ │ │ │ adder_env.sv (환경 상자) │ │
│ │ │ │ │ │ │ │ │
│ │ 물리적 핀 │ │ │ │ │ ┌────────────────────────┐ │ │
│ ▼ │ │ │ │ │ │ adder_agent.sv (에이전트)│ │ │
│ ┌────────────────────────────┐ │ │ └─┼─┼─▶ adder_driver.sv │ │ │
│ │ adder.sv (DUT, 검증할 칩) │ │ │ │ │ (vif에 0과 1로 쏘기) │ │ │
│ │ - input a, b │ │ │ │ │ ▲ │ │ │
│ │ - output y │ │ │ │ │ │ (택배 상자) │ │ │
│ └────────────────────────────┘ │ │ │ │ uvm_sequencer │ │ │
└──────────────────────────────────┘ │ │ │ ▲ │ │ │
│ │ │ │ │ │ │
[데이터 패킷(트랜잭션)] │ │ │ adder_seq.sv │ │ │
📦 adder_item.sv (택배 상자) │ │ │ (a,b 랜덤 무한 생성) │ │ │
- rand int a, b; │ │ │ │ │ │
- int y; │ │ │ ┌────────────────────┐ │ │ │
│ │ │ │ adder_monitor.sv │ │ │ │
│ │ │ │(vif에서 결과 훔쳐봄) │ │ │ │
│ │ │ └─────────┬──────────┘ │ │ │
│ │ └───────────┼────────────┘ │ │
│ │ │ (결과 복사본) │ │
│ │ ┌───────────▼────────────┐ │ │
│ │ │ adder_scoreboard.sv │ │ │
│ │ │ (a + b == y 정답 채점) │ │ │
│ │ └────────────────────────┘ │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
각 유닛에 대충 뭐가 들어가는지
item
input, output 작성
sequencer
virtual task body()
uvm_do()
driver
set_item_port.get_next_item(req)
@(posedge vif.clk)
monitor
adder_item captured_item;
ap.write(captured_item)
scoreboard
uvm_analysis_imp #(adder_item, adder_scoreboard) item_export;
virtual function void write(adder.item.imp)
logic [8:0] expected y
expected y = item.a + item.b
대본 작성
adder_seq : "오늘은 A=5, B=10을 담은 택배 상자(adder_item)를 만들어야지!" 하고 무작위로 상자를 찍어내서 아래로 던집니다.
하드웨어 구동
adder_driver: 위에서 떨어진 택배 상자를 열어보니 A=5, B=10이 들어있습니다. 드라이버는 가상 리모컨(vif)을 조작해서 진짜 하드웨어 인터페이스(adder_if)의 선에 5와 10이라는 전기 신호를 인가합니다.
칩 동작
adder DUT: 하드웨어 칩은 선으로 들어온 5와 10을 받아 덧셈을 수행하고, 출력 핀 y에 15라는 결과를 내보냅니다.
결과 관찰
adder_monitor : 칩의 입출력 핀을 몰래 지켜보고 있던 모니터가 "오! A=5, B=10이 들어갔고 Y=15가 나왔군!" 하고 새로운 택배 상자에 이 값들을 싹 복사해서 채점기에게 몰래 넘겨줍니다.
정답 채점
adder_scoreboard: 모니터가 던져준 상자를 받은 채점기는 계산기를 두드립니다. "A(5) + B(10)은 15가 맞나? 칩에서 나온 Y도 15네? 통과(PASS)!" 만약 값이 다르면 에러(UVM_ERROR)를 터뜨립니다.
RTL 설계자에게 가장 익숙한 부분입니다. 핀을 정의하고 칩을 올려놓는 물리적인 작업 공간입니다.
어떤 값을 테스트할지, 몇 번 테스트할지 결정하는 가장 중요한 핵심 알맹이입니다. 직접 머리를 써서 채워 넣어야 할 부분이죠.
데이터를 하드웨어로 밀어 넣고, 결과를 빼와서 정답인지 확인하는 3인방입니다.
위에서 만든 행동 대원들이 굴러다니지 않게 상자에 차곡차곡 담는 과정입니다. (이 파일들은 강의에서 본 껍데기 코드와 거의 100% 똑같습니다.)
`ifndef ADDER_ITEM_SV // ifndef(If Not Defined): "만약 ADDER_ITEM_SV라는
`define ADDER_ITEM_SV //이름표가 아직 안 붙어있다면, 지금 붙이고(define) 코드를 읽어라
//우리가 설계할 검증 환경의 이름을 adder_item로 지음, uvm_sequence_item 상속
class adder_item extends uvm_sequence_item;
// Inputs: Randomized to generate various test cases
rand logic [7:0] a;
rand logic [7:0] b;
// Output: Not randomized, captured from the DUT by the monitor
logic [8:0] y;
// Register this object to the UVM factory
`uvm_object_utils(adder_item)
// Constructor
function new(string name = "adder_item");
super.new(name);
endfunction
// Utility function to easily print the item's contents
virtual function string convert2string();
return $sformatf("a=%0d, b=%0d | y=%0d", a, b, y);
endfunction
endclass
`endif
`ifndef ADDER_SEQ_SV
`define ADDER_SEQ_SV
class adder_seq extends uvm_sequence #(adder_item); // adder_item 상자만 전담해서 다룬다고 선언
`uvm_object_utils(adder_seq)
// Constructor
function new(string name = "adder_seq");
super.new(name);
endfunction
//------------------------------작성해야 하는 부분---------------------------
// The main execution block for the sequence
virtual task body(); // 시작 명령어
`uvm_info("SEQ", "Starting the generation of 10 random adder items...", UVM_LOW)
// Generate and send 10 random transactions
// The `uvm_do macro handles creation, randomization, and sending
repeat(10) begin
`uvm_do(req)
end
`uvm_info("SEQ", "Finished generating items.", UVM_LOW)
endtask
endclass
`endif
UVM 최고의 마법: `uvm_do(req)
원래대로라면 상자를 만들고(create), 드라이버에게 보낼 준비를 하고(start_item), 무작위 값을 채우고(randomize), 진짜로 전송하는(finish_item) 긴 코드를 짜야 합니다. 하지만 이 단 한 줄의 매크로가 그 모든 과정을 알아서 해줍니다.
req의 정체: 우리가 선언한 적도 없는데 튀어나온 이 req는 uvm_sequence 부모 클래스 안에 이미 기본으로 만들어져 있는 '내 전용 택배 상자 변수'입니다.
즉, repeat(10) 루프를 돌면서 "상자 만들고 무작위 값 채워서 던져!"를 10번 반복하는 아주 강력한 코드입니다.
`ifndef ADDER_DRIVER_SV
`define ADDER_DRIVER_SV
class adder_driver extends uvm_driver #(adder_item);
virtual adder_if vif;
`uvm_component_utils(adder_driver)
// Constructor
function new(string name = "adder_driver", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual adder_if)::get(this, "", "vif", vif)) begin
`uvm_fatal("DRV", "Failed to get virtual interface from config_db!")
end
endfunction
virtual task run_phase(uvm_phase phase);
//----------------------작성할 줄 알아햐 하는 부분------------------------------
forever begin
seq_item_port.get_next_item(req);
`uvm_info("DRV", $sformatf("Driving data to DUT: %s", req.convert2string()), UVM_LOW)
@(posedge vif.clk);
vif.a <= req.a;
vif.b <= req.b;
@(posedge vif.clk);
seq_item_port.item_done();
end
endtask
endclass
`endif
forever begin
왜 무한 루프를 쓸까요? 드라이버는 하드웨어의 핀과 직결된 녀석입니다. 하드웨어에 전원이 켜져 있는 한, 언제 데이터가 들어올지 모르니 항상 대기해야 합니다.
get_next_item: "다음 상자 주세요!" (Blocking)
seq_item_port.get_next_item(req);
Pin Wiggling: 진짜 0과 1의 전기 신호 쏘기
이곳이 바로 Verilog와 UVM이 만나는 가장 아름다운 구간입니다!
@(posedge vif.clk);
vif.a <= req.a;
vif.b <= req.b;
// Wait one more clock cycle to hold the values
@(posedge vif.clk);
seq_item_port.item_done();
핀에 값을 다 쏘고 났으니, 자신에게 상자를 넘겨줬던 시퀀서(Sequencer)에게 "나 핀에 다 썼어! 이번 건 끝!" 하고 보고하는 핸드셰이크(Handshake) 신호입니다.
`ifndef ADDER_MONITOR_SV
`define ADDER_MONITOR_SV
// Extend uvm_monitor
class adder_monitor extends uvm_monitor;
// Virtual interface to SPY on the physical pins
virtual adder_if vif;
// Analysis port: The "megaphone" to broadcast captured items to the scoreboard
uvm_analysis_port #(adder_item) ap;
// Register to the UVM factory
`uvm_component_utils(adder_monitor)
// Constructor
function new(string name = "adder_monitor", uvm_component parent);
super.new(name, parent);
endfunction
// Build phase: Set up the megaphone and get the remote control
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 1. Instantiate the megaphone (Analysis Port)
ap = new("ap", this);
// 2. Retrieve the virtual interface from config_db (Same as Driver!)
if(!uvm_config_db#(virtual adder_if)::get(this, "", "vif", vif)) begin
`uvm_fatal("MON", "Failed to get virtual interface from config_db!")
end
endfunction
// Run phase: The infinite loop watching the hardware
virtual task run_phase(uvm_phase phase);
// Create an empty box to store the captured data
adder_item captured_item;
captured_item = adder_item::type_id::create("captured_item");
// -----------------이 아래부분이 핵심---------------------------
// Run forever, spying on the bus
forever begin
// Wait for the clock edge where data becomes valid
@(posedge vif.clk);
// Capture the physical pin values and store them in our software box
// (Notice we use '=', reading the pins, not driving them with '<=')
captured_item.a = vif.a;
captured_item.b = vif.b;
captured_item.y = vif.y;
`uvm_info("MON", $sformatf("Captured data: %s", captured_item.convert2string()), UVM_LOW)
// 3. Broadcast the captured item to anyone listening (Scoreboard)
ap.write(captured_item);
end
endtask
endclass
`endif
확성기 설치 (uvm_analysis_port)
uvm_analysis_port #(adder_item) ap;
ap 뜻: analysis_port
Driver는 Sequencer와 1:1로 핑퐁(Handshake)을 했지만, Monitor는 방송국 확성기를 사용합니다.
수동적인 관찰 (captured_item.a = vif.a;)
하지만 Monitor는 핀의 상태를 그저 읽어오기만 합니다. 이미 계산이 끝나서 핀에 흐르고 있는 진짜 전기 신호(vif.a, vif.b, vif.y)를 떠내어, 빈 택배 상자(captured_item)에 쏙쏙 담아줍니다.
방송 송출 (ap.write(captured_item))
가장 통쾌한 부분입니다. 상자에 훔쳐 온 데이터를 다 담았다면, ap.write() 함수를 통해 확성기로 상자를 뻥 차서 날려 보냅니다.
get_next_item처럼 누군가 받을 때까지 기다리지(Blocking) 않습니다. 듣는 사람이 있든 없든 쿨하게 쏴버리고 다음 클럭을 관찰하러 다시 루프를 돕니다.
`ifndef ADDER_SCOREBOARD_SV
`define ADDER_SCOREBOARD_SV
// Extend from uvm_scoreboard
class adder_scoreboard extends uvm_scoreboard;
// [Block 1] Factory Registration
`uvm_component_utils(adder_scoreboard)
//-------------------------------------------------------------------------------------
// Receiver port to catch the megaphone broadcast from the Monitor
uvm_analysis_imp #(adder_item, adder_scoreboard) item_export;
//-------------------------------------------------------------------------------------
// [Block 2] Constructor
function new(string name = "adder_scoreboard", uvm_component parent);
super.new(name, parent);
endfunction
// [Block 3] Build Phase (Setup)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Instantiate the receiver port
item_export = new("item_export", this);
endfunction
// [Block 4] The Core Logic: write() function
//------------------------------------------------------------------------
// This function is AUTOMATICALLY called whenever the monitor broadcasts an item!
virtual function void write(adder_item item);
// 1. Declare a variable for the expected Reference Model
logic [8:0] expected_y;
// 2. Calculate the correct answer based on inputs (a + b)
expected_y = item.a + item.b;
// 3. Compare the expected answer with the actual DUT output (item.y)
if (expected_y == item.y) begin
`uvm_info("SCB", $sformatf("PASS! a(%0d) + b(%0d) = y(%0d)", item.a, item.b, item.y), UVM_LOW)
end else begin
`uvm_error("SCB", $sformatf("FAIL! a(%0d) + b(%0d) should be %0d, but got %0d", item.a, item.b, expected_y, item.y))
end
endfunction
//---------------------------------------------------------------------------
endclass
`endif
uvm_analysis_imp: 모니터의 확성기(port)에서 날아오는 방송을 캐치하는 안테나(수신기)입니다.
마법의 write() 함수: 모니터에서 ap.write(captured_item);를 실행하는 바로 그 순간! Scoreboard의 이 write() 함수가 자동으로 호출(Trigger)됩니다. * 따라서 무한 루프(forever)를 돌면서 언제 데이터가 오나 감시할 필요 없이, 데이터가 도착할 때만 깔끔하게 깨어나서 채점을 진행합니다. 소프트웨어적으로 아주 효율적인 구조죠.
이 부분이 검증 엔지니어의 진짜 실력이 들어가는 곳입니다! 지금은 expected_y = item.a + item.b;라는 아주 단순한 한 줄이지만, 만약 DUT가 CNN 가속기라면?
이 write 함수 안에서 파이썬이나 C++ 모델을 연동하거나, 복잡한 컨볼루션 연산 알고리즘을 직접 짜서 expected_y (정답)를 만들어내야 합니다.
정답과 칩의 결과(item.y)가 다르면 `uvm_error를 터뜨립니다. 이 에러 하나를 보기 위해 지금까지 그 긴 UVM 뼈대를 세워온 것입니다!
`ifndef ADDER_AGENT_SV
`define ADDER_AGENT_SV
// Extend from uvm_agent
class adder_agent extends uvm_agent;
// ============================================================================
// [Block 1] Declare the 3 core components of an Agent
// ============================================================================
// 1. Sequencer: The traffic controller that holds the items
uvm_sequencer #(adder_item) sqr;
// 2. Driver: The worker that wiggles the pins
adder_driver drv;
// 3. Monitor: The CCTV that observes the pins
adder_monitor mon;
// Register to the UVM factory
`uvm_component_utils(adder_agent)
// ============================================================================
// [Block 2] Constructor
// ============================================================================
function new(string name = "adder_agent", uvm_component parent);
super.new(name, parent);
endfunction
// ============================================================================
// [Block 3] Build Phase: Create the components!
// ============================================================================
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Create each component using the UVM Factory (type_id::create)
sqr = uvm_sequencer#(adder_item)::type_id::create("sqr", this);
drv = adder_driver::type_id::create("drv", this);
mon = adder_monitor::type_id::create("mon", this);
endfunction
// ============================================================================
// [Block 4] Connect Phase: Wire them together!
// ============================================================================
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Connect the Driver's port to the Sequencer's export
// This allows the get_next_item() and item_done() handshake to work!
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
`endif
type_id::create라는 UVM 전용 공장 생성 명령어를 사용해서 sqr(시퀀서), drv(드라이버), mon(모니터) 객체를 메모리에 찍어냅니다.
참고: Sequencer는 우리가 직접 클래스를 만들지 않았죠? UVM 라이브러리에 이미 완벽하게 짜여진 uvm_sequencer라는 기본 부품이 있기 때문에, 우리가 만든 adder_item만 취급하라고 #(adder_item) 꼬리표만 달아서 그대로 가져다 쓴 것입니다.
build_phase에서 부품들이 다 만들어졌다면, 시간 0초의 그다음 단계인 connect_phase에서 선을 연결합니다.
drv.seq_item_port.connect(sqr.seq_item_export);
이 단 한 줄의 코드가 바로 Driver의 get_next_item()이 Sequencer로 무사히 전달되게 만드는 마법의 파이프라인입니다. 하드웨어에서 assign wire_a = wire_b; 하듯이, 소프트웨어 포트를 찰칵 끼워 맞춘 것입니다.
이 상자의 가장 핵심적인 임무는 앞서 만들었던 Monitor의 확성기(ap)와 Scoreboard의 수신기(item_export)를 파이프로 연결해 주는 것입니다. 코드를 바로 보겠습니다! (코드 주석은 영어로 작성했습니다.)
`ifndef ADDER_ENV_SV
`define ADDER_ENV_SV
// Extend from uvm_env
class adder_env extends uvm_env;
// ============================================================================
// [Block 1] Declare the components
// ============================================================================
// 1. The Agent (which contains Sequencer, Driver, Monitor)
adder_agent agent;
// 2. The Scoreboard (which checks the answer)
adder_scoreboard scb;
// Register to the UVM factory
`uvm_component_utils(adder_env)
// ============================================================================
// [Block 2] Constructor
// ============================================================================
function new(string name = "adder_env", uvm_component parent);
super.new(name, parent);
endfunction
// ============================================================================
// [Block 3] Build Phase: Assemble the big parts!
// ============================================================================
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Create the Agent and Scoreboard using the factory
agent = adder_agent::type_id::create("agent", this);
scb = adder_scoreboard::type_id::create("scb", this);
endfunction
// ============================================================================
// [Block 4] Connect Phase: Wire the Monitor to the Scoreboard!
// ============================================================================
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Access the Monitor INSIDE the Agent, and connect its megaphone (ap)
// to the Scoreboard's receiver (item_export)
agent.mon.ap.connect(scb.item_export);
endfunction
endclass
`endif
더 큰 블록들의 생성 (build_phase)
계층을 뛰어넘는 통신선 연결 (connect_phase)
agent.mon.ap.connect(scb.item_export);
이 한 줄이 UVM 객체 지향 통신의 백미입니다! RTL에서 하위 모듈의 포트를 상위 모듈로 빼서(wire) 연결하듯, 객체 지향에서는 점(.)을 찍어서 하위 객체로 파고들어 갑니다.
"Agent 상자 안(agent.)에 있는 Monitor(mon.)의 확성기(ap)를 가져와서, Scoreboard 상자(scb.)의 수신기(item_export)에 연결(connect)해라!"
드디어 소프트웨어(UVM) 세상의 최고 지휘관, Test(adder_test.sv)를 만나볼 차례입니다!
지금까지 우리는 덧셈기(Adder)를 검증하기 위한 완벽한 '환경(Env)'이라는 거대한 기계를 조립했습니다. 하지만 이 기계는 스스로 굴러가지 않습니다. 누군가 전원 버튼을 누르고, "자, 지금부터 1번 대본(adder_seq)을 저 기계 안의 시퀀서에 넣고 돌려!"라고 명령을 내려야 합니다.
`ifndef ADDER_TEST_SV
`define ADDER_TEST_SV
// Extend from uvm_test
class adder_test extends uvm_test;
// ============================================================================
// [Block 1] Declare the Environment
// ============================================================================
// The grand container holding Agent and Scoreboard
adder_env env;
// Register to the UVM factory
`uvm_component_utils(adder_test)
// ============================================================================
// [Block 2] Constructor
// ============================================================================
function new(string name = "adder_test", uvm_component parent);
super.new(name, parent);
endfunction
// ============================================================================
// [Block 3] Build Phase: Create the Environment!
// ============================================================================
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Instantiate the environment using the factory
env = adder_env::type_id::create("env", this);
endfunction
// ============================================================================
// [Block 4] Run Phase: The Commander's Orders!
// ============================================================================
virtual task run_phase(uvm_phase phase);
// 1. Declare the sequence (The script)
adder_seq seq;
// Create the sequence using the factory
seq = adder_seq::type_id::create("seq");
// 2. IMPORTANT: Raise objection to keep the simulation alive
phase.raise_objection(this);
`uvm_info("TEST", "Starting the adder sequence...", UVM_LOW)
// 3. START the sequence on the specific sequencer inside the agent
seq.start(env.agent.sqr);
`uvm_info("TEST", "Finished the adder sequence.", UVM_LOW)
// 4. IMPORTANT: Drop objection to allow the simulation to end
phase.drop_objection(this);
endtask
endclass
`endif
환경 세팅 (build_phase)
대본 고르기 (run_phase의 seq 생성)
★ UVM 최고의 권력: Objection (시뮬레이션 생사 여탈권)
phase.raise_objection(this);
"나 지금부터 대본 시작할 거니까, 절대 시뮬레이션 끝내지 말고 기다려!" (손 번쩍 들기)
seq.start(env.agent.sqr);
"자, 내가 고른 대본(seq)을, 환경(env) 안의 에이전트(agent) 안에 있는 시퀀서(sqr)에 꽂아서 당장 시작해!" (실제 실행)
phase.drop_objection(this);
"대본 다 끝났다! 이제 더 볼일 없으니 시뮬레이션 종료해도 좋다!" (손 내리기)
`ifndef ADDER_IF_SV
`define ADDER_IF_SV
// The interface acts as a bundle of wires connecting the DUT and the Testbench
interface adder_if(input logic clk, input logic rst_n);
// ============================================================================
// Physical signals connecting to the Adder DUT
// ============================================================================
// Inputs to the DUT
logic [7:0] a;
logic [7:0] b;
// Output from the DUT
logic [8:0] y;
endinterface
`endif
"선 묶음(Bundle of Wires)"의 미학
과거 Verilog에서는 모듈을 연결할 때 wire [7:0] a; wire [7:0] b; wire [8:0] y;를 일일이 선언하고 맵핑해야 했습니다.
SystemVerilog의 interface는 이 선들을 하나의 굵은 케이블(단자대)로 묶어버립니다. 덕분에 나중에 신호가 수십 개인 AXI 버스를 연결할 때도, 포트 하나만 쓱 연결하면 끝나는 엄청난 편리함을 제공합니다.
드라이버와 모니터의 타겟 (vif.a, vif.b)
우리가 Driver에서 vif.a <= req.a; 라고 썼던 것 기억하시죠?
그 코드에서 vif가 가리키는 실제 도착지가 바로 이 인터페이스 안에 선언된 logic [7:0] a; 입니다. 소프트웨어 클래스 안에서 이 물리적인 선에 직접 전압(0과 1)을 인가할 수 있게 해주는 완벽한 브릿지 역할입니다.
시간의 기준점 (clk, rst_n)
interface adder_if(input logic clk, input logic rst_n);
인터페이스는 껍데기가 아니라 실제 시뮬레이션 시간을 공유하는 물리적 공간입니다. 그래서 최상위 테스트벤치(tb_top)에서 만들어진 진짜 클럭(clk)을 입력으로 받아서, 내부의 신호들이 이 클럭에 맞춰 춤출 수 있게 박자를 제공해 줍니다.
"인터페이스(Interface)와 가상 인터페이스(Virtual Interface)의 차이가 무엇인가요?"
이 질문에 이제 완벽하게 대답하실 수 있습니다.
Interface: 실제 하드웨어 모듈(DUT)과 연결되는 물리적인 구리선 묶음입니다. (지금 짜신 이 코드!)
Virtual Interface (vif): UVM이라는 '소프트웨어 클래스(동적 메모리)' 안에서, 저 멀리 있는 '물리적인 인터페이스(정적 메모리)'를 원격으로 조종하기 위해 들고 있는 포인터(리모컨)입니다.
`ifndef ADDER_SV
`define ADDER_SV
// The actual Design Under Test (DUT)
module adder (
input logic clk,
input logic rst_n,
input logic [7:0] a,
input logic [7:0] b,
output logic [8:0] y
);
// ============================================================================
// Sequential Logic: Calculate a + b at every positive clock edge
// ============================================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset state: Clear the output
y <= 9'd0;
end else begin
// Normal operation: Add inputs and register the output
y <= a + b;
end
end
endmodule
`endif
`timescale 1ns/1ps
// Import the UVM library and macros
import uvm_pkg::*;
`include "uvm_macros.svh"
// Include all the files we created (Normally handled by a filelist or Makefile)
`include "adder_item.sv"
`include "adder_seq.sv"
`include "adder_driver.sv"
`include "adder_monitor.sv"
`include "adder_agent.sv"
`include "adder_scoreboard.sv"
`include "adder_env.sv"
`include "adder_test.sv"
`include "adder_if.sv"
`include "adder.sv"
module tb_top;
// ============================================================================
// [Block 1] Clock and Reset Generation
// ============================================================================
logic clk;
logic rst_n;
// Generate a 100MHz clock (Period = 10ns)
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// Apply reset for the first 20ns
initial begin
rst_n = 0;
#20 rst_n = 1;
end
// ============================================================================
// [Block 2] Physical Connections (Breadboard)
// ============================================================================
// 1. Instantiate the physical Interface (The bundle of wires)
adder_if vif(clk, rst_n);
// 2. Instantiate the DUT and connect its pins to the Interface
adder dut (
.clk (vif.clk),
.rst_n(vif.rst_n),
.a (vif.a),
.b (vif.b),
.y (vif.y)
);
// ============================================================================
// [Block 3] The UVM Bridge and Start Button!
// ============================================================================
initial begin
// 1. Put the physical interface into the UVM Mailbox (config_db)
// The Driver and Monitor will call get() to pick up this "vif"
uvm_config_db#(virtual adder_if)::set(null, "uvm_test_top.env.agent.*", "vif", vif);
// 2. Push the big START button!
run_test("adder_test");
end
// ============================================================================
// [Block 4] Waveform Dumping (Optional but crucial for RTL designers)
// ============================================================================
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb_top);
end
endmodule
마법의 우체통 배달 (uvm_config_db::set)
uvm_config_db#(virtual adder_if)::set(...)
기억나시나요? Driver와 Monitor에서 get() 함수를 써서 가상 리모컨(vif)을 꺼내오려고 안달이 나 있었죠?
그 리모컨을 가장 먼저 우체통에 넣어주는 사람이 바로 이 Top 모듈입니다. 내가 물리적으로 연결해 둔 진짜 단자대(vif)를 우체통(set)에 넣어주면, UVM 시스템 깊숙한 곳에 있는 일꾼들이 알아서 꺼내 갑니다.
경로 설정의 비밀 ("uvm_test_top.env.agent.*")
이 리모컨을 아무나 가져가면 안 되겠죠? 그래서 이름표에 배송지 주소를 적어줍니다.
"최상위 테스트(uvm_test_top) 밑에, 환경(env) 밑에, 에이전트(agent) 안에 있는 모든 부품(*)들만 이 리모컨을 가져갈 수 있어!"라는 뜻입니다.
가장 위대한 한 줄: run_test("adder_test");
이 단 한 줄이 실행되는 순간, 시뮬레이션 시간 0초에 엄청난 일들이 연쇄적으로 폭발합니다.
UVM 시스템이 깨어나고 ➔ adder_test를 만들고 ➔ env를 조립하고 ➔ agent, scoreboard를 찍어내고 ➔ driver, monitor에 선을 연결한 뒤 ➔ run_phase가 시작되면서 대본(seq)이 드라이버에게 상자를 마구 던지기 시작합니다.