
[ cfs_algn_test_pkg ]
└── cfs_algn_test_reg_access.sv (지휘관: "레지스터 테스트 시작!")
└── cfs_algn_test_base.sv (기본 지휘관: Env 생성)
[ cfs_algn_pkg ]
└── cfs_algn_env.sv (거대한 몸통 조립)
│
▼
[ cfs_apb_pkg ] (그림의 가운데 기둥 파트)
├── cfs_apb_agent_config.sv
├── cfs_apb_types.sv
└── cfs_apb_agent.sv
│
▼
[ cfs_apb_if.sv ] (하드웨어 핀)
APB(Advanced Peripheral Bus)는 ARM사에서 만든 아주 단순하고 가벼운 하드웨어 통신 규약(프로토콜)입니다.
APB의 핵심: 딱 2박자(2-Phase)로 동작합니다
APB는 레지스터를 읽고 쓰기 위해 만들어졌기 때문에 구조가 아주 단순하며, 데이터를 주고받을 때 무조건 2번의 클럭(Clock)을 거칩니다.
Setup Phase (준비 단계):
Access Phase (실행 단계):
바로 다음 클럭 상승 에지에서 "자, 이제 진짜 써!" 하고 신호를 보냅니다. (PENABLE = 1)
슬레이브 칩이 데이터를 안전하게 잘 받았으면 "나 처리 다 했어!" 하고 PREADY = 1을 띄워줍니다.
이렇게 한 번의 데이터 전송(트랜잭션)이 깔끔하게 끝납니다.

문제점: Agent는 메모리에 동적으로 생성되었다 지워지는 소프트웨어 객체(class)입니다. 반면, DUT의 핀과 연결되는 Interface는 시뮬레이션 시작부터 끝까지 고정된 물리적 하드웨어(module 내부에 선언됨)입니다. 따라서 class 안에서 interface를 직접 생성할 수 없습니다.
해결책: testbench 최상단에서 물리적인 Interface를 먼저 생성해 두고, 그 인터페이스의 주소(Pointer)인 virtual interface를 Agent에게 "전달"해 주어야 합니다
Interface의 주소나 각종 설정값(예: Bus Width)을 계층을 뛰어넘어 하위 컴포넌트(Agent)에 전달하기 위해 UVM이 제공하는 전역 데이터베이스입니다. set과 get이라는 두 가지 핵심 함수로 동작합니다.
주로 상위 계층(testbench나 test)에서 사용합니다.

데이터를 받을 하위 계층(agent)에서 사용합니다.

UVM 검증 환경은 Test -> Env -> Agent -> Driver/Monitor처럼 계층(Hierarchy)이 아주 깊습니다.
가장 바깥쪽(testbench.sv)에서 만든 진짜 물리적 구리선(virtual interface)을 가장 깊숙한 곳에 있는 일꾼(Driver)에게 전달하려면 어떻게 해야 할까요?
하수 (전통적인 방식): Test가 Env에게 넘겨주고, Env가 Agent에게 넘겨주고, Agent가 Driver에게 넘겨주는 식으로 모든 파일의 코드를 수정해서 릴레이를 해야 합니다. (유지보수 지옥입니다.)
고수 (UVM 방식): 공용 '마법의 우체통(uvm_config_db)'을 사용합니다.
if(!uvm_config_db#(virtual cfs_apb_if)::get(this, "", vif_name, vif)) begin
uvm_config_db: 마법의 우체통(데이터베이스) 클래스 이름입니다.
#(virtual cfs_apb_if): (아주 중요!) "내가 지금 우체통에서 꺼내려는 물건의 종류(Type)가 무엇인가?"를 명시합니다. 우체통 안에는 정수(int), 문자열(string), 클래스 객체 등 온갖 잡동사니가 다 들어있기 때문에, "나는 virtual cfs_apb_if 모양의 박스를 찾고 있다!"라고 컴파일러에게 알려주는 파라미터입니다.
:: SystemVerilog의 범위 지정 연산자(Scope Resolution Operator)입니다. 쉽게 말해 "앞에 있는 클래스에 속한 기능을 쓰겠다"는 뜻입니다.
get: "우체통에서 물건을 꺼내라!"라는 정적 함수(Static Method)입니다.
get(this, "", vif_name, vif) 안의 4가지 인자
인자 1 (this) - 수신인 주소: "나의 현재 위치(Agent)를 기준으로 찾아라."
인자 2 ("") - 상세 주소: 인자 1에 덧붙이는 하위 경로입니다. ""(빈 문자열)을 썼다는 것은 "내 밑에 있는 부서 말고, 딱 내 위치로 배달된 물건을 찾겠다"는 뜻입니다.
인자 3 (vif_name) - 이름표 (Key): 우체통에 들어있는 수많은 물건 중, 겉면에 "vif"라고 적힌 이름표를 찾으라는 뜻입니다. (앞서 코드에서 string vif_name = "vif";라고 정의했었죠.)
인자 4 (vif) - 내 주머니 (Value): 우체통에서 찾은 그 물건을 내 코드에 있는 vif라는 빈 변수에 담아달라는 뜻입니다.
이 get 함수는 단순히 물건만 넘겨주고 끝나는 게 아니라, 성공 여부에 따라 숫자 1(True) 또는 0(False)을 반환합니다.
성공했을 때: 우체통에서 "vif"를 무사히 찾아서 내 변수에 담았다면 1을 반환합니다. 느낌표(!)를 만나 0(거짓)이 되므로, if문 안의 에러 메시지는 실행되지 않고 무사히 넘어갑니다.
실패했을 때: 누가 우체통에 넣는 걸 깜빡했거나 이름표 오타가 나서 물건을 못 찾았다면 0을 반환합니다. 느낌표(!)를 만나 1(참)이 되므로, if문 안으로 들어가서 "야! 우체통에 핀이 없잖아!! 시뮬레이션 당장 멈춰(uvm_fatal)!!" 하고 난리를 치게 됩니다.
결국 이 한 줄의 코드는 "우체통에서 안전하게 구리선을 꺼내오되, 만약 실패하면 칩이 먹통이 될 테니 즉시 시뮬레이션을 강제 종료시켜라"라는 아주 견고한 방어막(Defensive Programming)인 것입니다.
자, 이제 이 길고 복잡했던 문장이 "우체통에서 물건 꺼내오기"로 확실하게 번역이 되시나요?
여러 계층(예: testbench와 test)에서 똑같은 이름표(Key)로 동시에 set을 호출하여 값을 덮어쓰려 할 때, UVM은 누구의 값을 우선할까요?
build_phase 시점: 계층 구조상 가장 높은 곳(최상위)에 있는 컴포넌트가 set한 값이 무조건 이깁니다. (예: testbench의 값이 test의 값을 무시함)
run_phase 시점: 시간 순서상 가장 마지막에 set한 값이 이전 값을 덮어씁니다.
강사가 "디버깅 시간을 엄청나게 뺏길 수 있다"고 경고한 부분이 바로 이 계층 간 우선순위 규칙입니다.
새로운 파일이 5개나 쏟아져서 복잡해 보이지만, 사실 이 파일들은 소프트웨어(UVM)가 하드웨어(RTL 칩)를 원격 조종하기 위한 리모컨 세팅 과정일 뿐입니다.
RTL로 UART나 SPI 컨트롤러를 설계하실 때, 물리적인 핀(tx, rx, sclk 등)을 모듈의 port로 빼서 연결하셨던 것 기억하시죠? UVM에서는 클래스가 물리적인 핀을 가질 수 없기 때문에, 이 5개의 파일을 통해 가상의 리모컨(Virtual Interface)을 만들고 안전하게 보관하는 작업을 하는 것입니다.
가장 근본적인 목적은 "APB Agent(소프트웨어)가 Aligner 칩(하드웨어)의 레지스터 핀 신호를 직접 흔들 수 있도록 권한을 넘겨주는 것"입니다.
이 과정에서 누군가 실수로 핀 연결을 끊거나 덮어쓰지 못하도록, Agent Config라는 안전한 금고를 만들어서 리모컨을 보관하는 객체 지향적인 설계 패턴을 적용한 것입니다.
================================================================================
[하드웨어(물리) 영역 - Static] [소프트웨어(UVM) 영역 - Dynamic]
================================================================================
1️⃣ cfs_apb_if.sv
(물리적 핀 묶음: psel, paddr 등)
│
│ (가상화: virtual)
▼
2️⃣ cfs_apb_types.sv ────────────────────────────────────────┐
(typedef virtual cfs_apb_if cfs_apb_vif;) │
│ │
│ (testbench가 db에 넣음) │
▼ │
┌───────────────────────────┐ │
│ uvm_config_db │ │
│ (UVM 전역 데이터베이스) │ │
└───────────────────────────┘ │
│ │
│ (agent가 db에서 꺼냄) │
▼ ▼
╭────────────────────────────────────────────────────────────────────────────╮
│ 5️⃣ cfs_apb_pkg.sv (APB 패키지 상자 - 소프트웨어 부품들을 하나로 포장) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 4️⃣ cfs_apb_agent.sv (에이전트 총책임자) │ │
│ │ │ │
│ │ - build_phase: 금고(config) 객체 생성 │ │
│ │ - connect_phase: uvm_config_db에서 vif(리모컨) 꺼내서 금고에 전달 │ │
│ │ │ │ │
│ │ ▼ (vif 전달) │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ 3️⃣ cfs_apb_agent_config.sv (에이전트 설정 금고) │ │ │
│ │ │ │ │ │
│ │ │ - local cfs_apb_vif vif; (리모컨 안전 보관) │ │ │
│ │ │ - get_vif() / set_vif() (캡슐화된 접근) │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
╰────────────────────────────────────────────────────────────────────────────╯
cfs_apb_if.sv (물리적 인터페이스)
cfs_apb_types.sv (타입 정의)
cfs_apb_agent_config.sv (에이전트 설정 금고)
cfs_apb_agent.sv (에이전트 관리자)
cfs_apb_pkg.sv (APB 패키지 상자)
1. cfs_apb_if.sv (물리적 인터페이스): 테스트벤치 최상단에 실제 하드웨어 핀 다발이 설치됩니다.
2. cfs_apb_types.sv (타입 정의): 소프트웨어 세상에서 이 물리적 핀을 원격으로 조종하기 위해 cfs_apb_vif라는 이름의 '가상 리모컨' 타입을 정의합니다.
3. 테스트벤치가 이 리모컨을 uvm_config_db라는 전역 우체통에 몰래 넣어둡니다.
4. cfs_apb_pkg.sv (패키지 상자): 시뮬레이션이 본격적으로 시작되면, 이 패키지 상자 안의 소프트웨어들이 작동하기 시작합니다.
5. cfs_apb_agent.sv (관리자): 에이전트가 우체통(uvm_config_db)을 뒤져서 리모컨을 찾아냅니다.
6. cfs_apb_agent_config.sv (금고): 에이전트는 찾아낸 리모컨을 나중에 Driver나 Monitor가 안전하게 꺼내 쓸 수 있도록, agent_config라는 금고 객체 안에 단단히 잠가(set_vif) 보관합니다.
역할: RTL 설계에서 익숙하신 wire나 logic 신호들을 하나로 예쁘게 묶어놓은 물리적인 케이블 다발입니다.
`ifndef CFS_APB_IF_SV
`define CFS_APB_IF_SV
`ifndef CFS_APB_MAX_DATA_WIDTH
`define CFS_APB_MAX_DATA_WIDTH 32
`endif
`ifndef CFS_APB_MAX_ADDR_WIDTH
`define CFS_APB_MAX_ADDR_WIDTH 32
`endif
interface cfs_apb_if(input pclk);
logic preset_n;
logic[`CFS_APB_MAX_ADDR_WIDTH-1:0] paddr;
logic pwrite;
logic psel;
logic penable;
logic[`CFS_APB_MAX_DATA_WIDTH-1:0] pwdata;
logic pready;
logic[`CFS_APB_MAX_DATA_WIDTH-1:0] prdata;
logic pslverr;
endinterface
`endif
역할: 소프트웨어 세상에서 쓸 '포인터(리모컨)의 이름표'를 미리 정해두는 파일입니다.
코드 포인트: * typedef virtual cfs_apb_if cfs_apb_vif;
하드웨어인 cfs_apb_if를 소프트웨어 클래스 안으로 가져오기 위해 virtual을 붙여 가상 인터페이스로 만듭니다. 매번 길게 쓰기 귀찮으니 cfs_apb_vif라는 짧은 별명(typedef)을 붙여준 것입니다.
`ifndef CFS_APB_TYPES_SV
`define CFS_APB_TYPES_SV
//Virtual interface type
typedef virtual cfs_apb_if cfs_apb_vif;
`endif
역할: 리모컨(vif)과 각종 설정값들을 안전하게 보관하는 객체 지향적인 금고입니다.
코드 포인트:
`ifndef CFS_APB_AGENT_CONFIG_SV
`define CFS_APB_AGENT_CONFIG_SV
class cfs_apb_agent_config extends uvm_component;
//Virtual interface
local cfs_apb_vif vif;
`uvm_component_utils(cfs_apb_agent_config)
function new(string name = "", uvm_component parent);
super.new(name, parent);
endfunction
//Getter for the APB virtual interface
virtual function cfs_apb_vif get_vif();
return vif;
endfunction
//Setter for the APB virtual interface
virtual function void set_vif(cfs_apb_vif value);
if(vif == null) begin
vif = value;
end
else begin
`uvm_fatal("ALGORITHM_ISSUE", "Trying to set the APB virtual interface more than once")
end
endfunction
virtual function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
if(get_vif() == null) begin
`uvm_fatal("ALGORITHM_ISSUE", "The APB virtual interface is not configured at \"Start of simulation\" phase")
end
else begin
`uvm_info("APB_CONFIG", "The APB virtual interface is configured at \"Start of simulation\" phase", UVM_DEBUG)
end
endfunction
endclass
`endif
역할: 시뮬레이션이 시작될 때 리모컨을 찾아와서 금고(agent_config)에 넣어주는 총책임자입니다.
코드 포인트:
`ifndef CFS_APB_AGENT_SV
`define CFS_APB_AGENT_SV
class cfs_apb_agent extends uvm_agent;
//Agent configuration handler
cfs_apb_agent_config agent_config;
`uvm_component_utils(cfs_apb_agent)
function new(string name = "", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent_config = cfs_apb_agent_config::type_id::create("agent_config", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
cfs_apb_vif vif;
string vif_name = "vif";
super.connect_phase(phase);
if(!uvm_config_db#(virtual cfs_apb_if)::get(this, "", vif_name, vif)) begin
`uvm_fatal("APB_NO_VIF", $sformatf("Could not get from the database the APB virtual interface using name \"%0s\"", vif_name))
end
else begin
agent_config.set_vif(vif);
end
endfunction
endclass
`endif
역할: 지금까지 만든 APB 관련 파일 4개를 하나의 상자에 깔끔하게 담아 포장합니다.
코드 포인트:
주의할 점은 interface 파일(cfs_apb_if.sv)은 소프트웨어 클래스가 아니므로 패키지(package) 블록 바깥에서 include 해야 한다는 규칙이 정확히 지켜져 있습니다.
`ifndef CFS_APB_PKG_SV
`define CFS_APB_PKG_SV
`include "uvm_macros.svh"
`include "cfs_apb_if.sv"
package cfs_apb_pkg;
import uvm_pkg::*;
`include "cfs_apb_types.sv"
`include "cfs_apb_agent_config.sv"
`include "cfs_apb_agent.sv"
endpackage
`endif
이 5개의 파일은 결국 "APB 핀 신호를 UVM 클래스 안으로 가져와서, 누군가 실수로 지우지 못하게 금고(Config)에 안전하게 보관하는 시스템"을 구축한 것입니다.
지금 당장은 코드가 쪼개져 있어 번거로워 보이지만, 나중에 APB가 아니라 AXI나 I2C 에이전트를 새로 만들 때도 이와 똑같은 디자인 패턴을 그대로 복사해서 이름만 바꾸면 되기 때문에 엄청난 확장성을 가집니다.