
역할: "우리가 지금까지 무엇을 검증했는가?"에 대한 통계 장부를 작성하는 서기(Recorder)입니다.
데이터 출처: 스스로 버스를 관찰하지 않습니다. 모니터(Monitor)가 버스를 훔쳐보고 확성기(uvm_analysis_port)로 방송하는 '관찰 보고서'를 주워와서 장부에 기록합니다.
특징: 단일 시뮬레이션 하나로 끝나는 게 아니라, 수백~수천 번의 랜덤 시뮬레이션(Regression)을 돌리는 내내 장부 데이터가 영구적으로 누적(Persistent)됩니다. (참고로 UVM 라이브러리에는 uvm_monitor처럼 미리 만들어진 uvm_coverage 클래스가 없어서, 기본 uvm_component를 상속받아 직접 만들어야 합니다.)
강사는 단순히 로그를 찍는 게 아니라, 아주 체계적인 '데이터베이스 테이블'을 만든다고 강조합니다.
Coverpoint (단일 항목 검사): * 방향(Read/Write), 응답(OK/Error) 빈도수를 셉니다.
Cross (교차 검사): * 두 개 이상의 항목을 섞어서 봅니다. (예: "Read이면서 Error인 상황도 터져봤어?", "Write이면서 OK인 상황도 터져봤어?")
Transition (상태 변화 검사): * 시간의 흐름에 따른 시나리오를 봅니다. (예: "Read 통신 직후에 또 Read가 들어오는 Back-to-Back 상황도 겪어봤어?")

테이블이 수백 개로 늘어나면 사람이 일일이 코드를 읽고 분석할 수 없습니다. 그래서 현업에서는 코딩을 시작하기 전에 트리(Tree) 구조의 검증 계획서(vPlan)를 먼저 작성합니다.
마인드맵이나 XML 형태(예: Cadence의 vPlan X)로 칩의 모든 스펙을 나뭇가지처럼 분류합니다.
나뭇가지의 끝부분(Leaf)을 우리가 SystemVerilog로 작성한 coverpoint 코드와 1:1로 매핑(Mapping)시킵니다.

나중에 검증 툴을 돌리면 이 트리 구조 위에 초록색, 빨간색 그래픽으로 "현재 칩의 검증 진행률(Coverage)은 85%입니다"라고 아주 직관적으로 보여주게 됩니다.
가장 먼저, 커버리지를 껐다 켤 수 있는 리모컨 스위치를 만듭니다.
무엇을 추가했나:
왜 했나:
새로 고용한 서기(Coverage Collector)를 에이전트 텐트 안에 배치하고 무전기를 연결해 줍니다.
무엇을 추가했나:
왜 했나:
이 파일이 이번 강의의 메인 요리입니다. 코드가 길고 복잡하지만, 핵심은 3가지 장부를 만드는 것입니다.
내용: 모니터가 던져준 하나의 item_mon을 받을 때마다 기록합니다.
기록 항목:
coverpoint: 방향(Read/Write), 응답(OK/Error), 길이, 지연 시간.
cross: 방향과 응답의 조합 (예: Read이면서 Error인 경우).
transition: 연속 통신 시나리오 (예: Read 직후 또 Read).
내용: 32비트 주소(PADDR)나 데이터(PWDATA)의 '특정 비트'가 0과 1로 모두 뒤집혔는지(Toggle) 기록합니다.
특이사항: 강사가 아주 고급스러운 OOP 기술(가상 클래스 상속과 UVM get_children 함수)을 써서, 주소용 장부 2개, 쓰기 데이터용 32개, 읽기 데이터용 32개를 찍어내는(for loop) 스마트한 뼈대를 만들었습니다.
내용: "통신이 한창 진행 중일 때(PSEL==1) 밖에서 리셋 버튼을 누르는 가혹한 테스트를 해보았는가?"를 기록합니다.
`ifndef CFS_APB_COVERAGE_SV
`define CFS_APB_COVERAGE_SV
`uvm_analysis_imp_decl(_item)
virtual class cfs_apb_cover_index_wrapper_base extends uvm_component;
function new(string name = "", uvm_component parent);
super.new(name, parent);
endfunction
//Function used to sample the information
pure virtual function void sample(int unsigned value);
//Function to print the coverage information.
//This is only to be able to visualize some basic coverage information
//in EDA Playground.
//DON'T DO THIS IN A REAL PROJECT!!!
pure virtual function string coverage2string();
endclass
//Wrapper over the covergroup which covers indices.
//The MAX_VALUE parameter is used to determine the maximum value to sample
class cfs_apb_cover_index_wrapper#(int unsigned MAX_VALUE_PLUS_1 = 16) extends cfs_apb_cover_index_wrapper_base;
`uvm_component_param_utils(cfs_apb_cover_index_wrapper#(MAX_VALUE_PLUS_1))
covergroup cover_index with function sample(int unsigned value);
option.per_instance = 1;
index : coverpoint value {
option.comment = "Index";
bins values[MAX_VALUE_PLUS_1] = {[0:MAX_VALUE_PLUS_1-1]};
}
endgroup
function new(string name = "", uvm_component parent);
super.new(name, parent);
cover_index = new();
cover_index.set_inst_name($sformatf("%s_%s", get_full_name(), "cover_index"));
endfunction
//Function to print the coverage information.
//This is only to be able to visualize some basic coverage information
//in EDA Playground.
//DON'T DO THIS IN A REAL PROJECT!!!
virtual function string coverage2string();
return {
$sformatf("\n cover_index: %03.2f%%", cover_index.get_inst_coverage()),
$sformatf("\n index: %03.2f%%", cover_index.index.get_inst_coverage())
};
endfunction
//Function used to sample the information
virtual function void sample(int unsigned value);
cover_index.sample(value);
endfunction
endclass
class cfs_apb_coverage extends uvm_component;
//Pointer to agent configuration
cfs_apb_agent_config agent_config;
//Port for sending the collected item
uvm_analysis_imp_item#(cfs_apb_item_mon, cfs_apb_coverage) port_item;
//Wrapper over the coverage group covering the indices in the PADDR signal
//at which the bit of the PADDR was 0
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_ADDR_WIDTH) wrap_cover_addr_0;
//Wrapper over the coverage group covering the indices in the PADDR signal
//at which the bit of the PADDR was 1
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_ADDR_WIDTH) wrap_cover_addr_1;
//Wrapper over the coverage group covering the indices in the PWDATA signal
//at which the bit of the PWDATA was 0
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH) wrap_cover_wr_data_0;
//Wrapper over the coverage group covering the indices in the PWDATA signal
//at which the bit of the PWDATA was 1
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH) wrap_cover_wr_data_1;
//Wrapper over the coverage group covering the indices in the PRDATA signal
//at which the bit of the PRDATA was 0
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH) wrap_cover_rd_data_0;
//Wrapper over the coverage group covering the indices in the PRDATA signal
//at which the bit of the PRDATA was 1
cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH) wrap_cover_rd_data_1;
`uvm_component_utils(cfs_apb_coverage)
covergroup cover_item with function sample(cfs_apb_item_mon item);
option.per_instance = 1;
direction : coverpoint item.dir {
option.comment = "Direction of the APB access";
}
response : coverpoint item.response {
option.comment = "Response of the APB access";
}
length : coverpoint item.length {
option.comment = "Length of the APB access";
bins length_eq_2 = {2};
bins length_le_10[8] = {[3:10]};
bins length_gt_10 = {[11:$]};
illegal_bins length_lt_2 = {[$:1]};
}
prev_item_delay : coverpoint item.prev_item_delay {
option.comment = "Delay, in clock cycles, between two consecutive APB accesses";
bins back2back = {0};
bins delay_le_5[5] = {[1:5]};
bins delay_gt_5 = {[6:$]};
}
response_x_direction : cross response, direction;
trans_direction : coverpoint item.dir {
option.comment = "Transitions of APB direction";
bins direction_trans[] = (CFS_APB_READ, CFS_APB_WRITE => CFS_APB_READ, CFS_APB_WRITE);
}
endgroup
covergroup cover_reset with function sample(bit psel);
option.per_instance = 1;
access_ongoing : coverpoint psel {
option.comment = "An APB access was ongoing at reset";
}
endgroup
function new(string name = "", uvm_component parent);
super.new(name, parent);
port_item = new("port_item", this);
cover_item = new();
cover_item.set_inst_name($sformatf("%s_%s", get_full_name(), "cover_item"));
cover_reset = new();
cover_reset.set_inst_name($sformatf("%s_%s", get_full_name(), "cover_reset"));
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
wrap_cover_addr_0 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_ADDR_WIDTH)::type_id::create("wrap_cover_addr_0", this);
wrap_cover_addr_1 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_ADDR_WIDTH)::type_id::create("wrap_cover_addr_1", this);
wrap_cover_wr_data_0 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH)::type_id::create("wrap_cover_wr_data_0", this);
wrap_cover_wr_data_1 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH)::type_id::create("wrap_cover_wr_data_1", this);
wrap_cover_rd_data_0 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH)::type_id::create("wrap_cover_rd_data_0", this);
wrap_cover_rd_data_1 = cfs_apb_cover_index_wrapper#(`CFS_APB_MAX_DATA_WIDTH)::type_id::create("wrap_cover_rd_data_1", this);
endfunction
//Port associated with port_item port
virtual function void write_item(cfs_apb_item_mon item);
cover_item.sample(item);
for(int i = 0; i < `CFS_APB_MAX_ADDR_WIDTH; i++) begin
if(item.addr[i]) begin
wrap_cover_addr_1.sample(i);
end
else begin
wrap_cover_addr_0.sample(i);
end
end
for(int i = 0; i < `CFS_APB_MAX_DATA_WIDTH; i++) begin
case(item.dir)
CFS_APB_WRITE : begin
if(item.data[i]) begin
wrap_cover_wr_data_1.sample(i);
end
else begin
wrap_cover_wr_data_0.sample(i);
end
end
CFS_APB_READ : begin
if(item.data[i]) begin
wrap_cover_rd_data_1.sample(i);
end
else begin
wrap_cover_rd_data_0.sample(i);
end
end
default : begin
`uvm_error("ALGORITHM_ISSUE", $sformatf("Current version of the code does not support item.dir: %0s", item.dir.name()))
end
endcase
end
//IMPORTANT: DON'T DO THIS IN A REAL PROJECT!!!
`uvm_info("DEBUG", $sformatf("Coverage: %0s", coverage2string()), UVM_NONE)
endfunction
virtual task run_phase(uvm_phase phase);
cfs_apb_vif vif = agent_config.get_vif();
forever begin
@(negedge vif.preset_n);
cover_reset.sample(vif.psel);
end
endtask
//Function to print the coverage information.
//This is only to be able to visualize some basic coverage information
//in EDA Playground.
//DON'T DO THIS IN A REAL PROJECT!!!
virtual function string coverage2string();
string result = {
$sformatf("\n cover_item: %03.2f%%", cover_item.get_inst_coverage()),
$sformatf("\n direction: %03.2f%%", cover_item.direction.get_inst_coverage()),
$sformatf("\n trans_direction: %03.2f%%", cover_item.trans_direction.get_inst_coverage()),
$sformatf("\n response: %03.2f%%", cover_item.response.get_inst_coverage()),
$sformatf("\n response_x_direction: %03.2f%%", cover_item.response_x_direction.get_inst_coverage()),
$sformatf("\n length: %03.2f%%", cover_item.length.get_inst_coverage()),
$sformatf("\n prev_item_delay: %03.2f%%", cover_item.prev_item_delay.get_inst_coverage()),
$sformatf("\n "),
$sformatf("\n cover_reset: %03.2f%%", cover_reset.get_inst_coverage()),
$sformatf("\n access_ongoing: %03.2f%%", cover_reset.access_ongoing.get_inst_coverage())
};
uvm_component children[$];
get_children(children);
foreach(children[idx]) begin
cfs_apb_cover_index_wrapper_base wrapper;
if($cast(wrapper, children[idx])) begin
result = $sformatf("%s\n\nChild component: %0s%0s", result, wrapper.get_name(), wrapper.coverage2string());
end
end
return result;
endfunction
endclass
`endif
모니터가 확성기로 보고서(item_mon)를 하나 던져줄 때마다(write_item 함수가 실행될 때마다) 도장을 찍는 메인 장부입니다.
length : coverpoint item.length {
bins length_eq_2 = {2}; // 가장 짧은 2클럭짜리 통신 겪어봤나?
bins length_le_10[8] = {[3:10]}; // 3~10클럭짜리를 8개 구간으로 쪼개서 기록해!
illegal_bins length_lt_2 = {[$:1]}; // 만약 1클럭 이하면 시뮬레이션 터뜨려! (불법)
}
APB 통신은 최소 2클럭이 걸리기 때문에 1클럭 이하는 illegal_bins로 강력하게 막아둔 디테일이 훌륭합니다.
cross (교차 검증): response_x_direction은 Read/Write 상황 각각에서 Error가 터지는 상황을 모두 겪어봤는지 곱하기(Cross)해서 표를 만듭니다.
Transition (상태 변화): trans_direction은 Read 직후에 또 Read가 오는 등, 연속된 통신의 흐름을 검사합니다.
이 부분이 이 코드에서 가장 객체 지향(OOP)적으로 화려한 부분입니다! 하드웨어 엔지니어들은 32가닥의 주소선(PADDR)이나 데이터선(PWDATA)이 단 한 가닥도 끊어지지 않고 0과 1로 모두 잘 뒤집히는지(Toggle) 확인하고 싶어 합니다.
// 32비트 데이터를 1비트씩 쪼개서 0과 1이 다 떴는지 기록하는 로직
for(int i = 0; i < `CFS_APB_MAX_ADDR_WIDTH; i++) begin
if(item.addr[i]) begin
wrap_cover_addr_1.sample(i); // i번째 핀이 1이 된 적 있음! 도장 쾅!
end
else begin
wrap_cover_addr_0.sample(i); // i번째 핀이 0이 된 적 있음! 도장 쾅!
end
end
주소선(Addr), 쓰기 데이터(Wr_data), 읽기 데이터(Rd_data) 각각에 대해 '0이 된 적 있는가?', '1이 된 적 있는가?'를 따로따로 기록하기 위해 무려 6개의 래퍼(Wrapper) 클래스를 만들어 배열처럼 관리하고 있습니다.
커버리지 컴포넌트는 모니터가 주는 데이터만 수동적으로 받아먹지 않습니다. 자기가 직접 버스의 물리적 핀을 감시하기도 합니다.
virtual task run_phase(uvm_phase phase);
cfs_apb_vif vif = agent_config.get_vif();
forever begin
@(negedge vif.preset_n); // 리셋 핀이 0으로 떨어지는(negedge) 순간!
cover_reset.sample(vif.psel); // 그때 PSEL이 1이었는지 0이었는지 기록해!
end
endtask
목적: "칩이 한창 데이터를 주고받는 도중(PSEL==1)에 갑자기 리셋 버튼을 확 눌러버리는 악랄한 테스트를 우리가 해보았는가?"를 증명하기 위한 아주 중요한 현업용 장부입니다.
맨 위에 cfs_apb_cover_index_wrapper_base라는 가상(virtual) 부모 클래스를 만든 이유가 뭘까요?
우리가 만든 6개의 토글 장부들은 주소(16비트)와 데이터(32비트)로 파라미터 크기가 다릅니다. SystemVerilog에서는 파라미터가 다르면 완전히 남남인 클래스로 취급해서 for 문으로 한 번에 돌릴 수가 없습니다.
하지만 공통된 '가상 부모 클래스'를 만들어 두면, 나중에 coverage2string 함수에서 get_children()으로 6개의 자식들을 싹 다 끌어모은 뒤, "너희 크기가 어떻든 상관없고, 다 내 자식들이니까 coverage2string() 함수 실행해서 결과 내놔!"라고 한 방에 처리할 수 있습니다. (이것을 다형성, Polymorphism이라고 부릅니다!)
이로써 데이터를 생성(Sequence)하고, 흔들고(Driver), 훔쳐보고(Monitor), 채점표를 작성(Coverage)하는 APB 에이전트의 모든 컴포넌트가 완벽하게 완성되었습니다!