APB monitor

Seungyun Lee·2026년 3월 16일

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

모니터의 역할과 '택배 상자' 리모델링 (Item Refactoring)

모니터의 유일한 목표는 "버스의 핀 상태를 관찰해서 다시 소프트웨어 택배 상자(Item)로 포장하는 것"입니다. 강사는 여기서 객체 지향 프로그래밍(OOP)의 장점을 살려 택배 상자 구조를 아주 예쁘게 공사합니다.

공통 데이터 (item_base로 이동): * 주소(address), 데이터(data), 방향(direction)은 드라이버나 모니터나 똑같이 쓰는 기본 정보입니다. 그래서 부모 클래스인 cfs_apb_item_base로 올려버립니다.

모니터 전용 데이터 (item_mon 신규 생성):

모니터는 드라이버가 모르는 정보들도 훔쳐볼 수 있습니다.

슬레이브(DUT)가 정상적으로 응답했는지(response), 이전 통신과 몇 클럭이나 쉬었는지(delay), 이번 통신이 총 몇 클럭 걸렸는지(length, APB는 최소 2클럭)를 담기 위해 모니터 전용 상자인 cfs_apb_item_mon을 새로 만듭니다.

UVM의 확성기: uvm_analysis_port의 등장

이 강의에서 등장하는 가장 새롭고 중요한 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_item_mon.sv 와 cfs_apb_monitor.sv 차이

cfs_apb_monitor = APB Agent 안에 있는 장비 (CCTV 카메라)
cfs_apb_item_mon = 빈 관찰 보고서(택배 상자)/ 주소, 데이터 같은 알맹이 값만 담겨있는 껍데기입니다.

cfs_apb_item_mon.sv

`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가지 1급 비밀

드라이버(명령자)는 절대 알 수 없고, 오직 버스를 훔쳐본 모니터(관찰자)만 채워 넣을 수 있는 3가지 전용 빈칸입니다.

response: 슬레이브 칩이 데이터를 잘 받았는지(OKAY), 아니면 주소가 잘못됐다고 에러(ERROR)를 뱉었는지 기록하는 칸입니다.

length: 통신을 시작(PSEL=1)해서 끝날 때(PREADY=1)까지 총 몇 클럭 사이클이나 걸렸는지 스톱워치로 잰 기록입니다.

prev_item_delay: 이전 통신이 끝나고 이번 통신이 시작될 때까지 버스가 몇 클럭 동안 텅 비어 있었는지(Idle 상태) 관찰한 기록입니다.

보고서 예쁘게 출력하기 (convert2string)

// 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()을 통해 부모가 정리해 둔 정보(주소, 방향)를 먼저 쓱 가져오고, 그 뒤에 방금 측정한 모니터 전용 정보(데이터, 응답, 걸린 시간)를 덧붙여서 완벽한 한 줄짜리 문장을 완성해 냅니다. 실무에서 디버깅할 때 이 한 줄이 엔지니어의 퇴근 시간을 결정합니다!

cfs_apb_monitor.sv

`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. 숨 죽이고 기다리기 (Idle 시간 측정)

// 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
  • 빈 보고서(item)를 한 장 꺼냅니다.
  • 버스의 PSEL 핀이 1이 될 때까지(통신 시작) 클럭을 기다립니다.
  • 이때 그냥 노는 게 아니라, 클럭이 뛸 때마다 prev_item_delay를 1씩 증가시킵니다. "이전 통신 끝나고 버스가 3클럭 동안 놀고 있었네"라는 걸 기록하는 모니터만의 독점 기술입니다!

2. 첫 번째 사진 찍기 (Setup Phase)

// 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
  • PSEL이 1이 된 순간(Setup Phase), 버스에 깔려있는 주소(paddr)와 방향(pwrite)을 잽싸게 보고서에 베껴 적습니다.
  • 중요한 하드웨어 디테일: 만약 '쓰기(Write)' 통신이라면, 마스터가 이미 이 타이밍에 pwdata를 버스에 올려두었기 때문에 모니터도 지금 바로 pwdata를 훔쳐와야 합니다.

3. 끈질긴 대기, 그리고 최종 완성 (Access Phase & Broadcast)

@(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);
  • PENABLE이 뜨는 Access Phase로 넘어갑니다.
  • 슬레이브가 준비를 마칠 때까지(PREADY == 1) 무한정 기다리며, 클럭이 뛸 때마다 통신 길이(length)를 1씩 증가시킵니다.
  • 슬레이브가 드디어 PREADY를 1로 주면, 에러가 났는지(pslverr) 확인합니다.
  • 중요한 하드웨어 디테일: 만약 '읽기(Read)' 통신이라면, 슬레이브가 PREADY를 1로 띄운 바로 이 순간에만 prdata가 진짜 의미 있는 값(Valid Data)입니다. 그래서 읽기 데이터는 이 마지막 순간에 훔쳐옵니다.
  • 모든 빈칸을 다 채운 보고서를 확성기(output_port.write(item))를 통해 쩌렁쩌렁하게 방송하고 태스크를 끝냅니다!

실무 면접 꿀팁: 드라이버(Driver) vs 모니터(Monitor)의 차이

드라이버(<=): 핀에 값을 밀어 넣을 때는 vif.paddr <= item.addr; 처럼 Non-blocking 할당을 써서 하드웨어를 구동(Drive)합니다.

모니터(=): 반대로 핀의 값을 읽어올 때는 item.addr = vif.paddr; 처럼 Blocking 할당을 써서 소프트웨어 변수에 값을 즉시 캡처(Sample)합니다.

profile
RTL, FPGA Engineer

0개의 댓글