"드라이버가 대본을 받아 핀을 흔들면 ➔ 모니터는 핀을 훔쳐봐서 모니터 전용 상자에 담고 ➔ 확성기(Analysis Port)를 통해 채점기(Scoreboard)로 쏴준다!"


모니터의 유일한 목표는 "버스의 핀 상태를 관찰해서 다시 소프트웨어 택배 상자(Item)로 포장하는 것"입니다. 강사는 여기서 객체 지향 프로그래밍(OOP)의 장점을 살려 택배 상자 구조를 아주 예쁘게 공사합니다.
공통 데이터 (item_base로 이동): * 주소(address), 데이터(data), 방향(direction)은 드라이버나 모니터나 똑같이 쓰는 기본 정보입니다. 그래서 부모 클래스인 cfs_apb_item_base로 올려버립니다.
모니터 전용 데이터 (item_mon 신규 생성):
모니터는 드라이버가 모르는 정보들도 훔쳐볼 수 있습니다.
슬레이브(DUT)가 정상적으로 응답했는지(response), 이전 통신과 몇 클럭이나 쉬었는지(delay), 이번 통신이 총 몇 클럭 걸렸는지(length, APB는 최소 2클럭)를 담기 위해 모니터 전용 상자인 cfs_apb_item_mon을 새로 만듭니다.

이 강의에서 등장하는 가장 새롭고 중요한 UVM 개념입니다!
모니터가 버스에서 데이터를 열심히 훔쳐봐서 상자(item_mon)로 예쁘게 포장했습니다. 그럼 이걸 누구한테 줘야 할까요?
드라이버는 조감독과 1:1 무전기(seq_item_port)를 썼지만, 모니터는 자기가 본 정보를 채점기(Scoreboard)에도 주고, 커버리지(Coverage) 수집기에도 줘야 합니다.
그래서 모니터는 1:1 무전기 대신, 사방팔방으로 데이터를 뿌려대는 라디오 방송국 확성기(uvm_analysis_port)를 하나 장착합니다. 나중에 모니터가 output_port.write(item)이라고 외치면, 주파수를 맞춘 모든 컴포넌트들이 이 상자를 동시에 받아보게 됩니다.
강사가 설명한 모니터의 동작 방식(run_phase)은 방금 전 우리가 뜯어봤던 드라이버의 구조와 소름 돋게 똑같습니다.
collect_transactions() (복수형 - 무한 루프 관리자): 시뮬레이션 내내 forever 루프를 돌면서 실무자 태스크를 무한히 호출합니다.
collect_transaction() (단수형 - 1건 전담 실무자): 여기서 진짜 핀을 훔쳐봅니다. 새로운 item_mon 상자를 하나 만들고 ➔ 버스 핀의 1과 0을 읽어서 상자에 채워 넣은 뒤 ➔ 마지막에 확성기(output_port.write(item))로 방송을 때리고 쿨하게 퇴장합니다.
cfs_apb_monitor = APB Agent 안에 있는 장비 (CCTV 카메라)
cfs_apb_item_mon = 빈 관찰 보고서(택배 상자)/ 주소, 데이터 같은 알맹이 값만 담겨있는 껍데기입니다.
`ifndef CFS_APB_ITEM_MON_SV
`define CFS_APB_ITEM_MON_SV
class cfs_apb_item_mon extends cfs_apb_item_base;
//Response
cfs_apb_response response;
//Lenght, in clock cycles, of the APB transfer
int unsigned length;
//Number of clock cycles from the previous item
int unsigned prev_item_delay;
`uvm_object_utils(cfs_apb_item_mon)
function new(string name = "");
super.new(name);
endfunction
virtual function string convert2string();
string result = super.convert2string();
result = $sformatf("%s, data: %0x, response: %0s, length: %0d, prev_item_delay: %0d",
result, data, response.name(), length, prev_item_delay);
return result;
endfunction
endclass
`endif
드라이버(명령자)는 절대 알 수 없고, 오직 버스를 훔쳐본 모니터(관찰자)만 채워 넣을 수 있는 3가지 전용 빈칸입니다.
response: 슬레이브 칩이 데이터를 잘 받았는지(OKAY), 아니면 주소가 잘못됐다고 에러(ERROR)를 뱉었는지 기록하는 칸입니다.
length: 통신을 시작(PSEL=1)해서 끝날 때(PREADY=1)까지 총 몇 클럭 사이클이나 걸렸는지 스톱워치로 잰 기록입니다.
prev_item_delay: 이전 통신이 끝나고 이번 통신이 시작될 때까지 버스가 몇 클럭 동안 텅 비어 있었는지(Idle 상태) 관찰한 기록입니다.
// 1. Get the basic info from the parent class (e.g., Address, Direction)
string result = super.convert2string();
// 2. Append the monitor-specific info to the string
result = $sformatf("%s, data: %0x, response: %0s, length: %0d, prev_item_delay: %0d", ...);
이 함수는 나중에 시뮬레이션 로그 창에 띄울 "한 줄 요약본"을 만드는 과정입니다.
super.convert2string()을 통해 부모가 정리해 둔 정보(주소, 방향)를 먼저 쓱 가져오고, 그 뒤에 방금 측정한 모니터 전용 정보(데이터, 응답, 걸린 시간)를 덧붙여서 완벽한 한 줄짜리 문장을 완성해 냅니다. 실무에서 디버깅할 때 이 한 줄이 엔지니어의 퇴근 시간을 결정합니다!
`ifndef CFS_APB_MONITOR_SV
`define CFS_APB_MONITOR_SV
class cfs_apb_monitor extends uvm_monitor;
//Pointer to agent configuration
cfs_apb_agent_config agent_config;
//Port for sending the collected item
uvm_analysis_port#(cfs_apb_item_mon) output_port;
`uvm_component_utils(cfs_apb_monitor)
function new(string name = "", uvm_component parent);
super.new(name, parent);
output_port = new("output_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
collect_transactions();
endtask
//Task which drives one single item on the bus
protected virtual task collect_transaction();
cfs_apb_vif vif = agent_config.get_vif();
cfs_apb_item_mon item = cfs_apb_item_mon::type_id::create("item");
while(vif.psel !== 1) begin
@(posedge vif.pclk);
item.prev_item_delay++;
end
item.addr = vif.paddr;
item.dir = cfs_apb_dir'(vif.pwrite);
item.length = 1;
if(item.dir == CFS_APB_WRITE) begin
item.data = vif.pwdata;
end
@(posedge vif.pclk);
item.length++;
while(vif.pready !== 1) begin
@(posedge vif.pclk);
item.length++;
end
item.response = cfs_apb_response'(vif.pslverr);
if(item.dir == CFS_APB_READ) begin
item.data = vif.prdata;
end
output_port.write(item);
`uvm_info("DEBUG", $sformatf("Monitored item:: %0s", item.convert2string()), UVM_NONE)
@(posedge vif.pclk);
endtask
//Task for collecting all transactions
protected virtual task collect_transactions();
forever begin
collect_transaction();
end
endtask
endclass
`endif
// 1. Create a blank report
cfs_apb_item_mon item = cfs_apb_item_mon::type_id::create("item");
// 2. Wait until the master starts a transaction (PSEL == 1)
while(vif.psel !== 1) begin
@(posedge vif.pclk);
item.prev_item_delay++; // Count how many cycles the bus was idle!
end
// 3. Setup phase: Capture the initial signals
item.addr = vif.paddr;
item.dir = cfs_apb_dir'(vif.pwrite);
item.length = 1; // It took 1 cycle so far
// If it's a WRITE operation, capture the write data now!
if(item.dir == CFS_APB_WRITE) begin
item.data = vif.pwdata;
end
@(posedge vif.pclk);
item.length++; // Move to Access Phase
// 4. Wait states: Wait until the slave is ready
while(vif.pready !== 1) begin
@(posedge vif.pclk);
item.length++; // Count the wait states!
end
// 5. Capture the final slave response
item.response = cfs_apb_response'(vif.pslverr);
// If it's a READ operation, the data is ONLY valid when PREADY is 1!
if(item.dir == CFS_APB_READ) begin
item.data = vif.prdata;
end
// 6. Broadcast the completed report to the Scoreboard!
output_port.write(item);
드라이버(<=): 핀에 값을 밀어 넣을 때는 vif.paddr <= item.addr; 처럼 Non-blocking 할당을 써서 하드웨어를 구동(Drive)합니다.
모니터(=): 반대로 핀의 값을 읽어올 때는 item.addr = vif.paddr; 처럼 Blocking 할당을 써서 소프트웨어 변수에 값을 즉시 캡처(Sample)합니다.