APB Protocol Checks, SVA

Seungyun Lee·2026년 3월 16일

UVM

목록 보기
13/14

1.Protocol Checks?

우리가 검증하는 칩(DUT)의 내부 로직이 어떻게 생겼든 상관없이, "AMBA APB 표준 스펙 문서를 완벽하게 준수하고 있는가?"를 감시하는 절대적인 규칙들입니다.

스펙 문서의 문장 하나하나를 꼼꼼히 뜯어보면서 "이건 규칙으로 만들 수 있겠다!" 하고 뽑아내는 과정이 필요합니다.

2. The 5 Golden Rules

문서에 명시된 규칙 3가지와, 실무 경험상 반드시 넣어야 하는 숨은 규칙 2가지를 뽑아냈습니다.

  1. PENABLE 타이밍 (스펙): PENABLE 신호는 통신의 두 번째 사이클(Access Phase)에 반드시 1로 켜져야 한다.

  2. PENABLE 종료 (스펙): 통신이 끝날 때 PENABLE은 반드시 0으로 꺼져야 한다.

  3. 신호 고정 (스펙): 마스터가 보내는 신호들(주소 PADDR, 방향 PWRITE 등)은 통신이 진행되는 동안 절대 중간에 값이 변해선 안 된다.

  4. 미지값 금지 (실무): 버스 위의 어떤 신호도 X(Unknown)나 Z(High-Impedance) 같은 쓰레기 값을 가져선 안 된다.

  5. 무한 대기 방지 (실무/Timeout): 슬레이브가 PREADY를 주지 않고 무한정 버티는 '무한 루프(Infinite length)' 상태에 빠지면 안 된다. (일정 클럭 이상 길어지면 에러 처리!)

3. 이 규칙들을 어디에 코딩해야 할까?

이 부분이 이 강의의 가장 중요한 설계 철학입니다. 규칙을 검사하는 방법에는 두 가지가 있습니다.

방법 A:

  • 모니터(Monitor) 안에 if 문으로 짜기
  • 장점: 코드를 짜기도 쉽고, 나중에 버그가 터졌을 때 디버깅하기도 매우 쉽습니다.

방법 B:

  • 인터페이스(Interface) 안에 SVA(SystemVerilog Assertions)로 짜기
  • 장점: 정형 검증(Formal Verification) 툴에서 이 코드를 그대로 재사용할 수 있어서 엄청나게 효율적입니다.
  • 단점: 코드가 복잡해지고 디버깅이 까다롭습니다. 특히 엄청나게 긴 클럭 사이클을 검사할 때 Formal 툴이 버거워합니다.

강사의 완벽한 실무 타협점:

규칙 1~4번 (짧고 단순한 규칙): 인터페이스 파일(cfs_apb_if.sv) 안에 SVA(Assertion) 문법으로 깔끔하게 구현합니다.

규칙 5번 (수십~수백 클럭을 세어야 하는 규칙): SVA로 짜면 툴이 힘들어하므로, 기존에 우리가 짰던 모니터(monitor)의 관찰 로직 안에 소프트웨어적으로 구현합니다.

이 4가지 파일을 수정
1. cfs_apb_agent_config.sv (스마트 제어판)
2. cfs_apb_if.sv (물리적 인터페이스 구리선)
3. cfs_apb_monitor.sv (CCTV)


SVA

SVA는 SystemVerilog Assertions의 약자입니다. 직역하면 "시스템베릴로그 단언문(규칙 선언)"이라는 뜻인데, 현업 검증(DV) 엔지니어의 시각에서 아주 쉽게 비유하자면 "시간의 흐름을 기억하는 인공지능 CCTV"입니다.

1. SVA의 존재 이유: "시간차(Time) 검사"

일반적인 소프트웨어 프로그래밍의 if문은 '현재 이 순간'의 상태만 검사할 수 있습니다.

일반 if문: "지금 A가 1이야? 그럼 에러!" (순간포착)

하지만 하드웨어 칩은 클럭(심장 박동)에 맞춰 움직입니다. "A 신호가 들어오면, 정확히 3클럭 뒤에 B 신호가 나가야 한다" 같은 '시간차 규칙'이 스펙의 90%를 차지합니다. 이걸 일반 if문으로 짜려면 클럭을 세는 카운터를 만들고, 상태 머신(State Machine)을 돌려야 해서 코드가 엄청나게 지저분해집니다.

SVA 문법: "A가 1이 되면 ➔ 3클럭 뒤에(##3) B가 1이 되는지 지켜봐!" (A |-> ##3 B;)
이렇게 복잡한 시간차 타이밍 다이어그램을 단 한 줄의 코드로 우아하게 검사할 수 있게 해주는 마법의 문법이 바로 SVA입니다.

2. SVA의 3단 구조 (탐지 ➔ 규칙 ➔ 실행)

방금 우리가 cfs_apb_if.sv 파일에서 봤던 코드들이 정확히 이 3단계로 이루어져 있었습니다.

sequence (패턴 탐지기):
"특정 행동 패턴"을 정의합니다.
예: "PSEL이 1이고 이전 클럭에선 0이었던 순간" (setup_phase_s)

property (규칙 제정):
탐지기가 패턴을 찾았을 때, "다음에 무슨 일이 일어나야 하는가?"를 정의합니다.
예: "Setup Phase가 탐지되면 ➔ 다음 클럭(|=>)엔 PENABLE이 1이 되어야 해!" (penable_entering_access_phase_p)

assert (감시 시작):
만들어둔 규칙을 하드웨어에 진짜로 꽂아 넣고 "지금부터 감시 카메라 켜고 위반하면 에러($error) 띄워!"라고 명령하는 실행 버튼입니다.

3. 왜 모니터(Monitor) 대신 SVA를 쓸까요? (Formal Verification)

강사가 영상에서 "이 1~4번 규칙들은 모니터에 안 짜고 인터페이스에 SVA로 짤 거다"라고 했던 가장 큰 이유가 있습니다.

모니터는 시뮬레이터를 쌩쌩 돌려야만 에러를 잡을 수 있는 '소프트웨어 로직'입니다. 하지만 SVA로 작성된 코드는 시뮬레이션을 돌리지 않고도, 수학적인 공식을 이용해 칩 설계도면(RTL) 자체에 버그가 없는지 증명해 내는 정형 검증(Formal Verification) 툴에서 그대로 100% 재사용할 수 있습니다. 하나의 코드로 두 가지 강력한 검증 툴을 모두 쓸 수 있는 가성비 최고의 문법인 것이죠!

4. |->와 |=>의 차이점

이 화살표들을 SVA에서는 Implication Operator (조건부 연산자)라고 부릅니다. 쉽게 말해 "왼쪽 조건(원인)이 발생했을 때만 ➔ 오른쪽 조건(결과)을 검사해라!"라는 뜻이죠. 만약 왼쪽 조건이 발생하지 않으면, 오른쪽은 아예 검사조차 하지 않고 그냥 패스(Pass)해 버립니다.

1. |-> (Overlapping Implication / 동일 클럭 검사)

의미: 조건이 일치한 "바로 그 똑같은 클럭 사이클(Same Clock)"에 오른쪽 결과를 검사합니다.

비유: "상관이 방에 들어오면(왼쪽 조건) ➔ 그 즉시(동시에) 경례해라!(오른쪽 조건)"
언제 쓸까요? 상태가 유지되어야 하거나, 쓰레기 값(X, Z)이 없는지 실시간으로 감시할 때 씁니다.

실제 코드 예시:

// If PSEL is 1, PADDR must not be unknown AT THE SAME CLOCK CYCLE
property unknown_value_paddr_p;
  @(posedge pclk) 
  psel == 1 |-> $isunknown(paddr) == 0; 
endproperty

해석: PSEL이 1이 된 바로 그 찰나의 순간에, 주소(PADDR)에 쓰레기 값이 있는지를 동시에 찰칵! 찍어서 검사합니다.

2. |=> (Non-overlapping Implication / 다음 클럭 검사)

의미: 조건이 일치한 "바로 다음 클럭 사이클(Next Clock)"에 오른쪽 결과를 검사합니다. (사실 이 기호는 |-> ##1을 줄여 쓴 것과 완벽하게 똑같습니다.)

비유: "오늘 주문을 받으면(왼쪽 조건) ➔ 내일(다음 사이클) 배송을 출발해라!(오른쪽 조건)"
언제 쓸까요? APB 프로토콜의 Setup Phase에서 Access Phase로 넘어가는 것처럼, 상태가 변하는 '순차적인 흐름'을 검사할 때 씁니다.

실제 코드 예시:

// If Setup Phase is detected, PENABLE must become 1 IN THE NEXT CLOCK CYCLE
property penable_entering_access_phase_p;
  @(posedge pclk) 
  setup_phase_s |=> penable == 1;
endproperty

해석: 방금 setup_phase_s (PSEL=1, PENABLE=0) 패턴을 감지했다면, 지금 당장 검사하지 말고 클럭이 한 번 더 뛸 때까지 기다렸다가, 그 다음 클럭에 PENABLE이 1로 올라왔는지 확인합니다.

cfs_apb_agent_config.sv

결론적으로 이 config 파일은 설정값만 담고 있는 수동적인 폴더가 아닙니다. 리모컨(Control Panel)입니다. "잘못된 설정값이 들어오면 튕겨내고(Setter), 하드웨어 연결 상태를 점검하며(Start of sim phase), 누군가 시스템을 우회해서 조작하는지 24시간 감시(Run phase)하는 똑똑한 보안 관리자"입니다.
UVM 에이전트 직원들(Driver, Monitor)이 공통으로 지켜야 할 소프트웨어 설정값(규칙)을 담아두는 중앙 제어판입니다.

이 파일에서는 검사를 껐다 켜고, 기준값을 설정하는 '리모컨 버튼'들을 만듭니다.

  • 무엇을 추가했나:
  1. has_checks 변수 (기본값 1)와 Getter/Setter 함수.

  2. stuck_threshold 변수 (기본값 1000)와 Getter/Setter 함수. (Setter에 "최소 2클럭 이상이어야 함"이라는 에러 방지 로직 추가)

  3. set_has_checks 함수 안에서 vif.has_checks = has_checks로 물리적 선과 동기화하는 코드.

  4. run_phase에 핀(vif.has_checks)을 직접 건드리는지 감시하는 무한 루프.

  • 왜 했나 (이유):

    • 지휘관(Test)이 밖에서 "이번엔 에러 검사 꺼!", "타임아웃은 1000클럭 말고 2000클럭으로 해!" 라고 우아하게 조종할 수 있는 창구를 만들어주기 위해서입니다.

    • 특히 Setter에 조건을 걸어두어 사용자가 말도 안 되는 숫자(예: 1클럭 타임아웃)를 넣는 것을 원천 차단하는 똑똑한 리모컨을 만든 것입니다.

vif (Virtual Interface) = 멍청한 'HDMI 케이블'
정체: 하드웨어(Verilog) 세상과 소프트웨어(UVM) 세상을 이어주는 물리적인 구리선 다발입니다.

config (Agent Config) = 똑똑한 '스마트 리모컨'
정체: UVM 에이전트 직원들(Driver, Monitor)이 공통으로 지켜야 할 소프트웨어 설정값(규칙)을 담아두는 중앙 제어판입니다.

협업 스토리

  1. 최고 지휘관(Test): "어이 config 리모컨! 이번 테스트는 타임아웃을 500클럭으로 맞추고, 저기 있는 vif 구리선을 네 몸통에 꽂아놔!"

  2. 에이전트(Agent): (직원들을 모아놓고) "자, 드라이버랑 모니터야! 일할 때 필요한 설정값과 구리선은 모두 이 config 리모컨 안에 들어있으니까, 필요할 때마다 리모컨한테 달라고 해!"

  3. 일꾼(Driver): "엇, 핀을 흔들어야 하네? config님! 구리선(vif) 좀 빌려주세요!" ➔ vif = agent_config.get_vif();

  4. 감시자(Monitor): "엇, 무한 대기 에러를 검사해야 하네? config님! 타임아웃 몇 클럭으로 세팅되어 있어요?" ➔ threshold = agent_config.get_stuck_threshold();

`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;
    
    //Active/Passive control
    local uvm_active_passive_enum active_passive;
    
    //Switch to enable checks
    local bit has_checks;
    
    //Number of clock cycles after which an APB transfer is considered
    //stuck and an error is triggered
    local int unsigned stuck_threshold;

    `uvm_component_utils(cfs_apb_agent_config)
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);
      
      // Set default values when the panel is created
      active_passive  = UVM_ACTIVE;
      has_checks      = 1;
      stuck_threshold = 1000;

    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;
        
        set_has_checks(get_has_checks());
      end
      else begin
        `uvm_fatal("ALGORITHM_ISSUE", "Trying to set the APB virtual interface more than once")
      end
    endfunction
    
    //Getter for the APB Active/Passive control
    virtual function uvm_active_passive_enum get_active_passive();
      return active_passive;
    endfunction
    
    //Setter for the APB Active/Passive control
    virtual function void set_active_passive(uvm_active_passive_enum value);
      active_passive = value;
    endfunction
    
    //Getter for the has_checks control field
    virtual function bit get_has_checks();
      return has_checks;
    endfunction
    
    // Example 2: Smart Setter for Checkers
    virtual function void set_has_checks(bit value);
      has_checks = value;
      
      if(vif != null) begin
        vif.has_checks = has_checks;
      end
    endfunction
    
    //Getter for the stuck threshold
    virtual function int unsigned get_stuck_threshold();
      return stuck_threshold;
    endfunction
    
  // Example 1: Smart Setter for Timeout
    virtual function void set_stuck_threshold(int unsigned value);
      if(value <= 2) begin
        `uvm_error("ALGORITHM_ISSUE", $sformatf("Tried to set stuck_threshold to value %d but the minimum length of an APB transfer is 2", value))
      end
      
      stuck_threshold = value;
    endfunction
    
    // 1. Pre-flight Check (start_of_simulation_phase)
    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
    
    // 2. Real-time Security Camera (run_phase)
    virtual task run_phase(uvm_phase phase);
      forever begin
        @(vif.has_checks);
        
        if(vif.has_checks != get_has_checks()) begin
          `uvm_error("ALGORITHM_ISSUE", $sformatf("Can not change \"has_checks\" from APB interface directly - use %0s.set_has_checks()", get_full_name()))
        end
      end
    endtask
    
  endclass

`endif

왜 config 파일에 타이핑을 할까요?

  1. has_checks: "일부러 에러를 내고 싶을 때(Negative Test) 끄기 위해"
  • 보통은 칩이 스펙을 잘 지키는지 검사해야 하지만, 검증을 하다 보면 "일부러 이상한 쓰레기 값을 칩에 쑤셔 넣었을 때 칩이 어떻게 반응하는지" 테스트해야 할 때가 있습니다. (이를 Negative Testing이라고 합니다.)

  • 만약 인터페이스에 검사 로직이 항상 강제로 켜져 있다면? 우리가 일부러 이상한 값을 넣자마자 인터페이스가 "삐빅! 스펙 위반입니다!" 하고 시뮬레이션을 터뜨려 버릴 겁니다.

  • 그래서 지휘관(Test)이 config.set_has_checks(0) 버튼을 눌러서 인터페이스의 감시 카메라를 잠시 꺼둘 수 있도록 스위치를 만들어 둔 것입니다.

  1. stuck_threshold: "테스트마다 타임아웃 기준을 바꾸기 위해"
  • 5번 규칙(무한 대기 방지)을 검사할 때, 대체 몇 클럭을 기다려야 '에러'로 판정할까요?

  • 어떤 테스트에서는 100클럭만 넘어도 에러로 치고 싶고, 어떤 복잡한 칩을 연결한 테스트에서는 5000클럭까지는 기다려주고 싶을 수 있습니다.

  • 만약 모니터 코드 안에 if (delay > 1000) 이라고 하드코딩해버리면 나중에 수정하기가 너무 힘듭니다. 그래서 숫자 설정(Threshold)은 config에서 리모컨으로 받고, 모니터는 그 숫자만 쏙 빼가서 검사(Check)만 하도록 분업한 것입니다.

// Setter for the has_checks control field
virtual function void set_has_checks(bit value);
  has_checks = value; // 1. Update the config switch
  
  if(vif != null) begin
    vif.has_checks = has_checks; // 2. Pass the switch down to the Physical Interface!
  end
endfunction

지휘관이 config의 스위치를 조작하면, config가 물리적인 하드웨어 인터페이스(vif) 쪽에 선을 연결해서 "야! 검사 기능 켜(또는 꺼)!" 라고 명령을 하달(Pass down)하고 있습니다.

Test (지휘관): "이번 테스트는 엄격하게 갈 거야. 검사 기능(has_checks) 켜고, 타임아웃(stuck_threshold)은 500클럭으로 세팅해!"

Config (리모컨): "옙! 설정값 저장했습니다. 인터페이스한테도 전달할게요!"

Interface & Monitor (실무자): "config님이 세팅해 주신 기준값에 맞춰서 버스 핀들을 빡세게 검사(Check)하겠습니다!"

2. cfs_apb_if.sv

이 파일이 이번 강의의 진짜 주인공입니다! 짧고 명확한 규칙 1~4번을 하드웨어 핀에 직접 SVA(SystemVerilog Assertions) 문법으로 심어둡니다.

무엇을 추가했나:
1. 하드웨어용 has_checks 변수 선언 및 초기화.
2. sequence setup_phase와 sequence access_phase 정의. (PSEL과 PENABLE 타이밍을 묶어둔 조건문)
3. 규칙 1~4번 SVA 코드 작성:

  • PENABLE이 두 번째 사이클에 1이 되는지 확인.
  • 통신이 끝나면 PENABLE이 0이 되는지 확인.
  • 통신 중(Access Phase)에 주소(PADDR), 방향(PWRITE), 데이터(PWDATA)가 중간에 바뀌지 않고 유지($stable)되는지 확인.
  • 어떤 핀에도 쓰레기 값(X나 Z)이 들어오지 않는지($isunknown) 확인.

왜 했나 (이유):

  • 이 짧은 규칙들은 하드웨어 핀 바로 옆에 지뢰처럼 심어두는 게 가장 반응 속도도 빠르고 정확하기 때문입니다.

  • 강사의 꿀팁: 여기서 에러를 띄울 때 UVM 매크로(uvm_error) 대신 SystemVerilog 기본 문법인 $error를 썼습니다. 나중에 정형 검증(Formal Verification) 툴을 돌릴 때 UVM 패키지가 꼬이는 걸 방지하기 위한 엄청난 실무 노하우입니다!

 `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 16
  `endif

  interface cfs_apb_if(input pclk);
    
    // Physical pins
    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;
    
    //Switch to enable checks
    bit has_checks;
    
    initial begin
      has_checks = 1;
    end
    
    sequence setup_phase_s;
      (psel == 1) && (($past(psel) == 0) || ($past(psel) == 1 && $past(pready) == 1));
    endsequence
      
    sequence access_phase_s;
      (psel == 1) && (penable == 1);
    endsequence
      
    
    
    
    property penable_at_setup_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      setup_phase_s |-> penable == 0;
    endproperty
    
    PENABLE_AT_SETUP_PHASE_A : assert property(penable_at_setup_phase_p) else
      $error("PENABLE at \"Setup Phase\" is not equal to 0");
    
      
      
      
      
    property penable_entering_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      setup_phase_s |=> penable == 1;
    endproperty
    
    PENABLE_ENTERING_SETUP_PHASE_A : assert property(penable_entering_access_phase_p) else
      $error("PENABLE when entering \"Access Phase\" did not became 1");
      
      
      
      
      
    property penable_exiting_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      access_phase_s and (pready == 1) |=> penable == 0;
    endproperty
    
    PENABLE_EXITING_SETUP_PHASE_A : assert property(penable_exiting_access_phase_p) else
      $error("PENABLE when exiting \"Access Phase\" did not became 0");
      
      
      
      
      
    property penable_stable_at_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      access_phase_s |-> penable == 1;
    endproperty
    
    PENABLE_STABLE_AT_ACCESS_PHASE_A : assert property(penable_stable_at_access_phase_p) else
      $error("PENABLE was not stable during \"Access Phase\"");
      
    property pwrite_stable_at_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      access_phase_s |-> $stable(pwrite);
    endproperty
    
    PWRITE_STABLE_AT_ACCESS_PHASE_A : assert property(pwrite_stable_at_access_phase_p) else
      $error("PWRITE was not stable during \"Access Phase\"");
      
    property paddr_stable_at_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      access_phase_s |-> $stable(paddr);
    endproperty
    
    PADDR_STABLE_AT_ACCESS_PHASE_A : assert property(paddr_stable_at_access_phase_p) else
      $error("PADDR was not stable during \"Access Phase\"");
      
    property pwdata_stable_at_access_phase_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      access_phase_s and (pwrite == 1) |-> $stable(pwdata);
    endproperty
    
    PWDATA_STABLE_AT_ACCESS_PHASE_A : assert property(pwdata_stable_at_access_phase_p) else
      $error("PWDATA was not stable during \"Access Phase\"");
      
      
      
    
      
    property unknown_value_psel_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      $isunknown(psel) == 0;
    endproperty
    
    UNKNOWN_VALUE_PSEL_A : assert property(unknown_value_psel_p) else
      $error("Detected unknown value for APB signal PSEL");

    property unknown_value_penable_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 |-> $isunknown(penable) == 0;
    endproperty
    
    UNKNOWN_VALUE_PENABLE_A : assert property(unknown_value_penable_p) else
      $error("Detected unknown value for APB signal PENABLE");

    property unknown_value_pwrite_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 |-> $isunknown(pwrite) == 0;
    endproperty
    
    UNKNOWN_VALUE_PWRITE_A : assert property(unknown_value_pwrite_p) else
      $error("Detected unknown value for APB signal PWRITE");

    property unknown_value_paddr_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 |-> $isunknown(paddr) == 0;
    endproperty
    
    UNKNOWN_VALUE_PADDR_A : assert property(unknown_value_paddr_p) else
      $error("Detected unknown value for APB signal PADDR");

    property unknown_value_pwdata_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 && pwrite == 1 |-> $isunknown(pwdata) == 0;
    endproperty
    
    UNKNOWN_VALUE_PWDATA_A : assert property(unknown_value_pwdata_p) else
      $error("Detected unknown value for APB signal PWDATA");

    property unknown_value_prdata_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 && pwrite == 0 && pready == 1 && pslverr == 0 |-> $isunknown(prdata) == 0;
    endproperty
    
    UNKNOWN_VALUE_PRDATA_A : assert property(unknown_value_prdata_p) else
      $error("Detected unknown value for APB signal PRDATA");

    property unknown_value_pready_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 |-> $isunknown(pready) == 0;
    endproperty
    
    UNKNOWN_VALUE_PREADY_A : assert property(unknown_value_pready_p) else
      $error("Detected unknown value for APB signal PREADY");

    property unknown_value_pslverr_p;
      @(posedge pclk) disable iff(!preset_n || !has_checks)
      psel == 1 && pready == 1 |-> $isunknown(pslverr) == 0;
    endproperty
    
    UNKNOWN_VALUE_PSLVERR_A : assert property(unknown_value_pslverr_p) else
      $error("Detected unknown value for APB signal PSLVERR");
      
    

  endinterface

`endif

1. 기초 공사: 핀 선언과 리모컨 스위치

interface cfs_apb_if(input pclk);
  logic preset_n;
  logic paddr, pwrite, psel, penable ... // Physical pins

  // Switch to enable checks
  bit has_checks;
  initial begin has_checks = 1; end

일반적인 하드웨어 핀들을 선언합니다.

아까 우리가 그토록 열심히 세팅했던 그 has_checks 스위치가 여기 있습니다. 시뮬레이션이 켜지면(initial) 기본적으로 1(검사 켜짐)로 세팅됩니다.

2. 타겟 포착용 탐지기: sequence

SVA에서는 어떤 특정 타이밍을 잡아내기 위해 sequence라는 탐지기를 만듭니다.

sequence setup_phase_s;
  (psel == 1) && (($past(psel) == 0) || ($past(psel) == 1 && $past(pready) == 1));
endsequence

sequence access_phase_s;
  (psel == 1) && (penable == 1);
endsequence

$past(신호): "이전 클럭 사이클에서의 값"을 가져오는 SVA 전용 타임머신 함수입니다.

Setup Phase 탐지기 (setup_phase_s):

  • 현재 PSEL이 1인데,
  • (1) 이전 클럭에서는 PSEL이 0이었거나 (일반적인 통신 시작)
  • (2) 이전 클럭에서도 PSEL이 1이었고 PREADY도 1이었다면 (쉬지 않고 연속으로 2번 통신하는 Back-to-Back 상황)
  • "아하! 지금이 딱 Setup Phase(첫 번째 사이클)구나!" 하고 탐지합니다.

Access Phase 탐지기 (access_phase_s): PSEL도 1이고 PENABLE도 1이면 Access Phase(두 번째 이후 사이클)로 아주 쉽게 탐지합니다.

3. 1~2번 규칙: PENABLE 타이밍 검사

property penable_entering_access_phase_p;
  @(posedge pclk) disable iff(!preset_n || !has_checks) // Reset or Checks OFF? Then ignore!
  setup_phase_s |=> penable == 1;
endproperty
  • disable iff(...): 리셋이 눌려있거나, 리모컨 스위치(has_checks)가 꺼져있으면 이 검사는 무시하라는 안전장치입니다.
  • |-> (Overlapping): 탐지기가 반응한 "바로 그 똑같은 클럭 사이클"에 우측 조건이 맞는지 검사합니다.
  • |=> (Non-overlapping): 탐지기가 반응한 "바로 다음 클럭 사이클"에 우측 조건이 맞는지 검사합니다.
  • 해석: "Setup Phase가 탐지되었다면 ➔ 다음 클럭(|=>)에는 반드시 PENABLE이 1이 되어야 한다!"

4. 3~4번 규칙: 안정성 및 쓰레기 값 검사

이 부분은 SVA가 제공하는 아주 강력한 내장 함수 두 개를 사용합니다.

// 1. $stable (값이 안 변하고 유지되는가?)
property paddr_stable_at_access_phase_p;
  ...
  access_phase_s |-> $stable(paddr); 
endproperty

// 2. $isunknown (X나 Z 같은 쓰레기 값인가?)
property unknown_value_paddr_p;
  ...
  psel == 1 |-> $isunknown(paddr) == 0;
endproperty

$stable(신호): "너 방금 전이랑 값 똑같아?"

작동 원리: 이 함수가 호출되는 현재 클럭의 신호 값이, 바로 직전 클럭(1 cycle 전)의 값과 완벽하게 똑같은지(유지되었는지) 검사합니다.

내부 로직: 사실 이 함수는 $past(신호) == 현재_신호 와 완벽하게 똑같은 동작을 합니다. 그걸 쓰기 편하게 포장해 놓은 것입니다.

property paddr_stable_at_access_phase_p;
  @(posedge pclk) 
  // If we are in access phase, PADDR must be exactly the same as the previous clock
  access_phase_s |-> $stable(paddr); 
endproperty

$isunknown(신호): "혹시 쓰레기 값(X, Z) 섞여 있어?"

작동 원리: 검사하려는 신호(단일 비트든 32비트 버스든 상관없음) 안에 단 1비트라도 X (Unknown, 0인지 1인지 모름) 또는 Z (High-Impedance, 선이 끊어짐) 값이 섞여 있으면 1(True)을 반환합니다.

사용법: 우리는 에러가 "없길" 바라기 때문에, 보통 isunknown(신호)==0(또는!isunknown(신호) == 0 (또는 !isunknown(신호)) 형태로 사용해서 "쓰레기 값이 0개여야 한다(즉, 모두 깔끔한 0이나 1이어야 한다)"라고 검사합니다.

property unknown_value_paddr_p;
  @(posedge pclk) 
  // If PSEL is active, PADDR must not contain any X or Z values
  psel == 1 |-> $isunknown(paddr) == 0; 
endproperty

왜이렇게 property가 많나?

스나이퍼식 디버깅: "정확히 어디가 아픈지 알고 싶다!"
만약 이 수많은 property를 하나로 합쳐서 거대한 검사기를 하나 만들었다고 상상해 볼까요?

나쁜 예 (통짜 Property): "통신 중에 신호들이 다 안정적이고, 쓰레기 값도 없어야 해!"

결과: 에러가 떴을 때 로그 창에 "규칙 위반 발생!" 이라고만 뜹니다. 그럼 엔지니어는 수백 가닥의 핀 파형(Waveform)을 눈이 빠져라 뒤지면서 대체 주소(PADDR)가 흔들린 건지, 데이터(PWDATA)에 쓰레기 값이 낀 건지 일일이 찾아야 합니다.

좋은 예 (잘게 쪼갠 Property): 강사처럼 핀 하나, 상황 하나마다 property를 다 따로 만듭니다.

결과: 에러가 뜨면 로그 창에 "에러: Access Phase 동안 PADDR(주소) 값이 흔들렸음!" 또는 "에러: PENABLE 핀에 쓰레기 값(X)이 들어왔음!" 이라고 아주 정확하게 범인을 지목해 줍니다. 디버깅 시간이 10시간에서 10분으로 줄어드는 기적을 맛보게 됩니다.

3. cfs_apb_monitor.sv (CCTV)

이 파일에서는 인터페이스가 하기 버거운 5번 규칙(타임아웃 검사)을 처리합니다.

무엇을 추가했나:

  • collect_transaction 태스크 안에서 슬레이브의 응답(PREADY)을 기다리며 클럭을 세는 부분에 if (item.length > agent_config.get_stuck_threshold()) 에러 발생 로직 추가.

왜 했나 (이유):

  • "1000클럭 동안 아무 응답이 없다" 같은 무식하게 긴 대기 시간을 SVA(인터페이스)로 검사하면 시뮬레이터나 검증 툴이 엄청나게 버벅거립니다.
  • 그래서 클럭을 하나씩 세는 소프트웨어 카운터(Monitor)가 리모컨(Config)에서 타임아웃 기준값을 몰래 가져와서 직접 검사하게 만든 것입니다. (완벽한 역할 분담!)
`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++;
        
        if(agent_config.get_has_checks()) begin
          if(item.length >= agent_config.get_stuck_threshold()) begin
            `uvm_error("PROTOCOL_ERROR", $sformatf("The APB transfer reached the stuck threshold value of %0d", item.length))
          end
        end
      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

핵심 추가: Rule #5 "무한 대기(Stuck) 검출기"

가장 눈여겨봐야 할 곳은 슬레이브의 응답(PREADY)을 기다리는 while 루프 안쪽입니다.

while(vif.pready !== 1) begin
  @(posedge vif.pclk);
  item.length++; // Increment clock counter
  
  // 1. Check if the "remote control" switch is ON
  if(agent_config.get_has_checks()) begin
    
    // 2. Did we wait too long? (e.g., length >= 1000)
    if(item.length >= agent_config.get_stuck_threshold()) begin
      `uvm_error("PROTOCOL_ERROR", $sformatf("The APB transfer reached the stuck threshold value of %0d", item.length))
    end
    
  end
end
  • 이전 코드: 슬레이브가 PREADY를 안 주면 모니터는 영원히 while 루프에 갇혀서 시뮬레이션이 멈춰버렸습니다 (Deadlock).

  • 변경된 코드: 매 클럭마다 카운터(item.length)를 올리면서, 우리가 config 리모컨에 세팅해 둔 한계치(stuck_threshold, 예: 1000클럭)를 넘었는지 확인합니다.

한계치를 넘는 순간 가차 없이 uvm_error를 터뜨려서 "야! 슬레이브가 1000클럭째 대답을 안 해! 칩 뻗었어!"라고 강력하게 보고합니다.

스마트 리모컨(config)의 완벽한 활용

코드 곳곳에 agent_config.get_xxx() 함수들이 쓰인 것을 볼 수 있습니다.

  • agent_config.get_vif(): 구리선(물리적 핀) 가져오기
  • agent_config.get_has_checks(): 검사 기능 켜짐/꺼짐 상태 확인
  • agent_config.get_stuck_threshold(): 타임아웃 기준값 확인

이전에 우리가 "왜 귀찮게 설정 파일에 Getter/Setter를 만드냐"고 했던 이유가 바로 여기서 빛을 발합니다. 모니터는 자기가 직접 숫자를 하드코딩하지 않고, 무조건 중앙 통제실(config)의 지시를 따르도록 완벽하게 모듈화되어 있습니다.

정보의 수집과 방송 (Broadcast)

데이터를 모으는 순서도 APB 하드웨어 타이밍과 완벽하게 일치합니다.

  1. Setup Phase: PSEL이 뜨자마자 주소(addr), 방향(dir), 쓰기 데이터(pwdata)를 훔쳐 옵니다.

  2. Access Phase: PREADY가 뜰 때까지 기다렸다가(무한 대기 방지 포함), 에러 여부(response)와 읽기 데이터(prdata)를 마저 챙깁니다.

  3. Broadcast: 모든 빈칸이 채워진 완벽한 보고서(item)를 output_port.write(item);를 통해 채점기(Scoreboard)를 향해 힘껏 던집니다!

profile
RTL, FPGA Engineer

0개의 댓글