
포트 선언 (uvm_analysis_port output_port;)
함수 호출 (output_port.write(item);)
`ifndef ADDER_ITEM_SV // ifndef(If Not Defined): "만약 ADDER_ITEM_SV라는
`define ADDER_ITEM_SV //이름표가 아직 안 붙어있다면, 지금 붙이고(define) 코드를 읽어라
class adder_item extends uvm_sequence_item; //우리가 설계할 검증 환경의 이름을 adder_item
//으로 지음, uvm_sequence_item 상속
// Constructor
function new(string name = "adder_item");
super.new(name);
endfunction
개념: 공장에서 택배 상자가 막 찍혀 나와서 메모리에 처음 생겨날 때(태어날 때), 무조건 가장 먼저 자동으로 실행되는 초기화 함수입니다. OOP에서는 이를 '생성자(Constructor)'라고 부릅니다.
function new(string name = "adder_item");
상자가 태어날 때 이름을 붙여줍니다. 기본값으로 "adder_item"이라는 이름표를 달아주는 것입니다.
super.new(name);
내 부모 클래스(uvm_sequence_item)에게 "부모님, 저 지금 태어났고 이름은 이거예요. UVM 시스템(족보)에 저를 등록해 주세요!" 하고 신고하는 과정입니다. 이 줄이 빠지면 UVM이 이 상자의 존재를 모르게 되어 에러가 납니다.
이 코드는 UVM 전체를 통틀어 가장 많이 쓰이면서도, 소프트웨어와 하드웨어를 연결하는 가장 결정적인 역할을 하는 부분입니다.
이 코드는 "시뮬레이션 시작(build_phase)과 동시에 우체통(uvm_config_db)을 열어서 리모컨(vif)을 꺼내 내 주머니에 넣고, 만약 리모컨이 없으면 즉시 시뮬레이션을 중단시켜라"라는 완벽한 논리 구조를 가지고 있습니다.
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
build_phase: 시뮬레이션 시간이 1나노초라도 흐르기 전, 즉 시간 0초에 모든 검증 장비들을 조립하고 세팅하는 함수입니다.
하드웨어에 물리적인 클럭이 뛰기 전에, 드라이버가 먼저 "리모컨"을 손에 쥐고 준비를 마쳐야 하므로 반드시 이 Phase 안에서 코드를 작성해야 합니다.
super.build_phase(phase);: "부모 클래스(uvm_driver)가 원래 하던 기초 공사도 빼먹지 말고 해라!"라는 객체 지향(OOP)의 기본 규칙입니다.
#(virtual adder_if): "내가 우체통에서 꺼내려는 물건의 종류(Type)는 virtual adder_if야!"라고 명시합니다.
this 와 "": "내 현재 위치(Driver)를 기준으로 우체통을 뒤져볼게."라는 뜻의 경로 설정입니다.
"vif" (문자열): 누군가(최상위 테스트벤치)가 우체통에 리모컨을 넣을 때 붙여둔 이름표(Key)입니다. 이 이름표가 정확히 일치해야 물건을 찾을 수 있습니다.
vif (변수명): 우체통에서 성공적으로 꺼낸 진짜 리모컨을 내 주머니(Driver 클래스 안에 선언해 둔 vif 변수)에 집어넣어라! 라는 뜻입니다.
uvm_config_db::get(...) 함수는 우체통에서 물건을 찾는 데 성공하면 1을, 못 찾으면 0을 반환합니다.
build_phase, connect_phase, run_phase 등 모든 Phase의 첫 줄에는 무조건 super.xxx_phase(phase);를 습관처럼 박아넣고 시작하는 것입니다.
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
build_phase (부품 생성): "공장에서 Driver, Monitor, Scoreboard 부품을 찍어내라!"
connect_phase (선 연결): "부품이 다 만들어졌으니, 이제 부품 간의 파이프(Port)를 찰칵찰칵 연결해라!"
drv = adder_driver::type_id::create("drv", this);
adder_driver: "공장아, 내가 예전에 도면 넘겨준 'adder_driver' 부품 있지?"
::type_id::create: "그 도면대로 지금 당장 메모리에 부품 하나를 실제 플라스틱으로 찍어내 줘!" (이것이 UVM Factory 메커니즘입니다.)
"drv" (문자열): "그리고 그 부품 겉면에 매직으로 'drv'라고 이름표를 써서 붙여놔." (나중에 에러가 나거나 로그를 띄울 때 이 이름표가 출력됩니다.)
this: "이 부품의 부모(소속)는 바로 나(현재 실행 중인 Agent 상자)야!"
개념: 나중에 시뮬레이션이 돌아갈 때 터미널(콘솔) 창에 에러나 동작 상태를 글자로 띄워야 하잖아요? 그때 "이 상자 안에 지금 무슨 숫자가 들어있는지 사람이 읽기 편한 글자(String)로 예쁘게 번역해 주는 기능"입니다.
virtual function string convert2string();
return $sformatf("a=%0d, b=%0d | y=%0d", a, b, y);
endfunction
virtual 키워드:
$sformatf (String Format):
// Generate and send 10 random transactions
repeat(10) begin
// The `uvm_do macro handles creation, randomization, and sending
`uvm_do(req)
end
`uvm_info는 한마디로 Verilog 하드웨어 설계에서 화면에 글자를 띄울 때 쓰시던 $display의 초강력 업그레이드 버전(스마트 로깅 시스템)입니다.
`uvm_info("ID (이름표)", "실제 띄울 메시지", 중요도(Verbosity))
`uvm_info("SEQ", "Finished generating items.", UVM_LOW)
첫 번째 방: ID (이름표)
"SEQ": 이 메시지가 어디서 발생했는지 꼬리표를 달아줍니다.
나중에 시뮬레이션 로그가 수만 줄씩 쏟아질 때, 터미널 검색창에 "SEQ"만 검색하면 대본(Sequence)에서 띄운 메시지만 깔끔하게 모아서 볼 수 있습니다. Driver에서 띄울 때는 "DRV"라고 달아주면 편하겠죠?
두 번째 방: Message (내용)
"Starting the generation...": 화면에 띄우고 싶은 실제 텍스트입니다.
$sformatf와 결합해서 변수값을 띄울 수도 있습니다.
`uvm_info("DEBUG", $sformatf("[%0d] item: %0s", i, item.convert2string()),UVM_LOW)
왜 쓸까요? 시뮬레이션 코드를 다 짜놓고 실행할 때, 터미널 명령어 하나로 "오늘은 UVM_LOW 등급 이상의 중요한 메시지만 화면에 띄워!"라고 지시할 수 있습니다.
그냥 최상위 모듈에서 "우체통(config_db)에 넣어둘 테니, 필요한 놈이 알아서 꺼내 가!" 하고 던져버리면 끝입니다.
set(넣기)과 get(꺼내기)
공통 규칙: #() (택배 상자의 규격)
set을 하든 get을 하든, 괄호 앞의 #() 안에는 내가 주고받을 물건의 정확한 데이터 타입(Type)을 적어야 합니다.
예: #(virtual adder_if), #(int), #(string)
주의: 넣을 때 #(int)로 넣었는데, 꺼낼 때 #(bit)로 꺼내려고 하면 타입이 안 맞아서 절대 꺼낼 수 없습니다.
uvm_config_db#(타입)::set( 1.보내는위치, 2.받을사람주소, 3.이름표, 4.진짜물건 );
보내는 위치 (context): 보통 최상위 tb_top(하드웨어)에서 UVM 세상으로 보낼 때는 null을 씁니다. 만약 UVM 클래스 내부(Test 상자 등)에서 보낸다면 this를 씁니다.
받을 사람 주소 (inst_name): 물건을 수령할 권한이 있는 경로를 문자열로 적습니다.
"" : "이 우주에 있는 아무나 다 가져가라!"
"uvm_test_top.env.agent." : "Agent 상자 안에 있는 놈들(Driver, Monitor)만 가져가라!"
이름표 (field_name): 우편물 겉면에 적어두는 고유한 송장 번호(문자열)입니다. (예: "vif", "loop_count")
진짜 물건 (value): 실제로 넘겨줄 변수나 인터페이스입니다.
uvm_config_db#(타입)::get( 1.내위치, 2.상세경로, 3.이름표, 4.내주머니 );
내 위치 (context): 물건을 찾는 주체입니다. 클래스 내부이므로 항상 this를 씁니다.
상세 경로 (inst_name): 내 위치(this)를 기준으로 추가 경로가 있는지 적습니다. 보통은 나 자신이 쓸 거니까 빈 문자열인 ""를 씁니다.
이름표 (field_name): set에서 보낸 사람이 적어둔 그 송장 번호와 토씨 하나 안 틀리고 똑같이 적어야 합니다. (예: "vif")
내 주머니 (value): 우체통에서 꺼낸 물건을 저장할 내 클래스 내부의 변수 이름입니다.
`uvm_component_utils(adder_driver)
결론부터 말씀드리면, uvm_macros.svh는 UVM 라이브러리를 만든 사람들이 제공하는 "수십 줄짜리 복잡한 코드를 단 한 줄로 줄여주는 마법의 단축키(Macro) 모음집"입니다.
virtual task run_phase(uvm_phase phase);
시뮬레이션 0초부터 시작되는 메인 동작 무대입니다.
phase.raise_objection(this, "TEST_DONE");
phase.drop_objection(this, "TEST_DONE");
UVM은 아무도 raise_objection을 부르지 않으면 시뮬레이션을 0초 만에 끝내버립니다.
테스트를 시작할 땐 반드시 raise_objection을 부르고, 끝날 땐 drop_objection을 불러야 시간이 제대로 흐릅니다. (시작과 끝의 괄호 같은 존재입니다.)
cfs_apb_item_drv item = cfs_apb_item_drv::type_id::create("item");
공장(type_id::create)에 지시하여, 비어있는 택배 상자(item)를 1개 새로 찍어냅니다.
Factory Create (공장 주문 방식)
항상 타입::type_id::create("이름") 이라는 마법의 주문을 외워서 부품이나 상자를 찍어내야 한다는 사실 자체를 외워두세요.
void`(std::randomize(item));
item.randomize()
Randomization (랜덤화)
item.randomize() 또는 std::randomize(item)을 호출하는 순간, 클래스 안에 rand 라고 선언해둔 모든 변수들에 겹치지 않는 무작위 값들이 알아서 꽉꽉 채워집니다. 이것이 UVM 검증의 핵심 파워입니다.