AXI4 UVM (1) — axi4_seq_item

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
4/16

시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터


아이템 구조 다이어그램


axi4_seq_item — 자극의 단위 (아이템)

역할

axi4_seq_item = "AXI 버스트 하나"를 표현하는 UVM 객체

     Sequence
        │  randomize() → 제약 안에서 랜덤 버스트 생성
        ↓
   [axi4_seq_item]
        │
        ├──→ Driver: 이 정보로 핀을 흔듦
        ├──→ Reference Model: beat_addr()로 예측 주소 계산
        └──→ Monitor: 핀에서 다시 이 객체로 재조립
                       ↓
                  Scoreboard: do_compare()로 비교

역할이 4가지: 자극 생성 + 주소 계산 라이브러리 + 로그 포맷 + 비교 기준


클래스 선언

class axi4_seq_item extends uvm_sequence_item;
uvm_object
   └─ uvm_transaction
        └─ uvm_sequence_item   ← 여기
             (sequencer↔driver 핸드셰이크 기능 포함)

uvm_object를 직접 상속하면?
→ seq_item_port.get_next_item() / item_done() 못 씀
→ Sequencer에 실을 수 없음

필드 — rand vs non-rand

// 주소/제어 (AW와 AR 공통 형태)
rand bit [AXI_ID_WIDTH-1:0]   id;
rand bit [AXI_ADDR_WIDTH-1:0] addr;
rand bit [7:0]                len;    // beat 수 - 1
rand bit [2:0]                size;   // beat당 byte = 2**size
rand axi_burst_e              burst;
rand axi_dir_e                dir;    // READ or WRITE

// 데이터 페이로드 (beat당 1개)
rand bit [AXI_DATA_WIDTH-1:0] data [];
rand bit [AXI_STRB_WIDTH-1:0] strb [];

// DUT 결과 (랜덤화 안 함)
axi_resp_e                    resp = AXI_OKAY;
필드AXI 신호주의점
idAWID/ARIDOutstanding 매칭용
addrAWADDR/ARADDR
lenAWLEN/ARLEN⚠️ 0이면 1 beat!
sizeAWSIZE/ARSIZElog2 인코딩
burstAWBURST/ARBURSTFIXED/INCR/WRAP
dir(내부)AXI 신호 아님, 채널 선택용
respBRESP/RRESPrand 없음 — DUT 응답 저장용
dir이 rand인 점 주목 — Sequence에서 제약을 안 걸면 읽기/쓰기가 랜덤으로 섞임.
[] = 동적 배열. 버스트 길이가 런타임에 결정되므로 필수.

resp = AXI_OKAY 기본값:
  Driver: B/R 채널에서 받은 값을 여기 저장
  Monitor: BRESP/RRESP를 여기 저장
  Scoreboard: 이 값을 검사
  기본값 AXI_OKAY → 아직 응답 안 받은 상태

Factory 등록

`uvm_object_utils(axi4_seq_item)

function new(string name = "axi4_seq_item");
    super.new(name);
endfunction
`uvm_object_utils (필드 매크로 없는 버전)

해주는 것:
✅ Factory 등록 → type_id::create() 가능
✅ get_type_name()
✅ Factory override 대상

안 해주는 것:
❌ 자동 copy/compare/print/pack
   → 그래서 do_copy, do_compare를 직접 구현!

new(): uvm_object 생성자는 string name 하나 (default 값 있음)
       → super.new() 명시 호출 필수 (부모가 인자를 받으므로)

Constraints — 합법적인 AXI 자극 공간

이 파일의 핵심. "DUT가 실제로 받을 수 있는 값만 생성" 하는 규칙들.

c_len — 버스트 길이

constraint c_len  { len inside {[0:15]}; }
len 0~15 → 1~16 beats
AXI4 스펙상 INCR는 최대 256 beats(len=255)까지 가능하지만
환경 브링업 단계라 짧게 제한

⚠️ 나중에 늘릴 때 주의: FIXED/WRAP은 AXI 스펙상 최대 16 beats!
   constraint c_len {
       (burst == AXI_INCR) -> len inside {[0:255]};
       (burst != AXI_INCR) -> len inside {[0:15]};
   }

c_size — 전송 크기

constraint c_size { size <= $clog2(AXI_STRB_WIDTH); }
32비트 버스: STRB_WIDTH = 4, $clog2(4) = 2 → size <= 2 → 최대 4바이트/beat ✅
size=3(8바이트)면? → 32비트 버스에 못 실음 → 불법!

c_burst_dist — 버스트 타입 분포

constraint c_burst_dist {
    burst dist { AXI_INCR := 8, AXI_FIXED := 1, AXI_WRAP := 1 };
}
가중치 합 = 10
  INCR  : 80%   ← 실전에서 압도적으로 많음 (DMA, 메모리 접근)
  FIXED : 10%   ← 코너 케이스 커버리지
  WRAP  : 10%   ← 코너 케이스 커버리지

:= 는 각 값에 가중치 개별 부여,  :/ 는 범위 전체에 가중치 분배

c_wrap_len — WRAP 합법성

constraint c_wrap_len { (burst == AXI_WRAP) -> len inside {1, 3, 7, 15}; }
AXI 스펙: WRAP은 2, 4, 8, 16 beats만 합법!
  len = 1→2, 3→4, 7→8, 15→16 beats ✅

왜 2의 거듭제곱만? → WRAP window 크기가 2의 거듭제곱이어야
  주소 마스킹으로 구현 가능 (base = addr & ~(total-1))

-> 는 implication: "burst가 WRAP이면 → len은 이 값들 중 하나"
   WRAP이 아니면 이 제약은 무시됨

c_align — 주소 정렬

constraint c_align { addr % (1 << size) == 0; }
size=2 (4바이트) → addr는 4의 배수:  0x1000 ✅  0x1002 ❌
AXI4는 unaligned도 지원하지만 규칙이 복잡 → 정렬 전송으로 단순화

c_4k — 4KB 경계 규칙

constraint c_4k { (addr % 4096) + ((len + 1) << size) <= 4096; }
분해:
  addr % 4096        = 4KB 페이지 안 오프셋
  (len + 1) << size  = 전체 전송 바이트 (beat수 × beat당 바이트)
  오프셋 + 전체크기 <= 4096 → 4KB 페이지 안에서 끝나야 함 ✅

예시 (32비트, size=2):
  addr=0x0FF0(오프셋4080), len=3 → 4080+16=4096 <= 4096 ✅ (딱 경계)
  addr=0x0FF4(오프셋4084), len=3 → 4084+16=4100 > 4096  ❌ 거부

왜 4KB? OS가 4KB 페이지 단위로 메모리 관리 → 페이지마다 속성이 다를 수 있어
        경계 넘으면 잘못된 속성/없는 주소 접근 위험

c_payload — 배열 크기

constraint c_payload { data.size() == len + 1; strb.size() == len + 1; }
동적 배열의 크기 자체를 제약으로 지정!
솔버: len 먼저 정함 → data/strb 배열 할당 → 각 원소 랜덤화
len=7 → data[8], strb[8] 자동 생성

post_randomize() — 랜덤화 후 보정

function void post_randomize();
    if (dir == AXI_READ) begin
        foreach (strb[i]) strb[i] = '1;
    end
    else begin
        foreach (strb[i]) strb[i] &= lane_mask(i);
    end
endfunction

"constraint로 표현하기 어려운 로직은 post_randomize에서" 의 교과서적 예시.

읽기 경로:
  WSTRB 없음 → 전부 1로 채워 로그/비교 일관성 ('1 = 폭 상관없이 모든 비트 1)

쓰기 경로 — lane 마스킹:
  AXI 규칙: WSTRB는 addr+size가 지정하는 바이트 레인 위에만 있어야 함
  32비트, size=1(2바이트), addr%4==2 → 상위 2바이트 레인만 유효 → 4'b1100
  랜덤 strb 4'b1011 → 4'b1011 & 4'b1100 = 4'b1000 (합법 부분만 남음)

  &= 이유: 전부 켜는 게 아니라 "일부를 꺼도 합법"
           → 랜덤성 유지하면서 불법 레인만 제거

⚠️ 부작용: 마스킹 결과 strb=0이 될 수 있음 (AXI상 합법) → 스코어보드가 처리 필요

beat_addr() — 버스트 주소 계산기

function bit [AXI_ADDR_WIDTH-1:0] beat_addr(int i);
    int unsigned nbytes = 1 << size;
    int unsigned total;
    bit [AXI_ADDR_WIDTH-1:0] base;
    case (burst)
        AXI_FIXED: beat_addr = addr;                  // never moves
        AXI_WRAP: begin
            total = (len + 1) * nbytes;               // wrap window 크기
            base  = addr - (addr % total);            // window base
            beat_addr = base + (((addr - base) + i*nbytes) % total);
        end
        default:   beat_addr = addr + i*nbytes;       // AXI_INCR
    endcase
endfunction

주석의 의도: "the DUT increments linearly even for WRAP, which is exactly
how we catch that bug"
— DUT에 WRAP을 INCR처럼 처리하는 버그가 있고,
이 함수가 정확히 계산하니까 스코어보드에서 미스매치로 잡힌다. 실제 버그를 잡은 스토리.

FIXED:  beat_addr = addr        (i와 무관, 항상 같은 주소 - FIFO용)
INCR:   beat_addr = addr + i*nbytes  (직진)
        addr=0x1008, size=2, len=3 → 0x1008,0x100C,0x1010,0x1014

WRAP 세 줄 분해 (addr=0x1008, size=2(nbytes=4), len=3):
  ① total = 4×4 = 16                → window 크기 16
  ② base = 0x1008-(0x1008%16) = 0x1000 → window [0x1000~0x100F]
  ③ addr-base = 8                   → window 안 시작 오프셋
     beat0: 0x1000+((8+0)%16)  = 0x1008
     beat1: 0x1000+((8+4)%16)  = 0x100C
     beat2: 0x1000+((8+8)%16)  = 0x1000  ← WRAP!
     beat3: 0x1000+((8+12)%16) = 0x1004

  0x1008 → 0x100C → (경계!) → 0x1000 → 0x1004
  캐시 라인 채우기 시맨틱: CPU가 원한 0x1008 먼저, 나머지 라인 채움

lane_mask() — 유효 바이트 레인 계산

function bit [AXI_STRB_WIDTH-1:0] lane_mask(int i);
    int unsigned nbytes = 1 << size;
    int unsigned off    = beat_addr(i) % AXI_STRB_WIDTH;
    lane_mask = AXI_STRB_WIDTH'(((1 << nbytes) - 1) << off);
endfunction
라인설명
nbytes = 1 << size이 beat가 전송하는 바이트 수
off = beat_addr(i) % AXI_STRB_WIDTH버스 폭 안에서 몇 번째 레인부터 시작
((1 << nbytes) - 1)nbytes개의 1 (nbytes=2 → 0b11)
<< off시작 레인만큼 시프트 (0b11 << 2 = 0b1100)
AXI_STRB_WIDTH'(...)그 폭으로 사이즈 캐스트
실전 예시 — narrow INCR (32비트, size=1(2바이트), addr=0x1002, len=3):
  beat  beat_addr  off  mask
   0     0x1002     2   4'b1100  ← 상위 2바이트
   1     0x1004     0   4'b0011  ← 하위 2바이트
   2     0x1006     2   4'b1100
   3     0x1008     0   4'b0011
  레인이 beat마다 토글! → lane_mask(i)가 beat_addr(i)를 호출하는 이유

⚠️ 시프트 오버플로: (1 << nbytes)에서 1은 32비트 리터럴.
   nbytes >= 32면 결과 0 → -1 덕에 우연히 all-ones. 동작은 하나 "우연"에 의존.
   더 안전: ('1 >> (AXI_STRB_WIDTH - nbytes)) << off

Helper 함수

function int unsigned beats();          return len + 1;      endfunction
function int unsigned bytes_per_beat(); return (1 << size);  endfunction
AXI 로그 인코딩을 사람이 읽는 값으로 변환
  len=7  → beats() = 8
  size=2 → bytes_per_beat() = 4
"len + 1"을 코드 여기저기 흩뿌리는 대신 의미가 드러나는 이름으로 캡슐화

개선 여지: virtual을 붙이면 자식 클래스가 오버라이드 가능
          (지금은 non-virtual이라 확장 아이템에서 재정의해도 부모 버전 호출)

convert2string() — 로그 포맷

virtual function string convert2string();
    convert2string = $sformatf(
        "%-5s id=%0d addr=0x%04h len=%0d(%0d beats) size=%0d(%0dB) burst=%-5s resp=%s",
        dir.name(), id, addr, len, beats(), size, bytes_per_beat(),
        burst.name(), resp.name());
    foreach (data[i])
        convert2string = {convert2string,
            $sformatf("\n        beat[%0d] data=0x%08h strb=0x%01h", i, data[i], strb[i])};
endfunction
지정자의미
%-5s문자열, 5칸, 좌측 정렬 (컬럼 맞춤)
%0d10진수, 앞쪽 0 패딩 없음
%04h16진수 4자리, 0 패딩
%08h16진수 8자리
.name() → enum을 문자열로
  dir.name()="AXI_WRITE", burst.name()="AXI_INCR", resp.name()="AXI_OKAY"
  숫자 대신 이름 출력 → 로그 가독성 ↑ (monitor의 enum 캐스팅과 연결)

출력 예:
WRITE id=3 addr=0x1008 len=3(4 beats) size=2(4B) burst=AXI_WRAP resp=AXI_OKAY
        beat[0] data=0xDEADBEEF strb=0xf
        beat[1] data=0x12345678 strb=0xf
        ...

virtual → uvm_object::convert2string() 오버라이드
          → 부모 핸들로 호출해도 이 버전 실행

do_copy() — Deep Copy 구현

virtual function void do_copy(uvm_object rhs);
    axi4_seq_item t;
    if (!$cast(t, rhs))
        `uvm_fatal("DO_COPY", "rhs is not an axi4_seq_item")
    super.do_copy(rhs);
    id    = t.id;    addr  = t.addr;  len   = t.len;
    size  = t.size;  burst = t.burst; dir   = t.dir;   resp = t.resp;
    data  = new[t.data.size()](t.data);
    strb  = new[t.strb.size()](t.strb);
endfunction
호출 경로:
  tr2.copy(tr1);      → tr2.do_copy(tr1)
  tr2 = tr1.clone();  → create() + do_copy()
  do_copy는 훅(hook) — 직접 부르지 않음

$cast 필수: 인자 타입이 uvm_object (UVM 규약)
  → "갈색 박스"를 열어야 t.id, t.addr 접근 가능
  → 실패 시 uvm_fatal (타입 안 맞는데 복사 = 명백한 실수)

super.do_copy(rhs): uvm_sequence_item 필드 복사 (sequence_id 등)

동적 배열 복사:
  data = new[N](t.data);
  SystemVerilog 동적 배열은 값 타입! → data = t.data 만 써도 복사됨
  new[N](src)는 명시적 등가 표현 (더 읽기 좋음)
  ⚠️ 원소가 class handle이면 원소마다 clone() 필요 (여기선 bit vector라 안전)

do_compare() — 비교 로직

virtual function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    axi4_seq_item t;
    if (!$cast(t, rhs)) return 0;
    do_compare = super.do_compare(rhs, comparer)
               && (id == t.id) && (addr == t.addr) && (len == t.len)
               && (size == t.size) && (burst == t.burst) && (dir == t.dir);
    if (data.size() != t.data.size()) return 0;
    foreach (data[i])
        do_compare &= (data[i] === t.data[i]);
endfunction
$cast 실패 처리가 do_copy와 다름:
  do_copy:    실패 → uvm_fatal (복사는 타입 같아야 의미 있음)
  do_compare: 실패 → return 0 (타입 다르면 당연히 다름 = 정상 결과)

=== vs == (매우 중요!):
  ==  : 2-state, X/Z 있으면 결과 X (불확실)
  === : 4-state, X/Z까지 정확히 비교
  기대 32'h1234_5678 vs 실제 32'hXXXX_5678 (DUT가 X 뱉음)
    == → 결과 X (불명확)
    === → 명확히 0 (불일치) → X 전파 버그 확실히 잡음 ✅

크기 체크 위치: if(data.size() != t.data.size()) return 0;  ← foreach 앞!
  크기 다른데 foreach 돌면 인덱스 범위 밖 접근 → early return으로 차단

잠재적 이슈 / 개선 포인트

⚠️ 이슈 1 — c_4k가 WRAP을 과도하게 제약

WRAP은 window가 자연 정렬 → window 크기 <= 4096이면 절대 4KB 안 넘음

반례: addr=0x0FF8, size=2, len=3 (WRAP)
  total=16, base=0x0FF0, window=[0x0FF0~0x0FFF] ← 4KB 안! 합법 ✅
  근데 제약: (0x0FF8%4096)+16 = 4088+16 = 4104 > 4096 → 거부 ❌
→ 4KB 경계 근처 합법 WRAP이 절대 생성 안 됨 → 커버리지 구멍!

개선:
constraint c_4k {
    (burst != AXI_WRAP) -> (addr%4096) + ((len+1)<<size) <= 4096;
    (burst == AXI_WRAP) -> ((len+1)<<size) <= 4096;
}

⚠️ 이슈 2 — do_compare가 resp/strb 비교 안 함

resp 미비교: DUT가 SLVERR 뱉어도 compare() 통과
            → 스코어보드가 resp를 따로 검사하면 OK, 아니면 놓침
strb 미비교: 레퍼런스 모델이 strb를 적용해 예측한다면 data 비교로 충분 (설계 의도)
→ 인터뷰에서 "왜 resp를 뺐는지" 물어볼 만한 지점

⚠️ 이슈 3 — comparer 인자를 안 씀

uvm_comparer: 어떤 필드가 왜 틀렸는지 자동 로깅
현재 구현은 우회 → compare() 실패 시 상세 리포트 없음 → 수동 디버깅
개선: do_compare &= comparer.compare_field("addr", addr, t.addr, $bits(addr));

💡 개선 4 — 읽기 트랜잭션의 data 랜덤화 낭비

읽기일 때 data는 DUT가 채워줌 → 그런데도 솔버가 매번 랜덤 생성
16 beat × 32bit = 512 랜덤 비트 × 트랜잭션 수 → 대량 회귀에서 솔버 시간 낭비

💡 개선 5 — 필드 매크로 미사용의 트레이드오프

`uvm_object_utils (필드 매크로 없음) 선택 → 성능 ✅
대신: print()가 필드 안 보임 (do_print 필요), pack/unpack 미지원,
      필드 추가 시 do_copy/do_compare 수동 갱신 필요
      → 갱신 깜빡하면 "복사했는데 값이 안 넘어가는" 조용한 버그!

참고: 기본 상식

펼쳐보기 — beat/bit, 로그 인코딩, 골든 룰 등

기본 상식

데이터 단위 계층

bit   → 0 또는 1 (최소 단위)
  ↓ ×8
byte  → 8 bit
  ↓ ×(버스 폭)
beat  → 한 번의 핸드셰이크로 전송되는 데이터 (버스 폭만큼)
  ↓ ×(len+1)
burst → 여러 beat의 묶음 (주소 요청 1번)

beat는 음악의 "박자"에서 온 말. bit과 전혀 다른 개념.
beat ≠ clock cycle — wait state가 있으면 클록은 흘러도 beat는 진행 안 됨.
beat = VALID & READY 핸드셰이크가 성립한 횟수.

AXI의 두 가지 "로그 인코딩" 함정

필드의미주의
AWLEN / ARLEN실제 beat 수 - 1len=0 → 1 beat
AWSIZE / ARSIZElog2(beat당 byte 수)size=2 → 4 byte

VALID/READY 핸드셰이크 골든 룰

룰 1: VALID는 READY를 기다리지 않고 올려도 됨   (기다리면 Deadlock)
룰 2: VALID 한번 올리면 READY 올 때까지 못 내림
룰 3: READY는 언제든 자유롭게 변경 가능
룰 4: 전송 = VALID & READY = 1 인 rising edge
룰 5: 같은 채널 내 VALID → READY 의존성 금지

Clocking Block — 핵심 개념

"신호를 언제 읽고 언제 쓸지, 클록 기준으로 자동 맞춰주는 규칙 묶음"

input  #1step  → 클록 엣지 "직전"의 안정된 값 샘플 (race-free 읽기)
output #1      → 클록 엣지 "후" 1 time unit에 드라이브 (race-free 쓰기)

읽기는 엣지 이전, 쓰기는 엣지 이후 → 절대 안 겹침 → Race Condition 방지

Outstanding Transaction

"응답을 기다리지 않고 다음 요청을 계속 보내는 것"

인터페이스/드라이버/모니터가 전부 "채널 병렬" 구조인 이유가 바로 이것.
여러 버스트가 버스 위에 동시에 존재할 수 있어야 하므로,
각 채널을 독립 스레드로 처리해야 한다.

profile
Design Verification engineer

0개의 댓글