RAL

Seungyun Lee·2026년 4월 14일

UVM

목록 보기
11/12

RAL (Register Abstraction Layer)이란 무엇인가?

RAL(Register Abstraction Layer)
RAL 없이는 요즘 나오는 수십억 게이트짜리 SoC(System on Chip)를 절대 검증할 수 없습니다.

  • RAL이 없을 때 (과거의 노가다):
    엔지니어가 1000페이지짜리 스펙 문서를 뒤져가며 "아, UART 모듈 켜려면 주소 0x4000_1020에 0x0000_0001을 써야 하는구나"라고 하드코딩을 해야 합니다. 만약 설계자가 주소를 0x4000_2020으로 맵핑을 바꾸면? 검증 엔지니어는 수만 줄의 코드를 밤새워 다 고쳐야 합니다. (대참사 발생)

  • RAL을 도입했을 때 (현대 UVM의 마법):
    RAL은 검증 환경 메모리 위에 "하드웨어 레지스터들의 완벽한 거울(Shadow Model)"을 띄워놓는 기술입니다.
    이제 주소를 외울 필요가 없습니다. 테스트 코드에서 그냥 reg_block.uart_ctrl.enable.write(1); 라고만 치면 끝입니다. 뒤에서 무슨 주소로, AXI로 갈지 APB로 갈지는 UVM이 알아서 변환해 줍니다. 설계자가 주소를 바꿔도, 우리는 RAL 맵핑 딱 한 줄만 고치면 전체 테스트 환경이 그대로 정상 동작합니다.

왜 reg_block을 만들었는가? (설계도)

reg_block은 앞서 말한 '거울(Shadow Model)'의 본체이자 설계도입니다.

하드웨어 설계자(RTL Designer)가 만든 레지스터 스펙을 소프트웨어 세계로 똑같이 복사해 온 덩어리입니다.
우리가 짰던 코드를 떠올려 보면, 그 안에 이런 내용들이 들어있습니다.

  • "이 칩에는 ctrl_reg와 status_reg라는 두 개의 레지스터가 있다."
  • "ctrl_reg는 주소 0x00에 있고, 읽고 쓰기(RW)가 가능하다."
  • "status_reg는 주소 0x04에 있고, 읽기 전용(RO)이다."

검증할 때 값을 쓰거나 읽으면, 실제 하드웨어로 신호가 날아감과 동시에 이 reg_block 거울에도 그 값이 똑같이 저장됩니다. 그래서 나중에 "방금 하드웨어에 무슨 값 썼었지?" 궁금할 때 굳이 버스를 다시 읽어볼 필요 없이, 거울(reg_block)만 쳐다보면 현재 하드웨어의 상태를 바로 알 수 있습니다.

`ifndef SY_REG_BLOCK_SV
`define SY_REG_BLOCK_SV

import uvm_pkg::*;
`include "uvm_macros.svh"

// ==========================================================
// 1. CTRL_REG (Control Register) - 주소 0x00
// ==========================================================
class sy_ctrl_reg extends uvm_reg;
  `uvm_object_utils(sy_ctrl_reg)

  // 레지스터 내부의 개별 비트(Field) 선언
  rand uvm_reg_field enable;

  function new(string name = "sy_ctrl_reg");
    super.new(name, 32, UVM_NO_COVERAGE); // 32-bit 레지스터
  endfunction

  virtual function void build();
    enable = uvm_reg_field::type_id::create("enable");
    // 필드 설정: 부모, 비트크기, LSB위치, 접근권한(RW), 휘발성, 리셋값, 리셋적용유무, 랜덤화가능유무, 개별접근유무
    enable.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0); 
  endfunction
endclass

// ==========================================================
// 2. STATUS_REG (Status Register) - 주소 0x04
// ==========================================================
class sy_status_reg extends uvm_reg;
  `uvm_object_utils(sy_status_reg)

  uvm_reg_field ready;

  function new(string name = "sy_status_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    ready = uvm_reg_field::type_id::create("ready");
    ready.configure(this, 1, 0, "RO", 0, 1'b0, 1, 0, 0); // Read-Only (RO)
  endfunction
endclass

// ==========================================================
// 3. Register Block (레지스터들을 하나로 묶는 전체 맵)
// ==========================================================
class sy_reg_block extends uvm_reg_block;
  `uvm_object_utils(sy_reg_block)

  rand sy_ctrl_reg   ctrl_reg;
  rand sy_status_reg status_reg;
  
  //uvm_reg_map        default_map; // 주소 맵핑을 담당할 객체

  function new(string name = "sy_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    // 1. 레지스터 객체 생성 및 초기화
    ctrl_reg = sy_ctrl_reg::type_id::create("ctrl_reg");
    ctrl_reg.configure(this, null);
    ctrl_reg.build();

    status_reg = sy_status_reg::type_id::create("status_reg");
    status_reg.configure(this, null);
    status_reg.build();

    // 2. 메모리 맵 생성 (이름, 시작주소, 바이트수, 엔디안)
    default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN);

    // 3. 레지스터들을 메모리 맵에 주소와 함께 등록
    default_map.add_reg(ctrl_reg,   'h00, "RW"); // Offset 0x00
    default_map.add_reg(status_reg, 'h04, "RO"); // Offset 0x04
    
    lock_model(); // 구조가 완성되었으니 잠금!
  endfunction
endclass

`endif

왜 adapter를 만들었는가? (통역사)

가장 중요한 부분입니다. reg_block과 실제 하드웨어 버스(AXI4-Lite)는 서로 사용하는 언어가 완전히 다릅니다.

  • RAL의 언어: "어이, 저기 ctrl_reg에 데이터 1 좀 써줘." (매우 추상적이고 고급스러운 명령어)
  • AXI4-Lite의 언어 (우리의 sy_transaction): "AWADDR에 0x00 올리고, AWVALID 띄우고, WDATA에 1 올리고..." (매우 구체적이고 물리적인 하드웨어 신호)

이 둘 사이에는 말이 안 통하기 때문에 통역사(Adapter)가 무조건 필요합니다.
우리가 짰던 sy_reg_adapter 코드를 보면 딱 두 가지 일만 합니다.

  1. reg2bus: RAL이 "ctrl_reg에 1 써!"라고 하면, 어댑터가 "아, 주소는 0x00이고 데이터는 1인 AXI Write 트랜잭션이구나" 하고 sy_transaction 박스를 만들어 줍니다.
  2. bus2reg: 하드웨어에서 AXI Read 응답이 돌아오면, 어댑터가 이를 받아서 "RAL 님, 하드웨어가 상태값 1을 리턴했습니다" 하고 RAL의 언어로 번역해서 거울(reg_block)에 업데이트해 줍니다.

어댑터가 있기 때문에, 나중에 이 칩의 통신 방식이 AXI에서 APB나 I2C로 완전히 바뀌어도 우리는 테스트 코드나 reg_block을 하나도 고칠 필요가 없습니다. 어댑터(통역사)만 교체해 주면 그만입니다.

`ifndef SY_REG_ADAPTER_SV
`define SY_REG_ADAPTER_SV

import uvm_pkg::*;
`include "uvm_macros.svh"

class sy_reg_adapter extends uvm_reg_adapter;
  `uvm_object_utils(sy_reg_adapter)

  function new(string name = "sy_reg_adapter");
    super.new(name);
    // AXI4-Lite는 byte enable을 복잡하게 쓰지 않고, 응답도 드라이버에서 바로 처리하므로 기본 설정
    supports_byte_enable = 0; 
    provides_responses = 0;   
  endfunction

  // 1. RAL 명령어 -> 버스 트랜잭션 변환 (Write/Read 요청 시)
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    sy_transaction tx;
    tx = sy_transaction::type_id::create("tx");
    tx.addr = rw.addr; // 주소 복사
    
    // UVM_WRITE / UVM_READ를 우리 트랜잭션의 trans_kind로 변환
    if (rw.kind == UVM_WRITE) begin
      tx.trans_kind = WRITE; 
      tx.data = rw.data; // 쓸 데이터 복사
    end else begin
      tx.trans_kind = READ;
    end
    
    return tx;
  endfunction

  // 2. 버스 트랜잭션 -> RAL 응답 변환 (Read 데이터 수신 시)
  virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    sy_transaction tx;
    
    // 들어온 아이템이 sy_transaction이 맞는지 캐스팅(형변환) 확인
    if (!$cast(tx, bus_item)) begin
      `uvm_fatal("ADAPTER", "Provided bus_item is not of the correct type sy_transaction")
    end
    
    // 읽어온 데이터와 상태를 RAL 쪽에 전달
    rw.addr = tx.addr;
    rw.data = tx.data;
    rw.kind = (tx.trans_kind == WRITE) ? UVM_WRITE : UVM_READ;
    rw.status = UVM_IS_OK; // 응답 에러가 없다고 가정 (bresp/rresp 확장 시 여기서 처리)
  endfunction

endclass

`endif
profile
Design Verification engineer

0개의 댓글