
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은 앞서 말한 '거울(Shadow Model)'의 본체이자 설계도입니다.
하드웨어 설계자(RTL Designer)가 만든 레지스터 스펙을 소프트웨어 세계로 똑같이 복사해 온 덩어리입니다.
우리가 짰던 코드를 떠올려 보면, 그 안에 이런 내용들이 들어있습니다.
검증할 때 값을 쓰거나 읽으면, 실제 하드웨어로 신호가 날아감과 동시에 이 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
가장 중요한 부분입니다. reg_block과 실제 하드웨어 버스(AXI4-Lite)는 서로 사용하는 언어가 완전히 다릅니다.
이 둘 사이에는 말이 안 통하기 때문에 통역사(Adapter)가 무조건 필요합니다.
우리가 짰던 sy_reg_adapter 코드를 보면 딱 두 가지 일만 합니다.
어댑터가 있기 때문에, 나중에 이 칩의 통신 방식이 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