시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터
AXI 버스 (5채널)
│
┌─────┬───┼───┬─────┬─────┐
↓ ↓ ↓ ↓ ↓ ↓
mon_aw mon_w mon_b mon_ar mon_r ← 5개 스레드 병렬
│ │ │ │ │
└──aw_q──w_done_q──┘ │
(write 파이프) │
ar_q ────┘
│
↓
ap.write(tr) → Scoreboard / Coverage

핵심: 채널마다 독립 스레드 + 큐(FIFO)로 phase를 이어붙임
(드라이버가 "구동"을 채널 병렬로 했듯, 모니터는 "관찰"을 채널 병렬로)
순차 "AW → W → B" 루프의 함정:
T=0: AW #1 잡음
T=1: AW #2 발생 → W beat 수집 중이라 못 봄! ❌
...
→ Outstanding 발생 순간 조용히 데이터 유실 (에러도 안 남)
채널 병렬:
mon_aw는 오직 AW만 감시 → 절대 안 놓침
큐로 phase 연결 → Outstanding depth 몇이든 캡처 ✅
protected axi4_seq_item aw_q[$]; // AW 봤음, W 아직 안 끝남
protected axi4_seq_item w_done_q[$]; // W 끝남, B 기다리는 중
protected axi4_seq_item ar_q[$]; // AR 봤음, R 아직 안 끝남
Write 생애: AW핸드셰이크 → [aw_q] → (WLAST) → [w_done_q] → (B) → ap.write()
Read 생애: AR핸드셰이크 → [ar_q] → (RLAST) → ap.write()
(읽기는 R에 RRESP 같이 오므로 B 단계 불필요)
task run_phase(uvm_phase phase);
@(negedge vif.rst); // 리셋 해제 대기 (X 관찰 방지)
fork
mon_aw(); mon_w(); mon_b(); mon_ar(); mon_r();
join // 전부 forever → 시뮬 끝까지 유지
endtask
join 선택 이유: 5개 태스크 전부 forever → 절대 안 끝남
→ join이 영원히 블록 → run_phase가 시뮬 끝까지 유지
(join_none이면 phase 종료로 판단해 스레드가 죽을 수 있음)
모니터는 objection을 걸지 않음 (Driver/Sequence가 관리)
task mon_aw();
axi4_seq_item tr;
forever begin
@(vif.monitor_cb);
if (vif.monitor_cb.awvalid && vif.monitor_cb.awready) begin // 핸드셰이크
tr = axi4_seq_item::type_id::create("mon_wr");
tr.dir = AXI_WRITE;
tr.id = vif.monitor_cb.awid;
tr.addr = vif.monitor_cb.awaddr;
tr.len = vif.monitor_cb.awlen;
tr.size = vif.monitor_cb.awsize;
tr.burst = axi_burst_e'(vif.monitor_cb.awburst); // enum cast
tr.data = new[tr.len + 1]; // 배열 미리 할당
tr.strb = new[tr.len + 1];
aw_q.push_back(tr);
end
end
endtask
@(vif.monitor_cb): Clocking Block의 #1step 덕에 race-free 샘플링
awvalid && awready: 골든 룰 4 → 핸드셰이크 성립 = 주소 전송 완료
enum cast: awburst(2'b01) → AXI_INCR → 로그 가독성 & 비교 편의
new[len+1]: AWLEN=3 → 4 beat → 배열 크기 4
task mon_w();
axi4_seq_item tr;
int unsigned beat = 0; // forever 반복 사이에 값 유지
forever begin
@(vif.monitor_cb);
if (vif.monitor_cb.wvalid && vif.monitor_cb.wready) begin
if (aw_q.size() == 0)
`uvm_warning("MON", "W beat with no outstanding AW - ignored")
else begin
tr = aw_q[0]; // ⚠️ pop 안 함! 참조만!
if (beat <= tr.len) begin
tr.data[beat] = vif.monitor_cb.wdata;
tr.strb[beat] = vif.monitor_cb.wstrb;
end
if (vif.monitor_cb.wlast) begin
void'(aw_q.pop_front()); // 이제 제거
w_done_q.push_back(tr); // B 대기로 이동
beat = 0;
end
else beat++;
end
end
end
endtask
핵심 트릭: tr = aw_q[0] (pop 아님!)
tr은 Handle 복사 → tr 수정 = 큐 안 객체 수정 (의도된 동작!)
aw_q[0] ──┐
├──→ [같은 Object]
tr ───────┘
tr.data[beat]=... → 큐 안 객체가 직접 채워짐 ✅
범위 가드 (beat <= tr.len): WLAST 안 오고 beat 계속 오면
배열 범위 밖 접근 방지
void'(): pop_front() 반환값 안 쓴다는 표시 (경고 억제)
task mon_b();
forever begin
@(vif.monitor_cb);
if (vif.monitor_cb.bvalid && vif.monitor_cb.bready) begin
if (w_done_q.size() == 0)
`uvm_warning("MON", "B response with no completed write burst")
else begin
tr = w_done_q.pop_front();
tr.resp = axi_resp_e'(vif.monitor_cb.bresp);
ap.write(tr); // Scoreboard/Coverage로 발행! 🎉
end
end
end
endtask
B 핸드셰이크 = 쓰기 완료 → w_done_q에서 pop → resp 저장 → ap.write()
ap.write(tr)는 Handle 전달
→ 모니터는 매번 create()로 새 객체 → 재사용 안 함 → 안전
→ 단, Scoreboard가 저장할 땐 copy()로 독립 복사본 필수
// mon_ar: mon_aw와 거의 동일, 유일한 차이:
foreach (tr.strb[j]) tr.strb[j] = '1; // 읽기는 STRB 없음 → 전부 1
// mon_r: mon_w와 구조 같지만 2가지 차이
tr.resp = axi_resp_e'(vif.monitor_cb.rresp); // 차이1: 응답이 R에 함께
if (vif.monitor_cb.rlast) begin // 차이2: RLAST에서 바로 발행
void'(ar_q.pop_front());
ap.write(tr); // B 대기 없이 즉시!
beat = 0;
end
차이 1 — 응답이 데이터 채널에 함께
읽기는 별도 응답 채널 없음 → RLAST 시점에 바로 ap.write()
차이 2 — RRESP는 beat마다 옴 (⚠️ 이슈)
tr.resp = ... 를 매 beat 덮어씀
→ 중간 beat의 SLVERR가 마지막 OKAY에 덮여 사라질 수 있음
사이클 버스 이벤트 큐 상태
───── ───────────── ─────────────────────
1 AW #1 aw_q=[tr1]
2 AW #2 aw_q=[tr1, tr2] ← 순차 구현이면 유실!
3 W beat (tr1) aw_q=[tr1, tr2]
4 W WLAST (tr1) aw_q=[tr2], w_done_q=[tr1]
5 W beat (tr2) aw_q=[tr2], w_done_q=[tr1]
6 W WLAST (tr2) aw_q=[], w_done_q=[tr1, tr2]
7 B #1 w_done_q=[tr2] → tr1 발행
8 B #2 w_done_q=[] → tr2 발행

채널마다 독립 스레드로 관찰하고, 큐로 phase를 이어붙여 Outstanding 트랜잭션을 유실 없이 캡처.

읽기는 더 단순 (B 단계 없음):
AR 핸드셰이크 → ar_q → R beats 채움 → RLAST에서 바로 ap.write()
(R 채널에 RRESP가 같이 오므로 별도 응답 채널 불필요)

핵심 트릭: tr = aw_q[0] (pop 아님!)
→ Handle 복사라 tr 수정 = 큐 안 객체 직접 채움
→ WLAST에서야 pop_front()로 제거

드라이버: 큐 → 버스 (pop 후 구동)
모니터: 버스 → 큐 ([0] 참조하며 채우고 발행)
방향이 정확히 반대
mindmap
root((axi4_monitor))
설계철학
채널 병렬 관찰
순차 루프는 Outstanding 유실
passive - monitor_cb만 사용
5개 스레드
mon_aw - 쓰기 주소
mon_w - 쓰기 데이터
mon_b - 쓰기 응답
mon_ar - 읽기 주소
mon_r - 읽기 데이터
3개 상관 큐
aw_q - AW봤음 W채우는중
w_done_q - W끝남 B대기
ar_q - AR봤음 R채우는중
핵심 트릭
"[0] 참조 - pop 안 함"
Handle 복사로 직접 채움
WLAST/RLAST에서 발행
이슈
같은 사이클 AW+W 레이스
RRESP 덮어쓰기
ID 재정렬 미지원