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


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에 실을 수 없음
// 주소/제어 (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 신호 | 주의점 |
|---|---|---|
id | AWID/ARID | Outstanding 매칭용 |
addr | AWADDR/ARADDR | |
len | AWLEN/ARLEN | ⚠️ 0이면 1 beat! |
size | AWSIZE/ARSIZE | log2 인코딩 |
burst | AWBURST/ARBURST | FIXED/INCR/WRAP |
dir | (내부) | AXI 신호 아님, 채널 선택용 |
resp | BRESP/RRESP | rand 없음 — DUT 응답 저장용 |
dir이 rand인 점 주목 — Sequence에서 제약을 안 걸면 읽기/쓰기가 랜덤으로 섞임.
[] = 동적 배열. 버스트 길이가 런타임에 결정되므로 필수.
resp = AXI_OKAY 기본값:
Driver: B/R 채널에서 받은 값을 여기 저장
Monitor: BRESP/RRESP를 여기 저장
Scoreboard: 이 값을 검사
기본값 AXI_OKAY → 아직 응답 안 받은 상태
`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() 명시 호출 필수 (부모가 인자를 받으므로)
이 파일의 핵심. "DUT가 실제로 받을 수 있는 값만 생성" 하는 규칙들.
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]};
}
constraint c_size { size <= $clog2(AXI_STRB_WIDTH); }
32비트 버스: STRB_WIDTH = 4, $clog2(4) = 2 → size <= 2 → 최대 4바이트/beat ✅
size=3(8바이트)면? → 32비트 버스에 못 실음 → 불법!
constraint c_burst_dist {
burst dist { AXI_INCR := 8, AXI_FIXED := 1, AXI_WRAP := 1 };
}
가중치 합 = 10
INCR : 80% ← 실전에서 압도적으로 많음 (DMA, 메모리 접근)
FIXED : 10% ← 코너 케이스 커버리지
WRAP : 10% ← 코너 케이스 커버리지
:= 는 각 값에 가중치 개별 부여, :/ 는 범위 전체에 가중치 분배
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이 아니면 이 제약은 무시됨
constraint c_align { addr % (1 << size) == 0; }
size=2 (4바이트) → addr는 4의 배수: 0x1000 ✅ 0x1002 ❌
AXI4는 unaligned도 지원하지만 규칙이 복잡 → 정렬 전송으로 단순화
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 페이지 단위로 메모리 관리 → 페이지마다 속성이 다를 수 있어
경계 넘으면 잘못된 속성/없는 주소 접근 위험
constraint c_payload { data.size() == len + 1; strb.size() == len + 1; }
동적 배열의 크기 자체를 제약으로 지정!
솔버: len 먼저 정함 → data/strb 배열 할당 → 각 원소 랜덤화
len=7 → data[8], strb[8] 자동 생성
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상 합법) → 스코어보드가 처리 필요
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 먼저, 나머지 라인 채움
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
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이라 확장 아이템에서 재정의해도 부모 버전 호출)
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칸, 좌측 정렬 (컬럼 맞춤) |
%0d | 10진수, 앞쪽 0 패딩 없음 |
%04h | 16진수 4자리, 0 패딩 |
%08h | 16진수 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() 오버라이드
→ 부모 핸들로 호출해도 이 버전 실행
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라 안전)
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으로 차단
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;
}
resp 미비교: DUT가 SLVERR 뱉어도 compare() 통과
→ 스코어보드가 resp를 따로 검사하면 OK, 아니면 놓침
strb 미비교: 레퍼런스 모델이 strb를 적용해 예측한다면 data 비교로 충분 (설계 의도)
→ 인터뷰에서 "왜 resp를 뺐는지" 물어볼 만한 지점
uvm_comparer: 어떤 필드가 왜 틀렸는지 자동 로깅
현재 구현은 우회 → compare() 실패 시 상세 리포트 없음 → 수동 디버깅
개선: do_compare &= comparer.compare_field("addr", addr, t.addr, $bits(addr));
읽기일 때 data는 DUT가 채워줌 → 그런데도 솔버가 매번 랜덤 생성
16 beat × 32bit = 512 랜덤 비트 × 트랜잭션 수 → 대량 회귀에서 솔버 시간 낭비
`uvm_object_utils (필드 매크로 없음) 선택 → 성능 ✅
대신: print()가 필드 안 보임 (do_print 필요), pack/unpack 미지원,
필드 추가 시 do_copy/do_compare 수동 갱신 필요
→ 갱신 깜빡하면 "복사했는데 값이 안 넘어가는" 조용한 버그!
bit → 0 또는 1 (최소 단위)
↓ ×8
byte → 8 bit
↓ ×(버스 폭)
beat → 한 번의 핸드셰이크로 전송되는 데이터 (버스 폭만큼)
↓ ×(len+1)
burst → 여러 beat의 묶음 (주소 요청 1번)
beat는 음악의 "박자"에서 온 말. bit과 전혀 다른 개념.
beat ≠ clock cycle — wait state가 있으면 클록은 흘러도 beat는 진행 안 됨.
beat = VALID & READY 핸드셰이크가 성립한 횟수.
| 필드 | 의미 | 주의 |
|---|---|---|
AWLEN / ARLEN | 실제 beat 수 - 1 | len=0 → 1 beat |
AWSIZE / ARSIZE | log2(beat당 byte 수) | size=2 → 4 byte |
룰 1: VALID는 READY를 기다리지 않고 올려도 됨 (기다리면 Deadlock)
룰 2: VALID 한번 올리면 READY 올 때까지 못 내림
룰 3: READY는 언제든 자유롭게 변경 가능
룰 4: 전송 = VALID & READY = 1 인 rising edge
룰 5: 같은 채널 내 VALID → READY 의존성 금지
"신호를 언제 읽고 언제 쓸지, 클록 기준으로 자동 맞춰주는 규칙 묶음"
input #1step → 클록 엣지 "직전"의 안정된 값 샘플 (race-free 읽기)
output #1 → 클록 엣지 "후" 1 time unit에 드라이브 (race-free 쓰기)
읽기는 엣지 이전, 쓰기는 엣지 이후 → 절대 안 겹침 → Race Condition 방지
"응답을 기다리지 않고 다음 요청을 계속 보내는 것"
인터페이스/드라이버/모니터가 전부 "채널 병렬" 구조인 이유가 바로 이것.
여러 버스트가 버스 위에 동시에 존재할 수 있어야 하므로,
각 채널을 독립 스레드로 처리해야 한다.