APB coverage

Seungyun Lee·2026년 3월 17일

1.Coverage Collector의 정체

  • 역할: "우리가 지금까지 무엇을 검증했는가?"에 대한 통계 장부를 작성하는 서기(Recorder)입니다.

  • 데이터 출처: 스스로 버스를 관찰하지 않습니다. 모니터(Monitor)가 버스를 훔쳐보고 확성기(uvm_analysis_port)로 방송하는 '관찰 보고서'를 주워와서 장부에 기록합니다.

  • 특징: 단일 시뮬레이션 하나로 끝나는 게 아니라, 수백~수천 번의 랜덤 시뮬레이션(Regression)을 돌리는 내내 장부 데이터가 영구적으로 누적(Persistent)됩니다. (참고로 UVM 라이브러리에는 uvm_monitor처럼 미리 만들어진 uvm_coverage 클래스가 없어서, 기본 uvm_component를 상속받아 직접 만들어야 합니다.)

2. 장부 작성법 (SystemVerilog Coverage 문법 3대장)

강사는 단순히 로그를 찍는 게 아니라, 아주 체계적인 '데이터베이스 테이블'을 만든다고 강조합니다.

  1. Coverpoint (단일 항목 검사): * 방향(Read/Write), 응답(OK/Error) 빈도수를 셉니다.

    • 하드웨어 엔지니어 포인트: 주소(Address)나 데이터(Data)는 "100번지에 값이 들어갔나?"를 보지 않습니다. 대신 32가닥의 핀들이 각각 0과 1로 골고루 뒤집혔는지 확인하는 토글(Toggle) 커버리지 방식으로 인덱스를 수집합니다.
  2. Cross (교차 검사): * 두 개 이상의 항목을 섞어서 봅니다. (예: "Read이면서 Error인 상황도 터져봤어?", "Write이면서 OK인 상황도 터져봤어?")

  3. Transition (상태 변화 검사): * 시간의 흐름에 따른 시나리오를 봅니다. (예: "Read 통신 직후에 또 Read가 들어오는 Back-to-Back 상황도 겪어봤어?")

3. Verification Plan / vPlan

테이블이 수백 개로 늘어나면 사람이 일일이 코드를 읽고 분석할 수 없습니다. 그래서 현업에서는 코딩을 시작하기 전에 트리(Tree) 구조의 검증 계획서(vPlan)를 먼저 작성합니다.

마인드맵이나 XML 형태(예: Cadence의 vPlan X)로 칩의 모든 스펙을 나뭇가지처럼 분류합니다.

나뭇가지의 끝부분(Leaf)을 우리가 SystemVerilog로 작성한 coverpoint 코드와 1:1로 매핑(Mapping)시킵니다.

나중에 검증 툴을 돌리면 이 트리 구조 위에 초록색, 빨간색 그래픽으로 "현재 칩의 검증 진행률(Coverage)은 85%입니다"라고 아주 직관적으로 보여주게 됩니다.

강의 순서

1. cfs_apb_agent_config.sv (스마트 제어판)

가장 먼저, 커버리지를 껐다 켤 수 있는 리모컨 스위치를 만듭니다.

무엇을 추가했나:

  • has_coverage 변수 (기본값 1)와 Getter/Setter 함수 추가.

왜 했나:

  • 칩이 너무 복잡할 때 커버리지를 계속 수집하면 시뮬레이션이 느려집니다. 그래서 지휘관(Test)이 원할 때만 커버리지 기록 기능을 켤 수 있도록 스위치를 달아준 것입니다.

2. cfs_apb_agent.sv (에이전트 조립 공장)

새로 고용한 서기(Coverage Collector)를 에이전트 텐트 안에 배치하고 무전기를 연결해 줍니다.

무엇을 추가했나:

  • cfs_apb_coverage 인스턴스(객체) 선언.
  • build_phase에서 has_coverage 스위치가 켜져 있으면 커버리지 컴포넌트를 생성.
  • connect_phase에서 모니터의 확성기(output_port)와 커버리지의 수신기(analysis_export)를 파이프라인으로 연결.
  • 커버리지가 물리적 핀(Reset 신호)을 읽을 수 있도록 agent_config 포인터 전달.

왜 했나:

  • 모니터가 버스를 보고 쓴 일기장(item_mon)을 커버리지 컴포넌트가 자동으로 받아볼 수 있도록 UVM 통신망을 개통한 것입니다.

3. cfs_apb_coverage.sv (본격적인 통계 장부)

이 파일이 이번 강의의 메인 요리입니다. 코드가 길고 복잡하지만, 핵심은 3가지 장부를 만드는 것입니다.

① 기본 장부 (covergroup cover_item)

내용: 모니터가 던져준 하나의 item_mon을 받을 때마다 기록합니다.
기록 항목:
coverpoint: 방향(Read/Write), 응답(OK/Error), 길이, 지연 시간.
cross: 방향과 응답의 조합 (예: Read이면서 Error인 경우).
transition: 연속 통신 시나리오 (예: Read 직후 또 Read).

② 토글(Toggle) 장부 (class cover_index_wrapper_base & 파라미터 클래스)

내용: 32비트 주소(PADDR)나 데이터(PWDATA)의 '특정 비트'가 0과 1로 모두 뒤집혔는지(Toggle) 기록합니다.

특이사항: 강사가 아주 고급스러운 OOP 기술(가상 클래스 상속과 UVM get_children 함수)을 써서, 주소용 장부 2개, 쓰기 데이터용 32개, 읽기 데이터용 32개를 찍어내는(for loop) 스마트한 뼈대를 만들었습니다.

③ 리셋 장부 (covergroup cover_reset)

내용: "통신이 한창 진행 중일 때(PSEL==1) 밖에서 리셋 버튼을 누르는 가혹한 테스트를 해보았는가?"를 기록합니다.

코드 리뷰

cfs_apb_coverage.sv

`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

1. 기본 통계 장부: covergroup cover_item

모니터가 확성기로 보고서(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가 오는 등, 연속된 통신의 흐름을 검사합니다.

2. 토글(Toggle) 장부: 6개의 wrap_cover_xxx

이 부분이 이 코드에서 가장 객체 지향(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) 클래스를 만들어 배열처럼 관리하고 있습니다.

3. 극한 상황 장부: covergroup cover_reset

커버리지 컴포넌트는 모니터가 주는 데이터만 수동적으로 받아먹지 않습니다. 자기가 직접 버스의 물리적 핀을 감시하기도 합니다.

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)에 갑자기 리셋 버튼을 확 눌러버리는 악랄한 테스트를 우리가 해보았는가?"를 증명하기 위한 아주 중요한 현업용 장부입니다.

4. 실무자 인사이트: 왜 virtual class를 썼을까요? (OOP 마법)

맨 위에 cfs_apb_cover_index_wrapper_base라는 가상(virtual) 부모 클래스를 만든 이유가 뭘까요?

우리가 만든 6개의 토글 장부들은 주소(16비트)와 데이터(32비트)로 파라미터 크기가 다릅니다. SystemVerilog에서는 파라미터가 다르면 완전히 남남인 클래스로 취급해서 for 문으로 한 번에 돌릴 수가 없습니다.

하지만 공통된 '가상 부모 클래스'를 만들어 두면, 나중에 coverage2string 함수에서 get_children()으로 6개의 자식들을 싹 다 끌어모은 뒤, "너희 크기가 어떻든 상관없고, 다 내 자식들이니까 coverage2string() 함수 실행해서 결과 내놔!"라고 한 방에 처리할 수 있습니다. (이것을 다형성, Polymorphism이라고 부릅니다!)

이로써 데이터를 생성(Sequence)하고, 흔들고(Driver), 훔쳐보고(Monitor), 채점표를 작성(Coverage)하는 APB 에이전트의 모든 컴포넌트가 완벽하게 완성되었습니다!

profile
RTL, FPGA Engineer

0개의 댓글