본문 바로가기

카테고리 없음

[UVM] UVM subscriber의 개념 잡기

 

Observer Design Pattern 

* observer = 관찰자 

UVM subscribe의 개념은 "Observer Design Pattern"(Twitter Design Pattern) 이라는 디자인패턴으로부터 출발한다.

 

디자인 패턴이란,  소프트웨어 개발 과정에서 빈번히 발생하는 문제들에 대한 일반적인 해결책들을 분류해놓은 것이다.

 

( 정형화된 문제 해결 방식? 정형화된 코딩 패턴? 정도로 생각하면 된다. )

 

 

Observer Design Pattern은 Twitter Design Pattern으로도 불리는데, 

트위터에서 트윗을 전송하면 당신의 팔로워들이 그 트윗을 볼 수 있다는 점이 이 패턴의 동작 방식과 유사하다. 

 

일단 유저가 트윗을 보내면, 해당 유저의 팔로워가 누군지, 팔로워들이 해당 트윗으로 뭔 짓거리를 하는지 알 필요도 없고 신경쓸 필요도 없다.

 

심지어 팔로워가 없어도 된다.

 

당신은 그냥 쓰고 (write) 엔터만 누르면 된다...

 

 

 

 

Observer Design Pattern도 똑같다.

Object가 Data를 create 하면, 그 data를 세상과 공유할 수 있다.

얼마나 많은 follower(observer)가 있던 간에!!

 

 

모든 observer들이 동일한 data copy를 가지며, 그 data copy를 각자 입맛에 맞게 사용함.

난 그냥 data tweet을 날릴 뿐! data의 사용은 각 observer(subscriber) 손에 달려있다..

 

UVM Subscriber

UVM에서는 Observer를 Subscriber라고 부른다. 

 

 

 

백문이 불여일견 예제

UVM/OOP 클래스를 사용하여 다음과 같은 과제를 한다고 가정하자.

문제 : 2개의 정육면체 주사위를 20번 굴리고 다음을 프린트하는 프로그램을 작성하라

1) 주사위의 평균값

2) 각 주사위 눈 빈도수 히스토그램

3) 모든 가능한 값인 2~12를 도달했는지 대한 coverage report

 

목표 : Object Oriented Programming Techniques를 지향하여 작성한다.

 --> OOP 정신 : 각 object는 한가지 일만 한다. 어떠한 맥락에서도 손쉽게 재사용 가능해야 한다.

 

문제 해결 접근법 with oop & uvm mindset

- 위에서 요구되는 3개 report를 전달할 3개 uvm_component를 생성한다.

(다만 여기 예제에서는 average class만 살펴본다. 나머지는 생략스~)

 

- Average Class는 UVM Component이며, write()라는 method를 통해 data를 모은다.

그리고 report_phase()라는 phase method를 통해 end_of_simulation에서 결과를 print 함.

 

observer design pattern을 모를 때 ! 

class average extends uvm_component;
    `uvm_component_utils(average);
    
    protected real dice_total;
    protected real count; // protected 는 다른 user들의 direct access를 막는다.
    
    function new ( string name, umv_parent = null);
    	super.new(name, parent);
        dice_total = 0.0;
        count= 0.0;
    endfunction : new
    
 ...
 ...
endclass :average

 

위와 같은 average class를 구현할 수 있다.

average class의 write() 함수를 구현해보자. 

write는 주사위를 굴려서 나온 두 눈을 avg class가 받는 부분이다.

 

...
function void write( int t );
	dice_total = dice_total + t;
    count ++;
endfunction : write
...

function void report_phase(uvm_phase phase);
    $display("DICE AVERAGE : %2.1f", dice_total/count);
endfunction : report_phase


endclass : average

 

program이 주사위 눈t를 인자로 write를 호출하면 

avg class는 dice_total에 차곡차곡 더한다.

 

그리고 report_phase에서 average 값을 display로 출력한다.

(report_phase는 simulation이 complete됐을 때 UVM이 call하는 메소드)

 

 

 

histogram과 coverage class도 위 average class와 같은 방식으로정의한다. (write() 와 report_phase()를 각각 가짐. write는 역시나 주사위를 던지는 놈이 호출한다. ) 

 

Dice Test **

testbench에서는 앞서 정의한 object들을 선언하고 연결한다.

class dice_test extends uvm_test;
    `uvm_component_utils(dice_test);
    
    dice_roller dice_roller_h;
    coverage    coverage_h;
    histogram   histogram_h;
    average     average_h;
    
    // test 에 필요한 object들을 instantiation
    function void build_phase(uvm_phase phase);
        coverage_h    = new("coverage_h", this);
        histogram_h   = new("histogram_h", this);
        average_h     = new("average_h", this);
        dice_roller_h = new("dice_roller_h", this);
    endfunction : build_phase
    
    // test를 invoke
    task run_phase(uvm_phase phase);
        int the_roll;
        phase.raise_objection(this);
        repeat(20) begin
            the_roll = dice_roller_h.two_dice();
            coverage_h.write(the_roll);
            histogram_h.write(the_roll);
            average_h.write(the_roll);
        end
        phase.drop_objection(this);
    endtask : run_phase 
    
    ...

 

* run_phase 부분을 유심히 보자.. 위 코드는 만점이 아니다. 80점짜리 코드이다.

the_roll을 coverage, histogram, average에 write 하는 부분이 그저 script에 지나지 않음.

당신이 test하고자 하는 항목이 달라질 때마다 수정해서 나열해줘야 하는 부분.

우리는 이 부분을 조금 더  추상화할 수 있다.

한가지만 잘하는 class를 만들고, top level에서는 그냥 그 class들을 한 데 모아 한 번만 실행하는 것이 OOP 정신이다.

 

 

UVM에서는 object connection을 위해서 observer design pattern과 UVM anlaysis port를 활용한다.

 

Observer Design Pattern in UVM

UVM은 observer design pattern을 쉽게 구현할 수 있도록 다음 두 개의 class를 제공해준다.

1. uvm_analysis_port

   object가 subscribers에게 data를 전송할 수 있도록 한다.

2. uvm_subscriber

   uvm_component를 상속받음

   uvm_analysis_port를 subscribe(observe)할 수 있다.

 

 

uvm_analysis_port

- data를 subscribers에 전송할 수 있다.

- analysis port에 연결될 수 있는 subscribers 의 수에 대한 제한은 없다.

- analysis port를 사용하기 위한 3-step process

1) analysis port variable들을 선언, 그리고 그 port를 통해 전송할 data의 type도 선언

2) build phase에서 analysis port를 instantiate

3) write() method를 사용해 anaysis port에 data를 쓴다.

 

analysis port가 write를 호출한다

subscriber들은 write에 대한 action을 구현한다.

port에서 한 번 data를 write()하면 이에 연결된 모든 subscriber에 전송된다.

 

- uvm_analysis_port에는 write method뿐만 아니라 connect()라는 메소드도 있다.

subscribers를 port에 연결하기 위해서 쓴다.

 

 

uvm_subscriber

- uvm_subscriber는 uvm_component를 extends하며, analysis port에 연결되도록 도와준다.

- subscriber 클래스는 analysis_export라는 object를 건네준다. ( uvm_component를 uvm_subscriber로 extend함으로써 이런 기능이 구현됨)

- subscriber 클래스는 전송받은 data를 다루는 write를 구현하도록 요구된다.

 

 

 

subscriber 구현하기.

앞선 예제인 Dice Rolling 테스트벤치에서는 3개 subscriber가 있다.

- average, coverage, histogram

 

average class를 uvm_subscriber로 작성해보자. (나머지도 방식은 똑같다.)

class average extends uvm_subscriber #(int);
    `uvm_component_utils(average);
    real dice_total;
    real count;
    
    function void write(int t);
        dice_total = dice_total ++;
        count ++;
    endfunction
    
    ...

- 이제 average class는 uvm_subscriber 클래스를 extend한다.

- uvm subscriber 클래스는 parameterize 되어 있다.

uvm subscriber는 우리가 다뤄야할 data의 type을 제공해주길 요구함.

write() 메소드는 전과 똑같음. ( observer pattern 미사용 예제에서 구현했던 것!! )

또한 uvm_subscriber를 extend할 때 썼던 parameter와 같은 type 즉 int t를 argument로 취한다.

 

 

 

이제 dice roller를 수정해서 object들을 연결해보자!!

Dice Roller 내부에서 uvm_analysis_port 사용하기.

- observer pattern의 트위터같은 동작을 다시 살펴보자.

- 한 object는 data를 write하고 다른 subscriber object들은 data 카피들을 얻어감.

dice_roller 클래스에서 uvm_analysis_port를 instantiate하여 observer pattern을 구현해보자.

class dice_roller extends uvm_component;
    `uvm_component_utils(dice_roller);
    uvm_analysis_port #(int) roll_ap;
    
    function void build_phase(uvm_phase phase);
        roll_ap = new("roll_ap", this);
    endfunction : build_phase
    
    ...

 

analysis port를 홀드할 variable인 roll_ap를 선언했다. ( 왜 홀드라는 표현을 썼냐면.. 포인터 핸들이라고 생각해서)

#(int) --> analysis port로 전송할 data가 int이므로 int type으로 parametrize한다.

그리고 이렇게 선언한 roll_ap라는 analysis port를 build_phase에서 instantiate한다.

 

 

 

이렇게 analisys port를 선언 & 객체화 했다면.. 다음 step은 !!

주사위를 굴리고, 무한정 object들에게 결과를 써주기!! ( observer pattern에서 data를 쓰는 녀석은 구독자가 몇명인지 누구인지 노상관!! )

 

task run_phase( uvm_phase phase);
   int the_roll;
   phase.raise_objection(this);
   
   void`(randomize());
   repeat(20) begin
       void`(randomize());
       the_roll = die1 + die2;
       roll_ap.write(the_roll); // analysis port의 write를 호출하면 subscriber들의 write()들이 호출된다.
   end
   
   phase.drop_objection(this);
   
endtask : run_phase

 

analysis port의 write()를 한 번만 호출하면 된다!

그러면 ap는 subscriber들 안에 정의된 write()를 호출하고 data를 건내준다.

--> 이것이 analysis port의 미덕

 

검증자는, 어떤 오브젝트가 data를 사용하는지.. 구체적인 detail을 신경쓰지 않아도 됨.

코드 꼬임, 오류를 방지할 수 있다.

 

 

connect_phase()

앞서 말했듯이 observer pattern을 twitter에 비유할 수 있다!

오브젝트가 어떤 데이터를 트랙킹하기 위해 다른 오브젝트를 "팔로우"함

 

위에서는 여러 구독자(UVM subscribers)를 갖는 analysis port로 "data source"를 구현해봤다.

이제는 그 data source를 팔로우하는(subscribe하는) UVM subscriber를 만들어보자!

(쉽게말해 analysis port를 구현했고, 이제 subscriber 에서 팔로잉 로직을 구현해보자)

 

 

이러한 과정을 connecting object라고 한다.

이를 위해 UVM은 phase method를 제공한다. (여러 오브젝트들 간의 싱크를 맞춘다!)

 

cf ) build phase 에서는  오브젝트들을 top down으로 instantiate하고, connect_phase에서는 bottom - up으로  connect_phase()를 콜하여 오브젝트들을 연결한다.

 

Connect Process는 2 부분으로 나눌 수 있다.

 

- uvm_subscriber들은 내부에 analysis_export라는 오브젝트를 갖고 있다. ( uvm_subscriber를 extend하면 이 오브젝트는 알아서 instantiate된다.)

- uvm_analysis_port는 connect()라는 메소드를 제공한다.

- analysis_port는 connect() 메소드를 호출한다. 이때 subscriber의 analysis_export를 필요로 함. ( subscriber가 팔로우하는거임 ) Subscriber들은 analysis_export 오브젝트를  analysis_port에게 건네준다.

 

예시 코드를 보면 이해가 더 쉽다.

이제 dice_test는 subscribers를 dice roller에 연결하기 위한 connect_phase()를 가짐

 

class dice_test extends uvm_test;

    `uvm_component_utils(dice_test);
    dice_roller dice_roller_h;
    coverage    coverage_h;
    histogram   histogram_h;
    average     average_h;
    
    ...
    
    function void connect_phase( uvm_phase phase );
        dice_roller_h.roll_ap.connect(coverage_h.analysis_export);
        dice_roller_h.roll_ap.connect(histogram_h.analysis_export);
        dice_roller_h.roll_ap.connect(average_h.analysis_export)
    endfunction : connect_phase
    
endclass : dice_test

 

- 우리가 가진 3 개 subscribers는 analysis_export라는 object를 갖는다.

- 그리고 dice_roller_h 오브젝트 안에 하나의 analysis port를 갖는다.

- 각 subscriber는 자신의 analysis export 오브젝트를 analysis port의 connect를 호출할 때 넘겨준다. 

 

 

 

요약

이 포스트에서는 UVM에서 data object를 공유하는 방법인 observer design pattern, 즉 UVM subscriber와 analysis port의 사용에 대해 공부했다.

UVM에서 검증자는 여러 object들을 구현하고, 그 object들 사이의 연결관계만을 기술한다.

각 object들은 uvm phase에 따라서 알아서 상호작용하며 동작한다.

바로 이런 점에서 observer design pattern 구현 전/후 의미가 달라진다.

아주 oop 스러운 문제 접근 방법이다.

 

이런 tb에서는 새로운 statistics를 프로그램에 추가하고 싶을 때, 다른 subscriber를 구현하고 원래 TB env에서 새롭게 connect만 하면 된다. --> 어느 시점에 이 객체의 write()를 호출해야할지 직접 수정해줄 필요가 없다.