APB sequence code

Seungyun Lee·2026년 3월 15일

UVM

목록 보기
11/14

작성하신 5개의 파일은 완벽하게 '조감독 1명'과 '대본 4세트'로 나뉩니다. 이 5개 파일의 계층 구조(족보)와 각각의 역할을 직관적인 다이어그램으로 먼저 보여드릴게요.

APB Sequence 5형제 구조도

[ UVM Components ] (시뮬레이션 내내 살아있는 정규직)
      │
      └── cfs_apb_sequencer.sv (조감독)


[ UVM Objects ] (쓰고 버리는 대본들 - 상속 관계)
      │
      └── cfs_apb_sequence_base.sv (기본 대본 뼈대)
                │
                ├── cfs_apb_sequence_simple.sv (단순 동작 대본)
                │
                ├── cfs_apb_sequence_rw.sv     (특정 주소 읽기/쓰기 대본)
                │
                └── cfs_apb_sequence_random.sv (무작위 트래픽 융단폭격 대본)

파일별 완벽 역할 해부

1. cfs_apb_sequencer.sv (조감독)

  • 역할: 대본(Sequence)과 일꾼(Driver) 사이에서 택배 상자를 전달해 주는 중개자입니다.

  • 특징: 이 파일은 코드가 텅텅 비어있을 겁니다. 왜냐하면 UVM에서 기본 제공하는 uvm_sequencer가 이미 전달자 역할을 완벽하게 수행하기 때문에, 이름표만 달아주고 따로 기능을 추가할 필요가 거의 없기 때문입니다.

2. cfs_apb_sequence_base.sv (모든 대본의 부모 / 뼈대)

  • 역할: 아래 3개의 자식 대본들이 공통으로 쓸 기능들을 모아두는 곳입니다.

  • 특징: 앞서 우리가 배웠던 그 마법의 매크로, `uvm_declare_p_sequencer가 바로 여기에 들어갑니다. 이 뼈대만 잘 만들어두면 자식 대본들은 무전기를 업그레이드하는 코드를 매번 칠 필요가 없습니다.

3. cfs_apb_sequence_simple.sv (단순 대본 - Sanity Check 용)

  • 역할: "통신망이 뚫렸는지 아이템 딱 1개만 던져보자!" 할 때 쓰는 가장 기본적인 대본입니다.

  • 특징: 복잡한 조건 없이 `uvm_do(req) 하나만 달랑 호출해서 시스템이 정상적으로 돌아가는지 확인하는 용도입니다.

4. cfs_apb_sequence_rw.sv (지시형 대본 - Directed Test 용)

  • 역할: "무작위 랜덤 말고, 내가 딱 지정한 'A 주소'에 'B 데이터'를 직접 써라!"라고 콕 집어서 명령할 때 쓰는 대본입니다.

  • 특징: 하드웨어의 특정 레지스터를 세팅하거나 초기화할 때 유용합니다. 대본 안에 주소(addr)와 데이터(data) 변수를 밖에서 입력받을 수 있게 열어둡니다.

5. cfs_apb_sequence_random.sv (랜덤 폭격 대본 - Random Test 용)

  • 역할: "루프 100번 돌리면서 무작위 주소와 데이터로 하드웨어를 마구마구 때려봐!"라고 지시하는 대본입니다.

  • 특징: 칩의 예상치 못한 버그를 잡기 위해 for 루프와 soft 제약 조건(constraint)을 섞어서 극한의 상황을 만들어내는 실무 검증의 핵심 무기입니다.

실전 역추적 예시 (Base Sequence)

가장 뼈대가 되는 base 파일의 형태를 머릿속에 넣어두시면 구조가 훨씬 명확해집니다.

// Inherit from the standard UVM sequence, parameterized with our APB item
class cfs_apb_sequence_base extends uvm_sequence #(cfs_apb_item_drv);

  // Register this sequence to the UVM factory
  `uvm_object_utils(cfs_apb_sequence_base)

  // Upgrade the default m_sequencer to our specific p_sequencer
  `uvm_declare_p_sequencer(cfs_apb_sequencer)

  // Constructor
  function new(string name = "");
    super.new(name);
  endfunction

endclass

cfs_apb_driver.sv

///////////////////////////////////////////////////////////////////////////////
// File:        cfs_apb_driver.sv
// Author:      Cristian Florin Slav
// Date:        2023-08-22
// Description: APB driver class.
///////////////////////////////////////////////////////////////////////////////
`ifndef CFS_APB_DRIVER_SV
  `define CFS_APB_DRIVER_SV

//파라미터화된 상속 (#(.REQ(...)))
  class cfs_apb_driver extends uvm_driver#(.REQ(cfs_apb_item_drv));
    
    `uvm_component_utils(cfs_apb_driver)
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);
    endfunction
    
    //통신망 확인용 무한 루프 (forever begin ~ end)
    virtual task run_phase(uvm_phase phase);
      forever begin
      // cfs_apb_item_drv: "내가 지금부터 받을 물건은 다른 게 아니라, 
      //우리가 아까 rand 변수들(주소, 데이터)을 꽉꽉 채워 넣고 예쁘게
      //포장했던 바로 그 'APB 전용 택배 상자' 규격이야!"
     //item: "그리고 그 상자를 앞으로 내 코드 안에서는 item이라는 이름표를 붙여서 부를게!"
        cfs_apb_item_drv item;
        
        seq_item_port.get_next_item(item);
        
        // 2단계: 가짜 연기 (원래는 여기서 핀을 흔들어야 함!)
        `uvm_info("DEBUG", $sformatf("Driving \"%0s\": %0s", item.get_full_name(), item.convert2string()), UVM_NONE)
        
        // 3단계: 연기 끝! 다음 대본 요청
        seq_item_port.item_done();
      end
    endtask

  endclass

`endif

족보의 마법: 파라미터화된 상속 (#(.REQ(...)))

  • 부모 클래스인 uvm_driver를 상속받을 때, 옆에 #(.REQ(cfs_apb_item_drv))라는 꼬리표를 붙였습니다.

  • 의미: "부모님, 제가 쓸 무전기(seq_item_port)와 택배 상자(REQ)의 규격은 무조건 cfs_apb_item_drv로 통일하겠습니다!"라고 시스템에 못을 박는 겁니다.

  • 이렇게 해두면 나중에 get_next_item으로 상자를 받을 때 귀찮은 타입 변환(Casting) 절차 없이 아주 깔끔하게 상자를 열어볼 수 있습니다.

통신망 확인용 무한 루프 (forever begin ~ end)

이 run_phase는 시뮬레이션 시작과 동시에 영원히 도는 쳇바퀴입니다.

get_next_item을 부르는 순간, 대본(Sequence) 쪽에서 아이템을 던져줄 때까지 이 코드 줄에서 시뮬레이션 시간이 일시 정지(Block)된 채로 얌전히 기다립니다.

가짜 연기와 완료 보고 (uvm_info & item_done)

왜 가짜 연기를 할까요? 현업에서는 거대한 검증 환경을 지을 때, 하드웨어 핀을 흔드는 복잡한 코드를 짜기 전에 "대본(Sequence) ➔ 조감독(Sequencer) ➔ 배우(Driver)"로 이어지는 이 소프트웨어 통신망 파이프가 제대로 뚫렸는지 먼저 확인(Sanity Check)해야 합니다.

cfs_apb_sequencer.sv

`ifndef CFS_APB_SEQUENCER_SV
  `define CFS_APB_SEQUENCER_SV

  class cfs_apb_sequencer extends uvm_sequencer#(.REQ(cfs_apb_item_drv));
    
    `uvm_component_utils(cfs_apb_sequencer)
    
    function new(string name = "", uvm_component parent);
      super.new(name, parent);
    endfunction

  endclass

`endif
  1. 왜 조감독의 코드는 텅 비어있을까?
    부모님(uvm_sequencer)이 이미 다 해놨기 때문입니다.

  2. 드라이버와의 '규격 통일' (#(.REQ(...)))
    아까 드라이버(cfs_apb_driver)를 만들 때도 똑같이 #(.REQ(cfs_apb_item_drv)) 꼬리표를 달았던 것 기억하시나요?

의미: "조감독님! 당신이 취급할 택배 상자 규격도 무조건 cfs_apb_item_drv입니다!"

cfs_apb_sequence_base.sv

`ifndef CFS_APB_SEQUENCE_BASE_SV
  `define CFS_APB_SEQUENCE_BASE_SV

  class cfs_apb_sequence_base extends uvm_sequence#(.REQ(cfs_apb_item_drv));
    
    `uvm_declare_p_sequencer(cfs_apb_sequencer)
    
    `uvm_object_utils(cfs_apb_sequence_base)
    
    function new(string name = "");
      super.new(name);
    endfunction

  endclass

`endif

삼각편대 규격 통일 완수! (#(.REQ(...)))
조감독(Sequencer)과 배우(Driver)를 만들 때 꼬리표로 #(.REQ(cfs_apb_item_drv))를 붙였던 것 기억하시죠?

이제 대본(Sequence)의 최고 조상님인 여기서도 똑같이 꼬리표를 붙여줍니다.

의미: 이로써 대본, 조감독, 배우 3인방이 모두 "우리는 cfs_apb_item_drv라는 상자 하나만 취급한다!"라고 완벽하게 합의를 마친 겁니다. 이제 에러 없이 데이터가 뻥 뚫린 고속도로처럼 흐르게 됩니다.

마법의 무전기 업그레이드 (p_sequencer)
`uvm_declare_p_sequencer(cfs_apb_sequencer)
이 파일의 존재 이유이자 핵심입니다.

기본적으로 제공되는 깡통 무전기(m_sequencer)를 APB 전용 최신 스마트폰(p_sequencer)으로 바꿔줍니다.

괄호 안에는 반드시 "내가 달리고 있는 조감독의 정확한 타입(cfs_apb_sequencer)"을 적어줘야 합니다.

cfs_apb_sequence_simple.sv

`ifndef CFS_APB_SEQUENCE_SIMPLE_SV
  `define CFS_APB_SEQUENCE_SIMPLE_SV

  class cfs_apb_sequence_simple extends cfs_apb_sequence_base;
    
    //Item to drive
    rand cfs_apb_item_drv item;
    
    `uvm_object_utils(cfs_apb_sequence_simple)
    
    function new(string name = "");
      super.new(name);
      
      item = cfs_apb_item_drv::type_id::create("item");
    endfunction
    
    virtual task body();
      start_item(item);  //request permission
      finish_item(item);  //send the item
      
      //An alternative with macros is to use `uvm_send().
      //Macro `uvm_do() will not work because any constraints from sequence call will have no effect
      //`uvm_send(item)
    endtask

  endclass

`endif

item = cfs_apb_item_drv::type_id::create("item");
보통 대본은 task body() 안에서 실행될 때 상자를 만듭니다. 하지만 이 녀석은 대본이 태어나자마자(new) 빈 택배 상자(item)부터 미리 하나 만들어 둡니다.

왜 그럴까요? 지휘관(Test)이 대본을 실행(start)하기 전에, 미리 저 상자 안에 접근해서 특정 주소나 데이터를 강제로 욱여넣을 수 있게(Directed Test) 길을 열어둔 것입니다.

주사위(Randomize)가 사라졌다?
즉, 이 대본은 자기가 알아서 무작위 값을 채우는 게 아니라, "누군가(Test)가 밖에서 채워준 값 그대로, 묻지도 따지지도 않고 조감독에게 던져버리는" 아주 단순한 배달부 역할만 하는 겁니다.

가장 중요한 주석 해독: uvm_send vs uvm_do uvm_do(item)을 쓰면 망하는 이유: 이 마법의 매크로는 [상자 새로 만들기 ➔ 랜덤 돌리기 ➔ 전송]을 한 방에 다 해버립니다. 만약 이걸 쓰면, 지휘관(Test)이 밖에서 기껏 item.addr = 100; 이라고 정성껏 세팅해 놨는데, 대본이 상자를 콱 부수고 새 상자를 만들어서 지멋대로 랜덤 값을 덮어씌워 버립니다.

uvm_send(item)의 등장: 그래서 "랜덤은 안 돌리고, 이미 만들어진 상자를 전송(start_item + finish_item)만 깔끔하게 해주는" 단축키가 바로 uvm_send입니다.

cfs_apb_sequence_rw.sv

///////////////////////////////////////////////////////////////////////////////
// File:        cfs_apb_sequence_rw.sv
// Author:      Cristian Florin Slav
// Date:        2023-08-23
// Description: APB read-write sequence class.
///////////////////////////////////////////////////////////////////////////////
`ifndef CFS_APB_SEQUENCE_RW_SV
  `define CFS_APB_SEQUENCE_RW_SV

  class cfs_apb_sequence_rw extends cfs_apb_sequence_base;
    
    //Address
    rand cfs_apb_addr addr;
    
    //Write data
    rand cfs_apb_data wr_data;
    
    `uvm_object_utils(cfs_apb_sequence_rw)
    
    function new(string name = "");
      super.new(name);
    endfunction
    
    virtual task body();
//       cfs_apb_item_drv item = cfs_apb_item_drv::type_id::create("item");
      
//       void'(item.randomize() with {
//         dir  == CFS_APB_READ;
//         //Pay attention to the "local::" in order to avoid name confusion
//         addr == local::addr;
//       });
//       start_item(item);
//       finish_item(item);
      
//       void'(item.randomize() with {
//         dir  == CFS_APB_WRITE;
      

//Pay attention to the "local::" in order to avoid name confusion
//         addr == local::addr;
//         data == wr_data;
//       });
//       start_item(item);
//       finish_item(item);
      
      //The above code can be replaced with `uvm_do macros
      cfs_apb_item_drv item;
      
      // 1st Action: READ from the address
      `uvm_do_with(item, {
        dir  == CFS_APB_READ;
        addr == local::addr;
      });
      
      // 2nd Action: WRITE to the same address
      `uvm_do_with(item, {
        dir  == CFS_APB_WRITE;
        addr == local::addr;
        data == wr_data;
      });
      
    endtask

  endclass

`endif

지휘관(Test)을 위한 "리모컨 버튼" 만들기

// Address
rand cfs_apb_addr addr;

// Write data
rand cfs_apb_data wr_data;

대본 클래스 바로 밑에 변수 두 개를 파놓았습니다.

이유: 지휘관(Test)이 밖에서 이 대본을 실행하기 전에 "야! 100번지 주소(addr)에 55라는 데이터(wr_data)를 써라!"라고 콕 집어서 명령할 수 있도록 창구를 열어둔 것입니다. (앞서 봤던 simple 대본을 유연하게 업그레이드한 거죠!)

작전 내용: "읽고 ➔ 쓰기" 연속 콤보

  • `uvm_do_with의 등장: 아까 배웠던 그 마법의 단축키 uvm_do에 _with가 붙었습니다! "새 상자를 만들어서 전송하되, 내가 정해준 규칙(Constraint)대로 값을 채워서 보내라!"라는 뜻입니다.

  • 작전 순서:

  1. 먼저 지휘관이 정해준 주소(addr)에서 데이터를 한 번 읽어옵니다. (기존 값이 뭔지 확인)
  2. 바로 이어서 똑같은 주소에 새로운 데이터(wr_data)를 덮어씁니다.
  3. 현업에서는 이 패턴을 특정 레지스터가 잘 살아있는지 테스트할 때 아주 요긴하게 씁니다.

local::의 비밀 (면접 단골 질문!)
강사가 주석으로 //Pay attention to the "local::" in order to avoid name confusion 이라고 경고한 부분입니다. 이거 실무에서 주니어들이 정말 많이 실수하는 부분입니다.

만약 local::을 빼고 그냥 addr == addr; 이라고 적으면 어떻게 될까요?

with { ... } 블록 안에서는 기본적으로 택배 상자(item) 안에 있는 변수를 먼저 쳐다봅니다.

그래서 addr == addr;은 "내 상자 안의 addr 값은 내 상자 안의 addr 값과 같다"라는 뜻이 되어버립니다. 1은 1이다, 2는 2이다 처럼 항상 참(True)이 되는 아무 쓸모 없는 문장이 되고, 주소는 그냥 완전 랜덤한 쓰레기 값이 들어가 버립니다.

해결책 (local::): "상자 안에 있는 addr 말고, 상자 밖에 있는 대본(Sequence) 본체의 addr 변수를 쳐다봐라!"라고 시야를 바깥으로 돌려주는 마법의 키워드입니다.

결론부터 말씀드리면, 여기서 local::이 가리키는 곳은 "이 randomize 명령어를 실행시킨 주체(즉, 대본인 cfs_apb_sequence_rw 클래스 그 자체)"입니다.

SystemVerilog에서 item.randomize() with { ... } 라는 문장을 만나는 순간, 우리 시야(Scope)의 기준점이 완전히 뒤바뀝니다.

  1. with { } 안으로 들어가는 순간: "상자 안으로 다이브!"
  • with { } 괄호가 열리면, 우리의 시야는 대본(방)에서 택배 상자(item) 내부로 쑥 빨려 들어갑니다.

  • 이제부터 이 괄호 안에서 그냥 addr 이라고 부르면, 무조건 상자 안에 들어있는 addr (즉, item.addr)을 가리키게 됩니다.

  1. 이름 충돌의 딜레마 (Name Collision)
  • 상자(item) 안에도 addr 이라는 이름의 변수가 있고,

  • 방(sequence) 안에도 아까 우리가 선언한 addr 이라는 변수가 있습니다.

  • 괄호 안에서 addr == addr; 이라고 쓰면 시뮬레이터는 "아~ 상자 안의 addr을 상자 안의 addr이랑 똑같이 맞추라는 뜻이구나!" 하고 멍청하게 이해해 버립니다. (결과: 아무 일도 안 일어남)

  1. local::의 진짜 의미: "나를 호출했던 바로 그 동네!"
  • 상자 안에서 허우적대고 있을 때, 시뮬레이터에게 "야! 상자 안에 있는 addr 말고, 아까 이 상자 밖의 원래 방(Sequence)에 있던 그 addr 좀 가져와!" 라고 지시해야 합니다.

  • SystemVerilog에서 local의 정확한 정의는 "상자 내부"가 아니라, "이 randomize() 함수를 호출했던 현재 문맥(Local Context), 즉 바깥의 방(Sequence)의 스코프"를 의미합니다.

cfs_apb_sequence_random.sv

"지정된 횟수(num_items)만큼 루프를 돌면서, 하급 배달부(simple sequence)를 끊임없이 소환해 무작위 상자를 마구 던지는 무자비한 관리자!"

`ifndef CFS_APB_SEQUENCE_RANDOM_SV
  `define CFS_APB_SEQUENCE_RANDOM_SV

  class cfs_apb_sequence_random extends cfs_apb_sequence_base;
    
    //NUmber of items to drive
    rand int unsigned num_items;
    
    constraint num_items_default {
      soft num_items inside {[1:10]}; 
    }
    
    `uvm_object_utils(cfs_apb_sequence_random)
    
    function new(string name = "");
      super.new(name);
    endfunction
    
    virtual task body();
      for(int i = 0; i < num_items; i++) begin
        cfs_apb_sequence_simple seq = cfs_apb_sequence_simple::type_id::create("seq");
        
        void'(seq.randomize());
        
        seq.start(m_sequencer, this);
      
        //An alternative with macros is to use `uvm_do().
        //`uvm_do(seq)
      end
    endtask

  endclass

`endif

"몇 발이나 쏠까요?" (유연한 횟수 조절과 soft)

앞서 드라이버(item_drv)에서 봤던 그 반가운 soft 제약 조건이 또 등장했습니다!

의미: "기본적으로는 1번에서 10번 사이로 적당히 랜덤하게 쏠게. 하지만 밖에서 지휘관(Test)이 10,000번 쏘라고 강제로 명령하면 군말 없이 10,000번 쏠게!"

이렇게 짜두면 밤새도록 칩을 테스트하는 'Overnight Regression'을 돌릴 때 루프 횟수를 밖에서 마음대로 늘릴 수 있어서 엄청나게 편리합니다.

최대 반전: 대본이 대본을 부른다! (Sequence Inception)

virtual task body();
  for(int i = 0; i < num_items; i++) begin
    // 1. Create a "simple sequence", NOT an item!
    cfs_apb_sequence_simple seq = cfs_apb_sequence_simple::type_id::create("seq");
    
    // 2. Randomize the simple sequence
    void'(seq.randomize());
    
    // 3. Start the simple sequence
    seq.start(m_sequencer, this);
  end
endtask

어? 분명히 택배 상자(item)를 만들어서 던져야 하는데, 상자는 안 만들고 아까 우리가 분석했던 하급 배달부 대본(simple)을 만들고 있습니다!

왜 이럴까요?: UVM에서는 상위 대본(Manager)이 하위 대본(Worker)을 부려 먹을 수 있습니다. 이것을 '계층적 시퀀스(Hierarchical Sequence)'라고 부릅니다.

방금 부른 simple 대본 안에 rand cfs_apb_item_drv item; 이 들어있던 것 기억하시죠? 여기서 seq.randomize()를 부르면, 그 안에 있는 상자(item)까지 연쇄적으로 무작위 값이 꽉꽉 채워집니다.

// An alternative with macros is to use `uvm_do().
`uvm_do(seq)

아까 uvm_do가 [상자 만들기 ➔ 랜덤 돌리기 ➔ 전송하기]를 한 방에 해준다고 했죠?

놀랍게도 이 매크로는 상자(item)뿐만 아니라 하위 대본(sequence)을 부를 때도 똑같이 작동합니다!

즉, 저 복잡한 3줄짜리 수동 코드를 지우고 `uvm_do(seq) 딱 한 줄만 쓰면, "하급 배달부 불러와서, 랜덤 값 채우고, 조감독한테 보내라!"라는 명령이 한 방에 끝납니다.

profile
RTL, FPGA Engineer

0개의 댓글